From 4a5ec63ddf12e4b17f06e078911dc3c26be9e6a8 Mon Sep 17 00:00:00 2001 From: Gabor Keszthelyi Date: Thu, 6 Jul 2017 18:09:18 +0200 Subject: [PATCH] Reformat all files with project code style. #346 (#369) --- dmfs_eclipse_codestyle.xml | 864 +++-- .../src/main/AndroidManifest.xml | 2 +- .../java/org/dmfs/ngrams/NGramGenerator.java | 322 +- .../dmfs/provider/tasks/ContentOperation.java | 803 ++-- .../provider/tasks/FTSDatabaseHelper.java | 1048 +++--- .../provider/tasks/ProviderOperation.java | 207 +- .../provider/tasks/ProviderOperationsLog.java | 140 +- .../provider/tasks/SQLiteContentProvider.java | 535 ++- .../org/dmfs/provider/tasks/TaskContract.java | 3244 +++++++++-------- .../provider/tasks/TaskDatabaseHelper.java | 675 ++-- .../org/dmfs/provider/tasks/TaskProvider.java | 2563 ++++++------- .../tasks/TaskProviderBroadcastReceiver.java | 128 +- .../org/dmfs/provider/tasks/UriFactory.java | 42 +- .../java/org/dmfs/provider/tasks/Utils.java | 177 +- .../provider/tasks/handler/AlarmHandler.java | 204 +- .../tasks/handler/CategoryHandler.java | 485 +-- .../tasks/handler/DefaultPropertyHandler.java | 47 +- .../tasks/handler/PropertyHandler.java | 226 +- .../tasks/handler/PropertyHandlerFactory.java | 58 +- .../tasks/handler/RelationHandler.java | 454 +-- .../tasks/model/AbstractListAdapter.java | 40 +- .../tasks/model/AbstractTaskAdapter.java | 60 +- .../tasks/model/ContentValuesListAdapter.java | 152 +- .../tasks/model/ContentValuesTaskAdapter.java | 154 +- .../model/CursorContentValuesListAdapter.java | 155 +- .../model/CursorContentValuesTaskAdapter.java | 240 +- .../provider/tasks/model/EntityAdapter.java | 246 +- .../provider/tasks/model/ListAdapter.java | 87 +- .../provider/tasks/model/TaskAdapter.java | 605 ++- .../model/adapters/BinaryFieldAdapter.java | 128 +- .../model/adapters/BooleanFieldAdapter.java | 122 +- .../adapters/DateTimeArrayFieldAdapter.java | 468 +-- .../model/adapters/DateTimeFieldAdapter.java | 418 +-- .../model/adapters/DurationFieldAdapter.java | 150 +- .../tasks/model/adapters/FieldAdapter.java | 235 +- .../model/adapters/FloatFieldAdapter.java | 128 +- .../model/adapters/IntegerFieldAdapter.java | 130 +- .../model/adapters/LongFieldAdapter.java | 108 +- .../model/adapters/RRuleFieldAdapter.java | 182 +- .../model/adapters/SimpleFieldAdapter.java | 113 +- .../model/adapters/StringFieldAdapter.java | 110 +- .../tasks/model/adapters/UrlFieldAdapter.java | 130 +- .../processors/AbstractEntityProcessor.java | 66 +- .../tasks/processors/EntityProcessor.java | 170 +- .../lists/ListExecutionProcessor.java | 36 +- .../lists/ListValidatorProcessor.java | 192 +- .../processors/tasks/AutoUpdateProcessor.java | 358 +- .../processors/tasks/ChangeListProcessor.java | 276 +- .../tasks/processors/tasks/FtsProcessor.java | 26 +- .../processors/tasks/RelationProcessor.java | 129 +- .../tasks/TaskExecutionProcessor.java | 58 +- .../tasks/TaskInstancesProcessor.java | 321 +- .../tasks/TaskValidatorProcessor.java | 448 +-- .../tasks/processors/tasks/TestProcessor.java | 68 +- .../main/res/values/opentasks_defaults.xml | 3 +- opentasks/lint.xml | 3 +- opentasks/src/main/AndroidManifest.xml | 280 +- .../draglinearlayout/DragLinearLayout.java | 1668 ++++----- .../android/widgets/ColoredShapeCheckBox.java | 6 +- .../java/org/dmfs/tasks/EditTaskActivity.java | 370 +- .../java/org/dmfs/tasks/EditTaskFragment.java | 1335 +++---- .../org/dmfs/tasks/EmptyTaskFragment.java | 1 + .../dmfs/tasks/InputTextDialogFragment.java | 62 +- .../org/dmfs/tasks/ManageListActivity.java | 55 +- .../dmfs/tasks/QuickAddDialogFragment.java | 946 ++--- .../org/dmfs/tasks/SettingsListFragment.java | 1058 +++--- .../org/dmfs/tasks/SyncSettingsActivity.java | 252 +- .../org/dmfs/tasks/TaskGroupPagerAdapter.java | 268 +- .../java/org/dmfs/tasks/TaskListActivity.java | 1371 +++---- .../java/org/dmfs/tasks/TaskListFragment.java | 2020 +++++----- .../java/org/dmfs/tasks/ViewTaskActivity.java | 222 +- .../java/org/dmfs/tasks/ViewTaskFragment.java | 1483 ++++---- .../DashClockPreferenceActivity.java | 26 +- .../dmfs/tasks/dashclock/TasksExtension.java | 768 ++-- .../groupings/AbstractGroupingFactory.java | 152 +- .../groupings/BaseTaskViewDescriptor.java | 316 +- .../org/dmfs/tasks/groupings/ByDueDate.java | 563 +-- .../java/org/dmfs/tasks/groupings/ByList.java | 620 ++-- .../org/dmfs/tasks/groupings/ByPriority.java | 608 +-- .../org/dmfs/tasks/groupings/ByProgress.java | 517 +-- .../org/dmfs/tasks/groupings/BySearch.java | 687 ++-- .../org/dmfs/tasks/groupings/ByStartDate.java | 588 +-- .../org/dmfs/tasks/groupings/TabConfig.java | 463 +-- .../AbstractCursorLoaderFactory.java | 19 +- .../AbstractCustomCursorFactory.java | 44 +- .../cursorloaders/CursorLoaderFactory.java | 58 +- .../cursorloaders/CustomCursorLoader.java | 172 +- .../EmptyCursorLoaderFactory.java | 24 +- .../cursorloaders/PriorityCursorFactory.java | 77 +- .../PriorityCursorLoaderFactory.java | 22 +- .../cursorloaders/ProgressCursorFactory.java | 83 +- .../ProgressCursorLoaderFactory.java | 22 +- .../SearchHistoryCursorFactory.java | 40 +- .../SearchHistoryCursorLoaderFactory.java | 48 +- .../cursorloaders/TimeRangeCursorFactory.java | 343 +- .../cursorloaders/TimeRangeCursorLoader.java | 124 +- .../TimeRangeCursorLoaderFactory.java | 22 +- .../TimeRangeShortCursorFactory.java | 74 +- .../TimeRangeStartCursorFactory.java | 95 +- .../TimeRangeStartCursorLoader.java | 124 +- .../TimeRangeStartCursorLoaderFactory.java | 22 +- .../groupings/filters/AbstractFilter.java | 31 +- .../tasks/groupings/filters/AndFilter.java | 10 +- .../filters/BinaryOperationFilter.java | 104 +- .../groupings/filters/ConstantFilter.java | 72 +- .../tasks/groupings/filters/OrFilter.java | 10 +- .../homescreen/TaskListSelectionFragment.java | 280 +- .../tasks/homescreen/TaskListWidgetItem.java | 198 +- .../homescreen/TaskListWidgetProvider.java | 277 +- .../TaskListWidgetProviderLarge.java | 49 +- .../TaskListWidgetProviderLargeLegacy.java | 53 +- .../TaskListWidgetProviderLegacy.java | 254 +- .../TaskListWidgetSettingsActivity.java | 133 +- .../TaskListWidgetUpdaterService.java | 772 ++-- .../model/AbstractArrayChoicesAdapter.java | 161 +- .../dmfs/tasks/model/ArrayChoicesAdapter.java | 97 +- .../org/dmfs/tasks/model/CheckListItem.java | 45 +- .../java/org/dmfs/tasks/model/ContentSet.java | 1273 +++---- .../tasks/model/CursorChoicesAdapter.java | 110 +- .../org/dmfs/tasks/model/DefaultModel.java | 295 +- .../org/dmfs/tasks/model/FieldDescriptor.java | 752 ++-- .../org/dmfs/tasks/model/IChoicesAdapter.java | 92 +- .../tasks/model/IllegalDataKindException.java | 16 +- .../org/dmfs/tasks/model/MinimalModel.java | 64 +- .../main/java/org/dmfs/tasks/model/Model.java | 390 +- .../tasks/model/ModelInflaterException.java | 24 +- .../tasks/model/OnContentChangeListener.java | 31 +- .../model/ResourceArrayChoicesAdapter.java | 114 +- .../java/org/dmfs/tasks/model/Sources.java | 563 +-- .../dmfs/tasks/model/TaskFieldAdapters.java | 273 +- .../tasks/model/TimeZoneChoicesAdapter.java | 365 +- .../java/org/dmfs/tasks/model/XmlModel.java | 1119 +++--- .../model/adapters/BooleanFieldAdapter.java | 222 +- .../model/adapters/ChecklistFieldAdapter.java | 426 +-- .../model/adapters/ColorFieldAdapter.java | 178 +- .../CustomizedDefaultFieldAdapter.java | 59 +- .../DescriptionStringFieldAdapter.java | 254 +- .../tasks/model/adapters/FieldAdapter.java | 285 +- .../model/adapters/FloatFieldAdapter.java | 208 +- .../adapters/FormattedStringFieldAdapter.java | 202 +- .../model/adapters/IntegerFieldAdapter.java | 208 +- .../model/adapters/StringFieldAdapter.java | 208 +- .../model/adapters/TimeFieldAdapter.java | 424 +-- .../tasks/model/adapters/TimeZoneWrapper.java | 360 +- .../model/adapters/TimezoneFieldAdapter.java | 501 +-- .../tasks/model/adapters/UrlFieldAdapter.java | 252 +- .../model/constraints/AbstractConstraint.java | 32 +- .../constraints/AdjustPercentComplete.java | 48 +- .../dmfs/tasks/model/constraints/After.java | 42 +- .../model/constraints/BeforeOrShiftTime.java | 76 +- .../constraints/ChecklistConstraint.java | 98 +- .../tasks/model/constraints/NotAfter.java | 50 +- .../tasks/model/constraints/NotBefore.java | 50 +- .../tasks/model/constraints/ShiftIfAfter.java | 52 +- .../tasks/model/constraints/ShiftTime.java | 64 +- .../tasks/model/constraints/UpdateAllDay.java | 70 +- .../dmfs/tasks/model/defaults/Default.java | 22 +- .../tasks/model/defaults/DefaultAfter.java | 56 +- .../tasks/model/defaults/DefaultBefore.java | 56 +- .../tasks/model/layout/LayoutDescriptor.java | 270 +- .../tasks/model/layout/LayoutOptions.java | 218 +- .../notification/AlarmBroadcastReceiver.java | 172 +- .../notification/NotificationActionUtils.java | 997 ++--- .../NotificationUpdaterService.java | 1493 ++++---- .../notification/TaskNotificationHandler.java | 342 +- .../dmfs/tasks/utils/ActionBarActivity.java | 95 +- .../dmfs/tasks/utils/AppCompatActivity.java | 95 +- .../dmfs/tasks/utils/AsyncContentLoader.java | 168 +- .../dmfs/tasks/utils/AsyncModelLoader.java | 112 +- .../dmfs/tasks/utils/ContentValueMapper.java | 298 +- .../utils/DatabaseInitializedReceiver.java | 46 +- .../org/dmfs/tasks/utils/DateFormatter.java | 561 +-- .../utils/ExpandableChildDescriptor.java | 382 +- .../utils/ExpandableGroupDescriptor.java | 210 +- .../ExpandableGroupDescriptorAdapter.java | 312 +- .../org/dmfs/tasks/utils/FlingDetector.java | 1210 +++--- .../utils/ObservableSparseArrayCompat.java | 134 +- .../tasks/utils/OnChildLoadedListener.java | 20 +- .../tasks/utils/OnContentLoadedListener.java | 16 +- .../tasks/utils/OnModelLoadedListener.java | 16 +- .../dmfs/tasks/utils/RecentlyUsedLists.java | 63 +- .../tasks/utils/RetainExpandableListView.java | 205 +- .../tasks/utils/SearchChildDescriptor.java | 88 +- .../utils/SearchHistoryDatabaseHelper.java | 129 +- .../dmfs/tasks/utils/SearchHistoryHelper.java | 174 +- .../java/org/dmfs/tasks/utils/SetFromMap.java | 142 +- .../tasks/utils/TasksListCursorAdapter.java | 303 +- .../utils/TasksListCursorSpinnerAdapter.java | 214 +- .../dmfs/tasks/utils/TimeChangeListener.java | 32 +- .../dmfs/tasks/utils/TimeChangeObserver.java | 191 +- .../org/dmfs/tasks/utils/ViewDescriptor.java | 100 +- .../WidgetConfigurationDatabaseHelper.java | 249 +- .../tasks/widget/AbstractFieldEditor.java | 26 +- .../dmfs/tasks/widget/AbstractFieldView.java | 467 +-- .../org/dmfs/tasks/widget/BaseTaskView.java | 180 +- .../dmfs/tasks/widget/BooleanFieldEditor.java | 159 +- .../dmfs/tasks/widget/BooleanFieldView.java | 92 +- .../tasks/widget/CheckListFieldEditor.java | 531 +-- .../dmfs/tasks/widget/CheckListFieldView.java | 674 ++-- .../widget/CheckListFieldViewLegacy.java | 284 +- .../dmfs/tasks/widget/ChoicesFieldEditor.java | 520 +-- .../dmfs/tasks/widget/ChoicesFieldView.java | 140 +- .../org/dmfs/tasks/widget/ListColorView.java | 70 +- .../tasks/widget/ListenableScrollView.java | 81 +- .../dmfs/tasks/widget/LocationFieldView.java | 1 + .../tasks/widget/PercentageFieldEditor.java | 212 +- .../tasks/widget/PercentageFieldView.java | 124 +- .../tasks/widget/RecurrenceRuleEditor.java | 28 +- .../dmfs/tasks/widget/RecurrenceRuleView.java | 28 +- .../java/org/dmfs/tasks/widget/TaskEdit.java | 78 +- .../java/org/dmfs/tasks/widget/TaskView.java | 150 +- .../dmfs/tasks/widget/TextFieldEditor.java | 196 +- .../org/dmfs/tasks/widget/TextFieldView.java | 150 +- .../dmfs/tasks/widget/TimeFieldEditor.java | 825 +++-- .../org/dmfs/tasks/widget/TimeFieldView.java | 344 +- .../org/dmfs/tasks/widget/UrlFieldEditor.java | 222 +- .../org/dmfs/tasks/widget/UrlFieldView.java | 156 +- .../src/main/res/color-v11/due_date_text.xml | 6 +- .../main/res/color-v11/overdue_date_text.xml | 6 +- .../res/color-v11/task_list_title_text.xml | 6 +- .../src/main/res/drawable/bg_searchview.xml | 11 +- .../complete_task_background_overlay.xml | 4 +- .../src/main/res/drawable/light_gray.xml | 4 +- .../drawable/list_background_activated.xml | 4 +- .../res/drawable/list_background_pressed.xml | 4 +- .../res/drawable/list_background_selected.xml | 10 +- opentasks/src/main/res/drawable/no_image.xml | 4 +- ...dmfs_colorshape_checkbox_selector_dark.xml | 6 +- ...mfs_colorshape_checkbox_selector_light.xml | 6 +- ...lorshape_checkbox_white_shape_selector.xml | 6 +- .../src/main/res/drawable/oval_shape.xml | 2 +- .../src/main/res/drawable/overlay_bottom.xml | 28 +- .../src/main/res/drawable/overlay_top.xml | 28 +- .../src/main/res/drawable/rect_shape.xml | 2 +- .../main/res/drawable/rect_shape_corners.xml | 10 +- .../rect_shape_corners_white_insert.xml | 12 +- .../drawable/selectable_background_white.xml | 14 +- .../res/drawable/selectable_list_item.xml | 17 +- .../main/res/drawable/shape_color_circle.xml | 12 +- .../res/drawable/tab_indicator_ab_white.xml | 43 +- .../src/main/res/drawable/task_divider.xml | 6 +- opentasks/src/main/res/drawable/task_due.xml | 5 +- .../task_progress_background_shade.xml | 12 +- .../src/main/res/drawable/task_start.xml | 5 +- .../main/res/drawable/text_cursor_white.xml | 6 +- .../src/main/res/drawable/transparent.xml | 4 +- .../res/drawable/vertical_shade_r_to_l.xml | 26 +- .../main/res/drawable/widget_list_item_bg.xml | 10 +- .../drawable/widget_task_add_button_bg.xml | 7 +- .../src/main/res/drawable/window_overlay.xml | 28 +- .../res/layout-v11/checklist_field_view.xml | 62 +- .../checklist_field_view_element.xml | 92 +- .../fragment_task_list_selection.xml | 86 +- .../integer_choices_spinner_item.xml | 34 +- .../res/layout-v11/list_item_selection.xml | 82 +- .../main/res/layout-v11/task_list_widget.xml | 71 +- .../res/layout-v11/task_list_widget_item.xml | 80 +- .../layout-v11/task_list_widget_loading.xml | 6 +- .../main/res/layout-v11/text_field_editor.xml | 13 +- .../main/res/layout-v11/undo_notification.xml | 88 +- .../main/res/layout-v11/url_field_editor.xml | 13 +- .../res/layout/account_list_item_dialog.xml | 57 +- .../res/layout/activity_manage_task_list.xml | 148 +- .../src/main/res/layout/activity_settings.xml | 8 +- .../main/res/layout/activity_task_detail.xml | 16 +- .../main/res/layout/activity_task_editor.xml | 14 +- .../main/res/layout/activity_task_list.xml | 67 +- .../layout/activity_widget_configuration.xml | 14 +- .../main/res/layout/boolean_field_editor.xml | 39 +- .../main/res/layout/boolean_field_view.xml | 43 +- .../res/layout/checklist_field_editor.xml | 16 +- .../layout/checklist_field_editor_element.xml | 31 +- .../main/res/layout/checklist_field_view.xml | 23 +- .../layout/checklist_field_view_element.xml | 8 +- .../main/res/layout/choices_field_editor.xml | 14 +- .../main/res/layout/choices_field_view.xml | 21 +- .../src/main/res/layout/detail_label.xml | 22 +- .../src/main/res/layout/editor_header.xml | 20 +- .../layout/fragment_expandable_task_list.xml | 18 +- .../res/layout/fragment_input_text_dialog.xml | 118 +- .../res/layout/fragment_quick_add_dialog.xml | 109 +- .../fragment_quick_add_dialog_header.xml | 62 +- .../res/layout/fragment_synced_task_list.xml | 60 +- .../res/layout/fragment_task_edit_detail.xml | 39 +- .../fragment_task_edit_detail_twopane.xml | 49 +- .../layout/fragment_task_list_selection.xml | 78 +- .../res/layout/fragment_visiblelist_list.xml | 38 +- .../layout/integer_choices_spinner_item.xml | 34 +- .../integer_choices_spinner_selected_item.xml | 32 +- .../src/main/res/layout/list_color_view.xml | 4 +- .../main/res/layout/list_item_selection.xml | 82 +- .../res/layout/list_spinner_item_dropdown.xml | 59 +- .../res/layout/list_spinner_item_selected.xml | 36 +- .../list_spinner_item_selected_quick_add.xml | 21 +- .../res/layout/opentasks_layout_tab_icon.xml | 9 +- .../layout/opentasks_location_field_view.xml | 3 +- .../res/layout/percentage_field_editor.xml | 39 +- .../main/res/layout/percentage_field_view.xml | 45 +- .../src/main/res/layout/task_detail_view.xml | 8 +- opentasks/src/main/res/layout/task_edit.xml | 8 +- .../main/res/layout/task_editor_activity.xml | 13 +- .../src/main/res/layout/task_list_element.xml | 354 +- .../res/layout/task_list_field_editor.xml | 18 +- .../main/res/layout/task_list_field_view.xml | 20 +- .../src/main/res/layout/task_list_group.xml | 166 +- .../layout/task_list_group_single_line.xml | 98 +- .../res/layout/task_list_provider_bar.xml | 18 +- .../src/main/res/layout/task_list_widget.xml | 75 +- .../main/res/layout/task_list_widget_item.xml | 74 +- opentasks/src/main/res/layout/task_view.xml | 9 +- .../src/main/res/layout/text_field_editor.xml | 19 +- .../src/main/res/layout/text_field_view.xml | 21 +- .../res/layout/text_field_view_nodivider.xml | 27 +- .../text_field_view_nodivider_large.xml | 39 +- .../text_field_view_nodivider_small.xml | 40 +- .../src/main/res/layout/time_field_editor.xml | 51 +- .../src/main/res/layout/time_field_view.xml | 66 +- .../src/main/res/layout/url_field_editor.xml | 15 +- .../src/main/res/layout/url_field_view.xml | 19 +- .../res/layout/visible_task_list_item.xml | 110 +- .../res/menu-v11/edit_task_activity_menu.xml | 10 +- .../res/menu-v11/view_task_fragment_menu.xml | 48 +- .../main/res/menu/edit_task_activity_menu.xml | 22 +- .../src/main/res/menu/list_settings_menu.xml | 10 +- opentasks/src/main/res/menu/settings.xml | 10 +- .../main/res/menu/task_list_activity_menu.xml | 38 +- .../main/res/menu/task_list_fragment_menu.xml | 20 +- opentasks/src/main/res/menu/test_display.xml | 10 +- .../main/res/menu/view_task_fragment_menu.xml | 50 +- .../src/main/res/values-large-land/dimens.xml | 2 +- .../src/main/res/values-large-land/refs.xml | 12 +- .../src/main/res/values-large-v11/styles.xml | 2 +- opentasks/src/main/res/values-large/bools.xml | 2 +- .../src/main/res/values-large/styles.xml | 6 +- .../src/main/res/values-sw600dp-land/refs.xml | 8 +- .../src/main/res/values-sw600dp/dimens.xml | 2 +- .../src/main/res/values-sw600dp/styles.xml | 6 +- .../main/res/values-sw720dp-land/bools.xml | 2 +- .../main/res/values-sw720dp-port/bools.xml | 2 +- .../src/main/res/values-sw720dp-port/refs.xml | 8 +- opentasks/src/main/res/values-v11/styles.xml | 59 +- opentasks/src/main/res/values-v14/styles.xml | 36 +- opentasks/src/main/res/values-v21/styles.xml | 3 +- .../src/main/res/values-xlarge-port/bools.xml | 2 +- .../src/main/res/values-xlarge-port/refs.xml | 8 +- opentasks/src/main/res/values/arrays.xml | 3 +- opentasks/src/main/res/values/attrs.xml | 19 +- opentasks/src/main/res/values/dashclock.xml | 4 +- opentasks/src/main/res/values/ids.xml | 60 +- opentasks/src/main/res/values/refs.xml | 8 +- opentasks/src/main/res/values/styles.xml | 78 +- .../src/main/res/xml-v11/listview_tabs.xml | 46 +- .../src/main/res/xml-v11/task_widget_info.xml | 14 +- .../res/xml-v11/task_widget_info_large.xml | 14 +- .../main/res/xml/dashclock_preferences.xml | 16 +- opentasks/src/main/res/xml/listview_tabs.xml | 46 +- opentasks/src/main/res/xml/searchable.xml | 4 +- .../src/main/res/xml/task_widget_info.xml | 14 +- .../main/res/xml/task_widget_info_large.xml | 14 +- 359 files changed, 34588 insertions(+), 33857 deletions(-) diff --git a/dmfs_eclipse_codestyle.xml b/dmfs_eclipse_codestyle.xml index 4c4c948f..ba19af5f 100644 --- a/dmfs_eclipse_codestyle.xml +++ b/dmfs_eclipse_codestyle.xml @@ -1,291 +1,579 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentasks-provider/src/main/AndroidManifest.xml b/opentasks-provider/src/main/AndroidManifest.xml index b3288a70..cff495d1 100644 --- a/opentasks-provider/src/main/AndroidManifest.xml +++ b/opentasks-provider/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="org.dmfs.tasks.provider"> */ public final class NGramGenerator { - /** - * A {@link Pattern} that matches anything that doesn't belong to a word or number. - */ - private final static Pattern SEPARATOR_PATTERN = Pattern.compile("[^\\p{L}\\p{M}\\d]+"); - - /** - * A {@link Pattern} that matches anything that doesn't belong to a word. - */ - private final static Pattern SEPARATOR_PATTERN_NO_NUMBERS = Pattern.compile("[^\\p{L}\\p{M}]+"); - - private final int mN; - private final int mMinWordLen; - private boolean mAllLowercase = true; - private boolean mReturnNumbers = true; - private boolean mAddSpaceInFront = false; - private Locale mLocale = Locale.getDefault(); - - private char[] mTempArray; - - - public NGramGenerator(int n) - { - this(n, 1); - } - - - public NGramGenerator(int n, int minWordLen) - { - mN = n; - mMinWordLen = minWordLen; - mTempArray = new char[n]; - mTempArray[0] = ' '; - } - - - /** - * Set whether to convert all words to lower-case first. - * - * @param lowercase - * true to convert the test to lower case first. - * @return This instance. - */ - public NGramGenerator setAllLowercase(boolean lowercase) - { - mAllLowercase = lowercase; - return this; - } - - - /** - * Set whether to index the beginning of a word with a space in front. This slightly raises the weight of word beginnings when searching. - * - * @param addSpace - * true to add a space in front of each word, false otherwise. - * @return This instance. - */ - public NGramGenerator setAddSpaceInFront(boolean addSpace) - { - mAddSpaceInFront = addSpace; - return this; - } - - - /** - * Sets the {@link Locale} to use when converting the input string to lower case. This has no effect when {@link #setAllLowercase(boolean)} is called with - * false. - * - * @param locale - * The {@link Locale} to user for the conversion to lower case. - * @return This instance. - */ - public NGramGenerator setLocale(Locale locale) - { - mLocale = locale; - return this; - } - - - /** - * Get all N-grams contained in the given String. - * - * @param data - * The String to analyze. - * @return A {@link Set} containing all N-grams. - */ - public Set getNgrams(String data) - { - Set result = new HashSet(128); - - return getNgrams(result, data); - } - - - /** - * Get all N-grams contained in the given String. - * - * @param set - * The set to add all the N-grams to, or null to create a new set. - * @param data - * The String to analyze. - * - * @return The {@link Set} containing the N-grams. - */ - public Set getNgrams(Set set, String data) - { - if (mAllLowercase) - { - data = data.toLowerCase(mLocale); - } - - String[] words = mReturnNumbers ? SEPARATOR_PATTERN.split(data) : SEPARATOR_PATTERN_NO_NUMBERS.split(data); - - if (set == null) - { - set = new HashSet(128); - } - - for (String word : words) - { - getNgrams(word, set); - } - - return set; - } - - - public void getNgrams(String word, Set ngrams) - { - final int len = word.length(); - final int minWordLen = mMinWordLen; - - if (len < minWordLen) - { - return; - } - - final int n = mN; - final int last = Math.max(1, len - n + 1); - - for (int i = 0; i < last; ++i) - { - ngrams.add(word.substring(i, Math.min(i + n, len))); - } - - if (mAddSpaceInFront) - { - /* - * Add another String with a space and the first n-1 characters of the word. + /** + * A {@link Pattern} that matches anything that doesn't belong to a word or number. + */ + private final static Pattern SEPARATOR_PATTERN = Pattern.compile("[^\\p{L}\\p{M}\\d]+"); + + /** + * A {@link Pattern} that matches anything that doesn't belong to a word. + */ + private final static Pattern SEPARATOR_PATTERN_NO_NUMBERS = Pattern.compile("[^\\p{L}\\p{M}]+"); + + private final int mN; + private final int mMinWordLen; + private boolean mAllLowercase = true; + private boolean mReturnNumbers = true; + private boolean mAddSpaceInFront = false; + private Locale mLocale = Locale.getDefault(); + + private char[] mTempArray; + + + public NGramGenerator(int n) + { + this(n, 1); + } + + + public NGramGenerator(int n, int minWordLen) + { + mN = n; + mMinWordLen = minWordLen; + mTempArray = new char[n]; + mTempArray[0] = ' '; + } + + + /** + * Set whether to convert all words to lower-case first. + * + * @param lowercase + * true to convert the test to lower case first. + * + * @return This instance. + */ + public NGramGenerator setAllLowercase(boolean lowercase) + { + mAllLowercase = lowercase; + return this; + } + + + /** + * Set whether to index the beginning of a word with a space in front. This slightly raises the weight of word beginnings when searching. + * + * @param addSpace + * true to add a space in front of each word, false otherwise. + * + * @return This instance. + */ + public NGramGenerator setAddSpaceInFront(boolean addSpace) + { + mAddSpaceInFront = addSpace; + return this; + } + + + /** + * Sets the {@link Locale} to use when converting the input string to lower case. This has no effect when {@link #setAllLowercase(boolean)} is called with + * false. + * + * @param locale + * The {@link Locale} to user for the conversion to lower case. + * + * @return This instance. + */ + public NGramGenerator setLocale(Locale locale) + { + mLocale = locale; + return this; + } + + + /** + * Get all N-grams contained in the given String. + * + * @param data + * The String to analyze. + * + * @return A {@link Set} containing all N-grams. + */ + public Set getNgrams(String data) + { + Set result = new HashSet(128); + + return getNgrams(result, data); + } + + + /** + * Get all N-grams contained in the given String. + * + * @param set + * The set to add all the N-grams to, or null to create a new set. + * @param data + * The String to analyze. + * + * @return The {@link Set} containing the N-grams. + */ + public Set getNgrams(Set set, String data) + { + if (mAllLowercase) + { + data = data.toLowerCase(mLocale); + } + + String[] words = mReturnNumbers ? SEPARATOR_PATTERN.split(data) : SEPARATOR_PATTERN_NO_NUMBERS.split(data); + + if (set == null) + { + set = new HashSet(128); + } + + for (String word : words) + { + getNgrams(word, set); + } + + return set; + } + + + public void getNgrams(String word, Set ngrams) + { + final int len = word.length(); + final int minWordLen = mMinWordLen; + + if (len < minWordLen) + { + return; + } + + final int n = mN; + final int last = Math.max(1, len - n + 1); + + for (int i = 0; i < last; ++i) + { + ngrams.add(word.substring(i, Math.min(i + n, len))); + } + + if (mAddSpaceInFront) + { + /* + * Add another String with a space and the first n-1 characters of the word. * * We could just call * @@ -185,14 +189,14 @@ public final class NGramGenerator * * But it's probably way more efficient like this: */ - char[] tempArray = mTempArray; - - int count = Math.min(len, n - 1); - for (int i = 0; i < count; ++i) - { - tempArray[i + 1] = word.charAt(i); - } - ngrams.add(new String(tempArray)); - } - } + char[] tempArray = mTempArray; + + int count = Math.min(len, n - 1); + for (int i = 0; i < count; ++i) + { + tempArray[i + 1] = word.charAt(i); + } + ngrams.add(new String(tempArray)); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java index 21769af1..3d750d58 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java @@ -17,15 +17,6 @@ package org.dmfs.provider.tasks; -import java.util.TimeZone; - -import org.dmfs.provider.tasks.TaskContract.Instances; -import org.dmfs.provider.tasks.TaskContract.Tasks; -import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter; -import org.dmfs.provider.tasks.model.TaskAdapter; -import org.dmfs.provider.tasks.processors.tasks.TaskInstancesProcessor; -import org.dmfs.rfc5545.DateTime; - import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; @@ -40,399 +31,411 @@ import android.os.Build; import android.os.Handler; import android.util.Log; +import org.dmfs.provider.tasks.TaskContract.Instances; +import org.dmfs.provider.tasks.TaskContract.Tasks; +import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter; +import org.dmfs.provider.tasks.model.TaskAdapter; +import org.dmfs.provider.tasks.processors.tasks.TaskInstancesProcessor; +import org.dmfs.rfc5545.DateTime; + +import java.util.TimeZone; + public enum ContentOperation { - /** - * When the local timezone has been changed we need to update the due and start sorting values. This handler will take care of running the appropriate - * update. In addition it fires an operation to update all notifications. - */ - UPDATE_TIMEZONE(new OperationHandler() - { - @Override - public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values) - { - long start = System.currentTimeMillis(); - - // request an update of all instance values - ContentValues vals = new ContentValues(1); - TaskInstancesProcessor.addUpdateRequest(vals); - - // execute update that triggers a recalculation of all due and start sorting values - int count = context.getContentResolver().update( - TaskContract.Tasks.getContentUri(uri.getAuthority()).buildUpon().appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true").build(), - vals, null, null); - - Log.i("TaskProvider", "time to update " + count + " tasks: " + (System.currentTimeMillis() - start) + " ms"); - - // now update alarms as well - UPDATE_NOTIFICATION_ALARM.fire(context, null); - } - }), - - /** - * Takes care of everything we need to send task start and task due broadcasts. - */ - POST_NOTIFICATIONS(new OperationHandler() - { - - @Override - public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values) - { - TimeZone localTimeZone = TimeZone.getDefault(); - - // the date-time of when the last notification was shown - DateTime lastAlarm = getLastAlarmTimestamp(context); - // the current time, we show all notifications between and now - DateTime now = DateTime.nowAndHere(); - - String lastAlarmString = Long.toString(lastAlarm.getInstance()); - String nowString = Long.toString(now.getInstance()); - - // load all tasks that have started or became due since the last time we've shown a notification. - Cursor taskCursor = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, null, "((" + TaskContract.Instances.INSTANCE_DUE_SORTING + ">? and " - + TaskContract.Instances.INSTANCE_DUE_SORTING + "<=?) or (" + TaskContract.Instances.INSTANCE_START_SORTING + ">? and " - + TaskContract.Instances.INSTANCE_START_SORTING + "<=?)) and " + Instances.IS_CLOSED + " = 0 and " + Tasks._DELETED + "=0", new String[] { - lastAlarmString, nowString, lastAlarmString, nowString }, null, null, null); - - try - { - while (taskCursor.moveToNext()) - { - TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(taskCursor), taskCursor, null); - - DateTime instanceDue = task.valueOf(TaskAdapter.INSTANCE_DUE); - if (instanceDue != null && !instanceDue.isFloating()) - { - // make sure we compare instances in local time - instanceDue = instanceDue.shiftTimeZone(localTimeZone); - } - - DateTime instanceStart = task.valueOf(TaskAdapter.INSTANCE_START); - if (instanceStart != null && !instanceStart.isFloating()) - { - // make sure we compare instances in local time - instanceStart = instanceStart.shiftTimeZone(localTimeZone); - } - - if (instanceDue != null && lastAlarm.getInstance() < instanceDue.getInstance() && instanceDue.getInstance() <= now.getInstance()) - { - // this task became due since the last alarm, send a due broadcast - sendBroadcast(context, TaskContract.ACTION_BROADCAST_TASK_DUE, task.uri(uri.getAuthority()), instanceDue, - task.valueOf(TaskAdapter.TITLE)); - } - else if (instanceStart != null && lastAlarm.getInstance() < instanceStart.getInstance() && instanceStart.getInstance() <= now.getInstance()) - { - // this task has started since the last alarm, send a start broadcast - sendBroadcast(context, TaskContract.ACTION_BROADCAST_TASK_STARTING, task.uri(uri.getAuthority()), instanceStart, - task.valueOf(TaskAdapter.TITLE)); - } - } - } - finally - { - taskCursor.close(); - } - - // all notifications up to now have been triggered - saveLastAlarmTime(context, now); - - // set the alarm for the next notification - UPDATE_NOTIFICATION_ALARM.fire(context, null); - } - - - @SuppressLint("NewApi") - private void saveLastAlarmTime(Context context, DateTime time) - { - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - Editor editor = prefs.edit(); - editor.putLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, time.getTimestamp()); - if (Build.VERSION.SDK_INT >= 9) - { - editor.apply(); - } - else - { - editor.commit(); - } - } - - - private DateTime getLastAlarmTimestamp(Context context) - { - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - return new DateTime(TimeZone.getDefault(), prefs.getLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, System.currentTimeMillis())); - } - - - /** - * Sends a notification broadcast for a task that has started or became due. - * - * @param context - * A {@link Context}. - * @param action - * The broadcast action. - * @param uri - * The task uri. - * @param datetime - * The datetime to add. - * @param title - * The task title. - */ - private void sendBroadcast(Context context, String action, Uri uri, DateTime datetime, String title) - { - Intent intent = new Intent(action); - intent.setData(uri); - intent.putExtra(TaskContract.EXTRA_TASK_TIMESTAMP, datetime.getTimestamp()); - intent.putExtra(TaskContract.EXTRA_TASK_ALLDAY, datetime.isAllDay()); - if (!datetime.isFloating()) - { - intent.putExtra(TaskContract.EXTRA_TASK_TIMEZONE, datetime.getTimeZone().getID()); - } - intent.putExtra(TaskContract.EXTRA_TASK_TITLE, title); - context.sendBroadcast(intent); - } - }), - - /** - * Determines the date-time of when the next task becomes due or starts (whatever happens first) and sets an alarm to trigger a notification. - */ - UPDATE_NOTIFICATION_ALARM(new OperationHandler() - { - - @Override - public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values) - { - TimeZone localTimeZone = TimeZone.getDefault(); - DateTime lastAlarm = getLastAlarmTimestamp(context); - DateTime now = DateTime.nowAndHere(); - - if (now.before(lastAlarm)) - { - // time went backwards, set last alarm time to now - lastAlarm = now; - saveLastAlarmTime(context, now); - } - - String lastAlarmString = Long.toString(lastAlarm.getInstance()); - - DateTime nextAlarm = null; - - // find the next task that starts - Cursor nextTaskStartCursor = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, null, TaskContract.Instances.INSTANCE_START_SORTING + ">? and " - + Instances.IS_CLOSED + " = 0 and " + Tasks._DELETED + "=0", new String[] { lastAlarmString }, null, null, - TaskContract.Instances.INSTANCE_START_SORTING, "1"); - - try - { - if (nextTaskStartCursor.moveToNext()) - { - TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(nextTaskStartCursor), nextTaskStartCursor, null); - nextAlarm = task.valueOf(TaskAdapter.INSTANCE_START); - if (!nextAlarm.isFloating()) - { - nextAlarm = nextAlarm.shiftTimeZone(localTimeZone); - } - } - } - finally - { - nextTaskStartCursor.close(); - } - - // find the next task that's due - Cursor nextTaskDueCursor = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, null, TaskContract.Instances.INSTANCE_DUE_SORTING + ">? and " - + Instances.IS_CLOSED + " = 0 and " + Tasks._DELETED + "=0", new String[] { lastAlarmString }, null, null, - TaskContract.Instances.INSTANCE_DUE_SORTING, "1"); - - try - { - if (nextTaskDueCursor.moveToNext()) - { - TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(nextTaskDueCursor), nextTaskDueCursor, null); - DateTime nextDue = task.valueOf(TaskAdapter.INSTANCE_DUE); - if (!nextDue.isFloating()) - { - nextDue = nextDue.shiftTimeZone(localTimeZone); - } - - if (nextAlarm == null || nextAlarm.getInstance() > nextDue.getInstance()) - { - nextAlarm = nextDue; - } - } - } - finally - { - nextTaskDueCursor.close(); - } - - if (nextAlarm != null) - { - TaskProviderBroadcastReceiver.planNotificationUpdate(context, nextAlarm); - } - else - { - saveLastAlarmTime(context, now); - } - } - - - @SuppressLint("NewApi") - private void saveLastAlarmTime(Context context, DateTime time) - { - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - Editor editor = prefs.edit(); - editor.putLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, time.getTimestamp()); - if (Build.VERSION.SDK_INT >= 9) - { - editor.apply(); - } - else - { - editor.commit(); - } - } - - - private DateTime getLastAlarmTimestamp(Context context) - { - SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - return new DateTime(TimeZone.getDefault(), prefs.getLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, System.currentTimeMillis())); - } - - }); - - /** - * A lock object to serialize the execution of all incoming {@link ContentOperation}. - */ - private final static Object mLock = new Object(); - - /** - * The base path of the Uri to trigger content operations. - */ - private final static String BASE_PATH = "content_operation"; - - /** - * The {@link OperationHandler} that handles this {@link ContentOperation}. - */ - private final OperationHandler mHandler; - - private static final String PREFS_NAME = "org.dmfs.provider.tasks"; - private static final String PREFS_KEY_LAST_ALARM_TIMESTAMP = "org.dmfs.provider.tasks.prefs.LAST_ALARM_TIMESTAMP"; - - - private ContentOperation(OperationHandler handler) - { - mHandler = handler; - } - - - /** - * Execute this {@link ContentOperation} with the given values. - * - * @param context - * A {@link Context}. - * @param values - * Optional {@link ContentValues}, may be null. - */ - public void fire(Context context, ContentValues values) - { - context.getContentResolver().update(uri(TaskContract.taskAuthority(context)), values == null ? new ContentValues() : values, null, null); - } - - - /** - * Run the operation on the given handler. - * - * @param context - * A {@link Context}. - * @param handler - * A {@link Handler} to run the operation on. - * @param uri - * The {@link Uri} that triggered this operation. - * @param db - * The database. - * @param values - * The {@link ContentValues} that were supplied. - */ - void run(final Context context, Handler handler, final Uri uri, final SQLiteDatabase db, final ContentValues values) - { - handler.post(new Runnable() - { - @Override - public void run() - { - synchronized (mLock) - { - mHandler.handleOperation(context, uri, db, values); - } - } - }); - } - - - /** - * Returns the {@link Uri} that triggers this {@link ContentOperation}. - * - * @param authority - * The authority of this provide. - * @return A {@link Uri}. - */ - private Uri uri(String authority) - { - return new Uri.Builder().scheme("content").authority(authority).path(BASE_PATH).appendPath(this.toString()).build(); - } - - - /** - * Register the operations with the given {@link UriMatcher}. - * - * @param uriMatcher - * The {@link UriMatcher}. - * @param authority - * The authority of this TaskProvider. - * @param firstID - * Teh first Id to use for our Uris. - */ - public static void register(UriMatcher uriMatcher, String authority, int firstID) - { - for (ContentOperation op : values()) - { - Uri uri = op.uri(authority); - uriMatcher.addURI(authority, uri.getPath().substring(1) /* remove leading slash */, firstID + op.ordinal()); - } - } - - - /** - * Return a {@link ContentOperation} that belongs to the given id. - * - * @param id - * The id or the {@link ContentOperation}. - * @param firstId - * The first ID to use for Uris. - * @return The respective {@link ContentOperation} or null if none was found. - */ - public static ContentOperation get(int id, int firstId) - { - if (id < firstId) - { - return null; - } - - if (id - firstId >= values().length) - { - return null; - } - - return values()[id - firstId]; - } - - public interface OperationHandler - { - public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values); - } + /** + * When the local timezone has been changed we need to update the due and start sorting values. This handler will take care of running the appropriate + * update. In addition it fires an operation to update all notifications. + */ + UPDATE_TIMEZONE(new OperationHandler() + { + @Override + public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values) + { + long start = System.currentTimeMillis(); + + // request an update of all instance values + ContentValues vals = new ContentValues(1); + TaskInstancesProcessor.addUpdateRequest(vals); + + // execute update that triggers a recalculation of all due and start sorting values + int count = context.getContentResolver().update( + TaskContract.Tasks.getContentUri(uri.getAuthority()).buildUpon().appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true").build(), + vals, null, null); + + Log.i("TaskProvider", "time to update " + count + " tasks: " + (System.currentTimeMillis() - start) + " ms"); + + // now update alarms as well + UPDATE_NOTIFICATION_ALARM.fire(context, null); + } + }), + + /** + * Takes care of everything we need to send task start and task due broadcasts. + */ + POST_NOTIFICATIONS(new OperationHandler() + { + + @Override + public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values) + { + TimeZone localTimeZone = TimeZone.getDefault(); + + // the date-time of when the last notification was shown + DateTime lastAlarm = getLastAlarmTimestamp(context); + // the current time, we show all notifications between and now + DateTime now = DateTime.nowAndHere(); + + String lastAlarmString = Long.toString(lastAlarm.getInstance()); + String nowString = Long.toString(now.getInstance()); + + // load all tasks that have started or became due since the last time we've shown a notification. + Cursor taskCursor = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, null, "((" + TaskContract.Instances.INSTANCE_DUE_SORTING + ">? and " + + TaskContract.Instances.INSTANCE_DUE_SORTING + "<=?) or (" + TaskContract.Instances.INSTANCE_START_SORTING + ">? and " + + TaskContract.Instances.INSTANCE_START_SORTING + "<=?)) and " + Instances.IS_CLOSED + " = 0 and " + Tasks._DELETED + "=0", new String[] { + lastAlarmString, nowString, lastAlarmString, nowString }, null, null, null); + + try + { + while (taskCursor.moveToNext()) + { + TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(taskCursor), taskCursor, null); + + DateTime instanceDue = task.valueOf(TaskAdapter.INSTANCE_DUE); + if (instanceDue != null && !instanceDue.isFloating()) + { + // make sure we compare instances in local time + instanceDue = instanceDue.shiftTimeZone(localTimeZone); + } + + DateTime instanceStart = task.valueOf(TaskAdapter.INSTANCE_START); + if (instanceStart != null && !instanceStart.isFloating()) + { + // make sure we compare instances in local time + instanceStart = instanceStart.shiftTimeZone(localTimeZone); + } + + if (instanceDue != null && lastAlarm.getInstance() < instanceDue.getInstance() && instanceDue.getInstance() <= now.getInstance()) + { + // this task became due since the last alarm, send a due broadcast + sendBroadcast(context, TaskContract.ACTION_BROADCAST_TASK_DUE, task.uri(uri.getAuthority()), instanceDue, + task.valueOf(TaskAdapter.TITLE)); + } + else if (instanceStart != null && lastAlarm.getInstance() < instanceStart.getInstance() && instanceStart.getInstance() <= now.getInstance()) + { + // this task has started since the last alarm, send a start broadcast + sendBroadcast(context, TaskContract.ACTION_BROADCAST_TASK_STARTING, task.uri(uri.getAuthority()), instanceStart, + task.valueOf(TaskAdapter.TITLE)); + } + } + } + finally + { + taskCursor.close(); + } + + // all notifications up to now have been triggered + saveLastAlarmTime(context, now); + + // set the alarm for the next notification + UPDATE_NOTIFICATION_ALARM.fire(context, null); + } + + + @SuppressLint("NewApi") + private void saveLastAlarmTime(Context context, DateTime time) + { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + Editor editor = prefs.edit(); + editor.putLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, time.getTimestamp()); + if (Build.VERSION.SDK_INT >= 9) + { + editor.apply(); + } + else + { + editor.commit(); + } + } + + + private DateTime getLastAlarmTimestamp(Context context) + { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return new DateTime(TimeZone.getDefault(), prefs.getLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, System.currentTimeMillis())); + } + + + /** + * Sends a notification broadcast for a task that has started or became due. + * + * @param context + * A {@link Context}. + * @param action + * The broadcast action. + * @param uri + * The task uri. + * @param datetime + * The datetime to add. + * @param title + * The task title. + */ + private void sendBroadcast(Context context, String action, Uri uri, DateTime datetime, String title) + { + Intent intent = new Intent(action); + intent.setData(uri); + intent.putExtra(TaskContract.EXTRA_TASK_TIMESTAMP, datetime.getTimestamp()); + intent.putExtra(TaskContract.EXTRA_TASK_ALLDAY, datetime.isAllDay()); + if (!datetime.isFloating()) + { + intent.putExtra(TaskContract.EXTRA_TASK_TIMEZONE, datetime.getTimeZone().getID()); + } + intent.putExtra(TaskContract.EXTRA_TASK_TITLE, title); + context.sendBroadcast(intent); + } + }), + + /** + * Determines the date-time of when the next task becomes due or starts (whatever happens first) and sets an alarm to trigger a notification. + */ + UPDATE_NOTIFICATION_ALARM(new OperationHandler() + { + + @Override + public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values) + { + TimeZone localTimeZone = TimeZone.getDefault(); + DateTime lastAlarm = getLastAlarmTimestamp(context); + DateTime now = DateTime.nowAndHere(); + + if (now.before(lastAlarm)) + { + // time went backwards, set last alarm time to now + lastAlarm = now; + saveLastAlarmTime(context, now); + } + + String lastAlarmString = Long.toString(lastAlarm.getInstance()); + + DateTime nextAlarm = null; + + // find the next task that starts + Cursor nextTaskStartCursor = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, null, TaskContract.Instances.INSTANCE_START_SORTING + ">? and " + + Instances.IS_CLOSED + " = 0 and " + Tasks._DELETED + "=0", new String[] { lastAlarmString }, null, null, + TaskContract.Instances.INSTANCE_START_SORTING, "1"); + + try + { + if (nextTaskStartCursor.moveToNext()) + { + TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(nextTaskStartCursor), nextTaskStartCursor, null); + nextAlarm = task.valueOf(TaskAdapter.INSTANCE_START); + if (!nextAlarm.isFloating()) + { + nextAlarm = nextAlarm.shiftTimeZone(localTimeZone); + } + } + } + finally + { + nextTaskStartCursor.close(); + } + + // find the next task that's due + Cursor nextTaskDueCursor = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, null, TaskContract.Instances.INSTANCE_DUE_SORTING + ">? and " + + Instances.IS_CLOSED + " = 0 and " + Tasks._DELETED + "=0", new String[] { lastAlarmString }, null, null, + TaskContract.Instances.INSTANCE_DUE_SORTING, "1"); + + try + { + if (nextTaskDueCursor.moveToNext()) + { + TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(nextTaskDueCursor), nextTaskDueCursor, null); + DateTime nextDue = task.valueOf(TaskAdapter.INSTANCE_DUE); + if (!nextDue.isFloating()) + { + nextDue = nextDue.shiftTimeZone(localTimeZone); + } + + if (nextAlarm == null || nextAlarm.getInstance() > nextDue.getInstance()) + { + nextAlarm = nextDue; + } + } + } + finally + { + nextTaskDueCursor.close(); + } + + if (nextAlarm != null) + { + TaskProviderBroadcastReceiver.planNotificationUpdate(context, nextAlarm); + } + else + { + saveLastAlarmTime(context, now); + } + } + + + @SuppressLint("NewApi") + private void saveLastAlarmTime(Context context, DateTime time) + { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + Editor editor = prefs.edit(); + editor.putLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, time.getTimestamp()); + if (Build.VERSION.SDK_INT >= 9) + { + editor.apply(); + } + else + { + editor.commit(); + } + } + + + private DateTime getLastAlarmTimestamp(Context context) + { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return new DateTime(TimeZone.getDefault(), prefs.getLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, System.currentTimeMillis())); + } + + }); + + /** + * A lock object to serialize the execution of all incoming {@link ContentOperation}. + */ + private final static Object mLock = new Object(); + + /** + * The base path of the Uri to trigger content operations. + */ + private final static String BASE_PATH = "content_operation"; + + /** + * The {@link OperationHandler} that handles this {@link ContentOperation}. + */ + private final OperationHandler mHandler; + + private static final String PREFS_NAME = "org.dmfs.provider.tasks"; + private static final String PREFS_KEY_LAST_ALARM_TIMESTAMP = "org.dmfs.provider.tasks.prefs.LAST_ALARM_TIMESTAMP"; + + + private ContentOperation(OperationHandler handler) + { + mHandler = handler; + } + + + /** + * Execute this {@link ContentOperation} with the given values. + * + * @param context + * A {@link Context}. + * @param values + * Optional {@link ContentValues}, may be null. + */ + public void fire(Context context, ContentValues values) + { + context.getContentResolver().update(uri(TaskContract.taskAuthority(context)), values == null ? new ContentValues() : values, null, null); + } + + + /** + * Run the operation on the given handler. + * + * @param context + * A {@link Context}. + * @param handler + * A {@link Handler} to run the operation on. + * @param uri + * The {@link Uri} that triggered this operation. + * @param db + * The database. + * @param values + * The {@link ContentValues} that were supplied. + */ + void run(final Context context, Handler handler, final Uri uri, final SQLiteDatabase db, final ContentValues values) + { + handler.post(new Runnable() + { + @Override + public void run() + { + synchronized (mLock) + { + mHandler.handleOperation(context, uri, db, values); + } + } + }); + } + + + /** + * Returns the {@link Uri} that triggers this {@link ContentOperation}. + * + * @param authority + * The authority of this provide. + * + * @return A {@link Uri}. + */ + private Uri uri(String authority) + { + return new Uri.Builder().scheme("content").authority(authority).path(BASE_PATH).appendPath(this.toString()).build(); + } + + + /** + * Register the operations with the given {@link UriMatcher}. + * + * @param uriMatcher + * The {@link UriMatcher}. + * @param authority + * The authority of this TaskProvider. + * @param firstID + * Teh first Id to use for our Uris. + */ + public static void register(UriMatcher uriMatcher, String authority, int firstID) + { + for (ContentOperation op : values()) + { + Uri uri = op.uri(authority); + uriMatcher.addURI(authority, uri.getPath().substring(1) /* remove leading slash */, firstID + op.ordinal()); + } + } + + + /** + * Return a {@link ContentOperation} that belongs to the given id. + * + * @param id + * The id or the {@link ContentOperation}. + * @param firstId + * The first ID to use for Uris. + * + * @return The respective {@link ContentOperation} or null if none was found. + */ + public static ContentOperation get(int id, int firstId) + { + if (id < firstId) + { + return null; + } + + if (id - firstId >= values().length) + { + return null; + } + + return values()[id - firstId]; + } + + + public interface OperationHandler + { + public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/FTSDatabaseHelper.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/FTSDatabaseHelper.java index 60a098f0..957a925f 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/FTSDatabaseHelper.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/FTSDatabaseHelper.java @@ -17,8 +17,10 @@ package org.dmfs.provider.tasks; -import java.util.HashSet; -import java.util.Set; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; import org.dmfs.ngrams.NGramGenerator; import org.dmfs.provider.tasks.TaskContract.Properties; @@ -27,10 +29,8 @@ import org.dmfs.provider.tasks.TaskContract.Tasks; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; import org.dmfs.provider.tasks.model.TaskAdapter; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.text.TextUtils; +import java.util.HashSet; +import java.util.Set; /** @@ -42,518 +42,526 @@ import android.text.TextUtils; public class FTSDatabaseHelper { - private final static float SEARCH_RESULTS_MIN_SCORE = 0.4f; - - /** - * A Generator for 3-grams. - */ - private final static NGramGenerator TRIGRAM_GENERATOR = new NGramGenerator(3, 1).setAddSpaceInFront(true); - - /** - * A Generator for 4-grams. - */ - private final static NGramGenerator TETRAGRAM_GENERATOR = new NGramGenerator(4, 3 /* shorter words are fully covered by trigrams */).setAddSpaceInFront(true); - - /** - * Search content columns. Defines all the columns for the full text search - * - * @author Tobias Reinsch - */ - public interface FTSContentColumns - { - /** - * The row id of the belonging task. - */ - public static final String TASK_ID = "fts_task_id"; - - /** - * The the property id of the searchable entry or null if the entry is not related to a property. - */ - public static final String PROPERTY_ID = "fts_property_id"; - - /** - * The the type of the searchable entry - */ - public static final String TYPE = "fts_type"; - - /** - * An n-gram for a task. - */ - public static final String NGRAM_ID = "fts_ngram_id"; - - } - - /** - * The columns of the N-gram table for the FTS search - * - * @author Tobias Reinsch - */ - public interface NGramColumns - { - /** - * The row id of the N-gram. - */ - public static final String NGRAM_ID = "ngram_id"; - - /** - * The content of the N-gram - */ - public static final String TEXT = "ngram_text"; - - } - - public static final String FTS_CONTENT_TABLE = "FTS_Content"; - public static final String FTS_NGRAM_TABLE = "FTS_Ngram"; - public static final String FTS_TASK_VIEW = "FTS_Task_View"; - public static final String FTS_TASK_PROPERTY_VIEW = "FTS_Task_Property_View"; - - /** - * SQL command to create the table for full text search and contains relationships between ngrams and tasks - */ - private final static String SQL_CREATE_SEARCH_CONTENT_TABLE = "CREATE TABLE " + FTS_CONTENT_TABLE + "( " + FTSContentColumns.TASK_ID + " Integer, " - + FTSContentColumns.NGRAM_ID + " Integer, " + FTSContentColumns.PROPERTY_ID + " Integer, " + FTSContentColumns.TYPE + " Integer, " + "FOREIGN KEY(" - + FTSContentColumns.TASK_ID + ") REFERENCES " + Tables.TASKS + "(" + TaskColumns._ID + ")," + "FOREIGN KEY(" + FTSContentColumns.TASK_ID - + ") REFERENCES " + Tables.TASKS + "(" + TaskColumns._ID + ") UNIQUE (" + FTSContentColumns.TASK_ID + ", " + FTSContentColumns.TYPE + ", " - + FTSContentColumns.PROPERTY_ID + ") ON CONFLICT IGNORE )"; - - /** - * SQL command to create the table that stores the NGRAMS - */ - private final static String SQL_CREATE_NGRAM_TABLE = "CREATE TABLE " + FTS_NGRAM_TABLE + "( " + NGramColumns.NGRAM_ID - + " Integer PRIMARY KEY AUTOINCREMENT, " + NGramColumns.TEXT + " Text)"; - - // FIXME: at present the minimum score is hard coded can we leave that decision to the caller? - private final static String SQL_RAW_QUERY_SEARCH_TASK = "SELECT %s " + ", min(1.0*count(*)/?, 1.0) as " + TaskContract.Tasks.SCORE + " from " - + FTS_NGRAM_TABLE + " join " + FTS_CONTENT_TABLE + " on (" + FTS_NGRAM_TABLE + "." + NGramColumns.NGRAM_ID + "=" + FTS_CONTENT_TABLE + "." - + FTSContentColumns.NGRAM_ID + ") join " + Tables.INSTANCE_VIEW + " on (" + Tables.INSTANCE_VIEW + "." + TaskContract.Instances.TASK_ID + " = " + FTS_CONTENT_TABLE + "." - + FTSContentColumns.TASK_ID + ") where %s group by " + TaskContract.Instances.TASK_ID + " having " + TaskContract.Tasks.SCORE + " >= " + SEARCH_RESULTS_MIN_SCORE - + " order by %s;"; - - private final static String SQL_RAW_QUERY_SEARCH_TASK_DEFAULT_PROJECTION = Tables.INSTANCE_VIEW + ".* ," + FTS_NGRAM_TABLE + "." + NGramColumns.TEXT; - - private final static String SQL_CREATE_SEARCH_TASK_DELETE_TRIGGER = "CREATE TRIGGER search_task_delete_trigger AFTER DELETE ON " + Tables.TASKS + " BEGIN " - + " DELETE FROM " + FTS_CONTENT_TABLE + " WHERE " + FTSContentColumns.TASK_ID + " = old." + Tasks._ID + "; END"; - - private final static String SQL_CREATE_SEARCH_TASK_DELETE_PROPERTY_TRIGGER = "CREATE TRIGGER search_task_delete_property_trigger AFTER DELETE ON " - + Tables.PROPERTIES + " BEGIN " + " DELETE FROM " + FTS_CONTENT_TABLE + " WHERE " + FTSContentColumns.TASK_ID + " = old." + Properties.TASK_ID - + " AND " + FTSContentColumns.PROPERTY_ID + " = old." + Properties.PROPERTY_ID + "; END"; - - /** - * The different types of searchable entries for tasks linked to the TYPE column. - * - * @author Tobias Reinsch - * @author Marten Gajda - */ - public interface SearchableTypes - { - /** - * This is an entry for the title of a task. - */ - public static final int TITLE = 1; - - /** - * This is an entry for the description of a task. - */ - public static final int DESCRIPTION = 2; - - /** - * This is an entry for the location of a task. - */ - public static final int LOCATION = 3; - - /** - * This is an entry for a property of a task. - */ - public static final int PROPERTY = 4; - - } - - - public static void onCreate(SQLiteDatabase db) - { - initializeFTS(db); - } - - - public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) - { - if (oldVersion < 8) - { - initializeFTS(db); - initializeFTSContent(db); - } - if (oldVersion < 16) - { - db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, true, FTSContentColumns.TYPE, FTSContentColumns.TASK_ID, - FTSContentColumns.PROPERTY_ID)); - } - } - - - /** - * Creates the tables and triggers used in FTS. - * - * @param db - * The {@link SQLiteDatabase}. - */ - private static void initializeFTS(SQLiteDatabase db) - { - db.execSQL(SQL_CREATE_SEARCH_CONTENT_TABLE); - db.execSQL(SQL_CREATE_NGRAM_TABLE); - db.execSQL(SQL_CREATE_SEARCH_TASK_DELETE_TRIGGER); - db.execSQL(SQL_CREATE_SEARCH_TASK_DELETE_PROPERTY_TRIGGER); - - // create indices - db.execSQL(TaskDatabaseHelper.createIndexString(FTS_NGRAM_TABLE, true, NGramColumns.TEXT)); - db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, false, FTSContentColumns.NGRAM_ID)); - db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, false, FTSContentColumns.TASK_ID)); - db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, true, FTSContentColumns.PROPERTY_ID, FTSContentColumns.TASK_ID, - FTSContentColumns.NGRAM_ID)); - - db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, true, FTSContentColumns.TYPE, FTSContentColumns.TASK_ID, - FTSContentColumns.PROPERTY_ID)); - - } - - - /** - * Creates the FTS entries for the existing tasks. - * - * @param db - * The writable {@link SQLiteDatabase}. - */ - private static void initializeFTSContent(SQLiteDatabase db) - { - String[] task_projection = new String[] { Tasks._ID, Tasks.TITLE, Tasks.DESCRIPTION, Tasks.LOCATION }; - Cursor c = db.query(Tables.TASKS_PROPERTY_VIEW, task_projection, null, null, null, null, null); - while (c.moveToNext()) - { - insertTaskFTSEntries(db, c.getLong(0), c.getString(1), c.getString(2), c.getString(3)); - } - c.close(); - } - - - /** - * Inserts the searchable texts of the task in the database. - * - * @param db - * The writable {@link SQLiteDatabase}. - * - * @param taskId - * The row id of the task. - * @param title - * The title of the task. - * @param description - * The description of the task. - */ - private static void insertTaskFTSEntries(SQLiteDatabase db, long taskId, String title, String description, String location) - { - // title - if (title != null && title.length() > 0) - { - updateEntry(db, taskId, -1, SearchableTypes.TITLE, title); - } - - // location - if (location != null && location.length() > 0) - { - updateEntry(db, taskId, -1, SearchableTypes.LOCATION, location); - } - - // description - if (description != null && description.length() > 0) - { - updateEntry(db, taskId, -1, SearchableTypes.DESCRIPTION, description); - } - - } - - - /** - * Updates the existing searchables entries for the task. - * - * @param db - * The writable {@link SQLiteDatabase}. - * @param task - * The {@link TaskAdapter} containing the new values. - */ - public static void updateTaskFTSEntries(SQLiteDatabase db, TaskAdapter task) - { - // title - if (task.isUpdated(TaskAdapter.TITLE)) - { - updateEntry(db, task.id(), -1, SearchableTypes.TITLE, task.valueOf(TaskAdapter.TITLE)); - } - - // location - if (task.isUpdated(TaskAdapter.LOCATION)) - { - updateEntry(db, task.id(), -1, SearchableTypes.LOCATION, task.valueOf(TaskAdapter.LOCATION)); - } - - // description - if (task.isUpdated(TaskAdapter.DESCRIPTION)) - { - updateEntry(db, task.id(), -1, SearchableTypes.DESCRIPTION, task.valueOf(TaskAdapter.DESCRIPTION)); - } - - } - - - /** - * Updates or creates the searchable entries for a property. Passing null as searchable text will remove the entry. - * - * @param db - * The writable {@link SQLiteDatabase}. - * @param taskId - * the row id of the task this property belongs to. - * @param propertyId - * the id of the property - * @param searchableText - * the searchable text value of the property - */ - public static void updatePropertyFTSEntry(SQLiteDatabase db, long taskId, long propertyId, String searchableText) - { - updateEntry(db, taskId, propertyId, SearchableTypes.PROPERTY, searchableText); - } - - - /** - * Inserts NGrams into the NGram database. - * - * @param db - * A writable {@link SQLiteDatabase}. - * @param ngrams - * The set of NGrams. - * @return The ids of the ngrams in the given set. - */ - private static Set insertNGrams(SQLiteDatabase db, Set ngrams) - { - Set nGramIds = new HashSet(ngrams.size()); - ContentValues values = new ContentValues(1); - for (String ngram : ngrams) - { - values.put(NGramColumns.TEXT, ngram); - long nGramId = db.insertWithOnConflict(FTS_NGRAM_TABLE, null, values, SQLiteDatabase.CONFLICT_IGNORE); - if (nGramId == -1) - { - // the docs say insertWithOnConflict returns the existing row id when CONFLICT_IGNORE is specified an the values conflict with an existing - // column, however, that doesn't seem to work reliably, so we when for an error condition and get the row id ourselves - Cursor c = db - .query(FTS_NGRAM_TABLE, new String[] { NGramColumns.NGRAM_ID }, NGramColumns.TEXT + "=?", new String[] { ngram }, null, null, null); - try - { - if (c.moveToFirst()) - { - nGramId = c.getLong(0); - } - } - finally - { - c.close(); - } - - } - nGramIds.add(nGramId); - } - return nGramIds; - - } - - - private static void updateEntry(SQLiteDatabase db, long taskId, long propertyId, int type, String searchableText) - { - // delete existing NGram relations - deleteNGramRelations(db, taskId, propertyId, type); - - if (searchableText != null && searchableText.length() > 0) - { - // generate nGrams - Set propertyNgrams = TRIGRAM_GENERATOR.getNgrams(searchableText); - - TETRAGRAM_GENERATOR.getNgrams(propertyNgrams, searchableText); - - // insert ngrams - Set propertyNgramIds = insertNGrams(db, propertyNgrams); - - // insert ngram relations - insertNGramRelations(db, propertyNgramIds, taskId, propertyId, type); - } - } - - - /** - * Inserts NGrams relations for a task entry. - * - * @param db - * A writable {@link SQLiteDatabase}. - * @param ngramIds - * The set of NGram ids. - * @param taskId - * The row id of the task. - * @param propertyId - * The row id of the property. - * @param The - * entry type of the relation (title, description, property). - */ - private static void insertNGramRelations(SQLiteDatabase db, Set ngramIds, long taskId, Long propertyId, int contentType) - { - ContentValues values = new ContentValues(4); - for (Long ngramId : ngramIds) - { - values.put(FTSContentColumns.TASK_ID, taskId); - values.put(FTSContentColumns.NGRAM_ID, ngramId); - values.put(FTSContentColumns.TYPE, contentType); - if (contentType == SearchableTypes.PROPERTY) - { - values.put(FTSContentColumns.PROPERTY_ID, propertyId); - } - else - { - values.putNull(FTSContentColumns.PROPERTY_ID); - } - db.insertWithOnConflict(FTS_CONTENT_TABLE, null, values, SQLiteDatabase.CONFLICT_IGNORE); - } - - } - - - /** - * Deletes the NGram relations of a task - * - * @param db - * The writable {@link SQLiteDatabase}. - * @param taskId - * The task row id. - * @param propertyId - * The property row id, ignored if contentType is not {@link SearchableTypes#PROPERTY}. - * @param contentType - * The {@link SearchableTypes} type. - * @return The number of deleted relations. - */ - private static int deleteNGramRelations(SQLiteDatabase db, long taskId, long propertyId, int contentType) - { - StringBuilder whereClause = new StringBuilder(FTSContentColumns.TASK_ID).append(" = ").append(taskId); - whereClause.append(" AND ").append(FTSContentColumns.TYPE).append(" = ").append(contentType); - if (contentType == SearchableTypes.PROPERTY) - { - whereClause.append(" AND ").append(FTSContentColumns.PROPERTY_ID).append(" = ").append(propertyId); - } - return db.delete(FTS_CONTENT_TABLE, whereClause.toString(), null); - } - - - /** - * Queries the task database to get a cursor with the search results. - * - * @param db - * The {@link SQLiteDatabase}. - * @param searchString - * The search query string. - * @param projection - * The database projection for the query. - * @param selection - * The selection for the query. - * @param selectionArgs - * The arguments for the query. - * @param sortOrder - * The sorting order of the query. - * @return A cursor of the task database with the search result. - */ - public static Cursor getTaskSearchCursor(SQLiteDatabase db, String searchString, String[] projection, String selection, String[] selectionArgs, - String sortOrder) - { - - StringBuilder selectionBuilder = new StringBuilder(1024); - - if (!TextUtils.isEmpty(selection)) - { - selectionBuilder.append(" ("); - selectionBuilder.append(selection); - selectionBuilder.append(") AND ("); - } - else - { - selectionBuilder.append(" ("); - } - - Set ngrams = TRIGRAM_GENERATOR.getNgrams(searchString); - TETRAGRAM_GENERATOR.getNgrams(ngrams, searchString); - - String[] queryArgs; - - if (searchString != null && searchString.length() > 1) - { - - selectionBuilder.append(NGramColumns.TEXT); - selectionBuilder.append(" in ("); - - for (int i = 0, count = ngrams.size(); i < count; ++i) - { - if (i > 0) - { - selectionBuilder.append(","); - } - selectionBuilder.append("?"); - - } - - // selection arguments - if (selectionArgs != null && selectionArgs.length > 0) - { - queryArgs = new String[selectionArgs.length + ngrams.size() + 1]; - queryArgs[0] = String.valueOf(ngrams.size()); - System.arraycopy(selectionArgs, 0, queryArgs, 1, selectionArgs.length); - String[] ngramArray = ngrams.toArray(new String[ngrams.size()]); - System.arraycopy(ngramArray, 0, queryArgs, selectionArgs.length + 1, ngramArray.length); - } - else - { - String[] temp = ngrams.toArray(new String[ngrams.size()]); - - queryArgs = new String[temp.length + 1]; - queryArgs[0] = String.valueOf(ngrams.size()); - System.arraycopy(temp, 0, queryArgs, 1, temp.length); - } - selectionBuilder.append(" ) "); - } - else - { - selectionBuilder.append(NGramColumns.TEXT); - selectionBuilder.append(" like ?"); - - // selection arguments - if (selectionArgs != null && selectionArgs.length > 0) - { - queryArgs = new String[selectionArgs.length + 2]; - queryArgs[0] = String.valueOf(ngrams.size()); - System.arraycopy(selectionArgs, 0, queryArgs, 1, selectionArgs.length); - queryArgs[queryArgs.length - 1] = " " + searchString + "%"; - } - else - { - queryArgs = new String[2]; - queryArgs[0] = String.valueOf(ngrams.size()); - queryArgs[1] = " " + searchString + "%"; - } - - } - - selectionBuilder.append(") AND "); - selectionBuilder.append(Tasks._DELETED); - selectionBuilder.append(" = 0"); - - if (sortOrder == null) - { - sortOrder = Tasks.SCORE + " desc"; - } - else - { - sortOrder = Tasks.SCORE + " desc, " + sortOrder; - } - Cursor c = db.rawQueryWithFactory(null, - String.format(SQL_RAW_QUERY_SEARCH_TASK, SQL_RAW_QUERY_SEARCH_TASK_DEFAULT_PROJECTION, selectionBuilder.toString(), sortOrder), queryArgs, null); - return c; - } + private final static float SEARCH_RESULTS_MIN_SCORE = 0.4f; + + /** + * A Generator for 3-grams. + */ + private final static NGramGenerator TRIGRAM_GENERATOR = new NGramGenerator(3, 1).setAddSpaceInFront(true); + + /** + * A Generator for 4-grams. + */ + private final static NGramGenerator TETRAGRAM_GENERATOR = new NGramGenerator(4, 3 /* shorter words are fully covered by trigrams */).setAddSpaceInFront( + true); + + + /** + * Search content columns. Defines all the columns for the full text search + * + * @author Tobias Reinsch + */ + public interface FTSContentColumns + { + /** + * The row id of the belonging task. + */ + public static final String TASK_ID = "fts_task_id"; + + /** + * The the property id of the searchable entry or null if the entry is not related to a property. + */ + public static final String PROPERTY_ID = "fts_property_id"; + + /** + * The the type of the searchable entry + */ + public static final String TYPE = "fts_type"; + + /** + * An n-gram for a task. + */ + public static final String NGRAM_ID = "fts_ngram_id"; + + } + + + /** + * The columns of the N-gram table for the FTS search + * + * @author Tobias Reinsch + */ + public interface NGramColumns + { + /** + * The row id of the N-gram. + */ + public static final String NGRAM_ID = "ngram_id"; + + /** + * The content of the N-gram + */ + public static final String TEXT = "ngram_text"; + + } + + + public static final String FTS_CONTENT_TABLE = "FTS_Content"; + public static final String FTS_NGRAM_TABLE = "FTS_Ngram"; + public static final String FTS_TASK_VIEW = "FTS_Task_View"; + public static final String FTS_TASK_PROPERTY_VIEW = "FTS_Task_Property_View"; + + /** + * SQL command to create the table for full text search and contains relationships between ngrams and tasks + */ + private final static String SQL_CREATE_SEARCH_CONTENT_TABLE = "CREATE TABLE " + FTS_CONTENT_TABLE + "( " + FTSContentColumns.TASK_ID + " Integer, " + + FTSContentColumns.NGRAM_ID + " Integer, " + FTSContentColumns.PROPERTY_ID + " Integer, " + FTSContentColumns.TYPE + " Integer, " + "FOREIGN KEY(" + + FTSContentColumns.TASK_ID + ") REFERENCES " + Tables.TASKS + "(" + TaskColumns._ID + ")," + "FOREIGN KEY(" + FTSContentColumns.TASK_ID + + ") REFERENCES " + Tables.TASKS + "(" + TaskColumns._ID + ") UNIQUE (" + FTSContentColumns.TASK_ID + ", " + FTSContentColumns.TYPE + ", " + + FTSContentColumns.PROPERTY_ID + ") ON CONFLICT IGNORE )"; + + /** + * SQL command to create the table that stores the NGRAMS + */ + private final static String SQL_CREATE_NGRAM_TABLE = "CREATE TABLE " + FTS_NGRAM_TABLE + "( " + NGramColumns.NGRAM_ID + + " Integer PRIMARY KEY AUTOINCREMENT, " + NGramColumns.TEXT + " Text)"; + + // FIXME: at present the minimum score is hard coded can we leave that decision to the caller? + private final static String SQL_RAW_QUERY_SEARCH_TASK = "SELECT %s " + ", min(1.0*count(*)/?, 1.0) as " + TaskContract.Tasks.SCORE + " from " + + FTS_NGRAM_TABLE + " join " + FTS_CONTENT_TABLE + " on (" + FTS_NGRAM_TABLE + "." + NGramColumns.NGRAM_ID + "=" + FTS_CONTENT_TABLE + "." + + FTSContentColumns.NGRAM_ID + ") join " + Tables.INSTANCE_VIEW + " on (" + Tables.INSTANCE_VIEW + "." + TaskContract.Instances.TASK_ID + " = " + FTS_CONTENT_TABLE + "." + + FTSContentColumns.TASK_ID + ") where %s group by " + TaskContract.Instances.TASK_ID + " having " + TaskContract.Tasks.SCORE + " >= " + SEARCH_RESULTS_MIN_SCORE + + " order by %s;"; + + private final static String SQL_RAW_QUERY_SEARCH_TASK_DEFAULT_PROJECTION = Tables.INSTANCE_VIEW + ".* ," + FTS_NGRAM_TABLE + "." + NGramColumns.TEXT; + + private final static String SQL_CREATE_SEARCH_TASK_DELETE_TRIGGER = "CREATE TRIGGER search_task_delete_trigger AFTER DELETE ON " + Tables.TASKS + " BEGIN " + + " DELETE FROM " + FTS_CONTENT_TABLE + " WHERE " + FTSContentColumns.TASK_ID + " = old." + Tasks._ID + "; END"; + + private final static String SQL_CREATE_SEARCH_TASK_DELETE_PROPERTY_TRIGGER = "CREATE TRIGGER search_task_delete_property_trigger AFTER DELETE ON " + + Tables.PROPERTIES + " BEGIN " + " DELETE FROM " + FTS_CONTENT_TABLE + " WHERE " + FTSContentColumns.TASK_ID + " = old." + Properties.TASK_ID + + " AND " + FTSContentColumns.PROPERTY_ID + " = old." + Properties.PROPERTY_ID + "; END"; + + + /** + * The different types of searchable entries for tasks linked to the TYPE column. + * + * @author Tobias Reinsch + * @author Marten Gajda + */ + public interface SearchableTypes + { + /** + * This is an entry for the title of a task. + */ + public static final int TITLE = 1; + + /** + * This is an entry for the description of a task. + */ + public static final int DESCRIPTION = 2; + + /** + * This is an entry for the location of a task. + */ + public static final int LOCATION = 3; + + /** + * This is an entry for a property of a task. + */ + public static final int PROPERTY = 4; + + } + + + public static void onCreate(SQLiteDatabase db) + { + initializeFTS(db); + } + + + public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) + { + if (oldVersion < 8) + { + initializeFTS(db); + initializeFTSContent(db); + } + if (oldVersion < 16) + { + db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, true, FTSContentColumns.TYPE, FTSContentColumns.TASK_ID, + FTSContentColumns.PROPERTY_ID)); + } + } + + + /** + * Creates the tables and triggers used in FTS. + * + * @param db + * The {@link SQLiteDatabase}. + */ + private static void initializeFTS(SQLiteDatabase db) + { + db.execSQL(SQL_CREATE_SEARCH_CONTENT_TABLE); + db.execSQL(SQL_CREATE_NGRAM_TABLE); + db.execSQL(SQL_CREATE_SEARCH_TASK_DELETE_TRIGGER); + db.execSQL(SQL_CREATE_SEARCH_TASK_DELETE_PROPERTY_TRIGGER); + + // create indices + db.execSQL(TaskDatabaseHelper.createIndexString(FTS_NGRAM_TABLE, true, NGramColumns.TEXT)); + db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, false, FTSContentColumns.NGRAM_ID)); + db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, false, FTSContentColumns.TASK_ID)); + db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, true, FTSContentColumns.PROPERTY_ID, FTSContentColumns.TASK_ID, + FTSContentColumns.NGRAM_ID)); + + db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, true, FTSContentColumns.TYPE, FTSContentColumns.TASK_ID, + FTSContentColumns.PROPERTY_ID)); + + } + + + /** + * Creates the FTS entries for the existing tasks. + * + * @param db + * The writable {@link SQLiteDatabase}. + */ + private static void initializeFTSContent(SQLiteDatabase db) + { + String[] task_projection = new String[] { Tasks._ID, Tasks.TITLE, Tasks.DESCRIPTION, Tasks.LOCATION }; + Cursor c = db.query(Tables.TASKS_PROPERTY_VIEW, task_projection, null, null, null, null, null); + while (c.moveToNext()) + { + insertTaskFTSEntries(db, c.getLong(0), c.getString(1), c.getString(2), c.getString(3)); + } + c.close(); + } + + + /** + * Inserts the searchable texts of the task in the database. + * + * @param db + * The writable {@link SQLiteDatabase}. + * @param taskId + * The row id of the task. + * @param title + * The title of the task. + * @param description + * The description of the task. + */ + private static void insertTaskFTSEntries(SQLiteDatabase db, long taskId, String title, String description, String location) + { + // title + if (title != null && title.length() > 0) + { + updateEntry(db, taskId, -1, SearchableTypes.TITLE, title); + } + + // location + if (location != null && location.length() > 0) + { + updateEntry(db, taskId, -1, SearchableTypes.LOCATION, location); + } + + // description + if (description != null && description.length() > 0) + { + updateEntry(db, taskId, -1, SearchableTypes.DESCRIPTION, description); + } + + } + + + /** + * Updates the existing searchables entries for the task. + * + * @param db + * The writable {@link SQLiteDatabase}. + * @param task + * The {@link TaskAdapter} containing the new values. + */ + public static void updateTaskFTSEntries(SQLiteDatabase db, TaskAdapter task) + { + // title + if (task.isUpdated(TaskAdapter.TITLE)) + { + updateEntry(db, task.id(), -1, SearchableTypes.TITLE, task.valueOf(TaskAdapter.TITLE)); + } + + // location + if (task.isUpdated(TaskAdapter.LOCATION)) + { + updateEntry(db, task.id(), -1, SearchableTypes.LOCATION, task.valueOf(TaskAdapter.LOCATION)); + } + + // description + if (task.isUpdated(TaskAdapter.DESCRIPTION)) + { + updateEntry(db, task.id(), -1, SearchableTypes.DESCRIPTION, task.valueOf(TaskAdapter.DESCRIPTION)); + } + + } + + + /** + * Updates or creates the searchable entries for a property. Passing null as searchable text will remove the entry. + * + * @param db + * The writable {@link SQLiteDatabase}. + * @param taskId + * the row id of the task this property belongs to. + * @param propertyId + * the id of the property + * @param searchableText + * the searchable text value of the property + */ + public static void updatePropertyFTSEntry(SQLiteDatabase db, long taskId, long propertyId, String searchableText) + { + updateEntry(db, taskId, propertyId, SearchableTypes.PROPERTY, searchableText); + } + + + /** + * Inserts NGrams into the NGram database. + * + * @param db + * A writable {@link SQLiteDatabase}. + * @param ngrams + * The set of NGrams. + * + * @return The ids of the ngrams in the given set. + */ + private static Set insertNGrams(SQLiteDatabase db, Set ngrams) + { + Set nGramIds = new HashSet(ngrams.size()); + ContentValues values = new ContentValues(1); + for (String ngram : ngrams) + { + values.put(NGramColumns.TEXT, ngram); + long nGramId = db.insertWithOnConflict(FTS_NGRAM_TABLE, null, values, SQLiteDatabase.CONFLICT_IGNORE); + if (nGramId == -1) + { + // the docs say insertWithOnConflict returns the existing row id when CONFLICT_IGNORE is specified an the values conflict with an existing + // column, however, that doesn't seem to work reliably, so we when for an error condition and get the row id ourselves + Cursor c = db + .query(FTS_NGRAM_TABLE, new String[] { NGramColumns.NGRAM_ID }, NGramColumns.TEXT + "=?", new String[] { ngram }, null, null, null); + try + { + if (c.moveToFirst()) + { + nGramId = c.getLong(0); + } + } + finally + { + c.close(); + } + + } + nGramIds.add(nGramId); + } + return nGramIds; + + } + + + private static void updateEntry(SQLiteDatabase db, long taskId, long propertyId, int type, String searchableText) + { + // delete existing NGram relations + deleteNGramRelations(db, taskId, propertyId, type); + + if (searchableText != null && searchableText.length() > 0) + { + // generate nGrams + Set propertyNgrams = TRIGRAM_GENERATOR.getNgrams(searchableText); + + TETRAGRAM_GENERATOR.getNgrams(propertyNgrams, searchableText); + + // insert ngrams + Set propertyNgramIds = insertNGrams(db, propertyNgrams); + + // insert ngram relations + insertNGramRelations(db, propertyNgramIds, taskId, propertyId, type); + } + } + + + /** + * Inserts NGrams relations for a task entry. + * + * @param db + * A writable {@link SQLiteDatabase}. + * @param ngramIds + * The set of NGram ids. + * @param taskId + * The row id of the task. + * @param propertyId + * The row id of the property. + * @param The + * entry type of the relation (title, description, property). + */ + private static void insertNGramRelations(SQLiteDatabase db, Set ngramIds, long taskId, Long propertyId, int contentType) + { + ContentValues values = new ContentValues(4); + for (Long ngramId : ngramIds) + { + values.put(FTSContentColumns.TASK_ID, taskId); + values.put(FTSContentColumns.NGRAM_ID, ngramId); + values.put(FTSContentColumns.TYPE, contentType); + if (contentType == SearchableTypes.PROPERTY) + { + values.put(FTSContentColumns.PROPERTY_ID, propertyId); + } + else + { + values.putNull(FTSContentColumns.PROPERTY_ID); + } + db.insertWithOnConflict(FTS_CONTENT_TABLE, null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + + } + + + /** + * Deletes the NGram relations of a task + * + * @param db + * The writable {@link SQLiteDatabase}. + * @param taskId + * The task row id. + * @param propertyId + * The property row id, ignored if contentType is not {@link SearchableTypes#PROPERTY}. + * @param contentType + * The {@link SearchableTypes} type. + * + * @return The number of deleted relations. + */ + private static int deleteNGramRelations(SQLiteDatabase db, long taskId, long propertyId, int contentType) + { + StringBuilder whereClause = new StringBuilder(FTSContentColumns.TASK_ID).append(" = ").append(taskId); + whereClause.append(" AND ").append(FTSContentColumns.TYPE).append(" = ").append(contentType); + if (contentType == SearchableTypes.PROPERTY) + { + whereClause.append(" AND ").append(FTSContentColumns.PROPERTY_ID).append(" = ").append(propertyId); + } + return db.delete(FTS_CONTENT_TABLE, whereClause.toString(), null); + } + + + /** + * Queries the task database to get a cursor with the search results. + * + * @param db + * The {@link SQLiteDatabase}. + * @param searchString + * The search query string. + * @param projection + * The database projection for the query. + * @param selection + * The selection for the query. + * @param selectionArgs + * The arguments for the query. + * @param sortOrder + * The sorting order of the query. + * + * @return A cursor of the task database with the search result. + */ + public static Cursor getTaskSearchCursor(SQLiteDatabase db, String searchString, String[] projection, String selection, String[] selectionArgs, + String sortOrder) + { + + StringBuilder selectionBuilder = new StringBuilder(1024); + + if (!TextUtils.isEmpty(selection)) + { + selectionBuilder.append(" ("); + selectionBuilder.append(selection); + selectionBuilder.append(") AND ("); + } + else + { + selectionBuilder.append(" ("); + } + + Set ngrams = TRIGRAM_GENERATOR.getNgrams(searchString); + TETRAGRAM_GENERATOR.getNgrams(ngrams, searchString); + + String[] queryArgs; + + if (searchString != null && searchString.length() > 1) + { + + selectionBuilder.append(NGramColumns.TEXT); + selectionBuilder.append(" in ("); + + for (int i = 0, count = ngrams.size(); i < count; ++i) + { + if (i > 0) + { + selectionBuilder.append(","); + } + selectionBuilder.append("?"); + + } + + // selection arguments + if (selectionArgs != null && selectionArgs.length > 0) + { + queryArgs = new String[selectionArgs.length + ngrams.size() + 1]; + queryArgs[0] = String.valueOf(ngrams.size()); + System.arraycopy(selectionArgs, 0, queryArgs, 1, selectionArgs.length); + String[] ngramArray = ngrams.toArray(new String[ngrams.size()]); + System.arraycopy(ngramArray, 0, queryArgs, selectionArgs.length + 1, ngramArray.length); + } + else + { + String[] temp = ngrams.toArray(new String[ngrams.size()]); + + queryArgs = new String[temp.length + 1]; + queryArgs[0] = String.valueOf(ngrams.size()); + System.arraycopy(temp, 0, queryArgs, 1, temp.length); + } + selectionBuilder.append(" ) "); + } + else + { + selectionBuilder.append(NGramColumns.TEXT); + selectionBuilder.append(" like ?"); + + // selection arguments + if (selectionArgs != null && selectionArgs.length > 0) + { + queryArgs = new String[selectionArgs.length + 2]; + queryArgs[0] = String.valueOf(ngrams.size()); + System.arraycopy(selectionArgs, 0, queryArgs, 1, selectionArgs.length); + queryArgs[queryArgs.length - 1] = " " + searchString + "%"; + } + else + { + queryArgs = new String[2]; + queryArgs[0] = String.valueOf(ngrams.size()); + queryArgs[1] = " " + searchString + "%"; + } + + } + + selectionBuilder.append(") AND "); + selectionBuilder.append(Tasks._DELETED); + selectionBuilder.append(" = 0"); + + if (sortOrder == null) + { + sortOrder = Tasks.SCORE + " desc"; + } + else + { + sortOrder = Tasks.SCORE + " desc, " + sortOrder; + } + Cursor c = db.rawQueryWithFactory(null, + String.format(SQL_RAW_QUERY_SEARCH_TASK, SQL_RAW_QUERY_SEARCH_TASK_DEFAULT_PROJECTION, selectionBuilder.toString(), sortOrder), queryArgs, + null); + return c; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ProviderOperation.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ProviderOperation.java index 0a050288..dd021b03 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ProviderOperation.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ProviderOperation.java @@ -17,120 +17,121 @@ package org.dmfs.provider.tasks; -import java.util.List; +import android.database.sqlite.SQLiteDatabase; import org.dmfs.provider.tasks.model.EntityAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; -import android.database.sqlite.SQLiteDatabase; -import android.util.Log; +import java.util.List; /** * Provides handlers for INSERT, UPDATE and DELETE operations for {@link EntityAdapter}s. - * + * * @author Marten Gajda */ public enum ProviderOperation { - /** - * Handles insert operations. - */ - INSERT { - @Override - > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) - { - processor.beforeInsert(db, entityAdapter, isSyncAdapter); - } - - - @Override - > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) - { - processor.afterInsert(db, entityAdapter, isSyncAdapter); - } - }, - - /** - * Handles update operations. - */ - UPDATE { - @Override - > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) - { - processor.beforeUpdate(db, entityAdapter, isSyncAdapter); - } - - - @Override - > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) - { - processor.afterUpdate(db, entityAdapter, isSyncAdapter); - } - }, - - /** - * Handles delete operations. - */ - DELETE { - @Override - > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) - { - processor.beforeDelete(db, entityAdapter, isSyncAdapter); - } - - - @Override - > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) - { - processor.afterDelete(db, entityAdapter, isSyncAdapter); - } - }; - - private final static String TAG = "OpenTasks.Operation"; - - - abstract > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter); - - - abstract > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter); - - - /** - * Executes this operation by running the respective methods of the given {@link EntityProcessor}s. - * - * @param db - * An {@link SQLiteDatabase}. - * @param processors - * The {@link EntityProcessor} chain. - * @param entityAdapter - * The {@link EntityAdapter} to operate on. - * @param isSyncAdapter - * true if this operation is triggered by a sync adapter, false otherwise. - * @param log - * An {@link ProviderOperationsLog} to log this operation. - * @param authority - * The authority of this provider. - */ - public > void execute(SQLiteDatabase db, List> processors, T entityAdapter, boolean isSyncAdapter, - ProviderOperationsLog log, String authority) - { - long start = System.currentTimeMillis(); - - for (EntityProcessor processor : processors) - { - executeBeforeProcessor(db, processor, entityAdapter, isSyncAdapter); - } - - for (EntityProcessor processor : processors) - { - executeAfterProcessor(db, processor, entityAdapter, isSyncAdapter); - } - - if (this != UPDATE || entityAdapter.hasUpdates()) // don't log empty operations - { - log.log(this, entityAdapter.uri(authority)); - } - } + /** + * Handles insert operations. + */ + INSERT + { + @Override + > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) + { + processor.beforeInsert(db, entityAdapter, isSyncAdapter); + } + + + @Override + > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) + { + processor.afterInsert(db, entityAdapter, isSyncAdapter); + } + }, + + /** + * Handles update operations. + */ + UPDATE + { + @Override + > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) + { + processor.beforeUpdate(db, entityAdapter, isSyncAdapter); + } + + + @Override + > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) + { + processor.afterUpdate(db, entityAdapter, isSyncAdapter); + } + }, + + /** + * Handles delete operations. + */ + DELETE + { + @Override + > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) + { + processor.beforeDelete(db, entityAdapter, isSyncAdapter); + } + + + @Override + > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) + { + processor.afterDelete(db, entityAdapter, isSyncAdapter); + } + }; + + private final static String TAG = "OpenTasks.Operation"; + + + abstract > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter); + + abstract > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter); + + + /** + * Executes this operation by running the respective methods of the given {@link EntityProcessor}s. + * + * @param db + * An {@link SQLiteDatabase}. + * @param processors + * The {@link EntityProcessor} chain. + * @param entityAdapter + * The {@link EntityAdapter} to operate on. + * @param isSyncAdapter + * true if this operation is triggered by a sync adapter, false otherwise. + * @param log + * An {@link ProviderOperationsLog} to log this operation. + * @param authority + * The authority of this provider. + */ + public > void execute(SQLiteDatabase db, List> processors, T entityAdapter, boolean isSyncAdapter, + ProviderOperationsLog log, String authority) + { + long start = System.currentTimeMillis(); + + for (EntityProcessor processor : processors) + { + executeBeforeProcessor(db, processor, entityAdapter, isSyncAdapter); + } + + for (EntityProcessor processor : processors) + { + executeAfterProcessor(db, processor, entityAdapter, isSyncAdapter); + } + + if (this != UPDATE || entityAdapter.hasUpdates()) // don't log empty operations + { + log.log(this, entityAdapter.uri(authority)); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ProviderOperationsLog.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ProviderOperationsLog.java index 30f09d2b..9ee0d6da 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ProviderOperationsLog.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/ProviderOperationsLog.java @@ -17,93 +17,95 @@ package org.dmfs.provider.tasks; -import java.util.ArrayList; - import android.net.Uri; import android.os.Bundle; +import java.util.ArrayList; + /** * A log to track all content provider operations. - * + * * @author Marten Gajda */ public class ProviderOperationsLog { - private ArrayList mUris = new ArrayList(16); + private ArrayList mUris = new ArrayList(16); - private ArrayList mOperations = new ArrayList(16); + private ArrayList mOperations = new ArrayList(16); - /** - * Add an operation on the given {@link Uri} to the log. - * - * @param operation - * The {@link ProviderOperation} that was executed. - * @param uri - * The {@link Uri} that the operation was executed on. - */ - public void log(ProviderOperation operation, Uri uri) - { - synchronized (this) - { - mUris.add(uri); - mOperations.add(operation.ordinal()); - } - } + /** + * Add an operation on the given {@link Uri} to the log. + * + * @param operation + * The {@link ProviderOperation} that was executed. + * @param uri + * The {@link Uri} that the operation was executed on. + */ + public void log(ProviderOperation operation, Uri uri) + { + synchronized (this) + { + mUris.add(uri); + mOperations.add(operation.ordinal()); + } + } - /** - * Adds the operations log to the given {@link Bundle}, creating one if the given bundle is null. - * - * @param bundle - * A {@link Bundle} or null. - * @param clearLog - * true to clear the log afterwards, false to keep it. - * @return The {@link Bundle} that was passed or created. - */ - public Bundle toBundle(Bundle bundle, boolean clearLog) - { - if (bundle == null) - { - bundle = new Bundle(2); - } + /** + * Adds the operations log to the given {@link Bundle}, creating one if the given bundle is null. + * + * @param bundle + * A {@link Bundle} or null. + * @param clearLog + * true to clear the log afterwards, false to keep it. + * + * @return The {@link Bundle} that was passed or created. + */ + public Bundle toBundle(Bundle bundle, boolean clearLog) + { + if (bundle == null) + { + bundle = new Bundle(2); + } - synchronized (this) - { - bundle.putParcelableArrayList(TaskContract.EXTRA_OPERATIONS_URIS, mUris); - bundle.putIntegerArrayList(TaskContract.EXTRA_OPERATIONS, mOperations); - if (clearLog) - { - // we can't just clear the ArrayLists, because the Bundle keeps a reference to them - mUris = new ArrayList(16); - mOperations = new ArrayList(16); - } - } - return bundle; - } + synchronized (this) + { + bundle.putParcelableArrayList(TaskContract.EXTRA_OPERATIONS_URIS, mUris); + bundle.putIntegerArrayList(TaskContract.EXTRA_OPERATIONS, mOperations); + if (clearLog) + { + // we can't just clear the ArrayLists, because the Bundle keeps a reference to them + mUris = new ArrayList(16); + mOperations = new ArrayList(16); + } + } + return bundle; + } - /** - * Returns a new {@link Bundle} containing the log. - * - * @param clearLog - * true to clear the log afterwards, false to keep it. - * @return The {@link Bundle} that was created. - */ - public Bundle toBundle(boolean clearLog) - { - return toBundle(null, clearLog); - } + /** + * Returns a new {@link Bundle} containing the log. + * + * @param clearLog + * true to clear the log afterwards, false to keep it. + * + * @return The {@link Bundle} that was created. + */ + public Bundle toBundle(boolean clearLog) + { + return toBundle(null, clearLog); + } - /** - * Returns whether any operations have been logged or not. - * - * @return true if this log is empty, false if it contains any logs of operations. - */ - public boolean isEmpty() - { - return mUris.size() == 0; - } + /** + * Returns whether any operations have been logged or not. + * + * @return true if this log is empty, false if it contains any logs of operations. + */ + public boolean isEmpty() + { + return mUris.size() == 0; + } } \ No newline at end of file diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/SQLiteContentProvider.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/SQLiteContentProvider.java index eade9a4d..cc421204 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/SQLiteContentProvider.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/SQLiteContentProvider.java @@ -16,10 +16,6 @@ package org.dmfs.provider.tasks; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; - import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; @@ -31,6 +27,10 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + /** * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage. @@ -46,270 +46,267 @@ import android.net.Uri; abstract class SQLiteContentProvider extends ContentProvider { - @SuppressWarnings("unused") - private static final String TAG = "SQLiteContentProvider"; - - private SQLiteOpenHelper mOpenHelper; - private Set mChangedUris; - - private final ThreadLocal mApplyingBatch = new ThreadLocal(); - private static final int SLEEP_AFTER_YIELD_DELAY = 4000; - - /** - * Maximum number of operations allowed in a batch between yield points. - */ - private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500; - - - @Override - public boolean onCreate() - { - Context context = getContext(); - mOpenHelper = getDatabaseHelper(context); - mChangedUris = new HashSet(); - return true; - } - - - /** - * Returns a {@link SQLiteOpenHelper} that can open the database. - */ - protected abstract SQLiteOpenHelper getDatabaseHelper(Context context); - - - /** - * The equivalent of the {@link #insert} method, but invoked within a transaction. - */ - public abstract Uri insertInTransaction(SQLiteDatabase db, Uri uri, ContentValues values, boolean callerIsSyncAdapter); - - - /** - * The equivalent of the {@link #update} method, but invoked within a transaction. - */ - public abstract int updateInTransaction(SQLiteDatabase db, Uri uri, ContentValues values, String selection, String[] selectionArgs, - boolean callerIsSyncAdapter); - - - /** - * The equivalent of the {@link #delete} method, but invoked within a transaction. - */ - public abstract int deleteInTransaction(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs, boolean callerIsSyncAdapter); - - - /** - * Call this to add a URI to the list of URIs to be notified when the transaction is committed. - */ - protected void postNotifyUri(Uri uri) - { - synchronized (mChangedUris) - { - mChangedUris.add(uri); - } - } - - - public boolean isCallerSyncAdapter(Uri uri) - { - return false; - } - - - public SQLiteOpenHelper getDatabaseHelper() - { - return mOpenHelper; - } - - - private boolean applyingBatch() - { - return mApplyingBatch.get() != null && mApplyingBatch.get(); - } - - - @Override - public Uri insert(Uri uri, ContentValues values) - { - Uri result = null; - boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); - boolean applyingBatch = applyingBatch(); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - if (!applyingBatch) - { - db.beginTransaction(); - try - { - result = insertInTransaction(db, uri, values, callerIsSyncAdapter); - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } - - onEndTransaction(callerIsSyncAdapter); - } - else - { - result = insertInTransaction(db, uri, values, callerIsSyncAdapter); - } - return result; - } - - - @Override - public int bulkInsert(Uri uri, ContentValues[] values) - { - int numValues = values.length; - boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try - { - for (int i = 0; i < numValues; i++) - { - insertInTransaction(db, uri, values[i], callerIsSyncAdapter); - db.yieldIfContendedSafely(); - } - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } - - onEndTransaction(callerIsSyncAdapter); - return numValues; - } - - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) - { - int count = 0; - boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); - boolean applyingBatch = applyingBatch(); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - if (!applyingBatch) - { - db.beginTransaction(); - try - { - count = updateInTransaction(db, uri, values, selection, selectionArgs, callerIsSyncAdapter); - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } - - onEndTransaction(callerIsSyncAdapter); - } - else - { - count = updateInTransaction(db, uri, values, selection, selectionArgs, callerIsSyncAdapter); - } - - return count; - } - - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) - { - int count = 0; - boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); - boolean applyingBatch = applyingBatch(); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - if (!applyingBatch) - { - db.beginTransaction(); - try - { - count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter); - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } - - onEndTransaction(callerIsSyncAdapter); - } - else - { - count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter); - } - return count; - } - - - @Override - public ContentProviderResult[] applyBatch(ArrayList operations) throws OperationApplicationException - { - int ypCount = 0; - int opCount = 0; - boolean callerIsSyncAdapter = false; - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try - { - mApplyingBatch.set(true); - final int numOperations = operations.size(); - final ContentProviderResult[] results = new ContentProviderResult[numOperations]; - for (int i = 0; i < numOperations; i++) - { - if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) - { - throw new OperationApplicationException("Too many content provider operations between yield points. " - + "The maximum number of operations per yield point is " + MAX_OPERATIONS_PER_YIELD_POINT, ypCount); - } - final ContentProviderOperation operation = operations.get(i); - if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) - { - callerIsSyncAdapter = true; - } - if (i > 0 && operation.isYieldAllowed()) - { - opCount = 0; - if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) - { - ypCount++; - } - } - results[i] = operation.apply(this, results, i); - } - db.setTransactionSuccessful(); - return results; - } - finally - { - mApplyingBatch.set(false); - db.endTransaction(); - onEndTransaction(callerIsSyncAdapter); - } - } - - - protected void onEndTransaction(boolean callerIsSyncAdapter) - { - Set changed; - synchronized (mChangedUris) - { - changed = new HashSet(mChangedUris); - mChangedUris.clear(); - } - ContentResolver resolver = getContext().getContentResolver(); - for (Uri uri : changed) - { - boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri); - resolver.notifyChange(uri, null, syncToNetwork); - } - } - - - protected boolean syncToNetwork(Uri uri) - { - return false; - } + @SuppressWarnings("unused") + private static final String TAG = "SQLiteContentProvider"; + + private SQLiteOpenHelper mOpenHelper; + private Set mChangedUris; + + private final ThreadLocal mApplyingBatch = new ThreadLocal(); + private static final int SLEEP_AFTER_YIELD_DELAY = 4000; + + /** + * Maximum number of operations allowed in a batch between yield points. + */ + private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500; + + + @Override + public boolean onCreate() + { + Context context = getContext(); + mOpenHelper = getDatabaseHelper(context); + mChangedUris = new HashSet(); + return true; + } + + + /** + * Returns a {@link SQLiteOpenHelper} that can open the database. + */ + protected abstract SQLiteOpenHelper getDatabaseHelper(Context context); + + /** + * The equivalent of the {@link #insert} method, but invoked within a transaction. + */ + public abstract Uri insertInTransaction(SQLiteDatabase db, Uri uri, ContentValues values, boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #update} method, but invoked within a transaction. + */ + public abstract int updateInTransaction(SQLiteDatabase db, Uri uri, ContentValues values, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #delete} method, but invoked within a transaction. + */ + public abstract int deleteInTransaction(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs, boolean callerIsSyncAdapter); + + + /** + * Call this to add a URI to the list of URIs to be notified when the transaction is committed. + */ + protected void postNotifyUri(Uri uri) + { + synchronized (mChangedUris) + { + mChangedUris.add(uri); + } + } + + + public boolean isCallerSyncAdapter(Uri uri) + { + return false; + } + + + public SQLiteOpenHelper getDatabaseHelper() + { + return mOpenHelper; + } + + + private boolean applyingBatch() + { + return mApplyingBatch.get() != null && mApplyingBatch.get(); + } + + + @Override + public Uri insert(Uri uri, ContentValues values) + { + Uri result = null; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + if (!applyingBatch) + { + db.beginTransaction(); + try + { + result = insertInTransaction(db, uri, values, callerIsSyncAdapter); + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } + else + { + result = insertInTransaction(db, uri, values, callerIsSyncAdapter); + } + return result; + } + + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) + { + int numValues = values.length; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try + { + for (int i = 0; i < numValues; i++) + { + insertInTransaction(db, uri, values[i], callerIsSyncAdapter); + db.yieldIfContendedSafely(); + } + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + return numValues; + } + + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) + { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + if (!applyingBatch) + { + db.beginTransaction(); + try + { + count = updateInTransaction(db, uri, values, selection, selectionArgs, callerIsSyncAdapter); + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } + else + { + count = updateInTransaction(db, uri, values, selection, selectionArgs, callerIsSyncAdapter); + } + + return count; + } + + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) + { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + if (!applyingBatch) + { + db.beginTransaction(); + try + { + count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter); + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } + else + { + count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter); + } + return count; + } + + + @Override + public ContentProviderResult[] applyBatch(ArrayList operations) throws OperationApplicationException + { + int ypCount = 0; + int opCount = 0; + boolean callerIsSyncAdapter = false; + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try + { + mApplyingBatch.set(true); + final int numOperations = operations.size(); + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; + for (int i = 0; i < numOperations; i++) + { + if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) + { + throw new OperationApplicationException("Too many content provider operations between yield points. " + + "The maximum number of operations per yield point is " + MAX_OPERATIONS_PER_YIELD_POINT, ypCount); + } + final ContentProviderOperation operation = operations.get(i); + if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) + { + callerIsSyncAdapter = true; + } + if (i > 0 && operation.isYieldAllowed()) + { + opCount = 0; + if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) + { + ypCount++; + } + } + results[i] = operation.apply(this, results, i); + } + db.setTransactionSuccessful(); + return results; + } + finally + { + mApplyingBatch.set(false); + db.endTransaction(); + onEndTransaction(callerIsSyncAdapter); + } + } + + + protected void onEndTransaction(boolean callerIsSyncAdapter) + { + Set changed; + synchronized (mChangedUris) + { + changed = new HashSet(mChangedUris); + mChangedUris.clear(); + } + ContentResolver resolver = getContext().getContentResolver(); + for (Uri uri : changed) + { + boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri); + resolver.notifyChange(uri, null, syncToNetwork); + } + } + + + protected boolean syncToNetwork(Uri uri) + { + return false; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskContract.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskContract.java index 50205381..e976b853 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskContract.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskContract.java @@ -17,11 +17,6 @@ package org.dmfs.provider.tasks; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -33,6 +28,11 @@ import android.net.Uri; import android.provider.BaseColumns; import android.provider.SyncStateContract; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + /** * Task contract. This class defines the interface to the task provider. @@ -48,1663 +48,1699 @@ import android.provider.SyncStateContract; *

* TODO: Also, we could use some refactoring... *

- * + * * @author Marten Gajda * @author Tobias Reinsch */ public final class TaskContract { - /** - * The task authority cache. - */ - private static Map sAuthorities = Collections.synchronizedMap(new HashMap(4)); - - private static Map sUriFactories = new HashMap(4); - - /** - * URI parameter to signal that the caller is a sync adapter. - */ - public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; - - /** - * URI parameter to signal the request of the extended properties of a task. - */ - public static final String LOAD_PROPERTIES = "load_properties"; - - /** - * URI parameter to submit the account name of the account we operate on. - */ - public static final String ACCOUNT_NAME = "account_name"; - - /** - * URI parameter to submit the account type of the account we operate on. - */ - public static final String ACCOUNT_TYPE = "account_type"; - - /** - * Account name for local, unsynced task lists. - */ - public static final String LOCAL_ACCOUNT_NAME = "Local"; - - /** - * Account type for local, unsynced task lists. - */ - public static final String LOCAL_ACCOUNT_TYPE = "org.dmfs.account.LOCAL"; - - /** - * Broadcast action that's sent when the task database has been initialized, either because the app was launched for the first time or because the app was - * launched after the user cleared the app data. - *

- * The intent data represents the authority of the provider, the MIME type will be {@link #MIMETYPE_AUTHORITY}. - */ - public static final String ACTION_DATABASE_INITIALIZED = "org.dmfs.tasks.DATABASE_INITIALIZED"; - - /** - * A MIME type of an authority. Authorities itself don't seem to have a MIME type in Android, so we just use our own. - */ - public static final String MIMETYPE_AUTHORITY = ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.org.dmfs.authority.mimetype"; - - /** - * The action of the broadcast that's send when a task becomes due. The intent data will be a {@link Uri} of the task that became due. - */ - public static final String ACTION_BROADCAST_TASK_DUE = "org.dmfs.android.tasks.TASK_DUE"; - - /** - * The action of the broadcast that's send when a task starts. The intent data will be a {@link Uri} of the task that has started. - */ - public static final String ACTION_BROADCAST_TASK_STARTING = "org.dmfs.android.tasks.TASK_START"; - - /** - * A Long extra that contains a timestamp of the event that's triggered. So this is either the timestamp of the start or due date of the task. - */ - public final static String EXTRA_TASK_TIMESTAMP = "org.dmfs.provider.tasks.extra.TIMESTAMP"; - - /** - * A Boolean extra to indicate that the event that was triggered is an all-day date. - */ - public final static String EXTRA_TASK_ALLDAY = "org.dmfs.provider.tasks.extra.ALLDAY"; - - /** - * A String extra containing the timezone id of the task. - */ - public final static String EXTRA_TASK_TIMEZONE = "org.dmfs.provider.tasks.extra.TIMEZONE"; - - /** - * A String extra containing the title of the task. - */ - public final static String EXTRA_TASK_TITLE = "org.dmfs.provider.tasks.extra.TITLE"; - - /** - * The name of the {@link Intent#ACTION_PROVIDER_CHANGED} extra that contains the {@link ArrayList} of {@link Uri}s that have been modified. This always - * goes along with an {@link #EXTRA_OPERATIONS} which contains a code for the operation executed on a Uri at the same index. - */ - public final static String EXTRA_OPERATIONS_URIS = "org.dmfs.tasks.OPERATIONS_URIS"; - - /** - * The name of the {@link Intent#ACTION_PROVIDER_CHANGED} extra that contains the {@link ArrayList} of provider operation codes. The following codes are - * used: - *

    - *
  • 0 - for inserts
  • - *
  • 1 - for updates
  • - *
  • 2 - for deletes
  • - *
- */ - public final static String EXTRA_OPERATIONS = "org.dmfs.tasks.OPERATIONS"; - - - /** - * Private constructor to prevent instantiation. - */ - private TaskContract() - { - } - - /** - * A table provided for sync adapters to use for storing private sync state data. - *

- * Only sync adapters are allowed to access this table and they may access their own rows only. - *

- * Note that only one row per account will be stored. Updating or inserting a sync state for a specific account will override any previous sync state for - * this account. - */ - public static class SyncState implements SyncStateContract.Columns, BaseColumns - { - final static String CONTENT_URI_PATH = "syncstate"; - - - /** - * Get the sync state content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * @return A {@link Uri}. - */ - public final static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - } - - - /** - * Get the base content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * @return A {@link Uri}. - */ - public final static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(); - } - - /** - * A set of columns for synchronization purposes. These columns exist in {@link Tasks} and in {@link TaskLists} but have different meanings. Only sync - * adapters are allowed to change these values. - * - * @author Marten Gajda - */ - public interface CommonSyncColumns - { - - /** - * A unique Sync ID as set by the sync adapter. - *

- * Value: String - *

- */ - public static final String _SYNC_ID = "_sync_id"; - - /** - * Sync version as set by the sync adapter. - *

- * Value: String - *

- */ - public static final String SYNC_VERSION = "sync_version"; - - /** - * Indicates that a task or a task list has been changed. - *

- * Value: Integer - *

- */ - public static final String _DIRTY = "_dirty"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - public static final String SYNC1 = "sync1"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - public static final String SYNC2 = "sync2"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - public static final String SYNC3 = "sync3"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - public static final String SYNC4 = "sync4"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - public static final String SYNC5 = "sync5"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - public static final String SYNC6 = "sync6"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - public static final String SYNC7 = "sync7"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - public static final String SYNC8 = "sync8"; - - } - - /** - * Additional sync columns for task lists. - * - * @author Marten Gajda - */ - public interface TaskListSyncColumns - { - - /** - * The name of the account this list belongs to. This field is write-once. - *

- * Value: String - *

- */ - public static final String ACCOUNT_NAME = "account_name"; - - /** - * The type of the account this list belongs to. This field is write-once. - *

- * Value: String - *

- */ - public static final String ACCOUNT_TYPE = "account_type"; - } - - /** - * Additional sync columns for tasks. - * - * @author Marten Gajda - */ - public interface TaskSyncColumns - { - /** - * The UID of a task. This is field can be changed by a sync adapter only. - *

- * Value: String - *

- */ - public static final String _UID = "_uid"; - - /** - * Deleted flag of a task. This is set to 1 by the content provider when a task app deletes a task. The sync adapter has to remove the task - * again to finish the removal. This value is read-only. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String _DELETED = "_deleted"; - } - - /** - * Data columns of task lists. - * - * @author Marten Gajda - */ - public interface TaskListColumns - { - - /** - * List ID. - *

- * Value: Long - *

- *

- * read-only - *

- */ - public static final String _ID = "_id"; - - /** - * The name of the task list. - *

- * Value: String - *

- */ - public static final String LIST_NAME = "list_name"; - - /** - * The color of this list as integer (0xaarrggbb). Only the sync adapter can change this. - *

- * Value: Integer - *

- */ - public static final String LIST_COLOR = "list_color"; - - /** - * The access level a user has on this list. This value is not used yet, sync adapters should set it to 0. - *

- * Value: Integer - *

- */ - public static final String ACCESS_LEVEL = "list_access_level"; - - /** - * Indicates that a task list is set to be visible. - *

- * Value: Integer (0 or 1) - *

- */ - public static final String VISIBLE = "visible"; - - /** - * Indicates that a task list is set to be synced. - *

- * Value: Integer (0 or 1) - *

- */ - public static final String SYNC_ENABLED = "sync_enabled"; - - /** - * The email address of the list owner. - *

- * Value: String - *

- */ - public static final String OWNER = "list_owner"; - - } - - /** - * The task list table holds one entry for each task list. - * - * @author Marten Gajda - */ - public static final class TaskLists implements TaskListColumns, TaskListSyncColumns, CommonSyncColumns - { - static final String CONTENT_URI_PATH = "tasklists"; - - /** - * The default sort order. - */ - public static final String DEFAULT_SORT_ORDER = ACCOUNT_NAME + ", " + LIST_NAME; - - /** - * An array of columns only a sync adapter is allowed to change. - */ - public static final String[] SYNC_ADAPTER_COLUMNS = new String[] { ACCESS_LEVEL, _DIRTY, OWNER, SYNC1, SYNC2, SYNC3, SYNC4, SYNC5, SYNC6, SYNC7, SYNC8, - _SYNC_ID, SYNC_VERSION, }; - - - /** - * Get the task list content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * @return A {@link Uri}. - */ - public final static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - } - - /** - * Task data columns. Defines all the values a task can have at most once. - * - * @author Marten Gajda - */ - public interface TaskColumns - { - - /** - * The row id of a task. This value is read-only - *

- * Value: Integer - *

- */ - public static final String _ID = "_id"; - - /** - * The id of the list this task belongs to. This value is write-once and must not be null. - *

- * Value: Integer - *

- */ - public static final String LIST_ID = "list_id"; - - /** - * The title of the task. - *

- * Value: String - *

- */ - public static final String TITLE = "title"; - - /** - * The location of the task. - *

- * Value: String - *

- */ - public static final String LOCATION = "location"; - - /** - * A geographic location related to the task. The should be a string in the format "longitude,latitude". - *

- * Value: String - *

- */ - public static final String GEO = "geo"; - - /** - * The description of a task. - *

- * Value: String - *

- */ - public static final String DESCRIPTION = "description"; - - /** - * An URL for this task. Must be a valid URL if not null- - *

- * Value: String - *

- */ - public static final String URL = "url"; - - /** - * The email address of the organizer if any, {@code null} otherwise. - *

- * Value: String - *

- */ - public static final String ORGANIZER = "organizer"; - - /** - * The priority of a task. This is an Integer between zero and 9. Zero means there is no priority set. 1 is the highest priority and 9 the lowest. - *

- * Value: Integer - *

- */ - public static final String PRIORITY = "priority"; - - /** - * The default value of {@link #PRIORITY}. - */ - public static final int PRIORITY_DEFAULT = 0; - - /** - * The classification of a task. This value must be either null or one of {@link #CLASSIFICATION_PUBLIC}, {@link #CLASSIFICATION_PRIVATE}, - * {@link #CLASSIFICATION_CONFIDENTIAL}. - *

- * Value: Integer - *

- */ - public static final String CLASSIFICATION = "class"; - - /** - * Classification value for public tasks. - */ - public static final int CLASSIFICATION_PUBLIC = 0; - - /** - * Classification value for private tasks. - */ - public static final int CLASSIFICATION_PRIVATE = 1; - - /** - * Classification value for confidential tasks. - */ - public static final int CLASSIFICATION_CONFIDENTIAL = 2; - - /** - * Default value of {@link #CLASSIFICATION}. - */ - public static final Integer CLASSIFICATION_DEFAULT = null; - - /** - * Date of completion of this task in milliseconds since the epoch or {@code null} if this task has not been completed yet. - *

- * Value: Long - *

- */ - public static final String COMPLETED = "completed"; - - /** - * Indicates that the date of completion is an all-day date. - *

- * Value: Integer - *

- */ - public static final String COMPLETED_IS_ALLDAY = "completed_is_allday"; - - /** - * A number between 0 and 100 that indicates the progress of the task or null. - *

- * Value: Integer (0-100) - *

- */ - public static final String PERCENT_COMPLETE = "percent_complete"; - - /** - * The status of this task. One of {@link #STATUS_NEEDS_ACTION},{@link #STATUS_IN_PROCESS}, {@link #STATUS_COMPLETED}, {@link #STATUS_CANCELLED}. - *

- * Value: Integer - *

- */ - public static final String STATUS = "status"; - - /** - * A specific status indicating that nothing has been done yet. - */ - public static final int STATUS_NEEDS_ACTION = 0; - - /** - * A specific status indicating that some work has been done. - */ - public static final int STATUS_IN_PROCESS = 1; - - /** - * A specific status indicating that the task is completed. - */ - public static final int STATUS_COMPLETED = 2; - - /** - * A specific status indicating that the task has been cancelled. - */ - public static final int STATUS_CANCELLED = 3; - - /** - * The default status is "needs action". - */ - public static final int STATUS_DEFAULT = STATUS_NEEDS_ACTION; - - /** - * A flag that indicates a task is new (i.e. not work has been done yet). This flag is read-only. Its value is 1 when - * {@link #STATUS} equals {@link #STATUS_NEEDS_ACTION} and 0 otherwise. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String IS_NEW = "is_new"; - - /** - * A flag that indicates a task is closed (no more work has to be done). This flag is read-only. Its value is 1 when - * {@link #STATUS} equals {@link #STATUS_COMPLETED} or {@link #STATUS_CANCELLED} and 0 otherwise. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String IS_CLOSED = "is_closed"; - - /** - * An individual color for this task in the format 0xaarrggbb or {@code null} to use {@link TaskListColumns#LIST_COLOR} instead. - *

- * Value: Integer - *

- */ - public static final String TASK_COLOR = "task_color"; - - /** - * When this task starts in milliseconds since the epoch. - *

- * Value: Long - *

- */ - public static final String DTSTART = "dtstart"; - - /** - * Boolean: flag that indicates that this is an all-day task. - */ - public static final String IS_ALLDAY = "is_allday"; - - /** - * When this task has been created in milliseconds since the epoch. - *

- * Value: Long - *

- */ - public static final String CREATED = "created"; - - /** - * When this task had been modified the last time in milliseconds since the epoch. - *

- * Value: Long - *

- */ - public static final String LAST_MODIFIED = "last_modified"; - - /** - * String: An Olson Id of the time zone of this task. If this value is null, it's automatically replaced by the local time zone. - */ - public static final String TZ = "tz"; - - /** - * When this task is due in milliseconds since the epoch. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or none of both if the task - * has no due date). - *

- * Value: Long - *

- */ - public static final String DUE = "due"; - - /** - * The duration of this task. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or none of both if the task has no due date). Setting a - * {@link #DURATION} is not allowed when {@link #DTSTART} is null. The Value must be a duration string as in RFC 5545 Section 3.3.6. - *

- * Value: String - *

- */ - public static final String DURATION = "duration"; - - /** - * A comma separated list of time Strings in RFC 5545 format (see RFC 5545 Section 3.3.4 - * and RFC 5545 Section 3.3.5) that contains dates of instances of e recurring task. - * All-day tasks must use the DATE format specified in section 3.3.4 of RFC 5545. - * - * This value must be {@code null} for exception instances. - *

- * Value: String - *

- */ - public static final String RDATE = "rdate"; - - /** - * A comma separated list of time Strings in RFC 5545 format (see RFC 5545 Section 3.3.4 - * and RFC 5545 Section 3.3.5) that contains dates of exceptions of a recurring task. - * All-day tasks must use the DATE format specified in section 3.3.4 of RFC 5545. - * - * This value must be {@code null} for exception instances. - *

- * Value: String - *

- */ - public static final String EXDATE = "exdate"; - - /** - * A recurrence rule as specified in RFC 5545 Section 3.3.10. - * - * This value must be {@code null} for exception instances. - *

- * Value: String - *

- */ - public static final String RRULE = "rrule"; - - /** - * The _sync_id of the original event if this is an exception, null otherwise. Only one of {@link #ORIGINAL_INSTANCE_SYNC_ID} or - * {@link #ORIGINAL_INSTANCE_ID} must be set if this task is an exception. The other one will be updated by the content provider. - *

- * Value: String - *

- */ - public static final String ORIGINAL_INSTANCE_SYNC_ID = "original_instance_sync_id"; - - /** - * The row id of the original event if this is an exception, null otherwise. Only one of {@link #ORIGINAL_INSTANCE_SYNC_ID} or - * {@link #ORIGINAL_INSTANCE_ID} must be set if this task is an exception. The other one will be updated by the content provider. - *

- * Value: Long - *

- */ - public static final String ORIGINAL_INSTANCE_ID = "original_instance_id"; - - /** - * The time in milliseconds since the Epoch of the original instance that is overridden by this instance or null if this task is not an - * exception. - *

- * Value: Long - *

- */ - public static final String ORIGINAL_INSTANCE_TIME = "original_instance_time"; - - /** - * A flag indicating that the original instance was an all-day task. - *

- * Value: Integer - *

- */ - public static final String ORIGINAL_INSTANCE_ALLDAY = "original_instance_allday"; - - /** - * The row id of the parent task. null if the task has no parent task. - *

- * Value: Long - *

- */ - public static final String PARENT_ID = "parent_id"; - - /** - * The sorting of this task under it's parent task. - *

- * Value: String - *

- */ - public static final String SORTING = "sorting"; - - /** - * Indicates how many alarms a task has. 0 means the task has no alarms. This field is read only as it's set automatically. - *

- * Value: Integer - *

- * Read-only - */ - public static final String HAS_ALARMS = "has_alarms"; - - /** - * Indicates that this task has extended properties like attachments, alarms or relations. This field is read only as it's set automatically. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String HAS_PROPERTIES = "has_properties"; - - /** - * Indicates that this task has been pinned to the notification area. This flag is moved to the exception when an exception for the first instance of a - * recurring task is created. That means, if you edit a pinned recurring task, the pinned flag is moved to the exception and cleared from the master - * task. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String PINNED = "pinned"; - } - - /** - * Columns that are valid in a search query. - * - * @author Marten Gajda - */ - public interface TaskSearchColumns - { - /** - * The score of a task in a search result. It's an indicator for the relevance of the task. Value is in (0, 1.0] where 0 would be "no relevance" at all - * (though the result doesn't contain such tasks). - *

- * Value: Float - *

- */ - public final static String SCORE = "score"; - } - - /** - * The task table stores the data of all tasks. - * - * @author Marten Gajda - */ - public static final class Tasks implements TaskColumns, CommonSyncColumns, TaskSyncColumns, TaskSearchColumns - { - /** - * The name of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String ACCOUNT_NAME = TaskLists.ACCOUNT_NAME; - - /** - * The type of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String ACCOUNT_TYPE = TaskLists.ACCOUNT_TYPE; - - /** - * The name of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value - * here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String LIST_NAME = TaskLists.LIST_NAME; - /** - * The color of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value - * here. To change the color of an individual task use {@code TASK_COLOR} instead. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String LIST_COLOR = TaskLists.LIST_COLOR; - - /** - * The owner of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String LIST_OWNER = TaskLists.OWNER; - - /** - * The access level of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String LIST_ACCESS_LEVEL = TaskLists.ACCESS_LEVEL; - - /** - * The visibility of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String VISIBLE = "visible"; - - static final String CONTENT_URI_PATH = "tasks"; - - static final String SEARCH_URI_PATH = "tasks_search"; - - static final String SEARCH_QUERY_PARAMETER = "q"; - - public static final String DEFAULT_SORT_ORDER = DUE; - - public static final String[] SYNC_ADAPTER_COLUMNS = new String[] { _DIRTY, SYNC1, SYNC2, SYNC3, SYNC4, SYNC5, SYNC6, SYNC7, SYNC8, _SYNC_ID, - SYNC_VERSION, }; - - - /** - * Get the tasks content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * @return A {@link Uri}. - */ - public final static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - - public final static Uri getSearchUri(String authority, String query) - { - Uri.Builder builder = getUriFactory(authority).getUri(SEARCH_URI_PATH).buildUpon(); - builder.appendQueryParameter(SEARCH_QUERY_PARAMETER, Uri.encode(query)); - return builder.build(); - } - } - - /** - * Columns of a task instance. - * - * @author Yannic Ahrens - * @author Marten Gajda - */ - public interface InstanceColumns - { - /** - * _ID of task this instance belongs to. - *

- * Value: Long - *

- */ - public static final String TASK_ID = "task_id"; - - /** - * The start date of an instance in milliseconds since the epoch or null if the instance has no start date. At present this is read only. - *

- * Value: Long - *

- */ - public static final String INSTANCE_START = "instance_start"; - - /** - * The due date of an instance in milliseconds since the epoch or null if the instance has no due date. At present this is read only. - *

- * Value: Long - *

- */ - public static final String INSTANCE_DUE = "instance_due"; - - /** - * This column should be used in an order clause to sort instances by start date. The only guarantee about the values in this column is the sort order. - * Don't make any other assumptions about the value. - *

- * Value: Long - *

- *

- * read-only - *

- */ - public static final String INSTANCE_START_SORTING = "instance_start_sorting"; - - /** - * This column should be used in an order clause to sort instances by due date. The only guarantee about the values in this column is the sort order. - * Don't make any other assumptions about the value. - *

- * Value: Long - *

- *

- * read-only - *

- */ - public static final String INSTANCE_DUE_SORTING = "instance_due_sorting"; - - /** - * The duration of an instance in milliseconds or null if the instance has only one of start or due date or none of both. At present this - * is read only. - *

- * Value: Long - *

- */ - public static final String INSTANCE_DURATION = "instance_duration"; - - } - - /** - * Instances of a task. At present this table is read only. Currently it contains exactly one entry per task (and task exception), so it's merely a copy of - * {@link Tasks}. - *

- * TODO: Insert all instances of recurring the tasks. - *

- *

- * TODO: In later releases it's planned to provide a convenient interface to add, change or delete task instances via this URI. - *

- * - * @author Yannic Ahrens - */ - public static final class Instances implements TaskColumns, InstanceColumns - { - - /** - * The name of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String ACCOUNT_NAME = TaskLists.ACCOUNT_NAME; - - /** - * The type of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String ACCOUNT_TYPE = TaskLists.ACCOUNT_TYPE; - - /** - * The name of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value - * here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String LIST_NAME = TaskLists.LIST_NAME; - /** - * The color of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value - * here. To change the color of an individual task use {@code TASK_COLOR} instead. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String LIST_COLOR = TaskLists.LIST_COLOR; - - /** - * The owner of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String LIST_OWNER = TaskLists.OWNER; - - /** - * The access level of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String LIST_ACCESS_LEVEL = TaskLists.ACCESS_LEVEL; - - /** - * The visibility of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String VISIBLE = "visible"; - - static final String CONTENT_URI_PATH = "instances"; - - public static final String DEFAULT_SORT_ORDER = INSTANCE_DUE_SORTING; - - - /** - * Get the instances content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * @return A {@link Uri}. - */ - public final static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - } - - /** - * Available values in Categories. - * - * Categories are per account. It's up to the front-end to ensure consistency of category colors across accounts. - * - * @author Marten Gajda - */ - public interface CategoriesColumns - { - - public static final String _ID = "_id"; - - public static final String ACCOUNT_NAME = "account_name"; + /** + * The task authority cache. + */ + private static Map sAuthorities = Collections.synchronizedMap(new HashMap(4)); + + private static Map sUriFactories = new HashMap(4); + + /** + * URI parameter to signal that the caller is a sync adapter. + */ + public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; + + /** + * URI parameter to signal the request of the extended properties of a task. + */ + public static final String LOAD_PROPERTIES = "load_properties"; + + /** + * URI parameter to submit the account name of the account we operate on. + */ + public static final String ACCOUNT_NAME = "account_name"; + + /** + * URI parameter to submit the account type of the account we operate on. + */ + public static final String ACCOUNT_TYPE = "account_type"; + + /** + * Account name for local, unsynced task lists. + */ + public static final String LOCAL_ACCOUNT_NAME = "Local"; + + /** + * Account type for local, unsynced task lists. + */ + public static final String LOCAL_ACCOUNT_TYPE = "org.dmfs.account.LOCAL"; + + /** + * Broadcast action that's sent when the task database has been initialized, either because the app was launched for the first time or because the app was + * launched after the user cleared the app data. + *

+ * The intent data represents the authority of the provider, the MIME type will be {@link #MIMETYPE_AUTHORITY}. + */ + public static final String ACTION_DATABASE_INITIALIZED = "org.dmfs.tasks.DATABASE_INITIALIZED"; + + /** + * A MIME type of an authority. Authorities itself don't seem to have a MIME type in Android, so we just use our own. + */ + public static final String MIMETYPE_AUTHORITY = ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.org.dmfs.authority.mimetype"; + + /** + * The action of the broadcast that's send when a task becomes due. The intent data will be a {@link Uri} of the task that became due. + */ + public static final String ACTION_BROADCAST_TASK_DUE = "org.dmfs.android.tasks.TASK_DUE"; + + /** + * The action of the broadcast that's send when a task starts. The intent data will be a {@link Uri} of the task that has started. + */ + public static final String ACTION_BROADCAST_TASK_STARTING = "org.dmfs.android.tasks.TASK_START"; + + /** + * A Long extra that contains a timestamp of the event that's triggered. So this is either the timestamp of the start or due date of the task. + */ + public final static String EXTRA_TASK_TIMESTAMP = "org.dmfs.provider.tasks.extra.TIMESTAMP"; + + /** + * A Boolean extra to indicate that the event that was triggered is an all-day date. + */ + public final static String EXTRA_TASK_ALLDAY = "org.dmfs.provider.tasks.extra.ALLDAY"; + + /** + * A String extra containing the timezone id of the task. + */ + public final static String EXTRA_TASK_TIMEZONE = "org.dmfs.provider.tasks.extra.TIMEZONE"; + + /** + * A String extra containing the title of the task. + */ + public final static String EXTRA_TASK_TITLE = "org.dmfs.provider.tasks.extra.TITLE"; + + /** + * The name of the {@link Intent#ACTION_PROVIDER_CHANGED} extra that contains the {@link ArrayList} of {@link Uri}s that have been modified. This always + * goes along with an {@link #EXTRA_OPERATIONS} which contains a code for the operation executed on a Uri at the same index. + */ + public final static String EXTRA_OPERATIONS_URIS = "org.dmfs.tasks.OPERATIONS_URIS"; + + /** + * The name of the {@link Intent#ACTION_PROVIDER_CHANGED} extra that contains the {@link ArrayList} of provider operation codes. The following codes are + * used: + *

    + *
  • 0 - for inserts
  • + *
  • 1 - for updates
  • + *
  • 2 - for deletes
  • + *
+ */ + public final static String EXTRA_OPERATIONS = "org.dmfs.tasks.OPERATIONS"; + + + /** + * Private constructor to prevent instantiation. + */ + private TaskContract() + { + } + + + /** + * A table provided for sync adapters to use for storing private sync state data. + *

+ * Only sync adapters are allowed to access this table and they may access their own rows only. + *

+ * Note that only one row per account will be stored. Updating or inserting a sync state for a specific account will override any previous sync state for + * this account. + */ + public static class SyncState implements SyncStateContract.Columns, BaseColumns + { + final static String CONTENT_URI_PATH = "syncstate"; + + + /** + * Get the sync state content {@link Uri} using the given authority. + * + * @param authority + * The authority. + * + * @return A {@link Uri}. + */ + public final static Uri getContentUri(String authority) + { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); + } + + } + + + /** + * Get the base content {@link Uri} using the given authority. + * + * @param authority + * The authority. + * + * @return A {@link Uri}. + */ + public final static Uri getContentUri(String authority) + { + return getUriFactory(authority).getUri(); + } + + + /** + * A set of columns for synchronization purposes. These columns exist in {@link Tasks} and in {@link TaskLists} but have different meanings. Only sync + * adapters are allowed to change these values. + * + * @author Marten Gajda + */ + public interface CommonSyncColumns + { + + /** + * A unique Sync ID as set by the sync adapter. + *

+ * Value: String + *

+ */ + public static final String _SYNC_ID = "_sync_id"; + + /** + * Sync version as set by the sync adapter. + *

+ * Value: String + *

+ */ + public static final String SYNC_VERSION = "sync_version"; + + /** + * Indicates that a task or a task list has been changed. + *

+ * Value: Integer + *

+ */ + public static final String _DIRTY = "_dirty"; + + /** + * A general purpose column for the sync adapter. + *

+ * Value: String + *

+ */ + public static final String SYNC1 = "sync1"; + + /** + * A general purpose column for the sync adapter. + *

+ * Value: String + *

+ */ + public static final String SYNC2 = "sync2"; + + /** + * A general purpose column for the sync adapter. + *

+ * Value: String + *

+ */ + public static final String SYNC3 = "sync3"; + + /** + * A general purpose column for the sync adapter. + *

+ * Value: String + *

+ */ + public static final String SYNC4 = "sync4"; + + /** + * A general purpose column for the sync adapter. + *

+ * Value: String + *

+ */ + public static final String SYNC5 = "sync5"; + + /** + * A general purpose column for the sync adapter. + *

+ * Value: String + *

+ */ + public static final String SYNC6 = "sync6"; + + /** + * A general purpose column for the sync adapter. + *

+ * Value: String + *

+ */ + public static final String SYNC7 = "sync7"; + + /** + * A general purpose column for the sync adapter. + *

+ * Value: String + *

+ */ + public static final String SYNC8 = "sync8"; + + } + + + /** + * Additional sync columns for task lists. + * + * @author Marten Gajda + */ + public interface TaskListSyncColumns + { + + /** + * The name of the account this list belongs to. This field is write-once. + *

+ * Value: String + *

+ */ + public static final String ACCOUNT_NAME = "account_name"; + + /** + * The type of the account this list belongs to. This field is write-once. + *

+ * Value: String + *

+ */ + public static final String ACCOUNT_TYPE = "account_type"; + } + + + /** + * Additional sync columns for tasks. + * + * @author Marten Gajda + */ + public interface TaskSyncColumns + { + /** + * The UID of a task. This is field can be changed by a sync adapter only. + *

+ * Value: String + *

+ */ + public static final String _UID = "_uid"; + + /** + * Deleted flag of a task. This is set to 1 by the content provider when a task app deletes a task. The sync adapter has to remove the task + * again to finish the removal. This value is read-only. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String _DELETED = "_deleted"; + } + + + /** + * Data columns of task lists. + * + * @author Marten Gajda + */ + public interface TaskListColumns + { + + /** + * List ID. + *

+ * Value: Long + *

+ *

+ * read-only + *

+ */ + public static final String _ID = "_id"; + + /** + * The name of the task list. + *

+ * Value: String + *

+ */ + public static final String LIST_NAME = "list_name"; + + /** + * The color of this list as integer (0xaarrggbb). Only the sync adapter can change this. + *

+ * Value: Integer + *

+ */ + public static final String LIST_COLOR = "list_color"; + + /** + * The access level a user has on this list. This value is not used yet, sync adapters should set it to 0. + *

+ * Value: Integer + *

+ */ + public static final String ACCESS_LEVEL = "list_access_level"; + + /** + * Indicates that a task list is set to be visible. + *

+ * Value: Integer (0 or 1) + *

+ */ + public static final String VISIBLE = "visible"; + + /** + * Indicates that a task list is set to be synced. + *

+ * Value: Integer (0 or 1) + *

+ */ + public static final String SYNC_ENABLED = "sync_enabled"; + + /** + * The email address of the list owner. + *

+ * Value: String + *

+ */ + public static final String OWNER = "list_owner"; + + } + + + /** + * The task list table holds one entry for each task list. + * + * @author Marten Gajda + */ + public static final class TaskLists implements TaskListColumns, TaskListSyncColumns, CommonSyncColumns + { + static final String CONTENT_URI_PATH = "tasklists"; + + /** + * The default sort order. + */ + public static final String DEFAULT_SORT_ORDER = ACCOUNT_NAME + ", " + LIST_NAME; + + /** + * An array of columns only a sync adapter is allowed to change. + */ + public static final String[] SYNC_ADAPTER_COLUMNS = new String[] { + ACCESS_LEVEL, _DIRTY, OWNER, SYNC1, SYNC2, SYNC3, SYNC4, SYNC5, SYNC6, SYNC7, SYNC8, + _SYNC_ID, SYNC_VERSION, }; + + + /** + * Get the task list content {@link Uri} using the given authority. + * + * @param authority + * The authority. + * + * @return A {@link Uri}. + */ + public final static Uri getContentUri(String authority) + { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); + } + + } + + + /** + * Task data columns. Defines all the values a task can have at most once. + * + * @author Marten Gajda + */ + public interface TaskColumns + { + + /** + * The row id of a task. This value is read-only + *

+ * Value: Integer + *

+ */ + public static final String _ID = "_id"; + + /** + * The id of the list this task belongs to. This value is write-once and must not be null. + *

+ * Value: Integer + *

+ */ + public static final String LIST_ID = "list_id"; + + /** + * The title of the task. + *

+ * Value: String + *

+ */ + public static final String TITLE = "title"; + + /** + * The location of the task. + *

+ * Value: String + *

+ */ + public static final String LOCATION = "location"; + + /** + * A geographic location related to the task. The should be a string in the format "longitude,latitude". + *

+ * Value: String + *

+ */ + public static final String GEO = "geo"; + + /** + * The description of a task. + *

+ * Value: String + *

+ */ + public static final String DESCRIPTION = "description"; + + /** + * An URL for this task. Must be a valid URL if not null- + *

+ * Value: String + *

+ */ + public static final String URL = "url"; + + /** + * The email address of the organizer if any, {@code null} otherwise. + *

+ * Value: String + *

+ */ + public static final String ORGANIZER = "organizer"; + + /** + * The priority of a task. This is an Integer between zero and 9. Zero means there is no priority set. 1 is the highest priority and 9 the lowest. + *

+ * Value: Integer + *

+ */ + public static final String PRIORITY = "priority"; + + /** + * The default value of {@link #PRIORITY}. + */ + public static final int PRIORITY_DEFAULT = 0; + + /** + * The classification of a task. This value must be either null or one of {@link #CLASSIFICATION_PUBLIC}, {@link #CLASSIFICATION_PRIVATE}, + * {@link #CLASSIFICATION_CONFIDENTIAL}. + *

+ * Value: Integer + *

+ */ + public static final String CLASSIFICATION = "class"; + + /** + * Classification value for public tasks. + */ + public static final int CLASSIFICATION_PUBLIC = 0; + + /** + * Classification value for private tasks. + */ + public static final int CLASSIFICATION_PRIVATE = 1; + + /** + * Classification value for confidential tasks. + */ + public static final int CLASSIFICATION_CONFIDENTIAL = 2; + + /** + * Default value of {@link #CLASSIFICATION}. + */ + public static final Integer CLASSIFICATION_DEFAULT = null; + + /** + * Date of completion of this task in milliseconds since the epoch or {@code null} if this task has not been completed yet. + *

+ * Value: Long + *

+ */ + public static final String COMPLETED = "completed"; + + /** + * Indicates that the date of completion is an all-day date. + *

+ * Value: Integer + *

+ */ + public static final String COMPLETED_IS_ALLDAY = "completed_is_allday"; + + /** + * A number between 0 and 100 that indicates the progress of the task or null. + *

+ * Value: Integer (0-100) + *

+ */ + public static final String PERCENT_COMPLETE = "percent_complete"; + + /** + * The status of this task. One of {@link #STATUS_NEEDS_ACTION},{@link #STATUS_IN_PROCESS}, {@link #STATUS_COMPLETED}, {@link #STATUS_CANCELLED}. + *

+ * Value: Integer + *

+ */ + public static final String STATUS = "status"; + + /** + * A specific status indicating that nothing has been done yet. + */ + public static final int STATUS_NEEDS_ACTION = 0; + + /** + * A specific status indicating that some work has been done. + */ + public static final int STATUS_IN_PROCESS = 1; + + /** + * A specific status indicating that the task is completed. + */ + public static final int STATUS_COMPLETED = 2; + + /** + * A specific status indicating that the task has been cancelled. + */ + public static final int STATUS_CANCELLED = 3; + + /** + * The default status is "needs action". + */ + public static final int STATUS_DEFAULT = STATUS_NEEDS_ACTION; + + /** + * A flag that indicates a task is new (i.e. not work has been done yet). This flag is read-only. Its value is 1 when + * {@link #STATUS} equals {@link #STATUS_NEEDS_ACTION} and 0 otherwise. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String IS_NEW = "is_new"; + + /** + * A flag that indicates a task is closed (no more work has to be done). This flag is read-only. Its value is 1 when + * {@link #STATUS} equals {@link #STATUS_COMPLETED} or {@link #STATUS_CANCELLED} and 0 otherwise. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String IS_CLOSED = "is_closed"; + + /** + * An individual color for this task in the format 0xaarrggbb or {@code null} to use {@link TaskListColumns#LIST_COLOR} instead. + *

+ * Value: Integer + *

+ */ + public static final String TASK_COLOR = "task_color"; + + /** + * When this task starts in milliseconds since the epoch. + *

+ * Value: Long + *

+ */ + public static final String DTSTART = "dtstart"; + + /** + * Boolean: flag that indicates that this is an all-day task. + */ + public static final String IS_ALLDAY = "is_allday"; + + /** + * When this task has been created in milliseconds since the epoch. + *

+ * Value: Long + *

+ */ + public static final String CREATED = "created"; + + /** + * When this task had been modified the last time in milliseconds since the epoch. + *

+ * Value: Long + *

+ */ + public static final String LAST_MODIFIED = "last_modified"; + + /** + * String: An Olson Id of the time zone of this task. If this value is null, it's automatically replaced by the local time zone. + */ + public static final String TZ = "tz"; + + /** + * When this task is due in milliseconds since the epoch. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or none of both if the task + * has no due date). + *

+ * Value: Long + *

+ */ + public static final String DUE = "due"; + + /** + * The duration of this task. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or none of both if the task has no due date). Setting a + * {@link #DURATION} is not allowed when {@link #DTSTART} is null. The Value must be a duration string as in RFC 5545 Section 3.3.6. + *

+ * Value: String + *

+ */ + public static final String DURATION = "duration"; + + /** + * A comma separated list of time Strings in RFC 5545 format (see RFC 5545 Section 3.3.4 + * and RFC 5545 Section 3.3.5) that contains dates of instances of e recurring task. + * All-day tasks must use the DATE format specified in section 3.3.4 of RFC 5545. + *

+ * This value must be {@code null} for exception instances. + *

+ * Value: String + *

+ */ + public static final String RDATE = "rdate"; + + /** + * A comma separated list of time Strings in RFC 5545 format (see RFC 5545 Section 3.3.4 + * and RFC 5545 Section 3.3.5) that contains dates of exceptions of a recurring task. + * All-day tasks must use the DATE format specified in section 3.3.4 of RFC 5545. + *

+ * This value must be {@code null} for exception instances. + *

+ * Value: String + *

+ */ + public static final String EXDATE = "exdate"; + + /** + * A recurrence rule as specified in RFC 5545 Section 3.3.10. + *

+ * This value must be {@code null} for exception instances. + *

+ * Value: String + *

+ */ + public static final String RRULE = "rrule"; + + /** + * The _sync_id of the original event if this is an exception, null otherwise. Only one of {@link #ORIGINAL_INSTANCE_SYNC_ID} or + * {@link #ORIGINAL_INSTANCE_ID} must be set if this task is an exception. The other one will be updated by the content provider. + *

+ * Value: String + *

+ */ + public static final String ORIGINAL_INSTANCE_SYNC_ID = "original_instance_sync_id"; + + /** + * The row id of the original event if this is an exception, null otherwise. Only one of {@link #ORIGINAL_INSTANCE_SYNC_ID} or + * {@link #ORIGINAL_INSTANCE_ID} must be set if this task is an exception. The other one will be updated by the content provider. + *

+ * Value: Long + *

+ */ + public static final String ORIGINAL_INSTANCE_ID = "original_instance_id"; + + /** + * The time in milliseconds since the Epoch of the original instance that is overridden by this instance or null if this task is not an + * exception. + *

+ * Value: Long + *

+ */ + public static final String ORIGINAL_INSTANCE_TIME = "original_instance_time"; + + /** + * A flag indicating that the original instance was an all-day task. + *

+ * Value: Integer + *

+ */ + public static final String ORIGINAL_INSTANCE_ALLDAY = "original_instance_allday"; + + /** + * The row id of the parent task. null if the task has no parent task. + *

+ * Value: Long + *

+ */ + public static final String PARENT_ID = "parent_id"; + + /** + * The sorting of this task under it's parent task. + *

+ * Value: String + *

+ */ + public static final String SORTING = "sorting"; + + /** + * Indicates how many alarms a task has. 0 means the task has no alarms. This field is read only as it's set automatically. + *

+ * Value: Integer + *

+ * Read-only + */ + public static final String HAS_ALARMS = "has_alarms"; + + /** + * Indicates that this task has extended properties like attachments, alarms or relations. This field is read only as it's set automatically. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String HAS_PROPERTIES = "has_properties"; + + /** + * Indicates that this task has been pinned to the notification area. This flag is moved to the exception when an exception for the first instance of a + * recurring task is created. That means, if you edit a pinned recurring task, the pinned flag is moved to the exception and cleared from the master + * task. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String PINNED = "pinned"; + } + + + /** + * Columns that are valid in a search query. + * + * @author Marten Gajda + */ + public interface TaskSearchColumns + { + /** + * The score of a task in a search result. It's an indicator for the relevance of the task. Value is in (0, 1.0] where 0 would be "no relevance" at all + * (though the result doesn't contain such tasks). + *

+ * Value: Float + *

+ */ + public final static String SCORE = "score"; + } + + + /** + * The task table stores the data of all tasks. + * + * @author Marten Gajda + */ + public static final class Tasks implements TaskColumns, CommonSyncColumns, TaskSyncColumns, TaskSearchColumns + { + /** + * The name of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: String + *

+ *

+ * read-only + *

+ */ + public static final String ACCOUNT_NAME = TaskLists.ACCOUNT_NAME; + + /** + * The type of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: String + *

+ *

+ * read-only + *

+ */ + public static final String ACCOUNT_TYPE = TaskLists.ACCOUNT_TYPE; + + /** + * The name of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value + * here. + *

+ * Value: String + *

+ *

+ * read-only + *

+ */ + public static final String LIST_NAME = TaskLists.LIST_NAME; + /** + * The color of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value + * here. To change the color of an individual task use {@code TASK_COLOR} instead. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String LIST_COLOR = TaskLists.LIST_COLOR; + + /** + * The owner of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: String + *

+ *

+ * read-only + *

+ */ + public static final String LIST_OWNER = TaskLists.OWNER; + + /** + * The access level of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String LIST_ACCESS_LEVEL = TaskLists.ACCESS_LEVEL; + + /** + * The visibility of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String VISIBLE = "visible"; + + static final String CONTENT_URI_PATH = "tasks"; + + static final String SEARCH_URI_PATH = "tasks_search"; + + static final String SEARCH_QUERY_PARAMETER = "q"; + + public static final String DEFAULT_SORT_ORDER = DUE; + + public static final String[] SYNC_ADAPTER_COLUMNS = new String[] { + _DIRTY, SYNC1, SYNC2, SYNC3, SYNC4, SYNC5, SYNC6, SYNC7, SYNC8, _SYNC_ID, + SYNC_VERSION, }; + + + /** + * Get the tasks content {@link Uri} using the given authority. + * + * @param authority + * The authority. + * + * @return A {@link Uri}. + */ + public final static Uri getContentUri(String authority) + { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); + } + + + public final static Uri getSearchUri(String authority, String query) + { + Uri.Builder builder = getUriFactory(authority).getUri(SEARCH_URI_PATH).buildUpon(); + builder.appendQueryParameter(SEARCH_QUERY_PARAMETER, Uri.encode(query)); + return builder.build(); + } + } + + + /** + * Columns of a task instance. + * + * @author Yannic Ahrens + * @author Marten Gajda + */ + public interface InstanceColumns + { + /** + * _ID of task this instance belongs to. + *

+ * Value: Long + *

+ */ + public static final String TASK_ID = "task_id"; + + /** + * The start date of an instance in milliseconds since the epoch or null if the instance has no start date. At present this is read only. + *

+ * Value: Long + *

+ */ + public static final String INSTANCE_START = "instance_start"; + + /** + * The due date of an instance in milliseconds since the epoch or null if the instance has no due date. At present this is read only. + *

+ * Value: Long + *

+ */ + public static final String INSTANCE_DUE = "instance_due"; + + /** + * This column should be used in an order clause to sort instances by start date. The only guarantee about the values in this column is the sort order. + * Don't make any other assumptions about the value. + *

+ * Value: Long + *

+ *

+ * read-only + *

+ */ + public static final String INSTANCE_START_SORTING = "instance_start_sorting"; + + /** + * This column should be used in an order clause to sort instances by due date. The only guarantee about the values in this column is the sort order. + * Don't make any other assumptions about the value. + *

+ * Value: Long + *

+ *

+ * read-only + *

+ */ + public static final String INSTANCE_DUE_SORTING = "instance_due_sorting"; + + /** + * The duration of an instance in milliseconds or null if the instance has only one of start or due date or none of both. At present this + * is read only. + *

+ * Value: Long + *

+ */ + public static final String INSTANCE_DURATION = "instance_duration"; + + } + + + /** + * Instances of a task. At present this table is read only. Currently it contains exactly one entry per task (and task exception), so it's merely a copy of + * {@link Tasks}. + *

+ * TODO: Insert all instances of recurring the tasks. + *

+ *

+ * TODO: In later releases it's planned to provide a convenient interface to add, change or delete task instances via this URI. + *

+ * + * @author Yannic Ahrens + */ + public static final class Instances implements TaskColumns, InstanceColumns + { + + /** + * The name of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: String + *

+ *

+ * read-only + *

+ */ + public static final String ACCOUNT_NAME = TaskLists.ACCOUNT_NAME; + + /** + * The type of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: String + *

+ *

+ * read-only + *

+ */ + public static final String ACCOUNT_TYPE = TaskLists.ACCOUNT_TYPE; + + /** + * The name of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value + * here. + *

+ * Value: String + *

+ *

+ * read-only + *

+ */ + public static final String LIST_NAME = TaskLists.LIST_NAME; + /** + * The color of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value + * here. To change the color of an individual task use {@code TASK_COLOR} instead. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String LIST_COLOR = TaskLists.LIST_COLOR; + + /** + * The owner of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: String + *

+ *

+ * read-only + *

+ */ + public static final String LIST_OWNER = TaskLists.OWNER; + + /** + * The access level of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String LIST_ACCESS_LEVEL = TaskLists.ACCESS_LEVEL; + + /** + * The visibility of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public static final String VISIBLE = "visible"; + + static final String CONTENT_URI_PATH = "instances"; + + public static final String DEFAULT_SORT_ORDER = INSTANCE_DUE_SORTING; + + + /** + * Get the instances content {@link Uri} using the given authority. + * + * @param authority + * The authority. + * + * @return A {@link Uri}. + */ + public final static Uri getContentUri(String authority) + { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); + } + + } + + + /** + * Available values in Categories. + *

+ * Categories are per account. It's up to the front-end to ensure consistency of category colors across accounts. + * + * @author Marten Gajda + */ + public interface CategoriesColumns + { + + public static final String _ID = "_id"; + + public static final String ACCOUNT_NAME = "account_name"; + + public static final String ACCOUNT_TYPE = "account_type"; + + public static final String NAME = "name"; + + public static final String COLOR = "color"; + } + + + public static final class Categories implements CategoriesColumns + { + + static final String CONTENT_URI_PATH = "categories"; + + public static final String DEFAULT_SORT_ORDER = NAME; + - public static final String ACCOUNT_TYPE = "account_type"; + /** + * Get the categories content {@link Uri} using the given authority. + * + * @param authority + * The authority. + * + * @return A {@link Uri}. + */ + public final static Uri getContentUri(String authority) + { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); + } - public static final String NAME = "name"; + } - public static final String COLOR = "color"; - } - public static final class Categories implements CategoriesColumns - { + public interface AlarmsColumns + { + public static final String ALARM_ID = "alarm_id"; - static final String CONTENT_URI_PATH = "categories"; + public static final String LAST_TRIGGER = "last_trigger"; - public static final String DEFAULT_SORT_ORDER = NAME; + public static final String NEXT_TRIGGER = "next_trigger"; + } - /** - * Get the categories content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * @return A {@link Uri}. - */ - public final static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } + public static final class Alarms implements AlarmsColumns + { - } + static final String CONTENT_URI_PATH = "alarms"; - public interface AlarmsColumns - { - public static final String ALARM_ID = "alarm_id"; - public static final String LAST_TRIGGER = "last_trigger"; + /** + * Get the alarms content {@link Uri} using the given authority. + * + * @param authority + * The authority. + * + * @return A {@link Uri}. + */ + public final static Uri getContentUri(String authority) + { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); + } - public static final String NEXT_TRIGGER = "next_trigger"; - } + } - public static final class Alarms implements AlarmsColumns - { - static final String CONTENT_URI_PATH = "alarms"; + public interface PropertySyncColumns + { + public static final String SYNC1 = "prop_sync1"; + public static final String SYNC2 = "prop_sync2"; - /** - * Get the alarms content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * @return A {@link Uri}. - */ - public final static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } + public static final String SYNC3 = "prop_sync3"; - } + public static final String SYNC4 = "prop_sync4"; - public interface PropertySyncColumns - { - public static final String SYNC1 = "prop_sync1"; + public static final String SYNC5 = "prop_sync5"; - public static final String SYNC2 = "prop_sync2"; + public static final String SYNC6 = "prop_sync6"; - public static final String SYNC3 = "prop_sync3"; + public static final String SYNC7 = "prop_sync7"; - public static final String SYNC4 = "prop_sync4"; + public static final String SYNC8 = "prop_sync8"; + } - public static final String SYNC5 = "prop_sync5"; - public static final String SYNC6 = "prop_sync6"; + public interface PropertyColumns + { - public static final String SYNC7 = "prop_sync7"; + public static final String PROPERTY_ID = "property_id"; - public static final String SYNC8 = "prop_sync8"; - } + public static final String TASK_ID = "task_id"; - public interface PropertyColumns - { + public static final String MIMETYPE = "mimetype"; - public static final String PROPERTY_ID = "property_id"; + public static final String VERSION = "prop_version"; - public static final String TASK_ID = "task_id"; + public static final String DATA0 = "data0"; - public static final String MIMETYPE = "mimetype"; + public static final String DATA1 = "data1"; - public static final String VERSION = "prop_version"; + public static final String DATA2 = "data2"; - public static final String DATA0 = "data0"; + public static final String DATA3 = "data3"; - public static final String DATA1 = "data1"; + public static final String DATA4 = "data4"; - public static final String DATA2 = "data2"; + public static final String DATA5 = "data5"; - public static final String DATA3 = "data3"; + public static final String DATA6 = "data6"; - public static final String DATA4 = "data4"; + public static final String DATA7 = "data7"; - public static final String DATA5 = "data5"; + public static final String DATA8 = "data8"; - public static final String DATA6 = "data6"; + public static final String DATA9 = "data9"; - public static final String DATA7 = "data7"; + public static final String DATA10 = "data10"; - public static final String DATA8 = "data8"; + public static final String DATA11 = "data11"; - public static final String DATA9 = "data9"; + public static final String DATA12 = "data12"; - public static final String DATA10 = "data10"; + public static final String DATA13 = "data13"; - public static final String DATA11 = "data11"; + public static final String DATA14 = "data14"; - public static final String DATA12 = "data12"; + public static final String DATA15 = "data15"; + } - public static final String DATA13 = "data13"; - public static final String DATA14 = "data14"; + public static final class Properties implements PropertySyncColumns, PropertyColumns + { - public static final String DATA15 = "data15"; - } + static final String CONTENT_URI_PATH = "properties"; - public static final class Properties implements PropertySyncColumns, PropertyColumns - { + public static final String DEFAULT_SORT_ORDER = DATA0; - static final String CONTENT_URI_PATH = "properties"; - public static final String DEFAULT_SORT_ORDER = DATA0; + /** + * Get the properties content {@link Uri} using the given authority. + * + * @param authority + * The authority. + * + * @return A {@link Uri}. + */ + public final static Uri getContentUri(String authority) + { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); + } + } - /** - * Get the properties content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * @return A {@link Uri}. - */ - public final static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - } + public interface Property + { + /** + * Attached documents. + *

+ * Note: Attachments are write-once. To change an attachment you'll have to remove and re-add it. + *

+ * + * @author Marten Gajda + */ + public static interface Attachment extends PropertyColumns + { + /** + * The mime-type of this property. + */ + public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/attachment"; - public interface Property - { - /** - * Attached documents. - *

- * Note: Attachments are write-once. To change an attachment you'll have to remove and re-add it. - *

- * - * @author Marten Gajda - */ - public static interface Attachment extends PropertyColumns - { - /** - * The mime-type of this property. - */ - public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/attachment"; + /** + * URL of the attachment. This is the link that points to the attached resource. + *

+ * Value: String + *

+ */ + public final static String URL = DATA1; - /** - * URL of the attachment. This is the link that points to the attached resource. - *

- * Value: String - *

- */ - public final static String URL = DATA1; - - /** - * The display name of the attachment, if any. - *

- * Value: String - *

- */ - public final static String DISPLAY_NAME = DATA2; - - /** - * Content-type of the attachment. - *

- * Value: String - *

- */ - public final static String FORMAT = DATA3; - - /** - * File size of the attachment or -1 if unknown. - *

- * Value: Long - *

- */ - public final static String SIZE = DATA4; - - /** - * A content {@link Uri} that can be used to retrieve the attachment. Sync adapters can set this field if they know how to download the attachment - * without going through the browser. - *

- * Value: String - *

- */ - public final static String CONTENT_URI = DATA5; - - } - - public static interface Attendee extends PropertyColumns - { - /** - * The mime-type of this property. - */ - public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/attendee"; - - /** - * Name of the contact, if known. - *

- * Value: String - *

- */ - public final static String NAME = DATA0; - - /** - * Email address of the contact. - *

- * Value: String - *

- */ - public final static String EMAIL = DATA1; - - public final static String ROLE = DATA2; - - public final static String STATUS = DATA3; - - public final static String RSVP = DATA4; - } - - /** - * Categories are immutable. For creation is either the category id or name necessary - * - */ - public static interface Category extends PropertyColumns - { - /** - * The mime-type of this property. - */ - public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/category"; - - /** - * Row id of the category. - *

- * Value: Long - *

- */ - public final static String CATEGORY_ID = DATA0; - - /** - * The name of the category - *

- * Value: String - *

- */ - public final static String CATEGORY_NAME = DATA1; - - /** - * The decimal coded color of the category - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public final static String CATEGORY_COLOR = DATA2; - } - - public static interface Comment extends PropertyColumns - { - /** - * The mime-type of this property. - */ - public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/comment"; - - /** - * Comment text. - *

- * Value: String - *

- */ - public final static String COMMENT = DATA0; - - /** - * Language code of the comment as defined in RFC5646 or null. - *

- * Value: String - *

- */ - public final static String LANGUAGE = DATA1; - } - - public static interface Contact extends PropertyColumns - { - /** - * The mime-type of this property. - */ - public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/contact"; - - public final static String NAME = DATA0; - - public final static String LANGUAGE = DATA1; - } - - /** - * Relations of a task. - *

- * When writing a relation, exactly one of {@link #RELATED_ID}, {@link #RELATED_UID} or {@link #RELATED_URI} must be given. {@link #RELATED_CONTENT_URI} - * will be populated automatically if possible. - *

- */ - public static interface Relation extends PropertyColumns - { - /** - * The mime-type of this property. - */ - public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/relation"; - - /** - * The row id of the related task. May be -1 if the property doesn't refer to a task in this database or if it doesn't refer to a task - * at all. - *

- * Value: long - *

- */ - public final static String RELATED_ID = DATA1; - - /** - * The relation type. This must be the ordinal value of a {@link RelType}. - *

- * Value: int - *

- */ - public final static String RELATED_TYPE = DATA2; - - /** - * The UID of the related object. - *

- * Value: String - *

- */ - public final static String RELATED_UID = DATA3; - - /** - * The URI of a related object. - *

- * Value: String (URI) - *

- */ - public final static String RELATED_URI = DATA4; - - /** - * The URI of a related object in another Android content provider. If the object is a task in this database, this is null. If the - * related object is an event or note this field may contain the content URI to the object. - *

- * Value: String (URI) - *

- *

- * This field is read-only. - *

- */ - public final static String RELATED_CONTENT_URI = DATA5; - - /** - * An optional gap value for temporal relationships. - *

- * Value: duration string - *

- */ - public final static String GAP = DATA6; - - /** - * Valid values for the {@link Relation#RELATED_TYPE} field. Note that the field actually takes the ordinal value of these. - */ - public enum RelType - { - /** - * The related object is the parent of the object owning this relation. - */ - PARENT, - - /** - * The related object is the child of the object owning this relation. - */ - CHILD, - - /** - * The related object is a sibling of the object owning this relation. - */ - SIBLING, - - DEPENDS_ON, - - REFID, - - STRUCTURED_CATEGORY, - - FINISHTOSTART, - - FINISHTOFINISH, - - STARTTOFINISH, - - STARTTOSTART; - } - } - - public static interface Alarm extends PropertyColumns - { - - public static final int ALARM_TYPE_NOTHING = 0; - - public static final int ALARM_TYPE_MESSAGE = 1; - - public static final int ALARM_TYPE_EMAIL = 2; - - public static final int ALARM_TYPE_SMS = 3; - - public static final int ALARM_TYPE_SOUND = 4; - - public static final int ALARM_REFERENCE_DUE_DATE = 1; - - public static final int ALARM_REFERENCE_START_DATE = 2; - - /** - * The mime-type of this property. - */ - public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/alarm"; - - /** - * Number of minutes from the reference date when the alarm goes off. If the value is < 0 the alarm will go off after the reference date. - *

- * Value: Integer - *

- */ - public final static String MINUTES_BEFORE = DATA0; - - /** - * The reference date for the alarm. Either {@link ALARM_REFERENCE_DUE_DATE} or {@link ALARM_REFERENCE_START_DATE}. - *

- * Value: Integer - *

- */ - public final static String REFERENCE = DATA1; - - /** - * A message that appears with the alarm. - *

- * Value: String - *

- */ - public final static String MESSAGE = DATA2; - - /** - * The type of the alarm. Use the provided alarm types {@link ALARM_TYPE_MESSAGE}, {@link ALARM_TYPE_SOUND}, {@link ALARM_TYPE_NOTHING}, - * {@link ALARM_TYPE_EMAIL} and {@link ALARM_TYPE_SMS}. - *

- * Value: Integer - *

- */ - public final static String ALARM_TYPE = DATA3; - } - - } - - - private static synchronized UriFactory getUriFactory(String authority) - { - UriFactory uriFactory = sUriFactories.get(authority); - if (uriFactory == null) - { - uriFactory = new UriFactory(authority); - uriFactory.addUri(SyncState.CONTENT_URI_PATH); - uriFactory.addUri(TaskLists.CONTENT_URI_PATH); - uriFactory.addUri(Tasks.CONTENT_URI_PATH); - uriFactory.addUri(Tasks.SEARCH_URI_PATH); - uriFactory.addUri(Instances.CONTENT_URI_PATH); - uriFactory.addUri(Categories.CONTENT_URI_PATH); - uriFactory.addUri(Alarms.CONTENT_URI_PATH); - uriFactory.addUri(Properties.CONTENT_URI_PATH); - sUriFactories.put(authority, uriFactory); - - } - return uriFactory; - } - - - /** - * Returns the authority of the {@link TaskProvider} in the given {@link Context}. - *

- * TODO: create an Authority class instead that handles everything about authorities. It could replace {@link UriFactory} as well. The Authority class could - * have a generic parameter that identifies the authority provider or contract class. - * - * @param context - * A {@link Context} of an app that contains a {@link TaskProvider}. - * @return The authority. - * - * @throws RuntimeException - * if there is no {@link TaskProvider} in that {@link Context}. - */ - public static synchronized String taskAuthority(Context context) - { - String packageName = context.getPackageName(); - if (sAuthorities.containsKey(packageName)) - { - return sAuthorities.get(packageName); - } - - PackageManager packageManager = context.getPackageManager(); - - // first get the PackageInfo of this app. - PackageInfo packageInfo; - try - { - packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS); - } - catch (NameNotFoundException e) - { - throw new RuntimeException("Could not find TaskProvider!", e); - } - - // next scan all providers for TaskProvider - for (ProviderInfo provider : packageInfo.providers) - { - Class providerClass; - try - { - providerClass = Class.forName(provider.name); - } - catch (ClassNotFoundException e) - { - continue; - } - - if (!TaskProvider.class.isAssignableFrom(providerClass)) - { - continue; - } - - sAuthorities.put(packageName, provider.authority); - return provider.authority; - } - throw new RuntimeException("Could not find TaskProvider! Make sure you added it to your AndroidManifest.xml."); - } + /** + * The display name of the attachment, if any. + *

+ * Value: String + *

+ */ + public final static String DISPLAY_NAME = DATA2; + + /** + * Content-type of the attachment. + *

+ * Value: String + *

+ */ + public final static String FORMAT = DATA3; + + /** + * File size of the attachment or -1 if unknown. + *

+ * Value: Long + *

+ */ + public final static String SIZE = DATA4; + + /** + * A content {@link Uri} that can be used to retrieve the attachment. Sync adapters can set this field if they know how to download the attachment + * without going through the browser. + *

+ * Value: String + *

+ */ + public final static String CONTENT_URI = DATA5; + + } + + + public static interface Attendee extends PropertyColumns + { + /** + * The mime-type of this property. + */ + public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/attendee"; + + /** + * Name of the contact, if known. + *

+ * Value: String + *

+ */ + public final static String NAME = DATA0; + + /** + * Email address of the contact. + *

+ * Value: String + *

+ */ + public final static String EMAIL = DATA1; + + public final static String ROLE = DATA2; + + public final static String STATUS = DATA3; + + public final static String RSVP = DATA4; + } + + + /** + * Categories are immutable. For creation is either the category id or name necessary + */ + public static interface Category extends PropertyColumns + { + /** + * The mime-type of this property. + */ + public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/category"; + + /** + * Row id of the category. + *

+ * Value: Long + *

+ */ + public final static String CATEGORY_ID = DATA0; + + /** + * The name of the category + *

+ * Value: String + *

+ */ + public final static String CATEGORY_NAME = DATA1; + + /** + * The decimal coded color of the category + *

+ * Value: Integer + *

+ *

+ * read-only + *

+ */ + public final static String CATEGORY_COLOR = DATA2; + } + + + public static interface Comment extends PropertyColumns + { + /** + * The mime-type of this property. + */ + public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/comment"; + + /** + * Comment text. + *

+ * Value: String + *

+ */ + public final static String COMMENT = DATA0; + + /** + * Language code of the comment as defined in RFC5646 or null. + *

+ * Value: String + *

+ */ + public final static String LANGUAGE = DATA1; + } + + + public static interface Contact extends PropertyColumns + { + /** + * The mime-type of this property. + */ + public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/contact"; + + public final static String NAME = DATA0; + + public final static String LANGUAGE = DATA1; + } + + + /** + * Relations of a task. + *

+ * When writing a relation, exactly one of {@link #RELATED_ID}, {@link #RELATED_UID} or {@link #RELATED_URI} must be given. {@link #RELATED_CONTENT_URI} + * will be populated automatically if possible. + *

+ */ + public static interface Relation extends PropertyColumns + { + /** + * The mime-type of this property. + */ + public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/relation"; + + /** + * The row id of the related task. May be -1 if the property doesn't refer to a task in this database or if it doesn't refer to a task + * at all. + *

+ * Value: long + *

+ */ + public final static String RELATED_ID = DATA1; + + /** + * The relation type. This must be the ordinal value of a {@link RelType}. + *

+ * Value: int + *

+ */ + public final static String RELATED_TYPE = DATA2; + + /** + * The UID of the related object. + *

+ * Value: String + *

+ */ + public final static String RELATED_UID = DATA3; + + /** + * The URI of a related object. + *

+ * Value: String (URI) + *

+ */ + public final static String RELATED_URI = DATA4; + + /** + * The URI of a related object in another Android content provider. If the object is a task in this database, this is null. If the + * related object is an event or note this field may contain the content URI to the object. + *

+ * Value: String (URI) + *

+ *

+ * This field is read-only. + *

+ */ + public final static String RELATED_CONTENT_URI = DATA5; + + /** + * An optional gap value for temporal relationships. + *

+ * Value: duration string + *

+ */ + public final static String GAP = DATA6; + + + /** + * Valid values for the {@link Relation#RELATED_TYPE} field. Note that the field actually takes the ordinal value of these. + */ + public enum RelType + { + /** + * The related object is the parent of the object owning this relation. + */ + PARENT, + + /** + * The related object is the child of the object owning this relation. + */ + CHILD, + + /** + * The related object is a sibling of the object owning this relation. + */ + SIBLING, + + DEPENDS_ON, + + REFID, + + STRUCTURED_CATEGORY, + + FINISHTOSTART, + + FINISHTOFINISH, + + STARTTOFINISH, + + STARTTOSTART; + } + } + + + public static interface Alarm extends PropertyColumns + { + + public static final int ALARM_TYPE_NOTHING = 0; + + public static final int ALARM_TYPE_MESSAGE = 1; + + public static final int ALARM_TYPE_EMAIL = 2; + + public static final int ALARM_TYPE_SMS = 3; + + public static final int ALARM_TYPE_SOUND = 4; + + public static final int ALARM_REFERENCE_DUE_DATE = 1; + + public static final int ALARM_REFERENCE_START_DATE = 2; + + /** + * The mime-type of this property. + */ + public final static String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/alarm"; + + /** + * Number of minutes from the reference date when the alarm goes off. If the value is < 0 the alarm will go off after the reference date. + *

+ * Value: Integer + *

+ */ + public final static String MINUTES_BEFORE = DATA0; + + /** + * The reference date for the alarm. Either {@link ALARM_REFERENCE_DUE_DATE} or {@link ALARM_REFERENCE_START_DATE}. + *

+ * Value: Integer + *

+ */ + public final static String REFERENCE = DATA1; + + /** + * A message that appears with the alarm. + *

+ * Value: String + *

+ */ + public final static String MESSAGE = DATA2; + + /** + * The type of the alarm. Use the provided alarm types {@link ALARM_TYPE_MESSAGE}, {@link ALARM_TYPE_SOUND}, {@link ALARM_TYPE_NOTHING}, + * {@link ALARM_TYPE_EMAIL} and {@link ALARM_TYPE_SMS}. + *

+ * Value: Integer + *

+ */ + public final static String ALARM_TYPE = DATA3; + } + + } + + + private static synchronized UriFactory getUriFactory(String authority) + { + UriFactory uriFactory = sUriFactories.get(authority); + if (uriFactory == null) + { + uriFactory = new UriFactory(authority); + uriFactory.addUri(SyncState.CONTENT_URI_PATH); + uriFactory.addUri(TaskLists.CONTENT_URI_PATH); + uriFactory.addUri(Tasks.CONTENT_URI_PATH); + uriFactory.addUri(Tasks.SEARCH_URI_PATH); + uriFactory.addUri(Instances.CONTENT_URI_PATH); + uriFactory.addUri(Categories.CONTENT_URI_PATH); + uriFactory.addUri(Alarms.CONTENT_URI_PATH); + uriFactory.addUri(Properties.CONTENT_URI_PATH); + sUriFactories.put(authority, uriFactory); + + } + return uriFactory; + } + + + /** + * Returns the authority of the {@link TaskProvider} in the given {@link Context}. + *

+ * TODO: create an Authority class instead that handles everything about authorities. It could replace {@link UriFactory} as well. The Authority class could + * have a generic parameter that identifies the authority provider or contract class. + * + * @param context + * A {@link Context} of an app that contains a {@link TaskProvider}. + * + * @return The authority. + * + * @throws RuntimeException + * if there is no {@link TaskProvider} in that {@link Context}. + */ + public static synchronized String taskAuthority(Context context) + { + String packageName = context.getPackageName(); + if (sAuthorities.containsKey(packageName)) + { + return sAuthorities.get(packageName); + } + + PackageManager packageManager = context.getPackageManager(); + + // first get the PackageInfo of this app. + PackageInfo packageInfo; + try + { + packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS); + } + catch (NameNotFoundException e) + { + throw new RuntimeException("Could not find TaskProvider!", e); + } + + // next scan all providers for TaskProvider + for (ProviderInfo provider : packageInfo.providers) + { + Class providerClass; + try + { + providerClass = Class.forName(provider.name); + } + catch (ClassNotFoundException e) + { + continue; + } + + if (!TaskProvider.class.isAssignableFrom(providerClass)) + { + continue; + } + + sAuthorities.put(packageName, provider.authority); + return provider.authority; + } + throw new RuntimeException("Could not find TaskProvider! Make sure you added it to your AndroidManifest.xml."); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java index 3c459699..18f5608f 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java @@ -17,99 +17,101 @@ package org.dmfs.provider.tasks; -import org.dmfs.provider.tasks.TaskContract.Properties; -import org.dmfs.provider.tasks.TaskContract.Property.Alarm; -import org.dmfs.provider.tasks.TaskContract.Property.Category; -import org.dmfs.provider.tasks.TaskContract.TaskLists; -import org.dmfs.provider.tasks.TaskContract.Tasks; - import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; +import org.dmfs.provider.tasks.TaskContract.Properties; +import org.dmfs.provider.tasks.TaskContract.Property.Alarm; +import org.dmfs.provider.tasks.TaskContract.Property.Category; +import org.dmfs.provider.tasks.TaskContract.TaskLists; +import org.dmfs.provider.tasks.TaskContract.Tasks; + /** * Task database helper takes care of creating and updating the task database, including tables, indices and triggers. - * + * * @author Marten Gajda * @author Tobias Reinsch */ public class TaskDatabaseHelper extends SQLiteOpenHelper { - /** - * Interface of a listener that's called when the database has been created or migrated. - */ - public interface OnDatabaseOperationListener - { - public void onDatabaseCreated(SQLiteDatabase db); + /** + * Interface of a listener that's called when the database has been created or migrated. + */ + public interface OnDatabaseOperationListener + { + public void onDatabaseCreated(SQLiteDatabase db); + public void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion); + } - public void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion); - } - private static final String TAG = "TaskDatabaseHelper"; + private static final String TAG = "TaskDatabaseHelper"; - /** - * The name of our database file. - */ - private static final String DATABASE_NAME = "tasks.db"; + /** + * The name of our database file. + */ + private static final String DATABASE_NAME = "tasks.db"; - /** - * The database version. - */ - static final int DATABASE_VERSION = 16; + /** + * The database version. + */ + static final int DATABASE_VERSION = 16; - /** - * List of all tables we provide. - */ - public interface Tables - { - public static final String LISTS = "Lists"; - public static final String WRITEABLE_LISTS = "Writeable_Lists"; + /** + * List of all tables we provide. + */ + public interface Tables + { + public static final String LISTS = "Lists"; - public static final String TASKS = "Tasks"; + public static final String WRITEABLE_LISTS = "Writeable_Lists"; - public static final String TASKS_VIEW = "Task_View"; + public static final String TASKS = "Tasks"; - public static final String TASKS_PROPERTY_VIEW = "Task_Property_View"; + public static final String TASKS_VIEW = "Task_View"; - public static final String INSTANCES = "Instances"; + public static final String TASKS_PROPERTY_VIEW = "Task_Property_View"; - public static final String INSTANCE_VIEW = "Instance_View"; + public static final String INSTANCES = "Instances"; - public static final String INSTANCE_PROPERTY_VIEW = "Instance_Property_View"; + public static final String INSTANCE_VIEW = "Instance_View"; - public static final String INSTANCE_CATEGORY_VIEW = "Instance_Cagetory_View"; + public static final String INSTANCE_PROPERTY_VIEW = "Instance_Property_View"; - public static final String CATEGORIES = "Categories"; + public static final String INSTANCE_CATEGORY_VIEW = "Instance_Cagetory_View"; - public static final String CATEGORIES_MAPPING = "Categories_Mapping"; + public static final String CATEGORIES = "Categories"; - public static final String PROPERTIES = "Properties"; + public static final String CATEGORIES_MAPPING = "Categories_Mapping"; - public static final String ALARMS = "Alarms"; + public static final String PROPERTIES = "Properties"; - public static final String SYNCSTATE = "SyncState"; - } + public static final String ALARMS = "Alarms"; - /** - * Columns of internal table for the category mapping. - */ - public interface CategoriesMapping - { - public static final String TASK_ID = "task_id"; + public static final String SYNCSTATE = "SyncState"; + } - public static final String CATEGORY_ID = "category_id"; - public static final String PROPERTY_ID = "property_id"; + /** + * Columns of internal table for the category mapping. + */ + public interface CategoriesMapping + { + public static final String TASK_ID = "task_id"; - } + public static final String CATEGORY_ID = "category_id"; - // @formatter:off + public static final String PROPERTY_ID = "property_id"; + + } + + // @formatter:off /** * SQL command to create a view that combines tasks with some data from the list they belong to. @@ -505,286 +507,289 @@ public class TaskDatabaseHelper extends SQLiteOpenHelper // @formatter:on - /** - * Builds a string that creates an index on the given table for the given columns. - * - * @param table - * The table to create the index on. - * @param fields - * The fields to index. - * @return An SQL command string. - */ - public final static String createIndexString(String table, boolean unique, String... fields) - { - if (fields == null || fields.length < 1) - { - throw new IllegalArgumentException("need at least one field to build an index!"); - } - - StringBuffer buffer = new StringBuffer(); - - // Index name is constructed like this: tablename_fields[0]_idx - buffer.append("CREATE "); - if (unique) - { - buffer.append(" UNIQUE "); - } - buffer.append("INDEX "); - buffer.append(table).append("_").append(fields[0]).append("_idx ON "); - buffer.append(table).append(" ("); - buffer.append(fields[0]); - for (int i = 1; i < fields.length; i++) - { - buffer.append(", ").append(fields[i]); - } - buffer.append(");"); - - return buffer.toString(); - } - - private final OnDatabaseOperationListener mListener; - - - TaskDatabaseHelper(Context context, OnDatabaseOperationListener listener) - { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - mListener = listener; - } - - - /** - * Creates the tables, views, triggers and indices. - * - * TODO: move all strings to separate final static variables. - */ - @Override - public void onCreate(SQLiteDatabase db) - { - - // create task list table - db.execSQL(SQL_CREATE_LISTS_TABLE); - - // trigger that removes tasks of a list that has been removed - db.execSQL("CREATE TRIGGER task_list_cleanup_trigger AFTER DELETE ON " + Tables.LISTS + " BEGIN DELETE FROM " + Tables.TASKS + " WHERE " - + TaskContract.Tasks.LIST_ID + "= old." + TaskContract.TaskLists._ID + "; END"); - - // create task table - db.execSQL(SQL_CREATE_TASKS_TABLE); - - // trigger that marks a list as dirty if a task in that list gets marked as dirty or deleted - db.execSQL("CREATE TRIGGER task_list_make_dirty_on_update AFTER UPDATE ON " + Tables.TASKS + " BEGIN UPDATE " + Tables.LISTS + " SET " - + TaskContract.TaskLists._DIRTY + "=" + TaskContract.TaskLists._DIRTY + " + " + "new." + TaskContract.Tasks._DIRTY + " + " + "new." - + TaskContract.Tasks._DELETED + " WHERE " + TaskContract.TaskLists._ID + "= new." + TaskContract.Tasks.LIST_ID + "; END"); - - // trigger that marks a list as dirty if a task in that list gets marked as dirty or deleted - db.execSQL("CREATE TRIGGER task_list_make_dirty_on_insert AFTER INSERT ON " + Tables.TASKS + " BEGIN UPDATE " + Tables.LISTS + " SET " - + TaskContract.TaskLists._DIRTY + "=" + TaskContract.TaskLists._DIRTY + " + " + "new." + TaskContract.Tasks._DIRTY + " + " + "new." - + TaskContract.Tasks._DELETED + " WHERE " + TaskContract.TaskLists._ID + "= new." + TaskContract.Tasks.LIST_ID + "; END"); - - // create instances table and view - db.execSQL(SQL_CREATE_INSTANCES_TABLE); - - // create categories table - db.execSQL(SQL_CREATE_CATEGORIES_TABLE); - - // create categories mapping table - db.execSQL(SQL_CREATE_CATEGORIES_MAPPING_TABLE); - - // create alarms table - db.execSQL(SQL_CREATE_ALARMS_TABLE); - - // create properties table - db.execSQL(SQL_CREATE_PROPERTIES_TABLE); - - // create syncstate table - db.execSQL(SQL_CREATE_SYNCSTATE_TABLE); - - // create views - db.execSQL(SQL_CREATE_TASK_VIEW); - db.execSQL(SQL_CREATE_TASK_PROPERTY_VIEW); - db.execSQL(SQL_CREATE_INSTANCE_VIEW); - db.execSQL(SQL_CREATE_INSTANCE_PROPERTY_VIEW); - db.execSQL(SQL_CREATE_INSTANCE_CATEGORY_VIEW); - - // create indices - db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.TASK_ID, TaskContract.Instances.INSTANCE_START, - TaskContract.Instances.INSTANCE_DUE)); - db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_START_SORTING)); - db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_DUE_SORTING)); - db.execSQL(createIndexString(Tables.LISTS, false, TaskContract.TaskLists.ACCOUNT_NAME, // not sure if necessary - TaskContract.TaskLists.ACCOUNT_TYPE)); - db.execSQL(createIndexString(Tables.TASKS, false, TaskContract.Tasks.STATUS, TaskContract.Tasks.LIST_ID, TaskContract.Tasks._SYNC_ID)); - db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.MIMETYPE, TaskContract.Properties.TASK_ID)); - db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.TASK_ID)); - db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.ACCOUNT_NAME, TaskContract.Categories.ACCOUNT_TYPE, - TaskContract.Categories.NAME)); - db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.NAME)); - db.execSQL(createIndexString(Tables.SYNCSTATE, true, TaskContract.SyncState.ACCOUNT_NAME, TaskContract.SyncState.ACCOUNT_TYPE)); - - // trigger that removes properties of a task that has been removed - db.execSQL(SQL_CREATE_TASKS_CLEANUP_TRIGGER); - - // trigger that removes alarms when an alarm property was deleted - db.execSQL(SQL_CREATE_ALARM_PROPERTY_CLEANUP_TRIGGER); - - // trigger that removes tasks when a list was removed - db.execSQL(SQL_CREATE_LISTS_CLEANUP_TRIGGER); - - // trigger that counts the alarms for tasks - db.execSQL(SQL_CREATE_ALARM_COUNT_CREATE_TRIGGER); - db.execSQL(SQL_CREATE_ALARM_COUNT_UPDATE_TRIGGER); - db.execSQL(SQL_CREATE_ALARM_COUNT_DELETE_TRIGGER); - - // add cleanup trigger for orphaned properties - db.execSQL(SQL_CREATE_TASK_PROPERTY_CLEANUP_TRIGGER); - - // initialize FTS - FTSDatabaseHelper.onCreate(db); - - if (mListener != null) - { - mListener.onDatabaseCreated(db); - } - } - - - /** - * Manages the database schema migration. - */ - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) - { - Log.i(TAG, "updgrading db from " + oldVersion + " to " + newVersion); - if (oldVersion < 2) - { - // add IS_NEW and IS_CLOSED columns and update their values - db.execSQL("ALTER TABLE " + Tables.TASKS + " ADD COLUMN " + TaskContract.Tasks.IS_NEW + " INTEGER"); - db.execSQL("ALTER TABLE " + Tables.TASKS + " ADD COLUMN " + TaskContract.Tasks.IS_CLOSED + " INTEGER"); - db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_NEW + " = 1 WHERE " + TaskContract.Tasks.STATUS + " = " - + TaskContract.Tasks.STATUS_NEEDS_ACTION); - db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_NEW + " = 0 WHERE " + TaskContract.Tasks.STATUS + " != " - + TaskContract.Tasks.STATUS_NEEDS_ACTION); - db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_CLOSED + " = 1 WHERE " + TaskContract.Tasks.STATUS + " > " - + TaskContract.Tasks.STATUS_IN_PROCESS); - db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_CLOSED + " = 0 WHERE " + TaskContract.Tasks.STATUS + " <= " - + TaskContract.Tasks.STATUS_IN_PROCESS); - } - - if (oldVersion < 3) - { - // add instance sortings - db.execSQL("ALTER TABLE " + Tables.INSTANCES + " ADD COLUMN " + TaskContract.Instances.INSTANCE_START_SORTING + " INTEGER"); - db.execSQL("ALTER TABLE " + Tables.INSTANCES + " ADD COLUMN " + TaskContract.Instances.INSTANCE_DUE_SORTING + " INTEGER"); - db.execSQL("UPDATE " + Tables.INSTANCES + " SET " + TaskContract.Instances.INSTANCE_START_SORTING + " = " + TaskContract.Instances.INSTANCE_START - + ", " + TaskContract.Instances.INSTANCE_DUE_SORTING + " = " + TaskContract.Instances.INSTANCE_DUE); - } - if (oldVersion < 4) - { - // drop old view before altering the schema - db.execSQL(SQL_DROP_TASK_VIEW); - db.execSQL(SQL_DROP_INSTANCE_VIEW); - - // change property id column name to work with the left join in task view - db.execSQL(SQL_DROP_TASKS_CLEANUP_TRIGGER); - db.execSQL(SQL_DROP_PROPERTIES_TABLE); - db.execSQL(SQL_CREATE_PROPERTIES_TABLE); - db.execSQL(SQL_CREATE_TASKS_CLEANUP_TRIGGER); - - // create categories mapping table - db.execSQL(SQL_CREATE_CATEGORIES_MAPPING_TABLE); - - // create alarms table - db.execSQL(SQL_CREATE_ALARMS_TABLE); - - // update views - db.execSQL(SQL_CREATE_TASK_VIEW); - db.execSQL(SQL_CREATE_TASK_PROPERTY_VIEW); - db.execSQL(SQL_CREATE_INSTANCE_VIEW); - db.execSQL(SQL_CREATE_INSTANCE_PROPERTY_VIEW); - db.execSQL(SQL_CREATE_INSTANCE_CATEGORY_VIEW); - - // create Indices - db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.MIMETYPE, TaskContract.Properties.TASK_ID)); - db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.TASK_ID)); - db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.ACCOUNT_NAME, TaskContract.Categories.ACCOUNT_TYPE, - TaskContract.Categories.NAME)); - db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.NAME)); - - // add new triggers - db.execSQL(SQL_CREATE_ALARM_PROPERTY_CLEANUP_TRIGGER); - db.execSQL(SQL_CREATE_ALARM_COUNT_CREATE_TRIGGER); - db.execSQL(SQL_CREATE_ALARM_COUNT_UPDATE_TRIGGER); - db.execSQL(SQL_CREATE_ALARM_COUNT_DELETE_TRIGGER); - - } - if (oldVersion < 6) - { - db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.PARENT_ID + " integer;"); - db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.HAS_ALARMS + " integer;"); - db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.SORTING + " text;"); - } - if (oldVersion < 7) - { - db.execSQL(SQL_CREATE_LISTS_CLEANUP_TRIGGER); - } - if (oldVersion < 8) - { - // replace priority 0 by null. We need this to sort the widget properly. Since 0 is the default this is no problem when syncing. - db.execSQL("update " + Tables.TASKS + " set " + Tasks.PRIORITY + "=null where " + Tasks.PRIORITY + "=0;"); - } - if (oldVersion < 9) - { - // add missing column _UID - db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks._UID + " integer;"); - // add cleanup trigger for orphaned properties - db.execSQL(SQL_CREATE_TASK_PROPERTY_CLEANUP_TRIGGER); - } - if (oldVersion < 10) - { - // add property column to categories_mapping table. Since adding a constraint is not supported by SQLite we have to remove and recreate the entire - // table - db.execSQL("drop table " + Tables.CATEGORIES_MAPPING); - db.execSQL(SQL_CREATE_CATEGORIES_MAPPING_TABLE); - db.execSQL(SQL_CREATE_CATEGORY_PROPERTY_CLEANUP_TRIGGER); - } - if (oldVersion < 11) - { - db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.PINNED + " integer;"); - db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.HAS_PROPERTIES + " integer;"); - } - - if (oldVersion < 12) - { - // rename the local account type - ContentValues values = new ContentValues(1); - values.put(TaskLists.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE); - db.update(Tables.LISTS, values, TaskLists.ACCOUNT_TYPE + "=?", new String[] { "LOCAL" }); - } - - if (oldVersion < 13) - { - db.execSQL(SQL_CREATE_SYNCSTATE_TABLE); - } - - if (oldVersion < 14) - { - // create a unique index for account name and account type on the sync state table - db.execSQL(createIndexString(Tables.SYNCSTATE, true, TaskContract.SyncState.ACCOUNT_NAME, TaskContract.SyncState.ACCOUNT_TYPE)); - } - - if (oldVersion < 16) - { - db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_START_SORTING)); - db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_DUE_SORTING)); - } - - // upgrade FTS - FTSDatabaseHelper.onUpgrade(db, oldVersion, newVersion); - - if (mListener != null) - { - mListener.onDatabaseUpdate(db, oldVersion, newVersion); - } - } + /** + * Builds a string that creates an index on the given table for the given columns. + * + * @param table + * The table to create the index on. + * @param fields + * The fields to index. + * + * @return An SQL command string. + */ + public final static String createIndexString(String table, boolean unique, String... fields) + { + if (fields == null || fields.length < 1) + { + throw new IllegalArgumentException("need at least one field to build an index!"); + } + + StringBuffer buffer = new StringBuffer(); + + // Index name is constructed like this: tablename_fields[0]_idx + buffer.append("CREATE "); + if (unique) + { + buffer.append(" UNIQUE "); + } + buffer.append("INDEX "); + buffer.append(table).append("_").append(fields[0]).append("_idx ON "); + buffer.append(table).append(" ("); + buffer.append(fields[0]); + for (int i = 1; i < fields.length; i++) + { + buffer.append(", ").append(fields[i]); + } + buffer.append(");"); + + return buffer.toString(); + + } + + + private final OnDatabaseOperationListener mListener; + + + TaskDatabaseHelper(Context context, OnDatabaseOperationListener listener) + { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + mListener = listener; + } + + + /** + * Creates the tables, views, triggers and indices. + *

+ * TODO: move all strings to separate final static variables. + */ + @Override + public void onCreate(SQLiteDatabase db) + { + + // create task list table + db.execSQL(SQL_CREATE_LISTS_TABLE); + + // trigger that removes tasks of a list that has been removed + db.execSQL("CREATE TRIGGER task_list_cleanup_trigger AFTER DELETE ON " + Tables.LISTS + " BEGIN DELETE FROM " + Tables.TASKS + " WHERE " + + TaskContract.Tasks.LIST_ID + "= old." + TaskContract.TaskLists._ID + "; END"); + + // create task table + db.execSQL(SQL_CREATE_TASKS_TABLE); + + // trigger that marks a list as dirty if a task in that list gets marked as dirty or deleted + db.execSQL("CREATE TRIGGER task_list_make_dirty_on_update AFTER UPDATE ON " + Tables.TASKS + " BEGIN UPDATE " + Tables.LISTS + " SET " + + TaskContract.TaskLists._DIRTY + "=" + TaskContract.TaskLists._DIRTY + " + " + "new." + TaskContract.Tasks._DIRTY + " + " + "new." + + TaskContract.Tasks._DELETED + " WHERE " + TaskContract.TaskLists._ID + "= new." + TaskContract.Tasks.LIST_ID + "; END"); + + // trigger that marks a list as dirty if a task in that list gets marked as dirty or deleted + db.execSQL("CREATE TRIGGER task_list_make_dirty_on_insert AFTER INSERT ON " + Tables.TASKS + " BEGIN UPDATE " + Tables.LISTS + " SET " + + TaskContract.TaskLists._DIRTY + "=" + TaskContract.TaskLists._DIRTY + " + " + "new." + TaskContract.Tasks._DIRTY + " + " + "new." + + TaskContract.Tasks._DELETED + " WHERE " + TaskContract.TaskLists._ID + "= new." + TaskContract.Tasks.LIST_ID + "; END"); + + // create instances table and view + db.execSQL(SQL_CREATE_INSTANCES_TABLE); + + // create categories table + db.execSQL(SQL_CREATE_CATEGORIES_TABLE); + + // create categories mapping table + db.execSQL(SQL_CREATE_CATEGORIES_MAPPING_TABLE); + + // create alarms table + db.execSQL(SQL_CREATE_ALARMS_TABLE); + + // create properties table + db.execSQL(SQL_CREATE_PROPERTIES_TABLE); + + // create syncstate table + db.execSQL(SQL_CREATE_SYNCSTATE_TABLE); + + // create views + db.execSQL(SQL_CREATE_TASK_VIEW); + db.execSQL(SQL_CREATE_TASK_PROPERTY_VIEW); + db.execSQL(SQL_CREATE_INSTANCE_VIEW); + db.execSQL(SQL_CREATE_INSTANCE_PROPERTY_VIEW); + db.execSQL(SQL_CREATE_INSTANCE_CATEGORY_VIEW); + + // create indices + db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.TASK_ID, TaskContract.Instances.INSTANCE_START, + TaskContract.Instances.INSTANCE_DUE)); + db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_START_SORTING)); + db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_DUE_SORTING)); + db.execSQL(createIndexString(Tables.LISTS, false, TaskContract.TaskLists.ACCOUNT_NAME, // not sure if necessary + TaskContract.TaskLists.ACCOUNT_TYPE)); + db.execSQL(createIndexString(Tables.TASKS, false, TaskContract.Tasks.STATUS, TaskContract.Tasks.LIST_ID, TaskContract.Tasks._SYNC_ID)); + db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.MIMETYPE, TaskContract.Properties.TASK_ID)); + db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.TASK_ID)); + db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.ACCOUNT_NAME, TaskContract.Categories.ACCOUNT_TYPE, + TaskContract.Categories.NAME)); + db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.NAME)); + db.execSQL(createIndexString(Tables.SYNCSTATE, true, TaskContract.SyncState.ACCOUNT_NAME, TaskContract.SyncState.ACCOUNT_TYPE)); + + // trigger that removes properties of a task that has been removed + db.execSQL(SQL_CREATE_TASKS_CLEANUP_TRIGGER); + + // trigger that removes alarms when an alarm property was deleted + db.execSQL(SQL_CREATE_ALARM_PROPERTY_CLEANUP_TRIGGER); + + // trigger that removes tasks when a list was removed + db.execSQL(SQL_CREATE_LISTS_CLEANUP_TRIGGER); + + // trigger that counts the alarms for tasks + db.execSQL(SQL_CREATE_ALARM_COUNT_CREATE_TRIGGER); + db.execSQL(SQL_CREATE_ALARM_COUNT_UPDATE_TRIGGER); + db.execSQL(SQL_CREATE_ALARM_COUNT_DELETE_TRIGGER); + + // add cleanup trigger for orphaned properties + db.execSQL(SQL_CREATE_TASK_PROPERTY_CLEANUP_TRIGGER); + + // initialize FTS + FTSDatabaseHelper.onCreate(db); + + if (mListener != null) + { + mListener.onDatabaseCreated(db); + } + } + + + /** + * Manages the database schema migration. + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) + { + Log.i(TAG, "updgrading db from " + oldVersion + " to " + newVersion); + if (oldVersion < 2) + { + // add IS_NEW and IS_CLOSED columns and update their values + db.execSQL("ALTER TABLE " + Tables.TASKS + " ADD COLUMN " + TaskContract.Tasks.IS_NEW + " INTEGER"); + db.execSQL("ALTER TABLE " + Tables.TASKS + " ADD COLUMN " + TaskContract.Tasks.IS_CLOSED + " INTEGER"); + db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_NEW + " = 1 WHERE " + TaskContract.Tasks.STATUS + " = " + + TaskContract.Tasks.STATUS_NEEDS_ACTION); + db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_NEW + " = 0 WHERE " + TaskContract.Tasks.STATUS + " != " + + TaskContract.Tasks.STATUS_NEEDS_ACTION); + db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_CLOSED + " = 1 WHERE " + TaskContract.Tasks.STATUS + " > " + + TaskContract.Tasks.STATUS_IN_PROCESS); + db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_CLOSED + " = 0 WHERE " + TaskContract.Tasks.STATUS + " <= " + + TaskContract.Tasks.STATUS_IN_PROCESS); + } + + if (oldVersion < 3) + { + // add instance sortings + db.execSQL("ALTER TABLE " + Tables.INSTANCES + " ADD COLUMN " + TaskContract.Instances.INSTANCE_START_SORTING + " INTEGER"); + db.execSQL("ALTER TABLE " + Tables.INSTANCES + " ADD COLUMN " + TaskContract.Instances.INSTANCE_DUE_SORTING + " INTEGER"); + db.execSQL("UPDATE " + Tables.INSTANCES + " SET " + TaskContract.Instances.INSTANCE_START_SORTING + " = " + TaskContract.Instances.INSTANCE_START + + ", " + TaskContract.Instances.INSTANCE_DUE_SORTING + " = " + TaskContract.Instances.INSTANCE_DUE); + } + if (oldVersion < 4) + { + // drop old view before altering the schema + db.execSQL(SQL_DROP_TASK_VIEW); + db.execSQL(SQL_DROP_INSTANCE_VIEW); + + // change property id column name to work with the left join in task view + db.execSQL(SQL_DROP_TASKS_CLEANUP_TRIGGER); + db.execSQL(SQL_DROP_PROPERTIES_TABLE); + db.execSQL(SQL_CREATE_PROPERTIES_TABLE); + db.execSQL(SQL_CREATE_TASKS_CLEANUP_TRIGGER); + + // create categories mapping table + db.execSQL(SQL_CREATE_CATEGORIES_MAPPING_TABLE); + + // create alarms table + db.execSQL(SQL_CREATE_ALARMS_TABLE); + + // update views + db.execSQL(SQL_CREATE_TASK_VIEW); + db.execSQL(SQL_CREATE_TASK_PROPERTY_VIEW); + db.execSQL(SQL_CREATE_INSTANCE_VIEW); + db.execSQL(SQL_CREATE_INSTANCE_PROPERTY_VIEW); + db.execSQL(SQL_CREATE_INSTANCE_CATEGORY_VIEW); + + // create Indices + db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.MIMETYPE, TaskContract.Properties.TASK_ID)); + db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.TASK_ID)); + db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.ACCOUNT_NAME, TaskContract.Categories.ACCOUNT_TYPE, + TaskContract.Categories.NAME)); + db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.NAME)); + + // add new triggers + db.execSQL(SQL_CREATE_ALARM_PROPERTY_CLEANUP_TRIGGER); + db.execSQL(SQL_CREATE_ALARM_COUNT_CREATE_TRIGGER); + db.execSQL(SQL_CREATE_ALARM_COUNT_UPDATE_TRIGGER); + db.execSQL(SQL_CREATE_ALARM_COUNT_DELETE_TRIGGER); + + } + if (oldVersion < 6) + { + db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.PARENT_ID + " integer;"); + db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.HAS_ALARMS + " integer;"); + db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.SORTING + " text;"); + } + if (oldVersion < 7) + { + db.execSQL(SQL_CREATE_LISTS_CLEANUP_TRIGGER); + } + if (oldVersion < 8) + { + // replace priority 0 by null. We need this to sort the widget properly. Since 0 is the default this is no problem when syncing. + db.execSQL("update " + Tables.TASKS + " set " + Tasks.PRIORITY + "=null where " + Tasks.PRIORITY + "=0;"); + } + if (oldVersion < 9) + { + // add missing column _UID + db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks._UID + " integer;"); + // add cleanup trigger for orphaned properties + db.execSQL(SQL_CREATE_TASK_PROPERTY_CLEANUP_TRIGGER); + } + if (oldVersion < 10) + { + // add property column to categories_mapping table. Since adding a constraint is not supported by SQLite we have to remove and recreate the entire + // table + db.execSQL("drop table " + Tables.CATEGORIES_MAPPING); + db.execSQL(SQL_CREATE_CATEGORIES_MAPPING_TABLE); + db.execSQL(SQL_CREATE_CATEGORY_PROPERTY_CLEANUP_TRIGGER); + } + if (oldVersion < 11) + { + db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.PINNED + " integer;"); + db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.HAS_PROPERTIES + " integer;"); + } + + if (oldVersion < 12) + { + // rename the local account type + ContentValues values = new ContentValues(1); + values.put(TaskLists.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE); + db.update(Tables.LISTS, values, TaskLists.ACCOUNT_TYPE + "=?", new String[] { "LOCAL" }); + } + + if (oldVersion < 13) + { + db.execSQL(SQL_CREATE_SYNCSTATE_TABLE); + } + + if (oldVersion < 14) + { + // create a unique index for account name and account type on the sync state table + db.execSQL(createIndexString(Tables.SYNCSTATE, true, TaskContract.SyncState.ACCOUNT_NAME, TaskContract.SyncState.ACCOUNT_TYPE)); + } + + if (oldVersion < 16) + { + db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_START_SORTING)); + db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_DUE_SORTING)); + } + + // upgrade FTS + FTSDatabaseHelper.onUpgrade(db, oldVersion, newVersion); + + if (mListener != null) + { + mListener.onDatabaseUpdate(db, oldVersion, newVersion); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java index 42adb411..c28751c7 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java @@ -17,12 +17,33 @@ package org.dmfs.provider.tasks; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map.Entry; -import java.util.Set; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.OnAccountsUpdateListener; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.UriMatcher; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.text.TextUtils; import org.dmfs.provider.tasks.TaskContract.Alarms; import org.dmfs.provider.tasks.TaskContract.Categories; @@ -57,1295 +78,1287 @@ import org.dmfs.provider.tasks.processors.tasks.TaskExecutionProcessor; import org.dmfs.provider.tasks.processors.tasks.TaskInstancesProcessor; import org.dmfs.provider.tasks.processors.tasks.TaskValidatorProcessor; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.OnAccountsUpdateListener; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.UriMatcher; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ProviderInfo; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.text.TextUtils; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; /** * The provider for tasks. - * + *

* TODO: add support for recurring tasks - * + *

* TODO: add support for reminders - * + *

* TODO: add support for attendees - * + *

* TODO: refactor the selection stuff - * + * * @author Marten Gajda * @author Tobias Reinsch - * */ public final class TaskProvider extends SQLiteContentProvider implements OnAccountsUpdateListener, OnDatabaseOperationListener { - private static final int LISTS = 1; - private static final int LIST_ID = 2; - private static final int TASKS = 101; - private static final int TASK_ID = 102; - private static final int INSTANCES = 103; - private static final int INSTANCE_ID = 104; - private static final int CATEGORIES = 1001; - private static final int CATEGORY_ID = 1002; - private static final int PROPERTIES = 1003; - private static final int PROPERTY_ID = 1004; - private static final int ALARMS = 1005; - private static final int ALARM_ID = 1006; - private static final int SEARCH = 1007; - private static final int SYNCSTATE = 1008; - private static final int SYNCSTATE_ID = 1009; + private static final int LISTS = 1; + private static final int LIST_ID = 2; + private static final int TASKS = 101; + private static final int TASK_ID = 102; + private static final int INSTANCES = 103; + private static final int INSTANCE_ID = 104; + private static final int CATEGORIES = 1001; + private static final int CATEGORY_ID = 1002; + private static final int PROPERTIES = 1003; + private static final int PROPERTY_ID = 1004; + private static final int ALARMS = 1005; + private static final int ALARM_ID = 1006; + private static final int SEARCH = 1007; + private static final int SYNCSTATE = 1008; + private static final int SYNCSTATE_ID = 1009; - private static final int OPERATIONS = 100000; + private static final int OPERATIONS = 100000; - private final static Set TASK_LIST_SYNC_COLUMNS = new HashSet(Arrays.asList(TaskLists.SYNC_ADAPTER_COLUMNS)); + private final static Set TASK_LIST_SYNC_COLUMNS = new HashSet(Arrays.asList(TaskLists.SYNC_ADAPTER_COLUMNS)); - /** - * A list of {@link TaskProcessor}s to execute when doing operations on the tasks table. - */ - private List> mTaskProcessors = new ArrayList>(16); + /** + * A list of {@link TaskProcessor}s to execute when doing operations on the tasks table. + */ + private List> mTaskProcessors = new ArrayList>(16); - /** - * A list of {@link ListProcessor}s to execute when doing operations on the task lists table. - */ - private List> mListProcessors = new ArrayList>(8); + /** + * A list of {@link ListProcessor}s to execute when doing operations on the task lists table. + */ + private List> mListProcessors = new ArrayList>(8); - /** - * Our authority. - */ - String mAuthority; - - /** - * The {@link UriMatcher} we use. - */ - private UriMatcher mUriMatcher; - - /** - * A handler to execute asynchronous jobs. - */ - Handler mAsyncHandler; - - /** - * An {@link ProviderOperationsLog} to track all changes within a transaction. - */ - private ProviderOperationsLog mOperationsLog = new ProviderOperationsLog(); - - - @Override - public boolean onCreate() - { - ProviderInfo providerInfo = getProviderInfo(); - - mAuthority = providerInfo.authority; - - mTaskProcessors.add(new TaskValidatorProcessor()); - mTaskProcessors.add(new AutoUpdateProcessor()); - mTaskProcessors.add(new RelationProcessor()); - mTaskProcessors.add(new TaskInstancesProcessor()); - mTaskProcessors.add(new FtsProcessor()); - mTaskProcessors.add(new ChangeListProcessor()); - mTaskProcessors.add(new TaskExecutionProcessor()); - - mListProcessors.add(new ListValidatorProcessor()); - mListProcessors.add(new ListExecutionProcessor()); - - mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - mUriMatcher.addURI(mAuthority, TaskContract.TaskLists.CONTENT_URI_PATH, LISTS); - - mUriMatcher.addURI(mAuthority, TaskContract.TaskLists.CONTENT_URI_PATH + "/#", LIST_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Tasks.CONTENT_URI_PATH, TASKS); - mUriMatcher.addURI(mAuthority, TaskContract.Tasks.CONTENT_URI_PATH + "/#", TASK_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Instances.CONTENT_URI_PATH, INSTANCES); - mUriMatcher.addURI(mAuthority, TaskContract.Instances.CONTENT_URI_PATH + "/#", INSTANCE_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Properties.CONTENT_URI_PATH, PROPERTIES); - mUriMatcher.addURI(mAuthority, TaskContract.Properties.CONTENT_URI_PATH + "/#", PROPERTY_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Categories.CONTENT_URI_PATH, CATEGORIES); - mUriMatcher.addURI(mAuthority, TaskContract.Categories.CONTENT_URI_PATH + "/#", CATEGORY_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Alarms.CONTENT_URI_PATH, ALARMS); - mUriMatcher.addURI(mAuthority, TaskContract.Alarms.CONTENT_URI_PATH + "/#", ALARM_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Tasks.SEARCH_URI_PATH, SEARCH); - - mUriMatcher.addURI(mAuthority, TaskContract.SyncState.CONTENT_URI_PATH, SYNCSTATE); - mUriMatcher.addURI(mAuthority, TaskContract.SyncState.CONTENT_URI_PATH + "/#", SYNCSTATE_ID); - - ContentOperation.register(mUriMatcher, mAuthority, OPERATIONS); - - boolean result = super.onCreate(); - - // create a HandlerThread to perform async operations - HandlerThread thread = new HandlerThread("backgroundHandler"); - thread.start(); - mAsyncHandler = new Handler(thread.getLooper()); - - AccountManager accountManager = AccountManager.get(getContext()); - accountManager.addOnAccountsUpdatedListener(this, mAsyncHandler, true); - - updateNotifications(); - - return result; - } - - - /** - * Return true if the caller is a sync adapter (i.e. if the Uri contains the query parameter {@link TaskContract#CALLER_IS_SYNCADAPTER} and its value is - * true). - * - * @param uri - * The {@link Uri} to check. - * @return true if the caller pretends to be a sync adapter, false otherwise. - */ - @Override - public boolean isCallerSyncAdapter(Uri uri) - { - String param = uri.getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER); - return param != null && !"false".equals(param); - } - - - /** - * Return true if the URI indicates to a load extended properties with {@link TaskContract#LOAD_PROPERTIES}. - * - * @param uri - * The {@link Uri} to check. - * @return true if the URI requests to load extended properties, false otherwise. - */ - public boolean shouldLoadProperties(Uri uri) - { - String param = uri.getQueryParameter(TaskContract.LOAD_PROPERTIES); - return param != null && !"false".equals(param); - } - - - /** - * Get the account name from the given {@link Uri}. - * - * @param uri - * The Uri to check. - * @return The account name or null if no account name has been specified. - */ - protected String getAccountName(Uri uri) - { - return uri.getQueryParameter(TaskContract.ACCOUNT_NAME); - } - - - /** - * Get the account type from the given {@link Uri}. - * - * @param uri - * The Uri to check. - * @return The account type or null if no account type has been specified. - */ - protected String getAccountType(Uri uri) - { - return uri.getQueryParameter(TaskContract.ACCOUNT_TYPE); - } - - - /** - * Get any id from the given {@link Uri}. - * - * @param uri - * The Uri. - * @return The last path segment (which should contain the id). - */ - private long getId(Uri uri) - { - return Long.parseLong(uri.getPathSegments().get(1)); - } - - - /** - * Build a selection string that selects the account specified in uri. - * - * @param uri - * A {@link Uri} that specifies an account. - * @return A {@link StringBuilder} with a selection string for the account. - */ - protected StringBuilder selectAccount(Uri uri) - { - StringBuilder sb = new StringBuilder(256); - return selectAccount(sb, uri); - } - - - /** - * Append the selection of the account specified in uri to the {@link StringBuilder} sb. - * - * @param sb - * A {@link StringBuilder} that the selection is appended to. - * @param uri - * A {@link Uri} that specifies an account. - * @return sb. - */ - protected StringBuilder selectAccount(StringBuilder sb, Uri uri) - { - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - - if (accountName != null || accountType != null) - { - - if (accountName != null) - { - if (sb.length() > 0) - { - sb.append(" AND "); - } - - sb.append(TaskListSyncColumns.ACCOUNT_NAME); - sb.append("="); - DatabaseUtils.appendEscapedSQLString(sb, accountName); - } - if (accountType != null) - { - - if (sb.length() > 0) - { - sb.append(" AND "); - } - - sb.append(TaskListSyncColumns.ACCOUNT_TYPE); - sb.append("="); - DatabaseUtils.appendEscapedSQLString(sb, accountType); - } - } - return sb; - } - - - /** - * Append the selection of the account specified in uri to the an {@link SQLiteQueryBuilder}. - * - * @param sqlBuilder - * A {@link SQLiteQueryBuilder} that the selection is appended to. - * @param uri - * A {@link Uri} that specifies an account. - */ - protected void selectAccount(SQLiteQueryBuilder sqlBuilder, Uri uri) - { - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - - if (accountName != null) - { - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(TaskListSyncColumns.ACCOUNT_NAME); - sqlBuilder.appendWhere("="); - sqlBuilder.appendWhereEscapeString(accountName); - } - if (accountType != null) - { - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(TaskListSyncColumns.ACCOUNT_TYPE); - sqlBuilder.appendWhere("="); - sqlBuilder.appendWhereEscapeString(accountType); - } - } - - - private StringBuilder _selectId(StringBuilder sb, long id, String key) - { - if (sb.length() > 0) - { - sb.append(" AND "); - } - sb.append(key); - sb.append("="); - sb.append(id); - return sb; - } - - - protected StringBuilder selectId(Uri uri) - { - StringBuilder sb = new StringBuilder(128); - return selectId(sb, uri); - } - - - protected StringBuilder selectId(StringBuilder sb, Uri uri) - { - return _selectId(sb, getId(uri), TaskListColumns._ID); - } - - - protected StringBuilder selectTaskId(Uri uri) - { - StringBuilder sb = new StringBuilder(128); - return selectTaskId(sb, uri); - } - - - protected StringBuilder selectTaskId(long id) - { - StringBuilder sb = new StringBuilder(128); - return selectTaskId(sb, id); - } - - - protected StringBuilder selectTaskId(StringBuilder sb, Uri uri) - { - return selectTaskId(sb, getId(uri)); - } - - - protected StringBuilder selectTaskId(StringBuilder sb, long id) - { - return _selectId(sb, id, Instances.TASK_ID); - - } - - - protected StringBuilder selectPropertyId(Uri uri) - { - StringBuilder sb = new StringBuilder(128); - return selectPropertyId(sb, uri); - } - - - protected StringBuilder selectPropertyId(StringBuilder sb, Uri uri) - { - return selectPropertyId(sb, getId(uri)); - } - - - protected StringBuilder selectPropertyId(long id) - { - StringBuilder sb = new StringBuilder(128); - return selectPropertyId(sb, id); - } - - - protected StringBuilder selectPropertyId(StringBuilder sb, long id) - { - return _selectId(sb, id, PropertyColumns.PROPERTY_ID); - } - - - /** - * Add a selection by ID to the given {@link SQLiteQueryBuilder}. The id is taken from the given Uri. - * - * @param sqlBuilder - * The {@link SQLiteQueryBuilder} to append the selection to. - * @param idColumn - * The column that must match the id. - * @param uri - * An {@link Uri} that contains the id. - */ - protected void selectId(SQLiteQueryBuilder sqlBuilder, String idColumn, Uri uri) - { - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(idColumn); - sqlBuilder.appendWhere("="); - sqlBuilder.appendWhere(String.valueOf(getId(uri))); - } - - - /** - * Append any arbitrary selection string to the selection in sb - * - * @param sb - * A {@link StringBuilder} that already contains a selection string. - * @param selection - * A valid SQL selection string. - * @return A string with the final selection. - */ - protected String updateSelection(StringBuilder sb, String selection) - { - if (selection != null) - { - if (sb.length() > 0) - { - sb.append("AND ( ").append(selection).append(" ) "); - } - else - { - sb.append(" ( ").append(selection).append(" ) "); - } - } - return sb.toString(); - } - - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) - { - final SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); - SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder(); - // initialize appendWhere, this allows us to append all other selections with a preceding "AND" - sqlBuilder.appendWhere(" 1=1 "); - boolean isSyncAdapter = isCallerSyncAdapter(uri); - - switch (mUriMatcher.match(uri)) - { - case SYNCSTATE_ID: - // the id is ignored, we only match by account type and name given in the Uri - case SYNCSTATE: - { - if (TextUtils.isEmpty(getAccountName(uri)) || TextUtils.isEmpty(getAccountType(uri))) - { - throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); - } - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.SYNCSTATE); - break; - } - case LISTS: - // add account to selection if any - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.LISTS); - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER; - } - break; - - case LIST_ID: - // add account to selection if any - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.LISTS); - selectId(sqlBuilder, TaskListColumns._ID, uri); - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER; - } - break; - - case TASKS: - if (shouldLoadProperties(uri)) - { - // extended properties were requested, therefore change to task view that includes these properties - sqlBuilder.setTables(Tables.TASKS_PROPERTY_VIEW); - } - else - { - sqlBuilder.setTables(Tables.TASKS_VIEW); - } - if (!isSyncAdapter) - { - // do not return deleted rows if caller is not a sync adapter - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(Tasks._DELETED); - sqlBuilder.appendWhere("=0"); - } - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Tasks.DEFAULT_SORT_ORDER; - } - break; - - case TASK_ID: - if (shouldLoadProperties(uri)) - { - // extended properties were requested, therefore change to task view that includes these properties - sqlBuilder.setTables(Tables.TASKS_PROPERTY_VIEW); - } - else - { - sqlBuilder.setTables(Tables.TASKS_VIEW); - } - selectId(sqlBuilder, TaskColumns._ID, uri); - if (!isSyncAdapter) - { - // do not return deleted rows if caller is not a sync adapter - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(Tasks._DELETED); - sqlBuilder.appendWhere("=0"); - } - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Tasks.DEFAULT_SORT_ORDER; - } - break; - - case INSTANCES: - if (shouldLoadProperties(uri)) - { - // extended properties were requested, therefore change to instance view that includes these properties - sqlBuilder.setTables(Tables.INSTANCE_PROPERTY_VIEW); - } - else - { - sqlBuilder.setTables(Tables.INSTANCE_VIEW); - } - if (!isSyncAdapter) - { - // do not return deleted rows if caller is not a sync adapter - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(Tasks._DELETED); - sqlBuilder.appendWhere("=0"); - } - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Instances.DEFAULT_SORT_ORDER; - } - break; - - case INSTANCE_ID: - if (shouldLoadProperties(uri)) - { - // extended properties were requested, therefore change to instance view that includes these properties - sqlBuilder.setTables(Tables.INSTANCE_PROPERTY_VIEW); - } - else - { - sqlBuilder.setTables(Tables.INSTANCE_VIEW); - } - selectId(sqlBuilder, Instances._ID, uri); - if (!isSyncAdapter) - { - // do not return deleted rows if caller is not a sync adapter - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(Tasks._DELETED); - sqlBuilder.appendWhere("=0"); - } - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Instances.DEFAULT_SORT_ORDER; - } - break; - - case CATEGORIES: - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.CATEGORIES); - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Categories.DEFAULT_SORT_ORDER; - } - break; - - case CATEGORY_ID: - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.CATEGORIES); - selectId(sqlBuilder, CategoriesColumns._ID, uri); - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Categories.DEFAULT_SORT_ORDER; - } - break; - - case PROPERTIES: - sqlBuilder.setTables(Tables.PROPERTIES); - break; - - case PROPERTY_ID: - sqlBuilder.setTables(Tables.PROPERTIES); - selectId(sqlBuilder, PropertyColumns.PROPERTY_ID, uri); - break; - - case SEARCH: - String searchString = uri.getQueryParameter(Tasks.SEARCH_QUERY_PARAMETER); - searchString = Uri.decode(searchString); - Cursor searchCursor = FTSDatabaseHelper.getTaskSearchCursor(db, searchString, projection, selection, selectionArgs, sortOrder); - if (searchCursor != null) - { - // attach tasks uri for notifications, that way the search results are updated when a task changes - searchCursor.setNotificationUri(getContext().getContentResolver(), Tasks.getContentUri(mAuthority)); - } - return searchCursor; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - Cursor c = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); - - if (c != null) - { - c.setNotificationUri(getContext().getContentResolver(), uri); - } - return c; - } - - - @Override - public int deleteInTransaction(final SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs, final boolean isSyncAdapter) - { - int count = 0; - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - - switch (mUriMatcher.match(uri)) - { - case SYNCSTATE_ID: - // the id is ignored, we only match by account type and name given in the Uri - case SYNCSTATE: - { - if (!isSyncAdapter) - { - throw new IllegalAccessError("only sync adapters may access syncstate"); - } - if (TextUtils.isEmpty(getAccountName(uri)) || TextUtils.isEmpty(getAccountType(uri))) - { - throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); - } - selection = updateSelection(selectAccount(uri), selection); - count = db.delete(Tables.SYNCSTATE, selection, selectionArgs); - break; - } - /* - * Deleting task lists is only allowed to sync adapters. They must provide ACCOUNT_NAME and ACCOUNT_TYPE. + /** + * Our authority. + */ + String mAuthority; + + /** + * The {@link UriMatcher} we use. + */ + private UriMatcher mUriMatcher; + + /** + * A handler to execute asynchronous jobs. + */ + Handler mAsyncHandler; + + /** + * An {@link ProviderOperationsLog} to track all changes within a transaction. + */ + private ProviderOperationsLog mOperationsLog = new ProviderOperationsLog(); + + + @Override + public boolean onCreate() + { + ProviderInfo providerInfo = getProviderInfo(); + + mAuthority = providerInfo.authority; + + mTaskProcessors.add(new TaskValidatorProcessor()); + mTaskProcessors.add(new AutoUpdateProcessor()); + mTaskProcessors.add(new RelationProcessor()); + mTaskProcessors.add(new TaskInstancesProcessor()); + mTaskProcessors.add(new FtsProcessor()); + mTaskProcessors.add(new ChangeListProcessor()); + mTaskProcessors.add(new TaskExecutionProcessor()); + + mListProcessors.add(new ListValidatorProcessor()); + mListProcessors.add(new ListExecutionProcessor()); + + mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mUriMatcher.addURI(mAuthority, TaskContract.TaskLists.CONTENT_URI_PATH, LISTS); + + mUriMatcher.addURI(mAuthority, TaskContract.TaskLists.CONTENT_URI_PATH + "/#", LIST_ID); + + mUriMatcher.addURI(mAuthority, TaskContract.Tasks.CONTENT_URI_PATH, TASKS); + mUriMatcher.addURI(mAuthority, TaskContract.Tasks.CONTENT_URI_PATH + "/#", TASK_ID); + + mUriMatcher.addURI(mAuthority, TaskContract.Instances.CONTENT_URI_PATH, INSTANCES); + mUriMatcher.addURI(mAuthority, TaskContract.Instances.CONTENT_URI_PATH + "/#", INSTANCE_ID); + + mUriMatcher.addURI(mAuthority, TaskContract.Properties.CONTENT_URI_PATH, PROPERTIES); + mUriMatcher.addURI(mAuthority, TaskContract.Properties.CONTENT_URI_PATH + "/#", PROPERTY_ID); + + mUriMatcher.addURI(mAuthority, TaskContract.Categories.CONTENT_URI_PATH, CATEGORIES); + mUriMatcher.addURI(mAuthority, TaskContract.Categories.CONTENT_URI_PATH + "/#", CATEGORY_ID); + + mUriMatcher.addURI(mAuthority, TaskContract.Alarms.CONTENT_URI_PATH, ALARMS); + mUriMatcher.addURI(mAuthority, TaskContract.Alarms.CONTENT_URI_PATH + "/#", ALARM_ID); + + mUriMatcher.addURI(mAuthority, TaskContract.Tasks.SEARCH_URI_PATH, SEARCH); + + mUriMatcher.addURI(mAuthority, TaskContract.SyncState.CONTENT_URI_PATH, SYNCSTATE); + mUriMatcher.addURI(mAuthority, TaskContract.SyncState.CONTENT_URI_PATH + "/#", SYNCSTATE_ID); + + ContentOperation.register(mUriMatcher, mAuthority, OPERATIONS); + + boolean result = super.onCreate(); + + // create a HandlerThread to perform async operations + HandlerThread thread = new HandlerThread("backgroundHandler"); + thread.start(); + mAsyncHandler = new Handler(thread.getLooper()); + + AccountManager accountManager = AccountManager.get(getContext()); + accountManager.addOnAccountsUpdatedListener(this, mAsyncHandler, true); + + updateNotifications(); + + return result; + } + + + /** + * Return true if the caller is a sync adapter (i.e. if the Uri contains the query parameter {@link TaskContract#CALLER_IS_SYNCADAPTER} and its value is + * true). + * + * @param uri + * The {@link Uri} to check. + * + * @return true if the caller pretends to be a sync adapter, false otherwise. + */ + @Override + public boolean isCallerSyncAdapter(Uri uri) + { + String param = uri.getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER); + return param != null && !"false".equals(param); + } + + + /** + * Return true if the URI indicates to a load extended properties with {@link TaskContract#LOAD_PROPERTIES}. + * + * @param uri + * The {@link Uri} to check. + * + * @return true if the URI requests to load extended properties, false otherwise. + */ + public boolean shouldLoadProperties(Uri uri) + { + String param = uri.getQueryParameter(TaskContract.LOAD_PROPERTIES); + return param != null && !"false".equals(param); + } + + + /** + * Get the account name from the given {@link Uri}. + * + * @param uri + * The Uri to check. + * + * @return The account name or null if no account name has been specified. + */ + protected String getAccountName(Uri uri) + { + return uri.getQueryParameter(TaskContract.ACCOUNT_NAME); + } + + + /** + * Get the account type from the given {@link Uri}. + * + * @param uri + * The Uri to check. + * + * @return The account type or null if no account type has been specified. + */ + protected String getAccountType(Uri uri) + { + return uri.getQueryParameter(TaskContract.ACCOUNT_TYPE); + } + + + /** + * Get any id from the given {@link Uri}. + * + * @param uri + * The Uri. + * + * @return The last path segment (which should contain the id). + */ + private long getId(Uri uri) + { + return Long.parseLong(uri.getPathSegments().get(1)); + } + + + /** + * Build a selection string that selects the account specified in uri. + * + * @param uri + * A {@link Uri} that specifies an account. + * + * @return A {@link StringBuilder} with a selection string for the account. + */ + protected StringBuilder selectAccount(Uri uri) + { + StringBuilder sb = new StringBuilder(256); + return selectAccount(sb, uri); + } + + + /** + * Append the selection of the account specified in uri to the {@link StringBuilder} sb. + * + * @param sb + * A {@link StringBuilder} that the selection is appended to. + * @param uri + * A {@link Uri} that specifies an account. + * + * @return sb. + */ + protected StringBuilder selectAccount(StringBuilder sb, Uri uri) + { + String accountName = getAccountName(uri); + String accountType = getAccountType(uri); + + if (accountName != null || accountType != null) + { + + if (accountName != null) + { + if (sb.length() > 0) + { + sb.append(" AND "); + } + + sb.append(TaskListSyncColumns.ACCOUNT_NAME); + sb.append("="); + DatabaseUtils.appendEscapedSQLString(sb, accountName); + } + if (accountType != null) + { + + if (sb.length() > 0) + { + sb.append(" AND "); + } + + sb.append(TaskListSyncColumns.ACCOUNT_TYPE); + sb.append("="); + DatabaseUtils.appendEscapedSQLString(sb, accountType); + } + } + return sb; + } + + + /** + * Append the selection of the account specified in uri to the an {@link SQLiteQueryBuilder}. + * + * @param sqlBuilder + * A {@link SQLiteQueryBuilder} that the selection is appended to. + * @param uri + * A {@link Uri} that specifies an account. + */ + protected void selectAccount(SQLiteQueryBuilder sqlBuilder, Uri uri) + { + String accountName = getAccountName(uri); + String accountType = getAccountType(uri); + + if (accountName != null) + { + sqlBuilder.appendWhere(" AND "); + sqlBuilder.appendWhere(TaskListSyncColumns.ACCOUNT_NAME); + sqlBuilder.appendWhere("="); + sqlBuilder.appendWhereEscapeString(accountName); + } + if (accountType != null) + { + sqlBuilder.appendWhere(" AND "); + sqlBuilder.appendWhere(TaskListSyncColumns.ACCOUNT_TYPE); + sqlBuilder.appendWhere("="); + sqlBuilder.appendWhereEscapeString(accountType); + } + } + + + private StringBuilder _selectId(StringBuilder sb, long id, String key) + { + if (sb.length() > 0) + { + sb.append(" AND "); + } + sb.append(key); + sb.append("="); + sb.append(id); + return sb; + } + + + protected StringBuilder selectId(Uri uri) + { + StringBuilder sb = new StringBuilder(128); + return selectId(sb, uri); + } + + + protected StringBuilder selectId(StringBuilder sb, Uri uri) + { + return _selectId(sb, getId(uri), TaskListColumns._ID); + } + + + protected StringBuilder selectTaskId(Uri uri) + { + StringBuilder sb = new StringBuilder(128); + return selectTaskId(sb, uri); + } + + + protected StringBuilder selectTaskId(long id) + { + StringBuilder sb = new StringBuilder(128); + return selectTaskId(sb, id); + } + + + protected StringBuilder selectTaskId(StringBuilder sb, Uri uri) + { + return selectTaskId(sb, getId(uri)); + } + + + protected StringBuilder selectTaskId(StringBuilder sb, long id) + { + return _selectId(sb, id, Instances.TASK_ID); + + } + + + protected StringBuilder selectPropertyId(Uri uri) + { + StringBuilder sb = new StringBuilder(128); + return selectPropertyId(sb, uri); + } + + + protected StringBuilder selectPropertyId(StringBuilder sb, Uri uri) + { + return selectPropertyId(sb, getId(uri)); + } + + + protected StringBuilder selectPropertyId(long id) + { + StringBuilder sb = new StringBuilder(128); + return selectPropertyId(sb, id); + } + + + protected StringBuilder selectPropertyId(StringBuilder sb, long id) + { + return _selectId(sb, id, PropertyColumns.PROPERTY_ID); + } + + + /** + * Add a selection by ID to the given {@link SQLiteQueryBuilder}. The id is taken from the given Uri. + * + * @param sqlBuilder + * The {@link SQLiteQueryBuilder} to append the selection to. + * @param idColumn + * The column that must match the id. + * @param uri + * An {@link Uri} that contains the id. + */ + protected void selectId(SQLiteQueryBuilder sqlBuilder, String idColumn, Uri uri) + { + sqlBuilder.appendWhere(" AND "); + sqlBuilder.appendWhere(idColumn); + sqlBuilder.appendWhere("="); + sqlBuilder.appendWhere(String.valueOf(getId(uri))); + } + + + /** + * Append any arbitrary selection string to the selection in sb + * + * @param sb + * A {@link StringBuilder} that already contains a selection string. + * @param selection + * A valid SQL selection string. + * + * @return A string with the final selection. + */ + protected String updateSelection(StringBuilder sb, String selection) + { + if (selection != null) + { + if (sb.length() > 0) + { + sb.append("AND ( ").append(selection).append(" ) "); + } + else + { + sb.append(" ( ").append(selection).append(" ) "); + } + } + return sb.toString(); + } + + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) + { + final SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); + SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder(); + // initialize appendWhere, this allows us to append all other selections with a preceding "AND" + sqlBuilder.appendWhere(" 1=1 "); + boolean isSyncAdapter = isCallerSyncAdapter(uri); + + switch (mUriMatcher.match(uri)) + { + case SYNCSTATE_ID: + // the id is ignored, we only match by account type and name given in the Uri + case SYNCSTATE: + { + if (TextUtils.isEmpty(getAccountName(uri)) || TextUtils.isEmpty(getAccountType(uri))) + { + throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); + } + selectAccount(sqlBuilder, uri); + sqlBuilder.setTables(Tables.SYNCSTATE); + break; + } + case LISTS: + // add account to selection if any + selectAccount(sqlBuilder, uri); + sqlBuilder.setTables(Tables.LISTS); + if (sortOrder == null || sortOrder.length() == 0) + { + sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER; + } + break; + + case LIST_ID: + // add account to selection if any + selectAccount(sqlBuilder, uri); + sqlBuilder.setTables(Tables.LISTS); + selectId(sqlBuilder, TaskListColumns._ID, uri); + if (sortOrder == null || sortOrder.length() == 0) + { + sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER; + } + break; + + case TASKS: + if (shouldLoadProperties(uri)) + { + // extended properties were requested, therefore change to task view that includes these properties + sqlBuilder.setTables(Tables.TASKS_PROPERTY_VIEW); + } + else + { + sqlBuilder.setTables(Tables.TASKS_VIEW); + } + if (!isSyncAdapter) + { + // do not return deleted rows if caller is not a sync adapter + sqlBuilder.appendWhere(" AND "); + sqlBuilder.appendWhere(Tasks._DELETED); + sqlBuilder.appendWhere("=0"); + } + if (sortOrder == null || sortOrder.length() == 0) + { + sortOrder = TaskContract.Tasks.DEFAULT_SORT_ORDER; + } + break; + + case TASK_ID: + if (shouldLoadProperties(uri)) + { + // extended properties were requested, therefore change to task view that includes these properties + sqlBuilder.setTables(Tables.TASKS_PROPERTY_VIEW); + } + else + { + sqlBuilder.setTables(Tables.TASKS_VIEW); + } + selectId(sqlBuilder, TaskColumns._ID, uri); + if (!isSyncAdapter) + { + // do not return deleted rows if caller is not a sync adapter + sqlBuilder.appendWhere(" AND "); + sqlBuilder.appendWhere(Tasks._DELETED); + sqlBuilder.appendWhere("=0"); + } + if (sortOrder == null || sortOrder.length() == 0) + { + sortOrder = TaskContract.Tasks.DEFAULT_SORT_ORDER; + } + break; + + case INSTANCES: + if (shouldLoadProperties(uri)) + { + // extended properties were requested, therefore change to instance view that includes these properties + sqlBuilder.setTables(Tables.INSTANCE_PROPERTY_VIEW); + } + else + { + sqlBuilder.setTables(Tables.INSTANCE_VIEW); + } + if (!isSyncAdapter) + { + // do not return deleted rows if caller is not a sync adapter + sqlBuilder.appendWhere(" AND "); + sqlBuilder.appendWhere(Tasks._DELETED); + sqlBuilder.appendWhere("=0"); + } + if (sortOrder == null || sortOrder.length() == 0) + { + sortOrder = TaskContract.Instances.DEFAULT_SORT_ORDER; + } + break; + + case INSTANCE_ID: + if (shouldLoadProperties(uri)) + { + // extended properties were requested, therefore change to instance view that includes these properties + sqlBuilder.setTables(Tables.INSTANCE_PROPERTY_VIEW); + } + else + { + sqlBuilder.setTables(Tables.INSTANCE_VIEW); + } + selectId(sqlBuilder, Instances._ID, uri); + if (!isSyncAdapter) + { + // do not return deleted rows if caller is not a sync adapter + sqlBuilder.appendWhere(" AND "); + sqlBuilder.appendWhere(Tasks._DELETED); + sqlBuilder.appendWhere("=0"); + } + if (sortOrder == null || sortOrder.length() == 0) + { + sortOrder = TaskContract.Instances.DEFAULT_SORT_ORDER; + } + break; + + case CATEGORIES: + selectAccount(sqlBuilder, uri); + sqlBuilder.setTables(Tables.CATEGORIES); + if (sortOrder == null || sortOrder.length() == 0) + { + sortOrder = TaskContract.Categories.DEFAULT_SORT_ORDER; + } + break; + + case CATEGORY_ID: + selectAccount(sqlBuilder, uri); + sqlBuilder.setTables(Tables.CATEGORIES); + selectId(sqlBuilder, CategoriesColumns._ID, uri); + if (sortOrder == null || sortOrder.length() == 0) + { + sortOrder = TaskContract.Categories.DEFAULT_SORT_ORDER; + } + break; + + case PROPERTIES: + sqlBuilder.setTables(Tables.PROPERTIES); + break; + + case PROPERTY_ID: + sqlBuilder.setTables(Tables.PROPERTIES); + selectId(sqlBuilder, PropertyColumns.PROPERTY_ID, uri); + break; + + case SEARCH: + String searchString = uri.getQueryParameter(Tasks.SEARCH_QUERY_PARAMETER); + searchString = Uri.decode(searchString); + Cursor searchCursor = FTSDatabaseHelper.getTaskSearchCursor(db, searchString, projection, selection, selectionArgs, sortOrder); + if (searchCursor != null) + { + // attach tasks uri for notifications, that way the search results are updated when a task changes + searchCursor.setNotificationUri(getContext().getContentResolver(), Tasks.getContentUri(mAuthority)); + } + return searchCursor; + + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + Cursor c = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); + + if (c != null) + { + c.setNotificationUri(getContext().getContentResolver(), uri); + } + return c; + } + + + @Override + public int deleteInTransaction(final SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs, final boolean isSyncAdapter) + { + int count = 0; + String accountName = getAccountName(uri); + String accountType = getAccountType(uri); + + switch (mUriMatcher.match(uri)) + { + case SYNCSTATE_ID: + // the id is ignored, we only match by account type and name given in the Uri + case SYNCSTATE: + { + if (!isSyncAdapter) + { + throw new IllegalAccessError("only sync adapters may access syncstate"); + } + if (TextUtils.isEmpty(getAccountName(uri)) || TextUtils.isEmpty(getAccountType(uri))) + { + throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); + } + selection = updateSelection(selectAccount(uri), selection); + count = db.delete(Tables.SYNCSTATE, selection, selectionArgs); + break; + } + /* + * Deleting task lists is only allowed to sync adapters. They must provide ACCOUNT_NAME and ACCOUNT_TYPE. */ - case LIST_ID: - // add _id to selection and fall through - selection = updateSelection(selectId(uri), selection); - case LISTS: - { - if (isSyncAdapter) - { - if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) - { - throw new IllegalArgumentException("Sync adapters must specify an account and account type: " + uri); - } - } - - // iterate over all lists that match the selection. We iterate "manually" to execute any processors before or after deletion. - final Cursor cursor = db.query(Tables.LISTS, null, selection, selectionArgs, null, null, null, null); - - try - { - while (cursor.moveToNext()) - { - final ListAdapter list = new CursorContentValuesListAdapter(ListAdapter._ID.getFrom(cursor), cursor, new ContentValues()); - - ProviderOperation.DELETE.execute(db, mListProcessors, list, isSyncAdapter, mOperationsLog, mAuthority); - count++; - } - } - finally - { - cursor.close(); - } - - break; - - } - /* + case LIST_ID: + // add _id to selection and fall through + selection = updateSelection(selectId(uri), selection); + case LISTS: + { + if (isSyncAdapter) + { + if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) + { + throw new IllegalArgumentException("Sync adapters must specify an account and account type: " + uri); + } + } + + // iterate over all lists that match the selection. We iterate "manually" to execute any processors before or after deletion. + final Cursor cursor = db.query(Tables.LISTS, null, selection, selectionArgs, null, null, null, null); + + try + { + while (cursor.moveToNext()) + { + final ListAdapter list = new CursorContentValuesListAdapter(ListAdapter._ID.getFrom(cursor), cursor, new ContentValues()); + + ProviderOperation.DELETE.execute(db, mListProcessors, list, isSyncAdapter, mOperationsLog, mAuthority); + count++; + } + } + finally + { + cursor.close(); + } + + break; + + } + /* * Task won't be removed, just marked as deleted if the caller isn't a sync adapter. Sync adapters can remove tasks immediately. */ - case TASK_ID: - // add id to selection and fall through - selection = updateSelection(selectId(uri), selection); - - case TASKS: - { - // TODO: filter by account name and type if present in uri. - - if (isSyncAdapter) - { - if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) - { - throw new IllegalArgumentException("Sync adapters must specify an account and account type: " + uri); - } - } - - // iterate over all tasks that match the selection. We iterate "manually" to execute any processors before or after deletion. - final Cursor cursor = db.query(Tables.TASKS_VIEW, null, selection, selectionArgs, null, null, null, null); - - try - { - while (cursor.moveToNext()) - { - final TaskAdapter task = new CursorContentValuesTaskAdapter(cursor, new ContentValues()); - - ProviderOperation.DELETE.execute(db, mTaskProcessors, task, isSyncAdapter, mOperationsLog, mAuthority); - count++; - } - } - finally - { - cursor.close(); - } - - break; - } - case ALARM_ID: - // add id to selection and fall through - selection = updateSelection(selectId(uri), selection); - - case ALARMS: - - count = db.delete(Tables.ALARMS, selection, selectionArgs); - break; - - case PROPERTY_ID: - selection = updateSelection(selectPropertyId(uri), selection); - - case PROPERTIES: - // fetch all properties that match the selection - Cursor cursor = db.query(Tables.PROPERTIES, null, selection, selectionArgs, null, null, null); - - try - { - int propIdCol = cursor.getColumnIndex(Properties.PROPERTY_ID); - int taskIdCol = cursor.getColumnIndex(Properties.TASK_ID); - int mimeTypeCol = cursor.getColumnIndex(Properties.MIMETYPE); - while (cursor.moveToNext()) - { - long propertyId = cursor.getLong(propIdCol); - long taskId = cursor.getLong(taskIdCol); - String mimeType = cursor.getString(mimeTypeCol); - if (mimeType != null) - { - PropertyHandler handler = PropertyHandlerFactory.get(mimeType); - count += handler.delete(db, taskId, propertyId, cursor, isSyncAdapter); - } - } - } - finally - { - cursor.close(); - } - postNotifyUri(Properties.getContentUri(mAuthority)); - break; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - if (count > 0) - { - postNotifyUri(uri); - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); - } - return count; - } - - - @Override - public Uri insertInTransaction(final SQLiteDatabase db, Uri uri, final ContentValues values, final boolean isSyncAdapter) - { - long rowId = 0; - Uri result_uri = null; - - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - - switch (mUriMatcher.match(uri)) - { - case SYNCSTATE: - { - if (!isSyncAdapter) - { - throw new IllegalAccessError("only sync adapters may access syncstate"); - } - if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) - { - throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); - } - values.put(SyncState.ACCOUNT_NAME, accountName); - values.put(SyncState.ACCOUNT_TYPE, accountType); - rowId = db.replace(Tables.SYNCSTATE, null, values); - result_uri = TaskContract.SyncState.getContentUri(mAuthority); - break; - } - case LISTS: - { - final ListAdapter list = new ContentValuesListAdapter(values); - list.set(ListAdapter.ACCOUNT_NAME, accountName); - list.set(ListAdapter.ACCOUNT_TYPE, accountType); - - ProviderOperation.INSERT.execute(db, mListProcessors, list, isSyncAdapter, mOperationsLog, mAuthority); - - rowId = list.id(); - result_uri = TaskContract.TaskLists.getContentUri(mAuthority); - - break; - } - case TASKS: - final TaskAdapter task = new ContentValuesTaskAdapter(values); - - ProviderOperation.INSERT.execute(db, mTaskProcessors, task, isSyncAdapter, mOperationsLog, mAuthority); - - rowId = task.id(); - result_uri = TaskContract.Tasks.getContentUri(mAuthority); - - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); - - break; - - case PROPERTIES: - String mimetype = values.getAsString(Properties.MIMETYPE); - - if (mimetype == null) - { - throw new IllegalArgumentException("missing mimetype in property values"); - } - - Long taskId = values.getAsLong(Properties.TASK_ID); - if (taskId == null) - { - throw new IllegalArgumentException("missing task id in property values"); - } - - if (values.containsKey(Properties.PROPERTY_ID)) - { - throw new IllegalArgumentException("property id can not be written"); - } - - PropertyHandler handler = PropertyHandlerFactory.get(mimetype); - rowId = handler.insert(db, taskId, values, isSyncAdapter); - result_uri = TaskContract.Properties.getContentUri(mAuthority); - if (rowId >= 0) - { - postNotifyUri(Tasks.getContentUri(mAuthority)); - postNotifyUri(Instances.getContentUri(mAuthority)); - } - break; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - if (rowId > 0 && result_uri != null) - { - result_uri = ContentUris.withAppendedId(result_uri, rowId); - postNotifyUri(result_uri); - postNotifyUri(uri); - return result_uri; - } - throw new SQLException("Failed to insert row into " + uri); - } - - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - @Override - public int updateInTransaction(final SQLiteDatabase db, Uri uri, final ContentValues values, String selection, String[] selectionArgs, - final boolean isSyncAdapter) - { - int count = 0; - switch (mUriMatcher.match(uri)) - { - case SYNCSTATE_ID: - // the id is ignored, we only match by account type and name given in the Uri - case SYNCSTATE: - { - if (!isSyncAdapter) - { - throw new IllegalAccessError("only sync adapters may access syncstate"); - } - - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) - { - throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); - } - - if (values.size() == 0) - { - // we're done - break; - } - - values.put(SyncState.ACCOUNT_NAME, accountName); - values.put(SyncState.ACCOUNT_TYPE, accountType); - - long id = db.replace(Tables.SYNCSTATE, null, values); - if (id >= 0) - { - count = 1; - } - break; - } - case LIST_ID: - // update selection and fall through - selection = updateSelection(selectId(uri), selection); - - case LISTS: - { - // iterate over all task lists that match the selection. We iterate "manually" to execute any processors before or after insert. - final Cursor cursor = db.query(Tables.LISTS, null, selection, selectionArgs, null, null, null, null); - - int idCol = cursor.getColumnIndex(TaskContract.TaskLists._ID); - - try - { - while (cursor.moveToNext()) - { - final long listId = cursor.getLong(idCol); - - // clone list values if we have more than one list to update - // we need this, because the processors may change the values - final ListAdapter list = new CursorContentValuesListAdapter(listId, cursor, cursor.getCount() > 1 ? new ContentValues(values) : values); - - ProviderOperation.UPDATE.execute(db, mListProcessors, list, isSyncAdapter, mOperationsLog, mAuthority); - count++; - } - } - finally - { - cursor.close(); - } - break; - } - case TASK_ID: - // update selection and fall through - selection = updateSelection(selectId(uri), selection); - - case TASKS: - { - // iterate over all tasks that match the selection. We iterate "manually" to execute any processors before or after insert. - final Cursor cursor = db.query(Tables.TASKS_VIEW, null, selection, selectionArgs, null, null, null, null); - - try - { - while (cursor.moveToNext()) - { - // clone task values if we have more than one task to update - // we need this, because the processors may change the values - final TaskAdapter task = new CursorContentValuesTaskAdapter(cursor, cursor.getCount() > 1 ? new ContentValues(values) : values); - - ProviderOperation.UPDATE.execute(db, mTaskProcessors, task, isSyncAdapter, mOperationsLog, mAuthority); - count++; - } - } - finally - { - cursor.close(); - } - - if (count > 0) - { - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); - } - break; - } - - case PROPERTY_ID: - selection = updateSelection(selectPropertyId(uri), selection); - - case PROPERTIES: - if (values.containsKey(Properties.MIMETYPE)) - { - throw new IllegalArgumentException("property mimetypes can not be modified"); - } - - if (values.containsKey(Properties.TASK_ID)) - { - throw new IllegalArgumentException("task id can not be changed"); - } - - if (values.containsKey(Properties.PROPERTY_ID)) - { - throw new IllegalArgumentException("property id can not be changed"); - } - - // fetch all properties that match the selection - Cursor cursor = db.query(Tables.PROPERTIES, null, selection, selectionArgs, null, null, null); - - try - { - int propIdCol = cursor.getColumnIndex(Properties.PROPERTY_ID); - int taskIdCol = cursor.getColumnIndex(Properties.TASK_ID); - int mimeTypeCol = cursor.getColumnIndex(Properties.MIMETYPE); - while (cursor.moveToNext()) - { - long propertyId = cursor.getLong(propIdCol); - long taskId = cursor.getLong(taskIdCol); - String mimeType = cursor.getString(mimeTypeCol); - if (mimeType != null) - { - PropertyHandler handler = PropertyHandlerFactory.get(mimeType); - count += handler.update(db, taskId, propertyId, values, cursor, isSyncAdapter); - } - } - } - finally - { - cursor.close(); - } - postNotifyUri(Properties.getContentUri(mAuthority)); - break; - - case CATEGORY_ID: - String newCategorySelection = updateSelection(selectId(uri), selection); - validateCategoryValues(values, false, isSyncAdapter); - count = db.update(Tables.CATEGORIES, values, newCategorySelection, selectionArgs); - break; - case ALARM_ID: - String newAlarmSelection = updateSelection(selectId(uri), selection); - validateAlarmValues(values, false, isSyncAdapter); - count = db.update(Tables.ALARMS, values, newAlarmSelection, selectionArgs); - break; - default: - ContentOperation operation = ContentOperation.get(mUriMatcher.match(uri), OPERATIONS); - - if (operation == null) - { - throw new IllegalArgumentException("Unknown URI " + uri); - } - - operation.run(getContext(), mAsyncHandler, uri, db, values); - } - - // get the keys in values - Set keys; - if (android.os.Build.VERSION.SDK_INT < 11) - { - keys = new HashSet(); - for (Entry entry : values.valueSet()) - { - keys.add(entry.getKey()); - } - } - else - { - keys = values.keySet(); - } - - if (!TASK_LIST_SYNC_COLUMNS.containsAll(keys)) - { - // send notifications, because non-sync columns have been updated - postNotifyUri(uri); - } - - return count; - } - - - /** - * Update task due and task start notifications. - */ - private void updateNotifications() - { - mAsyncHandler.post(new Runnable() - { - - @Override - public void run() - { - ContentOperation.UPDATE_NOTIFICATION_ALARM.fire(getContext(), null); - } - }); - } - - - /** - * Validate the given category values. - * - * @param values - * The category properties to validate. - * @throws IllegalArgumentException - * if any of the values is invalid. - */ - private void validateCategoryValues(ContentValues values, boolean isNew, boolean isSyncAdapter) - { - // row id can not be changed or set manually - if (values.containsKey(Categories._ID)) - { - throw new IllegalArgumentException("_ID can not be set manually"); - } - - if (isNew != values.containsKey(Categories.ACCOUNT_NAME) && (!isNew || values.get(Categories.ACCOUNT_NAME) != null)) - { - throw new IllegalArgumentException("ACCOUNT_NAME is write-once and required on INSERT"); - } - - if (isNew != values.containsKey(Categories.ACCOUNT_TYPE) && (!isNew || values.get(Categories.ACCOUNT_TYPE) != null)) - { - throw new IllegalArgumentException("ACCOUNT_TYPE is write-once and required on INSERT"); - } - } - - - /** - * Validate the given alarm values. - * - * @param values - * The alarm values to validate - * @throws IllegalArgumentException - * if any of the values is invalid. - */ - private void validateAlarmValues(ContentValues values, boolean isNew, boolean isSyncAdapter) - { - if (values.containsKey(Alarms.ALARM_ID)) - { - throw new IllegalArgumentException("ALARM_ID can not be set manually"); - } - } - - - @Override - public String getType(Uri uri) - { - switch (mUriMatcher.match(uri)) - { - case LISTS: - return ContentResolver.CURSOR_DIR_BASE_TYPE + "/org.dmfs.tasks." + TaskLists.CONTENT_URI_PATH; - case LIST_ID: - return ContentResolver.CURSOR_ITEM_BASE_TYPE + "/org.dmfs.tasks." + TaskLists.CONTENT_URI_PATH; - case TASKS: - return ContentResolver.CURSOR_DIR_BASE_TYPE + "/org.dmfs.tasks." + Tasks.CONTENT_URI_PATH; - case TASK_ID: - return ContentResolver.CURSOR_ITEM_BASE_TYPE + "/org.dmfs.tasks." + Tasks.CONTENT_URI_PATH; - case INSTANCES: - return ContentResolver.CURSOR_DIR_BASE_TYPE + "/org.dmfs.tasks." + Instances.CONTENT_URI_PATH; - default: - throw new IllegalArgumentException("Unsupported URI: " + uri); - } - } - - - @Override - protected void onEndTransaction(boolean callerIsSyncAdapter) - { - super.onEndTransaction(callerIsSyncAdapter); - Intent providerChangedIntent = new Intent(Intent.ACTION_PROVIDER_CHANGED, TaskContract.getContentUri(mAuthority)); - if (!mOperationsLog.isEmpty()) - { - updateNotifications(); - } - // add the change log to the broadcast - providerChangedIntent.putExtras(mOperationsLog.toBundle(true)); - getContext().sendBroadcast(providerChangedIntent); - }; - - - @Override - public SQLiteOpenHelper getDatabaseHelper(Context context) - { - TaskDatabaseHelper helper = new TaskDatabaseHelper(context, this); - - return helper; - } - - - @Override - public void onDatabaseCreated(SQLiteDatabase db) - { - // notify listeners that the database has been created - Intent dbInitializedIntent = new Intent(TaskContract.ACTION_DATABASE_INITIALIZED); - dbInitializedIntent.setDataAndType(TaskContract.getContentUri(mAuthority), TaskContract.MIMETYPE_AUTHORITY); - getContext().sendBroadcast(dbInitializedIntent); - } - - - @Override - public void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion) - { - if (oldVersion < 15) - { - mAsyncHandler.post(new Runnable() - { - @Override - public void run() - { - ContentOperation.UPDATE_TIMEZONE.fire(getContext(), null); - } - }); - } - } - - - @Override - protected boolean syncToNetwork(Uri uri) - { - return true; - } - - - /** - * Returns a {@link ProviderInfo} object for this provider. - * - * @return A {@link ProviderInfo} instance. - * @throws RuntimeException - * if the provider can't be found in the given context. - */ - @SuppressLint("NewApi") - private ProviderInfo getProviderInfo() - { - Context context = getContext(); - PackageManager packageManager = context.getPackageManager(); - Class providerClass = this.getClass(); - - if (Build.VERSION.SDK_INT <= 8) - { - // in Android 2.2 PackageManger.getProviderInfo doesn't exist. We need to find it ourselves. - - // First get the PackageInfo of this app. - PackageInfo packageInfo; - try - { - packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_META_DATA | PackageManager.GET_PROVIDERS); - } - catch (NameNotFoundException e) - { - throw new RuntimeException("Could not find Provider!", e); - } - - // next scan all providers for this class - for (ProviderInfo provider : packageInfo.providers) - { - try - { - Class providerInfoClass = Class.forName(provider.name); - if (providerInfoClass.equals(providerClass)) - { - // We've finally found to ourselves! Isn't that a good feeling? - return provider; - } - } - catch (ClassNotFoundException e) - { - throw new RuntimeException("Missing provider class '" + provider.name + "'"); - } - } - - // We got lost somewhere, no provider matched!? - throw new RuntimeException("Could not find Provider!"); - } - - // On Android 2.3+ we just call the appropriate method - try - { - return packageManager.getProviderInfo(new ComponentName(context, providerClass), PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); - } - catch (NameNotFoundException e) - { - throw new RuntimeException("Could not find Provider!", e); - } - } - - - @Override - public void onAccountsUpdated(Account[] accounts) - { - // TODO: we probably can move the cleanup code here and get rid of the Utils class - Utils.cleanUpLists(getContext(), getDatabaseHelper().getWritableDatabase(), accounts, mAuthority); - } + case TASK_ID: + // add id to selection and fall through + selection = updateSelection(selectId(uri), selection); + + case TASKS: + { + // TODO: filter by account name and type if present in uri. + + if (isSyncAdapter) + { + if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) + { + throw new IllegalArgumentException("Sync adapters must specify an account and account type: " + uri); + } + } + + // iterate over all tasks that match the selection. We iterate "manually" to execute any processors before or after deletion. + final Cursor cursor = db.query(Tables.TASKS_VIEW, null, selection, selectionArgs, null, null, null, null); + + try + { + while (cursor.moveToNext()) + { + final TaskAdapter task = new CursorContentValuesTaskAdapter(cursor, new ContentValues()); + + ProviderOperation.DELETE.execute(db, mTaskProcessors, task, isSyncAdapter, mOperationsLog, mAuthority); + count++; + } + } + finally + { + cursor.close(); + } + + break; + } + case ALARM_ID: + // add id to selection and fall through + selection = updateSelection(selectId(uri), selection); + + case ALARMS: + + count = db.delete(Tables.ALARMS, selection, selectionArgs); + break; + + case PROPERTY_ID: + selection = updateSelection(selectPropertyId(uri), selection); + + case PROPERTIES: + // fetch all properties that match the selection + Cursor cursor = db.query(Tables.PROPERTIES, null, selection, selectionArgs, null, null, null); + + try + { + int propIdCol = cursor.getColumnIndex(Properties.PROPERTY_ID); + int taskIdCol = cursor.getColumnIndex(Properties.TASK_ID); + int mimeTypeCol = cursor.getColumnIndex(Properties.MIMETYPE); + while (cursor.moveToNext()) + { + long propertyId = cursor.getLong(propIdCol); + long taskId = cursor.getLong(taskIdCol); + String mimeType = cursor.getString(mimeTypeCol); + if (mimeType != null) + { + PropertyHandler handler = PropertyHandlerFactory.get(mimeType); + count += handler.delete(db, taskId, propertyId, cursor, isSyncAdapter); + } + } + } + finally + { + cursor.close(); + } + postNotifyUri(Properties.getContentUri(mAuthority)); + break; + + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + if (count > 0) + { + postNotifyUri(uri); + postNotifyUri(Instances.getContentUri(mAuthority)); + postNotifyUri(Tasks.getContentUri(mAuthority)); + } + return count; + } + + + @Override + public Uri insertInTransaction(final SQLiteDatabase db, Uri uri, final ContentValues values, final boolean isSyncAdapter) + { + long rowId = 0; + Uri result_uri = null; + + String accountName = getAccountName(uri); + String accountType = getAccountType(uri); + + switch (mUriMatcher.match(uri)) + { + case SYNCSTATE: + { + if (!isSyncAdapter) + { + throw new IllegalAccessError("only sync adapters may access syncstate"); + } + if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) + { + throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); + } + values.put(SyncState.ACCOUNT_NAME, accountName); + values.put(SyncState.ACCOUNT_TYPE, accountType); + rowId = db.replace(Tables.SYNCSTATE, null, values); + result_uri = TaskContract.SyncState.getContentUri(mAuthority); + break; + } + case LISTS: + { + final ListAdapter list = new ContentValuesListAdapter(values); + list.set(ListAdapter.ACCOUNT_NAME, accountName); + list.set(ListAdapter.ACCOUNT_TYPE, accountType); + + ProviderOperation.INSERT.execute(db, mListProcessors, list, isSyncAdapter, mOperationsLog, mAuthority); + + rowId = list.id(); + result_uri = TaskContract.TaskLists.getContentUri(mAuthority); + + break; + } + case TASKS: + final TaskAdapter task = new ContentValuesTaskAdapter(values); + + ProviderOperation.INSERT.execute(db, mTaskProcessors, task, isSyncAdapter, mOperationsLog, mAuthority); + + rowId = task.id(); + result_uri = TaskContract.Tasks.getContentUri(mAuthority); + + postNotifyUri(Instances.getContentUri(mAuthority)); + postNotifyUri(Tasks.getContentUri(mAuthority)); + + break; + + case PROPERTIES: + String mimetype = values.getAsString(Properties.MIMETYPE); + + if (mimetype == null) + { + throw new IllegalArgumentException("missing mimetype in property values"); + } + + Long taskId = values.getAsLong(Properties.TASK_ID); + if (taskId == null) + { + throw new IllegalArgumentException("missing task id in property values"); + } + + if (values.containsKey(Properties.PROPERTY_ID)) + { + throw new IllegalArgumentException("property id can not be written"); + } + + PropertyHandler handler = PropertyHandlerFactory.get(mimetype); + rowId = handler.insert(db, taskId, values, isSyncAdapter); + result_uri = TaskContract.Properties.getContentUri(mAuthority); + if (rowId >= 0) + { + postNotifyUri(Tasks.getContentUri(mAuthority)); + postNotifyUri(Instances.getContentUri(mAuthority)); + } + break; + + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + if (rowId > 0 && result_uri != null) + { + result_uri = ContentUris.withAppendedId(result_uri, rowId); + postNotifyUri(result_uri); + postNotifyUri(uri); + return result_uri; + } + throw new SQLException("Failed to insert row into " + uri); + } + + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public int updateInTransaction(final SQLiteDatabase db, Uri uri, final ContentValues values, String selection, String[] selectionArgs, + final boolean isSyncAdapter) + { + int count = 0; + switch (mUriMatcher.match(uri)) + { + case SYNCSTATE_ID: + // the id is ignored, we only match by account type and name given in the Uri + case SYNCSTATE: + { + if (!isSyncAdapter) + { + throw new IllegalAccessError("only sync adapters may access syncstate"); + } + + String accountName = getAccountName(uri); + String accountType = getAccountType(uri); + if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) + { + throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); + } + + if (values.size() == 0) + { + // we're done + break; + } + + values.put(SyncState.ACCOUNT_NAME, accountName); + values.put(SyncState.ACCOUNT_TYPE, accountType); + + long id = db.replace(Tables.SYNCSTATE, null, values); + if (id >= 0) + { + count = 1; + } + break; + } + case LIST_ID: + // update selection and fall through + selection = updateSelection(selectId(uri), selection); + + case LISTS: + { + // iterate over all task lists that match the selection. We iterate "manually" to execute any processors before or after insert. + final Cursor cursor = db.query(Tables.LISTS, null, selection, selectionArgs, null, null, null, null); + + int idCol = cursor.getColumnIndex(TaskContract.TaskLists._ID); + + try + { + while (cursor.moveToNext()) + { + final long listId = cursor.getLong(idCol); + + // clone list values if we have more than one list to update + // we need this, because the processors may change the values + final ListAdapter list = new CursorContentValuesListAdapter(listId, cursor, cursor.getCount() > 1 ? new ContentValues(values) : values); + + ProviderOperation.UPDATE.execute(db, mListProcessors, list, isSyncAdapter, mOperationsLog, mAuthority); + count++; + } + } + finally + { + cursor.close(); + } + break; + } + case TASK_ID: + // update selection and fall through + selection = updateSelection(selectId(uri), selection); + + case TASKS: + { + // iterate over all tasks that match the selection. We iterate "manually" to execute any processors before or after insert. + final Cursor cursor = db.query(Tables.TASKS_VIEW, null, selection, selectionArgs, null, null, null, null); + + try + { + while (cursor.moveToNext()) + { + // clone task values if we have more than one task to update + // we need this, because the processors may change the values + final TaskAdapter task = new CursorContentValuesTaskAdapter(cursor, cursor.getCount() > 1 ? new ContentValues(values) : values); + + ProviderOperation.UPDATE.execute(db, mTaskProcessors, task, isSyncAdapter, mOperationsLog, mAuthority); + count++; + } + } + finally + { + cursor.close(); + } + + if (count > 0) + { + postNotifyUri(Instances.getContentUri(mAuthority)); + postNotifyUri(Tasks.getContentUri(mAuthority)); + } + break; + } + + case PROPERTY_ID: + selection = updateSelection(selectPropertyId(uri), selection); + + case PROPERTIES: + if (values.containsKey(Properties.MIMETYPE)) + { + throw new IllegalArgumentException("property mimetypes can not be modified"); + } + + if (values.containsKey(Properties.TASK_ID)) + { + throw new IllegalArgumentException("task id can not be changed"); + } + + if (values.containsKey(Properties.PROPERTY_ID)) + { + throw new IllegalArgumentException("property id can not be changed"); + } + + // fetch all properties that match the selection + Cursor cursor = db.query(Tables.PROPERTIES, null, selection, selectionArgs, null, null, null); + + try + { + int propIdCol = cursor.getColumnIndex(Properties.PROPERTY_ID); + int taskIdCol = cursor.getColumnIndex(Properties.TASK_ID); + int mimeTypeCol = cursor.getColumnIndex(Properties.MIMETYPE); + while (cursor.moveToNext()) + { + long propertyId = cursor.getLong(propIdCol); + long taskId = cursor.getLong(taskIdCol); + String mimeType = cursor.getString(mimeTypeCol); + if (mimeType != null) + { + PropertyHandler handler = PropertyHandlerFactory.get(mimeType); + count += handler.update(db, taskId, propertyId, values, cursor, isSyncAdapter); + } + } + } + finally + { + cursor.close(); + } + postNotifyUri(Properties.getContentUri(mAuthority)); + break; + + case CATEGORY_ID: + String newCategorySelection = updateSelection(selectId(uri), selection); + validateCategoryValues(values, false, isSyncAdapter); + count = db.update(Tables.CATEGORIES, values, newCategorySelection, selectionArgs); + break; + case ALARM_ID: + String newAlarmSelection = updateSelection(selectId(uri), selection); + validateAlarmValues(values, false, isSyncAdapter); + count = db.update(Tables.ALARMS, values, newAlarmSelection, selectionArgs); + break; + default: + ContentOperation operation = ContentOperation.get(mUriMatcher.match(uri), OPERATIONS); + + if (operation == null) + { + throw new IllegalArgumentException("Unknown URI " + uri); + } + + operation.run(getContext(), mAsyncHandler, uri, db, values); + } + + // get the keys in values + Set keys; + if (android.os.Build.VERSION.SDK_INT < 11) + { + keys = new HashSet(); + for (Entry entry : values.valueSet()) + { + keys.add(entry.getKey()); + } + } + else + { + keys = values.keySet(); + } + + if (!TASK_LIST_SYNC_COLUMNS.containsAll(keys)) + { + // send notifications, because non-sync columns have been updated + postNotifyUri(uri); + } + + return count; + } + + + /** + * Update task due and task start notifications. + */ + private void updateNotifications() + { + mAsyncHandler.post(new Runnable() + { + + @Override + public void run() + { + ContentOperation.UPDATE_NOTIFICATION_ALARM.fire(getContext(), null); + } + }); + } + + + /** + * Validate the given category values. + * + * @param values + * The category properties to validate. + * + * @throws IllegalArgumentException + * if any of the values is invalid. + */ + private void validateCategoryValues(ContentValues values, boolean isNew, boolean isSyncAdapter) + { + // row id can not be changed or set manually + if (values.containsKey(Categories._ID)) + { + throw new IllegalArgumentException("_ID can not be set manually"); + } + + if (isNew != values.containsKey(Categories.ACCOUNT_NAME) && (!isNew || values.get(Categories.ACCOUNT_NAME) != null)) + { + throw new IllegalArgumentException("ACCOUNT_NAME is write-once and required on INSERT"); + } + + if (isNew != values.containsKey(Categories.ACCOUNT_TYPE) && (!isNew || values.get(Categories.ACCOUNT_TYPE) != null)) + { + throw new IllegalArgumentException("ACCOUNT_TYPE is write-once and required on INSERT"); + } + } + + + /** + * Validate the given alarm values. + * + * @param values + * The alarm values to validate + * + * @throws IllegalArgumentException + * if any of the values is invalid. + */ + private void validateAlarmValues(ContentValues values, boolean isNew, boolean isSyncAdapter) + { + if (values.containsKey(Alarms.ALARM_ID)) + { + throw new IllegalArgumentException("ALARM_ID can not be set manually"); + } + } + + + @Override + public String getType(Uri uri) + { + switch (mUriMatcher.match(uri)) + { + case LISTS: + return ContentResolver.CURSOR_DIR_BASE_TYPE + "/org.dmfs.tasks." + TaskLists.CONTENT_URI_PATH; + case LIST_ID: + return ContentResolver.CURSOR_ITEM_BASE_TYPE + "/org.dmfs.tasks." + TaskLists.CONTENT_URI_PATH; + case TASKS: + return ContentResolver.CURSOR_DIR_BASE_TYPE + "/org.dmfs.tasks." + Tasks.CONTENT_URI_PATH; + case TASK_ID: + return ContentResolver.CURSOR_ITEM_BASE_TYPE + "/org.dmfs.tasks." + Tasks.CONTENT_URI_PATH; + case INSTANCES: + return ContentResolver.CURSOR_DIR_BASE_TYPE + "/org.dmfs.tasks." + Instances.CONTENT_URI_PATH; + default: + throw new IllegalArgumentException("Unsupported URI: " + uri); + } + } + + + @Override + protected void onEndTransaction(boolean callerIsSyncAdapter) + { + super.onEndTransaction(callerIsSyncAdapter); + Intent providerChangedIntent = new Intent(Intent.ACTION_PROVIDER_CHANGED, TaskContract.getContentUri(mAuthority)); + if (!mOperationsLog.isEmpty()) + { + updateNotifications(); + } + // add the change log to the broadcast + providerChangedIntent.putExtras(mOperationsLog.toBundle(true)); + getContext().sendBroadcast(providerChangedIntent); + } + + + ; + + + @Override + public SQLiteOpenHelper getDatabaseHelper(Context context) + { + TaskDatabaseHelper helper = new TaskDatabaseHelper(context, this); + + return helper; + } + + + @Override + public void onDatabaseCreated(SQLiteDatabase db) + { + // notify listeners that the database has been created + Intent dbInitializedIntent = new Intent(TaskContract.ACTION_DATABASE_INITIALIZED); + dbInitializedIntent.setDataAndType(TaskContract.getContentUri(mAuthority), TaskContract.MIMETYPE_AUTHORITY); + getContext().sendBroadcast(dbInitializedIntent); + } + + + @Override + public void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion) + { + if (oldVersion < 15) + { + mAsyncHandler.post(new Runnable() + { + @Override + public void run() + { + ContentOperation.UPDATE_TIMEZONE.fire(getContext(), null); + } + }); + } + } + + + @Override + protected boolean syncToNetwork(Uri uri) + { + return true; + } + + + /** + * Returns a {@link ProviderInfo} object for this provider. + * + * @return A {@link ProviderInfo} instance. + * + * @throws RuntimeException + * if the provider can't be found in the given context. + */ + @SuppressLint("NewApi") + private ProviderInfo getProviderInfo() + { + Context context = getContext(); + PackageManager packageManager = context.getPackageManager(); + Class providerClass = this.getClass(); + + if (Build.VERSION.SDK_INT <= 8) + { + // in Android 2.2 PackageManger.getProviderInfo doesn't exist. We need to find it ourselves. + + // First get the PackageInfo of this app. + PackageInfo packageInfo; + try + { + packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_META_DATA | PackageManager.GET_PROVIDERS); + } + catch (NameNotFoundException e) + { + throw new RuntimeException("Could not find Provider!", e); + } + + // next scan all providers for this class + for (ProviderInfo provider : packageInfo.providers) + { + try + { + Class providerInfoClass = Class.forName(provider.name); + if (providerInfoClass.equals(providerClass)) + { + // We've finally found to ourselves! Isn't that a good feeling? + return provider; + } + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("Missing provider class '" + provider.name + "'"); + } + } + + // We got lost somewhere, no provider matched!? + throw new RuntimeException("Could not find Provider!"); + } + + // On Android 2.3+ we just call the appropriate method + try + { + return packageManager.getProviderInfo(new ComponentName(context, providerClass), PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); + } + catch (NameNotFoundException e) + { + throw new RuntimeException("Could not find Provider!", e); + } + } + + + @Override + public void onAccountsUpdated(Account[] accounts) + { + // TODO: we probably can move the cleanup code here and get rid of the Utils class + Utils.cleanUpLists(getContext(), getDatabaseHelper().getWritableDatabase(), accounts, mAuthority); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProviderBroadcastReceiver.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProviderBroadcastReceiver.java index b0d63e9f..f06dd4a1 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProviderBroadcastReceiver.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProviderBroadcastReceiver.java @@ -17,10 +17,6 @@ package org.dmfs.provider.tasks; -import java.util.TimeZone; - -import org.dmfs.rfc5545.DateTime; - import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.PendingIntent; @@ -29,81 +25,85 @@ import android.content.Context; import android.content.Intent; import android.os.Build; +import org.dmfs.rfc5545.DateTime; + +import java.util.TimeZone; + /** * A receiver for all task provider related broadcasts. This receiver merely forwards all incoming broadcasts to the provider, so they can be handled * asynchronously in the provider context. - * + * * @author Marten Gajda */ public class TaskProviderBroadcastReceiver extends BroadcastReceiver { - private final static int REQUEST_CODE_ALARM = 1337; + private final static int REQUEST_CODE_ALARM = 1337; - private final static String ACTION_NOTIFICATION_ALARM = "org.dmfs.tasks.provider.NOTIFICATION_ALARM"; + private final static String ACTION_NOTIFICATION_ALARM = "org.dmfs.tasks.provider.NOTIFICATION_ALARM"; - /** - * Registers a system alarm to update notifications at a specific time. - * - * @param context - * A Context. - * @param updateTime - * When to fire the alarm. - */ - @SuppressLint("NewApi") - static void planNotificationUpdate(Context context, DateTime updateTime) - { - AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent alarmIntent = new Intent(context, TaskProviderBroadcastReceiver.class); - alarmIntent.setAction(ACTION_NOTIFICATION_ALARM); + /** + * Registers a system alarm to update notifications at a specific time. + * + * @param context + * A Context. + * @param updateTime + * When to fire the alarm. + */ + @SuppressLint("NewApi") + static void planNotificationUpdate(Context context, DateTime updateTime) + { + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent alarmIntent = new Intent(context, TaskProviderBroadcastReceiver.class); + alarmIntent.setAction(ACTION_NOTIFICATION_ALARM); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE_ALARM, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE_ALARM, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); - // cancel any previous alarm - am.cancel(pendingIntent); + // cancel any previous alarm + am.cancel(pendingIntent); - if (updateTime.isFloating()) - { - // convert floating times to absolute times - updateTime = new DateTime(TimeZone.getDefault(), updateTime.getYear(), updateTime.getMonth(), updateTime.getDayOfMonth(), updateTime.getHours(), - updateTime.getMinutes(), updateTime.getSeconds()); - } + if (updateTime.isFloating()) + { + // convert floating times to absolute times + updateTime = new DateTime(TimeZone.getDefault(), updateTime.getYear(), updateTime.getMonth(), updateTime.getDayOfMonth(), updateTime.getHours(), + updateTime.getMinutes(), updateTime.getSeconds()); + } - // AlarmManager API changed in v19 (KitKat) and the "set" method is not called at the exact time anymore - if (Build.VERSION.SDK_INT > 18) - { - am.setExact(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), pendingIntent); - } - else - { - am.set(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), pendingIntent); - } - } + // AlarmManager API changed in v19 (KitKat) and the "set" method is not called at the exact time anymore + if (Build.VERSION.SDK_INT > 18) + { + am.setExact(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), pendingIntent); + } + else + { + am.set(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), pendingIntent); + } + } - @Override - public void onReceive(Context context, Intent intent) - { - String action = intent.getAction(); - switch (action) - { - case Intent.ACTION_TIMEZONE_CHANGED: - { - // the local timezone has been changed, notify the provider to take the necessary steps. - // don't trigger the notifications update yet, because the timezone update will run asynhronously and we need to wait till that's finished - ContentOperation.UPDATE_TIMEZONE.fire(context, null); - } - case ACTION_NOTIFICATION_ALARM: - { - // it's time for the next notification - ContentOperation.POST_NOTIFICATIONS.fire(context, null); - } - default: - { - // at this time all other actions trigger an update of the notification alarm - ContentOperation.UPDATE_NOTIFICATION_ALARM.fire(context, null); - } - } - } + @Override + public void onReceive(Context context, Intent intent) + { + String action = intent.getAction(); + switch (action) + { + case Intent.ACTION_TIMEZONE_CHANGED: + { + // the local timezone has been changed, notify the provider to take the necessary steps. + // don't trigger the notifications update yet, because the timezone update will run asynhronously and we need to wait till that's finished + ContentOperation.UPDATE_TIMEZONE.fire(context, null); + } + case ACTION_NOTIFICATION_ALARM: + { + // it's time for the next notification + ContentOperation.POST_NOTIFICATIONS.fire(context, null); + } + default: + { + // at this time all other actions trigger an update of the notification alarm + ContentOperation.UPDATE_NOTIFICATION_ALARM.fire(context, null); + } + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/UriFactory.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/UriFactory.java index 2e330b6a..9c479060 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/UriFactory.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/UriFactory.java @@ -17,40 +17,40 @@ package org.dmfs.provider.tasks; +import android.net.Uri; + import java.util.HashMap; import java.util.Map; -import android.net.Uri; - public class UriFactory { - public final String authority; + public final String authority; - private final Map mUriMap = new HashMap(16); + private final Map mUriMap = new HashMap(16); - UriFactory(String authority) - { - this.authority = authority; - mUriMap.put((String) null, Uri.parse("content://" + authority)); - } + UriFactory(String authority) + { + this.authority = authority; + mUriMap.put((String) null, Uri.parse("content://" + authority)); + } - void addUri(String path) - { - mUriMap.put(path, Uri.parse("content://" + authority + "/" + path)); - } + void addUri(String path) + { + mUriMap.put(path, Uri.parse("content://" + authority + "/" + path)); + } - public Uri getUri() - { - return mUriMap.get(null); - } + public Uri getUri() + { + return mUriMap.get(null); + } - public Uri getUri(String path) - { - return mUriMap.get(path); - } + public Uri getUri(String path) + { + return mUriMap.get(path); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/Utils.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/Utils.java index c0a4a7f8..2bf28ad3 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/Utils.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/Utils.java @@ -17,9 +17,12 @@ package org.dmfs.provider.tasks; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import android.accounts.Account; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import org.dmfs.provider.tasks.TaskContract.Instances; import org.dmfs.provider.tasks.TaskContract.SyncState; @@ -29,99 +32,97 @@ import org.dmfs.provider.tasks.TaskContract.TaskLists; import org.dmfs.provider.tasks.TaskContract.Tasks; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; -import android.accounts.Account; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * The Class Utils. - * + * * @author Tobias Reinsch * @author Marten Gajda */ public class Utils { - public static void sendActionProviderChangedBroadCast(Context context, String authority) - { - // TODO: Using the TaskContract content uri results in a "Unknown URI content" error message. Using the Tasks content uri instead will break the - // broadcast receiver. We have to find away around this - Intent providerChangedIntent = new Intent(Intent.ACTION_PROVIDER_CHANGED, TaskContract.getContentUri(authority)); - context.sendBroadcast(providerChangedIntent); - } - - - public static void cleanUpLists(Context context, SQLiteDatabase db, Account[] accounts, String authority) - { - // make a list of the accounts array - List accountList = Arrays.asList(accounts); - - db.beginTransaction(); - - try - { - Cursor c = db.query(Tables.LISTS, new String[] { TaskListColumns._ID, TaskListSyncColumns.ACCOUNT_NAME, TaskListSyncColumns.ACCOUNT_TYPE }, null, - null, null, null, null); - - // build a list of all task list ids that no longer have an account - List obsoleteLists = new ArrayList(); - try - { - while (c.moveToNext()) - { - String accountType = c.getString(2); - // mark list for removal if it is non-local and the account - // is not in accountList - if (!TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType)) - { - Account account = new Account(c.getString(1), accountType); - if (!accountList.contains(account)) - { - obsoleteLists.add(c.getLong(0)); - - // remove syncstate for this account right away - db.delete(Tables.SYNCSTATE, SyncState.ACCOUNT_NAME + "=? and " + SyncState.ACCOUNT_TYPE + "=?", new String[] { account.name, - account.type }); - } - } - } - } - finally - { - c.close(); - } - - if (obsoleteLists.size() == 0) - { - // nothing to do here - return; - } - - // remove all accounts in the list - for (Long id : obsoleteLists) - { - if (id != null) - { - db.delete(Tables.LISTS, TaskListColumns._ID + "=" + id, null); - } - } - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } - // notify all observers - - ContentResolver cr = context.getContentResolver(); - cr.notifyChange(TaskLists.getContentUri(authority), null); - cr.notifyChange(Tasks.getContentUri(authority), null); - cr.notifyChange(Instances.getContentUri(authority), null); - - Utils.sendActionProviderChangedBroadCast(context, authority); - } + public static void sendActionProviderChangedBroadCast(Context context, String authority) + { + // TODO: Using the TaskContract content uri results in a "Unknown URI content" error message. Using the Tasks content uri instead will break the + // broadcast receiver. We have to find away around this + Intent providerChangedIntent = new Intent(Intent.ACTION_PROVIDER_CHANGED, TaskContract.getContentUri(authority)); + context.sendBroadcast(providerChangedIntent); + } + + + public static void cleanUpLists(Context context, SQLiteDatabase db, Account[] accounts, String authority) + { + // make a list of the accounts array + List accountList = Arrays.asList(accounts); + + db.beginTransaction(); + + try + { + Cursor c = db.query(Tables.LISTS, new String[] { TaskListColumns._ID, TaskListSyncColumns.ACCOUNT_NAME, TaskListSyncColumns.ACCOUNT_TYPE }, null, + null, null, null, null); + + // build a list of all task list ids that no longer have an account + List obsoleteLists = new ArrayList(); + try + { + while (c.moveToNext()) + { + String accountType = c.getString(2); + // mark list for removal if it is non-local and the account + // is not in accountList + if (!TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType)) + { + Account account = new Account(c.getString(1), accountType); + if (!accountList.contains(account)) + { + obsoleteLists.add(c.getLong(0)); + + // remove syncstate for this account right away + db.delete(Tables.SYNCSTATE, SyncState.ACCOUNT_NAME + "=? and " + SyncState.ACCOUNT_TYPE + "=?", new String[] { + account.name, + account.type }); + } + } + } + } + finally + { + c.close(); + } + + if (obsoleteLists.size() == 0) + { + // nothing to do here + return; + } + + // remove all accounts in the list + for (Long id : obsoleteLists) + { + if (id != null) + { + db.delete(Tables.LISTS, TaskListColumns._ID + "=" + id, null); + } + } + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + // notify all observers + + ContentResolver cr = context.getContentResolver(); + cr.notifyChange(TaskLists.getContentUri(authority), null); + cr.notifyChange(Tasks.getContentUri(authority), null); + cr.notifyChange(Instances.getContentUri(authority), null); + + Utils.sendActionProviderChangedBroadCast(context, authority); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/AlarmHandler.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/AlarmHandler.java index a1b93810..e6e6f341 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/AlarmHandler.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/AlarmHandler.java @@ -17,118 +17,118 @@ package org.dmfs.provider.tasks.handler; -import org.dmfs.provider.tasks.TaskContract.Property; - import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import org.dmfs.provider.tasks.TaskContract.Property; + /** * This class is used to handle alarm property values during database transactions. - * + * * @author Tobias Reinsch - * */ public class AlarmHandler extends PropertyHandler { - // private static final String[] ALARM_ID_PROJECTION = { Alarms.ALARM_ID }; - // private static final String ALARM_SELECTION = Alarms.ALARM_ID + " =?"; - - /** - * Validates the content of the alarm prior to insert and update transactions. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The id of the task this property belongs to. - * @param propertyId - * The id of the property if isNew is false. If isNew is true this value is ignored. - * @param isNew - * Indicates that the content is new and not an update. - * @param values - * The {@link ContentValues} to validate. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The valid {@link ContentValues}. - * - * @throws IllegalArgumentException - * if the {@link ContentValues} are invalid. - */ - @Override - public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) - { - // row id can not be changed or set manually - if (values.containsKey(Property.Alarm.PROPERTY_ID)) - { - throw new IllegalArgumentException("_ID can not be set manually"); - } - - if (!values.containsKey(Property.Alarm.MINUTES_BEFORE)) - { - throw new IllegalArgumentException("alarm property requires a time offset"); - } - - if (!values.containsKey(Property.Alarm.REFERENCE) || values.getAsInteger(Property.Alarm.REFERENCE) < 0) - { - throw new IllegalArgumentException("alarm property requires a valid reference date "); - } - - if (!values.containsKey(Property.Alarm.ALARM_TYPE)) - { - throw new IllegalArgumentException("alarm property requires an alarm type"); - } - - return values; - } - - - /** - * Inserts the alarm into the database. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The id of the task the new property belongs to. - * @param values - * The {@link ContentValues} to insert. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The row id of the new alarm as long - */ - @Override - public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) - { - values = validateValues(db, taskId, -1, true, values, isSyncAdapter); - return super.insert(db, taskId, values, isSyncAdapter); - } - - - /** - * Updates the alarm in the database. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The id of the task this property belongs to. - * @param propertyId - * The id of the property. - * @param values - * The {@link ContentValues} to update. - * @param oldValues - * A {@link Cursor} pointing to the old values in the database. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The number of rows affected. - */ - @Override - public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) - { - values = validateValues(db, taskId, propertyId, false, values, isSyncAdapter); - return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter); - } + // private static final String[] ALARM_ID_PROJECTION = { Alarms.ALARM_ID }; + // private static final String ALARM_SELECTION = Alarms.ALARM_ID + " =?"; + + + /** + * Validates the content of the alarm prior to insert and update transactions. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The id of the task this property belongs to. + * @param propertyId + * The id of the property if isNew is false. If isNew is true this value is ignored. + * @param isNew + * Indicates that the content is new and not an update. + * @param values + * The {@link ContentValues} to validate. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The valid {@link ContentValues}. + * + * @throws IllegalArgumentException + * if the {@link ContentValues} are invalid. + */ + @Override + public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) + { + // row id can not be changed or set manually + if (values.containsKey(Property.Alarm.PROPERTY_ID)) + { + throw new IllegalArgumentException("_ID can not be set manually"); + } + + if (!values.containsKey(Property.Alarm.MINUTES_BEFORE)) + { + throw new IllegalArgumentException("alarm property requires a time offset"); + } + + if (!values.containsKey(Property.Alarm.REFERENCE) || values.getAsInteger(Property.Alarm.REFERENCE) < 0) + { + throw new IllegalArgumentException("alarm property requires a valid reference date "); + } + + if (!values.containsKey(Property.Alarm.ALARM_TYPE)) + { + throw new IllegalArgumentException("alarm property requires an alarm type"); + } + + return values; + } + + + /** + * Inserts the alarm into the database. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The id of the task the new property belongs to. + * @param values + * The {@link ContentValues} to insert. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The row id of the new alarm as long + */ + @Override + public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) + { + values = validateValues(db, taskId, -1, true, values, isSyncAdapter); + return super.insert(db, taskId, values, isSyncAdapter); + } + + + /** + * Updates the alarm in the database. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The id of the task this property belongs to. + * @param propertyId + * The id of the property. + * @param values + * The {@link ContentValues} to update. + * @param oldValues + * A {@link Cursor} pointing to the old values in the database. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The number of rows affected. + */ + @Override + public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) + { + values = validateValues(db, taskId, propertyId, false, values, isSyncAdapter); + return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/CategoryHandler.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/CategoryHandler.java index 87d5148a..bd3fd9e9 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/CategoryHandler.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/CategoryHandler.java @@ -17,6 +17,10 @@ package org.dmfs.provider.tasks.handler; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract.Categories; import org.dmfs.provider.tasks.TaskContract.Properties; import org.dmfs.provider.tasks.TaskContract.Property.Category; @@ -24,254 +28,251 @@ import org.dmfs.provider.tasks.TaskContract.Tasks; import org.dmfs.provider.tasks.TaskDatabaseHelper.CategoriesMapping; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - /** * This class is used to handle category property values during database transactions. - * + * * @author Tobias Reinsch - * */ public class CategoryHandler extends PropertyHandler { - private static final String[] CATEGORY_ID_PROJECTION = { Categories._ID, Categories.NAME, Categories.COLOR }; - - private static final String CATEGORY_ID_SELECTION = Categories._ID + "=? and " + Categories.ACCOUNT_NAME + "=? and " + Categories.ACCOUNT_TYPE + "=?"; - private static final String CATEGORY_NAME_SELECTION = Categories.NAME + "=? and " + Categories.ACCOUNT_NAME + "=? and " + Categories.ACCOUNT_TYPE + "=?"; - - public static final String IS_NEW_CATEGORY = "is_new_category"; - - - /** - * Validates the content of the category prior to insert and update transactions. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The id of the task this property belongs to. - * @param propertyId - * The id of the property if isNew is false. If isNew is true this value is ignored. - * @param isNew - * Indicates that the content is new and not an update. - * @param values - * The {@link ContentValues} to validate. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The valid {@link ContentValues}. - * - * @throws IllegalArgumentException - * if the {@link ContentValues} are invalid. - */ - @Override - public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) - { - // the category requires a name or an id - if (!values.containsKey(Category.CATEGORY_ID) && !values.containsKey(Category.CATEGORY_NAME)) - { - throw new IllegalArgumentException("Neiter an id nor a category name was supplied for the category property."); - } - - // get the matching task & account for the property - if (!values.containsKey(Properties.TASK_ID)) - { - throw new IllegalArgumentException("No task id was supplied for the category property"); - } - String[] queryArgs = { values.getAsString(Properties.TASK_ID) }; - String[] queryProjection = { Tasks.ACCOUNT_NAME, Tasks.ACCOUNT_TYPE }; - String querySelection = Tasks._ID + "=?"; - Cursor taskCursor = db.query(Tables.TASKS_VIEW, queryProjection, querySelection, queryArgs, null, null, null); - - String accountName = null; - String accountType = null; - try - { - if (taskCursor.moveToNext()) - { - accountName = taskCursor.getString(0); - accountType = taskCursor.getString(1); - - values.put(Categories.ACCOUNT_NAME, accountName); - values.put(Categories.ACCOUNT_TYPE, accountType); - } - } - finally - { - if (taskCursor != null) - { - taskCursor.close(); - } - } - - if (accountName != null && accountType != null) - { - // search for matching categories - String[] categoryArgs; - Cursor cursor; - - if (values.containsKey(Categories._ID)) - { - // serach by ID - categoryArgs = new String[] { values.getAsString(Category.CATEGORY_ID), accountName, accountType }; - cursor = db.query(Tables.CATEGORIES, CATEGORY_ID_PROJECTION, CATEGORY_ID_SELECTION, categoryArgs, null, null, null); - } - else - { - // search by name - categoryArgs = new String[] { values.getAsString(Category.CATEGORY_NAME), accountName, accountType }; - cursor = db.query(Tables.CATEGORIES, CATEGORY_ID_PROJECTION, CATEGORY_NAME_SELECTION, categoryArgs, null, null, null); - } - try - { - if (cursor != null && cursor.getCount() == 1) - { - cursor.moveToNext(); - Long categoryID = cursor.getLong(0); - String categoryName = cursor.getString(1); - int color = cursor.getInt(2); - - values.put(Category.CATEGORY_ID, categoryID); - values.put(Category.CATEGORY_NAME, categoryName); - values.put(Category.CATEGORY_COLOR, color); - values.put(IS_NEW_CATEGORY, false); - } - else - { - values.put(IS_NEW_CATEGORY, true); - } - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } - - } - - return values; - } - - - /** - * Inserts the category into the database. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The id of the task the new property belongs to. - * @param values - * The {@link ContentValues} to insert. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The row id of the new category as long - */ - @Override - public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) - { - values = validateValues(db, taskId, -1, true, values, isSyncAdapter); - values = getOrInsertCategory(db, values); - - // insert property row and create relation - long id = super.insert(db, taskId, values, isSyncAdapter); - insertRelation(db, taskId, values.getAsLong(Category.CATEGORY_ID), id); - - // update FTS entry with category name - updateFTSEntry(db, taskId, id, values.getAsString(Category.CATEGORY_NAME)); - return id; - } - - - /** - * Updates the category in the database. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The id of the task this property belongs to. - * @param propertyId - * The id of the property. - * @param values - * The {@link ContentValues} to update. - * @param oldValues - * A {@link Cursor} pointing to the old values in the database. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The number of rows affected. - */ - @Override - public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) - { - values = validateValues(db, taskId, propertyId, false, values, isSyncAdapter); - values = getOrInsertCategory(db, values); - - if (values.containsKey(Category.CATEGORY_NAME)) - { - // update FTS entry with new category name - updateFTSEntry(db, taskId, propertyId, values.getAsString(Category.CATEGORY_NAME)); - } - - return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter); - } - - - /** - * Check if a category with matching {@link ContentValues} exists and returns the existing category or creates a new category in the database. - * - * @param db - * The {@link SQLiteDatabase}. - * @param values - * The {@link ContentValues} of the category. - * @return The {@link ContentValues} of the existing or new category. - */ - private ContentValues getOrInsertCategory(SQLiteDatabase db, ContentValues values) - { - if (values.getAsBoolean(IS_NEW_CATEGORY)) - { - // insert new category in category table - ContentValues newCategoryValues = new ContentValues(4); - newCategoryValues.put(Categories.ACCOUNT_NAME, values.getAsString(Categories.ACCOUNT_NAME)); - newCategoryValues.put(Categories.ACCOUNT_TYPE, values.getAsString(Categories.ACCOUNT_TYPE)); - newCategoryValues.put(Categories.NAME, values.getAsString(Category.CATEGORY_NAME)); - newCategoryValues.put(Categories.COLOR, values.getAsInteger(Category.CATEGORY_COLOR)); - - long categoryID = db.insert(Tables.CATEGORIES, "", newCategoryValues); - values.put(Category.CATEGORY_ID, categoryID); - } - - // remove redundant values - values.remove(IS_NEW_CATEGORY); - values.remove(Categories.ACCOUNT_NAME); - values.remove(Categories.ACCOUNT_TYPE); - - return values; - } - - - /** - * Inserts a relation entry in the database to link task and category. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The row id of the task. - * @param categoryId - * The row id of the category. - * @return The row id of the inserted relation. - */ - private long insertRelation(SQLiteDatabase db, long taskId, long categoryId, long propertyId) - { - ContentValues relationValues = new ContentValues(3); - relationValues.put(CategoriesMapping.TASK_ID, taskId); - relationValues.put(CategoriesMapping.CATEGORY_ID, categoryId); - relationValues.put(CategoriesMapping.PROPERTY_ID, propertyId); - return db.insert(Tables.CATEGORIES_MAPPING, "", relationValues); - } + private static final String[] CATEGORY_ID_PROJECTION = { Categories._ID, Categories.NAME, Categories.COLOR }; + + private static final String CATEGORY_ID_SELECTION = Categories._ID + "=? and " + Categories.ACCOUNT_NAME + "=? and " + Categories.ACCOUNT_TYPE + "=?"; + private static final String CATEGORY_NAME_SELECTION = Categories.NAME + "=? and " + Categories.ACCOUNT_NAME + "=? and " + Categories.ACCOUNT_TYPE + "=?"; + + public static final String IS_NEW_CATEGORY = "is_new_category"; + + + /** + * Validates the content of the category prior to insert and update transactions. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The id of the task this property belongs to. + * @param propertyId + * The id of the property if isNew is false. If isNew is true this value is ignored. + * @param isNew + * Indicates that the content is new and not an update. + * @param values + * The {@link ContentValues} to validate. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The valid {@link ContentValues}. + * + * @throws IllegalArgumentException + * if the {@link ContentValues} are invalid. + */ + @Override + public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) + { + // the category requires a name or an id + if (!values.containsKey(Category.CATEGORY_ID) && !values.containsKey(Category.CATEGORY_NAME)) + { + throw new IllegalArgumentException("Neiter an id nor a category name was supplied for the category property."); + } + + // get the matching task & account for the property + if (!values.containsKey(Properties.TASK_ID)) + { + throw new IllegalArgumentException("No task id was supplied for the category property"); + } + String[] queryArgs = { values.getAsString(Properties.TASK_ID) }; + String[] queryProjection = { Tasks.ACCOUNT_NAME, Tasks.ACCOUNT_TYPE }; + String querySelection = Tasks._ID + "=?"; + Cursor taskCursor = db.query(Tables.TASKS_VIEW, queryProjection, querySelection, queryArgs, null, null, null); + + String accountName = null; + String accountType = null; + try + { + if (taskCursor.moveToNext()) + { + accountName = taskCursor.getString(0); + accountType = taskCursor.getString(1); + + values.put(Categories.ACCOUNT_NAME, accountName); + values.put(Categories.ACCOUNT_TYPE, accountType); + } + } + finally + { + if (taskCursor != null) + { + taskCursor.close(); + } + } + + if (accountName != null && accountType != null) + { + // search for matching categories + String[] categoryArgs; + Cursor cursor; + + if (values.containsKey(Categories._ID)) + { + // serach by ID + categoryArgs = new String[] { values.getAsString(Category.CATEGORY_ID), accountName, accountType }; + cursor = db.query(Tables.CATEGORIES, CATEGORY_ID_PROJECTION, CATEGORY_ID_SELECTION, categoryArgs, null, null, null); + } + else + { + // search by name + categoryArgs = new String[] { values.getAsString(Category.CATEGORY_NAME), accountName, accountType }; + cursor = db.query(Tables.CATEGORIES, CATEGORY_ID_PROJECTION, CATEGORY_NAME_SELECTION, categoryArgs, null, null, null); + } + try + { + if (cursor != null && cursor.getCount() == 1) + { + cursor.moveToNext(); + Long categoryID = cursor.getLong(0); + String categoryName = cursor.getString(1); + int color = cursor.getInt(2); + + values.put(Category.CATEGORY_ID, categoryID); + values.put(Category.CATEGORY_NAME, categoryName); + values.put(Category.CATEGORY_COLOR, color); + values.put(IS_NEW_CATEGORY, false); + } + else + { + values.put(IS_NEW_CATEGORY, true); + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + + } + + return values; + } + + + /** + * Inserts the category into the database. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The id of the task the new property belongs to. + * @param values + * The {@link ContentValues} to insert. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The row id of the new category as long + */ + @Override + public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) + { + values = validateValues(db, taskId, -1, true, values, isSyncAdapter); + values = getOrInsertCategory(db, values); + + // insert property row and create relation + long id = super.insert(db, taskId, values, isSyncAdapter); + insertRelation(db, taskId, values.getAsLong(Category.CATEGORY_ID), id); + + // update FTS entry with category name + updateFTSEntry(db, taskId, id, values.getAsString(Category.CATEGORY_NAME)); + return id; + } + + + /** + * Updates the category in the database. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The id of the task this property belongs to. + * @param propertyId + * The id of the property. + * @param values + * The {@link ContentValues} to update. + * @param oldValues + * A {@link Cursor} pointing to the old values in the database. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The number of rows affected. + */ + @Override + public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) + { + values = validateValues(db, taskId, propertyId, false, values, isSyncAdapter); + values = getOrInsertCategory(db, values); + + if (values.containsKey(Category.CATEGORY_NAME)) + { + // update FTS entry with new category name + updateFTSEntry(db, taskId, propertyId, values.getAsString(Category.CATEGORY_NAME)); + } + + return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter); + } + + + /** + * Check if a category with matching {@link ContentValues} exists and returns the existing category or creates a new category in the database. + * + * @param db + * The {@link SQLiteDatabase}. + * @param values + * The {@link ContentValues} of the category. + * + * @return The {@link ContentValues} of the existing or new category. + */ + private ContentValues getOrInsertCategory(SQLiteDatabase db, ContentValues values) + { + if (values.getAsBoolean(IS_NEW_CATEGORY)) + { + // insert new category in category table + ContentValues newCategoryValues = new ContentValues(4); + newCategoryValues.put(Categories.ACCOUNT_NAME, values.getAsString(Categories.ACCOUNT_NAME)); + newCategoryValues.put(Categories.ACCOUNT_TYPE, values.getAsString(Categories.ACCOUNT_TYPE)); + newCategoryValues.put(Categories.NAME, values.getAsString(Category.CATEGORY_NAME)); + newCategoryValues.put(Categories.COLOR, values.getAsInteger(Category.CATEGORY_COLOR)); + + long categoryID = db.insert(Tables.CATEGORIES, "", newCategoryValues); + values.put(Category.CATEGORY_ID, categoryID); + } + + // remove redundant values + values.remove(IS_NEW_CATEGORY); + values.remove(Categories.ACCOUNT_NAME); + values.remove(Categories.ACCOUNT_TYPE); + + return values; + } + + + /** + * Inserts a relation entry in the database to link task and category. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The row id of the task. + * @param categoryId + * The row id of the category. + * + * @return The row id of the inserted relation. + */ + private long insertRelation(SQLiteDatabase db, long taskId, long categoryId, long propertyId) + { + ContentValues relationValues = new ContentValues(3); + relationValues.put(CategoriesMapping.TASK_ID, taskId); + relationValues.put(CategoriesMapping.CATEGORY_ID, categoryId); + relationValues.put(CategoriesMapping.PROPERTY_ID, propertyId); + return db.insert(Tables.CATEGORIES_MAPPING, "", relationValues); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/DefaultPropertyHandler.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/DefaultPropertyHandler.java index 54cbe66c..84c5f976 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/DefaultPropertyHandler.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/DefaultPropertyHandler.java @@ -23,34 +23,33 @@ import android.database.sqlite.SQLiteDatabase; /** * This class is used to handle properties with unknown / unsupported mime-types. - * + * * @author Tobias Reinsch - * */ public class DefaultPropertyHandler extends PropertyHandler { - /** - * Validates the content of the alarm prior to insert and update transactions. - * - * @param db - * The {@link SQLiteDatabase}. - * @param isNew - * Indicates that the content is new and not an update. - * @param values - * The {@link ContentValues} to validate. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The valid {@link ContentValues}. - * - * @throws IllegalArgumentException - * if the {@link ContentValues} are invalid. - */ - @Override - public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) - { - return values; - } + /** + * Validates the content of the alarm prior to insert and update transactions. + * + * @param db + * The {@link SQLiteDatabase}. + * @param isNew + * Indicates that the content is new and not an update. + * @param values + * The {@link ContentValues} to validate. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The valid {@link ContentValues}. + * + * @throws IllegalArgumentException + * if the {@link ContentValues} are invalid. + */ + @Override + public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) + { + return values; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandler.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandler.java index 2c3ae192..0c2d550c 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandler.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandler.java @@ -17,129 +17,129 @@ package org.dmfs.provider.tasks.handler; -import org.dmfs.provider.tasks.FTSDatabaseHelper; -import org.dmfs.provider.tasks.TaskContract.Properties; -import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; - import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import org.dmfs.provider.tasks.FTSDatabaseHelper; +import org.dmfs.provider.tasks.TaskContract.Properties; +import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; + /** * Abstract class that is used as template for specific property handlers. - * + * * @author Tobias Reinsch - * */ public abstract class PropertyHandler { - /** - * Validates the content of the property prior to insert and update transactions. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The id of the task this property belongs to. - * @param propertyId - * The id of the property if isNew is false. If isNew is true this value is ignored. - * @param isNew - * Indicates that the content is new and not an update. - * @param values - * The {@link ContentValues} to validate. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The valid {@link ContentValues}. - * - * @throws IllegalArgumentException - * if the {@link ContentValues} are invalid. - */ - public abstract ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter); - - - /** - * Inserts the property {@link ContentValues} into the database. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The id of the task the new property belongs to. - * @param values - * The {@link ContentValues} to insert. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The row id of the new property as long - */ - public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) - { - return db.insert(Tables.PROPERTIES, "", values); - } - - - /** - * Updates the property {@link ContentValues} in the database. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * The id of the task this property belongs to. - * @param propertyId - * The id of the property. - * @param values - * The {@link ContentValues} to update. - * @param oldValues - * A {@link Cursor} pointing to the old values in the database. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * - * @return The number of rows affected. - */ - public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) - { - return db.update(Tables.PROPERTIES, values, Properties.PROPERTY_ID + "=" + propertyId, null); - } - - - /** - * Deletes the property in the database. - * - * @param db - * The belonging database. - * @param taskId - * The id of the task this property belongs to. - * @param propertyId - * The id of the property. - * @param oldValues - * A {@link Cursor} pointing to the old values in the database. - * @param isSyncAdapter - * Indicates that the transaction was triggered from a SyncAdapter. - * @return - */ - public int delete(SQLiteDatabase db, long taskId, long propertyId, Cursor oldValues, boolean isSyncAdapter) - { - return db.delete(Tables.PROPERTIES, Properties.PROPERTY_ID + "=" + propertyId, null); - - } - - - /** - * Method hook to insert FTS entries on database migration. - * - * @param db - * The {@link SQLiteDatabase}. - * @param taskId - * the row id of the task this property belongs to - * @param propertyId - * the id of the property - * @param text - * the searchable text of the property. If the property has multiple text snippets to search in, concat them separated by a space. - */ - protected void updateFTSEntry(SQLiteDatabase db, long taskId, long propertyId, String text) - { - FTSDatabaseHelper.updatePropertyFTSEntry(db, taskId, propertyId, text); - - } + /** + * Validates the content of the property prior to insert and update transactions. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The id of the task this property belongs to. + * @param propertyId + * The id of the property if isNew is false. If isNew is true this value is ignored. + * @param isNew + * Indicates that the content is new and not an update. + * @param values + * The {@link ContentValues} to validate. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The valid {@link ContentValues}. + * + * @throws IllegalArgumentException + * if the {@link ContentValues} are invalid. + */ + public abstract ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter); + + + /** + * Inserts the property {@link ContentValues} into the database. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The id of the task the new property belongs to. + * @param values + * The {@link ContentValues} to insert. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The row id of the new property as long + */ + public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) + { + return db.insert(Tables.PROPERTIES, "", values); + } + + + /** + * Updates the property {@link ContentValues} in the database. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * The id of the task this property belongs to. + * @param propertyId + * The id of the property. + * @param values + * The {@link ContentValues} to update. + * @param oldValues + * A {@link Cursor} pointing to the old values in the database. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return The number of rows affected. + */ + public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) + { + return db.update(Tables.PROPERTIES, values, Properties.PROPERTY_ID + "=" + propertyId, null); + } + + + /** + * Deletes the property in the database. + * + * @param db + * The belonging database. + * @param taskId + * The id of the task this property belongs to. + * @param propertyId + * The id of the property. + * @param oldValues + * A {@link Cursor} pointing to the old values in the database. + * @param isSyncAdapter + * Indicates that the transaction was triggered from a SyncAdapter. + * + * @return + */ + public int delete(SQLiteDatabase db, long taskId, long propertyId, Cursor oldValues, boolean isSyncAdapter) + { + return db.delete(Tables.PROPERTIES, Properties.PROPERTY_ID + "=" + propertyId, null); + + } + + + /** + * Method hook to insert FTS entries on database migration. + * + * @param db + * The {@link SQLiteDatabase}. + * @param taskId + * the row id of the task this property belongs to + * @param propertyId + * the id of the property + * @param text + * the searchable text of the property. If the property has multiple text snippets to search in, concat them separated by a space. + */ + protected void updateFTSEntry(SQLiteDatabase db, long taskId, long propertyId, String text) + { + FTSDatabaseHelper.updatePropertyFTSEntry(db, taskId, propertyId, text); + + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandlerFactory.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandlerFactory.java index e424658a..449abda7 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandlerFactory.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandlerFactory.java @@ -24,39 +24,39 @@ import org.dmfs.provider.tasks.TaskContract.Property.Relation; /** * A factory that creates the matching {@link PropertyHandler} for the given mimetype. - * + * * @author Tobias Reinsch - * */ public class PropertyHandlerFactory { - private final static PropertyHandler CATEGORY_HANDLER = new CategoryHandler(); - private final static PropertyHandler ALARM_HANDLER = new AlarmHandler(); - private final static PropertyHandler RELATION_HANDLER = new RelationHandler(); - private final static PropertyHandler DEFAULT_PROPERTY_HANDLER = new DefaultPropertyHandler(); + private final static PropertyHandler CATEGORY_HANDLER = new CategoryHandler(); + private final static PropertyHandler ALARM_HANDLER = new AlarmHandler(); + private final static PropertyHandler RELATION_HANDLER = new RelationHandler(); + private final static PropertyHandler DEFAULT_PROPERTY_HANDLER = new DefaultPropertyHandler(); - /** - * Creates a specific {@link PropertyHandler}. - * - * @param mimeType - * The mimetype of the property. - * @return The matching {@link PropertyHandler} for the given mimetype or null - */ - public static PropertyHandler get(String mimeType) - { - if (Category.CONTENT_ITEM_TYPE.equals(mimeType)) - { - return CATEGORY_HANDLER; - } - if (Alarm.CONTENT_ITEM_TYPE.equals(mimeType)) - { - return ALARM_HANDLER; - } - if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) - { - return RELATION_HANDLER; - } - return DEFAULT_PROPERTY_HANDLER; - } + /** + * Creates a specific {@link PropertyHandler}. + * + * @param mimeType + * The mimetype of the property. + * + * @return The matching {@link PropertyHandler} for the given mimetype or null + */ + public static PropertyHandler get(String mimeType) + { + if (Category.CONTENT_ITEM_TYPE.equals(mimeType)) + { + return CATEGORY_HANDLER; + } + if (Alarm.CONTENT_ITEM_TYPE.equals(mimeType)) + { + return ALARM_HANDLER; + } + if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) + { + return RELATION_HANDLER; + } + return DEFAULT_PROPERTY_HANDLER; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/RelationHandler.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/RelationHandler.java index 574c1b98..7e2962bc 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/RelationHandler.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/handler/RelationHandler.java @@ -17,255 +17,255 @@ package org.dmfs.provider.tasks.handler; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract.Property.Relation; import org.dmfs.provider.tasks.TaskContract.Property.Relation.RelType; import org.dmfs.provider.tasks.TaskContract.Tasks; import org.dmfs.provider.tasks.TaskDatabaseHelper; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - /** * Handles any inserts, updates and deletes on the relations table. - * + * * @author Marten Gajda */ public class RelationHandler extends PropertyHandler { - @Override - public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) - { - if (values.containsKey(Relation.RELATED_CONTENT_URI)) - { - throw new IllegalArgumentException("setting of RELATED_CONTENT_URI not allowed"); - } - - Long id = values.getAsLong(Relation.RELATED_ID); - String uid = values.getAsString(Relation.RELATED_UID); - String uri = values.getAsString(Relation.RELATED_URI); - - if (id == null && uri == null && uid != null) - { - values.putNull(Relation.RELATED_ID); - values.putNull(Relation.RELATED_URI); - } - else if (id == null && uid == null && uri != null) - { - values.putNull(Relation.RELATED_ID); - values.putNull(Relation.RELATED_UID); - } - else if (id != null && uid == null && uri == null) - { - values.putNull(Relation.RELATED_URI); - values.putNull(Relation.RELATED_UID); - } - else - { - throw new IllegalArgumentException("exactly one of RELATED_ID, RELATED_UID and RELATED_URI must be non-null"); - } - - return values; - } - - - @Override - public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) - { - validateValues(db, taskId, -1, true, values, isSyncAdapter); - resolveFields(db, values); - updateParentId(db, taskId, values, null); - return super.insert(db, taskId, values, isSyncAdapter); - } - - - @Override - public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) - { - validateValues(db, taskId, propertyId, false, values, isSyncAdapter); - resolveFields(db, values); - updateParentId(db, taskId, values, oldValues); - return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter); - } - - - @Override - public int delete(SQLiteDatabase db, long taskId, long propertyId, Cursor oldValues, boolean isSyncAdapter) - { - clearParentId(db, taskId, oldValues); - return super.delete(db, taskId, propertyId, oldValues, isSyncAdapter); - } - - - /** - * Resolve _id or _uid, depending of which value is given. We can't resolve anything if only {@link Relation#RELATED_URI} is - * given. The given values are update in-place. - *

- * TODO: store links into the calendar provider if we find an event that matches the UID. - *

- * - * @param db - * The task database. - * @param values - * The {@link ContentValues}. - */ - private void resolveFields(SQLiteDatabase db, ContentValues values) - { - Long id = values.getAsLong(Relation.RELATED_ID); - String uid = values.getAsString(Relation.RELATED_UID); - - if (id != null) - { - values.put(Relation.RELATED_UID, resolveTaskStringField(db, Tasks._ID, id.toString(), Tasks._UID)); - } - else if (uid != null) - { - values.put(Relation.RELATED_ID, resolveTaskLongField(db, Tasks._UID, uid, Tasks._ID)); - } - } - - - private Long resolveTaskLongField(SQLiteDatabase db, String selectionField, String selectionValue, String resultField) - { - String result = resolveTaskStringField(db, selectionField, selectionValue, resultField); - if (result != null) - { - return Long.parseLong(result); - } - return null; - } - - - private String resolveTaskStringField(SQLiteDatabase db, String selectionField, String selectionValue, String resultField) - { - Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, new String[] { resultField }, selectionField + "=?", new String[] { selectionValue }, null, null, - null); - if (c != null) - { - try - { - if (c.moveToNext()) - { - return c.getString(0); - } - } - finally - { - c.close(); - } - } - return null; - } - - - /** - * Update {@link Tasks#PARENT_ID} when a parent is assigned to a child. - * - * @param db - * @param taskId - * @param values - * @param oldValues - */ - private void updateParentId(SQLiteDatabase db, long taskId, ContentValues values, Cursor oldValues) - { - int type; - if (values.containsKey(Relation.RELATED_TYPE)) - { - type = values.getAsInteger(Relation.RELATED_TYPE); - } - else - { - type = oldValues.getInt(oldValues.getColumnIndex(Relation.RELATED_TYPE)); - } - - if (type == RelType.PARENT.ordinal()) - { - // this is a link to the parent, we need to update the PARENT_ID of this task, if we can - - if (values.containsKey(Relation.RELATED_ID)) - { - ContentValues taskValues = new ContentValues(1); - taskValues.put(Tasks.PARENT_ID, values.getAsLong(Relation.RELATED_ID)); - db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null); - } - // else: the parent task is probably not synced yet, we have to fix this in RelationUpdaterHook - } - else if (type == RelType.CHILD.ordinal()) - { - // this is a link to a child, we need to update the PARENT_ID of the linked task - - if (values.getAsLong(Relation.RELATED_ID) != null) - { - ContentValues taskValues = new ContentValues(1); - taskValues.put(Tasks.PARENT_ID, taskId); - db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + values.getAsLong(Relation.RELATED_ID), null); - } - // else: the child task is probably not synced yet, we have to fix this in RelationUpdaterHook - } - else if (type == RelType.SIBLING.ordinal()) - { - // this is a link to a sibling, we need to copy the PARENT_ID of the linked task to this task - if (values.getAsLong(Relation.RELATED_ID) != null) - { - // get the parent of the other task first - Long otherParent = resolveTaskLongField(db, Tasks._ID, values.getAsString(Relation.RELATED_ID), Tasks.PARENT_ID); - - ContentValues taskValues = new ContentValues(1); - taskValues.put(Tasks.PARENT_ID, otherParent); - db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null); - } - // else: the sibling task is probably not synced yet, we have to fix this in RelationUpdaterHook - } - } - - - /** - * Clear {@link Tasks#PARENT_ID} if a link is removed. - * - * @param db - * @param taskId - * @param oldValues - */ - private void clearParentId(SQLiteDatabase db, long taskId, Cursor oldValues) - { - int type = oldValues.getInt(oldValues.getColumnIndex(Relation.RELATED_TYPE)); + @Override + public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) + { + if (values.containsKey(Relation.RELATED_CONTENT_URI)) + { + throw new IllegalArgumentException("setting of RELATED_CONTENT_URI not allowed"); + } + + Long id = values.getAsLong(Relation.RELATED_ID); + String uid = values.getAsString(Relation.RELATED_UID); + String uri = values.getAsString(Relation.RELATED_URI); + + if (id == null && uri == null && uid != null) + { + values.putNull(Relation.RELATED_ID); + values.putNull(Relation.RELATED_URI); + } + else if (id == null && uid == null && uri != null) + { + values.putNull(Relation.RELATED_ID); + values.putNull(Relation.RELATED_UID); + } + else if (id != null && uid == null && uri == null) + { + values.putNull(Relation.RELATED_URI); + values.putNull(Relation.RELATED_UID); + } + else + { + throw new IllegalArgumentException("exactly one of RELATED_ID, RELATED_UID and RELATED_URI must be non-null"); + } + + return values; + } + + + @Override + public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) + { + validateValues(db, taskId, -1, true, values, isSyncAdapter); + resolveFields(db, values); + updateParentId(db, taskId, values, null); + return super.insert(db, taskId, values, isSyncAdapter); + } + + + @Override + public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) + { + validateValues(db, taskId, propertyId, false, values, isSyncAdapter); + resolveFields(db, values); + updateParentId(db, taskId, values, oldValues); + return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter); + } + + + @Override + public int delete(SQLiteDatabase db, long taskId, long propertyId, Cursor oldValues, boolean isSyncAdapter) + { + clearParentId(db, taskId, oldValues); + return super.delete(db, taskId, propertyId, oldValues, isSyncAdapter); + } + + + /** + * Resolve _id or _uid, depending of which value is given. We can't resolve anything if only {@link Relation#RELATED_URI} is + * given. The given values are update in-place. + *

+ * TODO: store links into the calendar provider if we find an event that matches the UID. + *

+ * + * @param db + * The task database. + * @param values + * The {@link ContentValues}. + */ + private void resolveFields(SQLiteDatabase db, ContentValues values) + { + Long id = values.getAsLong(Relation.RELATED_ID); + String uid = values.getAsString(Relation.RELATED_UID); + + if (id != null) + { + values.put(Relation.RELATED_UID, resolveTaskStringField(db, Tasks._ID, id.toString(), Tasks._UID)); + } + else if (uid != null) + { + values.put(Relation.RELATED_ID, resolveTaskLongField(db, Tasks._UID, uid, Tasks._ID)); + } + } + + + private Long resolveTaskLongField(SQLiteDatabase db, String selectionField, String selectionValue, String resultField) + { + String result = resolveTaskStringField(db, selectionField, selectionValue, resultField); + if (result != null) + { + return Long.parseLong(result); + } + return null; + } + + + private String resolveTaskStringField(SQLiteDatabase db, String selectionField, String selectionValue, String resultField) + { + Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, new String[] { resultField }, selectionField + "=?", new String[] { selectionValue }, null, null, + null); + if (c != null) + { + try + { + if (c.moveToNext()) + { + return c.getString(0); + } + } + finally + { + c.close(); + } + } + return null; + } + + + /** + * Update {@link Tasks#PARENT_ID} when a parent is assigned to a child. + * + * @param db + * @param taskId + * @param values + * @param oldValues + */ + private void updateParentId(SQLiteDatabase db, long taskId, ContentValues values, Cursor oldValues) + { + int type; + if (values.containsKey(Relation.RELATED_TYPE)) + { + type = values.getAsInteger(Relation.RELATED_TYPE); + } + else + { + type = oldValues.getInt(oldValues.getColumnIndex(Relation.RELATED_TYPE)); + } + + if (type == RelType.PARENT.ordinal()) + { + // this is a link to the parent, we need to update the PARENT_ID of this task, if we can + + if (values.containsKey(Relation.RELATED_ID)) + { + ContentValues taskValues = new ContentValues(1); + taskValues.put(Tasks.PARENT_ID, values.getAsLong(Relation.RELATED_ID)); + db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null); + } + // else: the parent task is probably not synced yet, we have to fix this in RelationUpdaterHook + } + else if (type == RelType.CHILD.ordinal()) + { + // this is a link to a child, we need to update the PARENT_ID of the linked task + + if (values.getAsLong(Relation.RELATED_ID) != null) + { + ContentValues taskValues = new ContentValues(1); + taskValues.put(Tasks.PARENT_ID, taskId); + db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + values.getAsLong(Relation.RELATED_ID), null); + } + // else: the child task is probably not synced yet, we have to fix this in RelationUpdaterHook + } + else if (type == RelType.SIBLING.ordinal()) + { + // this is a link to a sibling, we need to copy the PARENT_ID of the linked task to this task + if (values.getAsLong(Relation.RELATED_ID) != null) + { + // get the parent of the other task first + Long otherParent = resolveTaskLongField(db, Tasks._ID, values.getAsString(Relation.RELATED_ID), Tasks.PARENT_ID); + + ContentValues taskValues = new ContentValues(1); + taskValues.put(Tasks.PARENT_ID, otherParent); + db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null); + } + // else: the sibling task is probably not synced yet, we have to fix this in RelationUpdaterHook + } + } + + + /** + * Clear {@link Tasks#PARENT_ID} if a link is removed. + * + * @param db + * @param taskId + * @param oldValues + */ + private void clearParentId(SQLiteDatabase db, long taskId, Cursor oldValues) + { + int type = oldValues.getInt(oldValues.getColumnIndex(Relation.RELATED_TYPE)); /* - * This is more complicated than it may sound. We don't know the order in which relations are created, updated or removed. So it's possible that a new + * This is more complicated than it may sound. We don't know the order in which relations are created, updated or removed. So it's possible that a new * parent relationship has been created and the old one is removed afterwards. In that case we can not simply clear the PARENT_ID. * * FIXME: For now we ignore that fact. But we should fix it. */ - if (type == RelType.PARENT.ordinal()) - { - // this was a link to the parent, we're orphaned now, so clear PARENT_ID of this task - - ContentValues taskValues = new ContentValues(1); - taskValues.putNull(Tasks.PARENT_ID); - db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null); - } - else if (type == RelType.CHILD.ordinal()) - { - // this was a link to a child, the child is orphaned now, clear its PARENT_ID - - int relIdCol = oldValues.getColumnIndex(Relation.RELATED_ID); - if (!oldValues.isNull(relIdCol)) - { - ContentValues taskValues = new ContentValues(1); - taskValues.putNull(Tasks.PARENT_ID); - db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + oldValues.getLong(relIdCol), null); - } - } - else if (type == RelType.SIBLING.ordinal()) - { - /* + if (type == RelType.PARENT.ordinal()) + { + // this was a link to the parent, we're orphaned now, so clear PARENT_ID of this task + + ContentValues taskValues = new ContentValues(1); + taskValues.putNull(Tasks.PARENT_ID); + db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null); + } + else if (type == RelType.CHILD.ordinal()) + { + // this was a link to a child, the child is orphaned now, clear its PARENT_ID + + int relIdCol = oldValues.getColumnIndex(Relation.RELATED_ID); + if (!oldValues.isNull(relIdCol)) + { + ContentValues taskValues = new ContentValues(1); + taskValues.putNull(Tasks.PARENT_ID); + db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + oldValues.getLong(relIdCol), null); + } + } + else if (type == RelType.SIBLING.ordinal()) + { + /* * This was a link to a sibling, since it's no longer our sibling either it or we're orphaned now We won't know unless we check all relations. * * FIXME: properly handle this case */ - } - } + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractListAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractListAdapter.java index a80cd03a..2ffd05cc 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractListAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractListAdapter.java @@ -17,41 +17,41 @@ package org.dmfs.provider.tasks.model; -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.model.adapters.FieldAdapter; - import android.content.ContentUris; import android.content.ContentValues; import android.net.Uri; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.model.adapters.FieldAdapter; + /** * An abstract implementation of a {@link ListAdapter} to server as the base for more concrete adapters. - * + * * @author Marten Gajda */ public abstract class AbstractListAdapter implements ListAdapter { - private final ContentValues mState = new ContentValues(10); + private final ContentValues mState = new ContentValues(10); - @Override - public Uri uri(String authority) - { - return ContentUris.withAppendedId(TaskContract.TaskLists.getContentUri(authority), id()); - } + @Override + public Uri uri(String authority) + { + return ContentUris.withAppendedId(TaskContract.TaskLists.getContentUri(authority), id()); + } - @Override - public T getState(FieldAdapter stateFieldAdater) - { - return stateFieldAdater.getFrom(mState); - } + @Override + public T getState(FieldAdapter stateFieldAdater) + { + return stateFieldAdater.getFrom(mState); + } - @Override - public void setState(FieldAdapter stateFieldAdater, T value) - { - stateFieldAdater.setIn(mState, value); - } + @Override + public void setState(FieldAdapter stateFieldAdater, T value) + { + stateFieldAdater.setIn(mState, value); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractTaskAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractTaskAdapter.java index 9a26215f..d0d6a608 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractTaskAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractTaskAdapter.java @@ -17,55 +17,55 @@ package org.dmfs.provider.tasks.model; -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.model.adapters.FieldAdapter; - import android.content.ContentUris; import android.content.ContentValues; import android.net.Uri; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.model.adapters.FieldAdapter; + /** * An abstract implementation of a {@link TaskAdapter} to server as the base for more concrete adapters. - * + * * @author Marten Gajda */ public abstract class AbstractTaskAdapter implements TaskAdapter { - private final ContentValues mState = new ContentValues(10); + private final ContentValues mState = new ContentValues(10); - @Override - public Uri uri(String authority) - { - return ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(authority), id()); - } + @Override + public Uri uri(String authority) + { + return ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(authority), id()); + } - @Override - public boolean isRecurring() - { - return valueOf(RRULE) != null || valueOf(RDATE) != null; - } + @Override + public boolean isRecurring() + { + return valueOf(RRULE) != null || valueOf(RDATE) != null; + } - @Override - public boolean recurrenceUpdated() - { - return isUpdated(RRULE) || isUpdated(DTSTART) || isUpdated(DUE) || isUpdated(DURATION) || isUpdated(RDATE) || isUpdated(EXDATE); - } + @Override + public boolean recurrenceUpdated() + { + return isUpdated(RRULE) || isUpdated(DTSTART) || isUpdated(DUE) || isUpdated(DURATION) || isUpdated(RDATE) || isUpdated(EXDATE); + } - @Override - public T getState(FieldAdapter stateFieldAdater) - { - return stateFieldAdater.getFrom(mState); - } + @Override + public T getState(FieldAdapter stateFieldAdater) + { + return stateFieldAdater.getFrom(mState); + } - @Override - public void setState(FieldAdapter stateFieldAdater, T value) - { - stateFieldAdater.setIn(mState, value); - } + @Override + public void setState(FieldAdapter stateFieldAdater, T value) + { + stateFieldAdater.setIn(mState, value); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesListAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesListAdapter.java index a08cdb23..e4abf408 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesListAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesListAdapter.java @@ -17,115 +17,115 @@ package org.dmfs.provider.tasks.model; +import android.content.ContentValues; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.adapters.FieldAdapter; -import android.content.ContentValues; -import android.database.sqlite.SQLiteDatabase; - /** * @author Marten Gajda */ public class ContentValuesListAdapter extends AbstractListAdapter { - private long mId; - private final ContentValues mValues; + private long mId; + private final ContentValues mValues; - public ContentValuesListAdapter(ContentValues values) - { - this(-1L, values); - } + public ContentValuesListAdapter(ContentValues values) + { + this(-1L, values); + } - public ContentValuesListAdapter(long id, ContentValues values) - { - mId = id; - mValues = values; - } + public ContentValuesListAdapter(long id, ContentValues values) + { + mId = id; + mValues = values; + } - @Override - public long id() - { - return mId; - } + @Override + public long id() + { + return mId; + } - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mValues); - } + @Override + public T valueOf(FieldAdapter fieldAdapter) + { + return fieldAdapter.getFrom(mValues); + } - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return null; - } + @Override + public T oldValueOf(FieldAdapter fieldAdapter) + { + return null; + } - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return fieldAdapter.isSetIn(mValues); - } + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) + { + return fieldAdapter.isSetIn(mValues); + } - @Override - public boolean isWriteable() - { - return true; - } + @Override + public boolean isWriteable() + { + return true; + } - @Override - public boolean hasUpdates() - { - return mValues.size() > 0; - } + @Override + public boolean hasUpdates() + { + return mValues.size() > 0; + } - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } + @Override + public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException + { + fieldAdapter.setIn(mValues, value); + } - @Override - public void unset(FieldAdapter fieldAdapter) throws IllegalStateException - { - fieldAdapter.removeFrom(mValues); - } + @Override + public void unset(FieldAdapter fieldAdapter) throws IllegalStateException + { + fieldAdapter.removeFrom(mValues); + } - @Override - public int commit(SQLiteDatabase db) - { - if (mValues.size() == 0) - { - return 0; - } + @Override + public int commit(SQLiteDatabase db) + { + if (mValues.size() == 0) + { + return 0; + } - if (mId < 0) - { - mId = db.insert(TaskDatabaseHelper.Tables.LISTS, null, mValues); - return mId > 0 ? 1 : 0; - } - else - { - return db.update(TaskDatabaseHelper.Tables.LISTS, mValues, TaskContract.TaskListColumns._ID + "=" + mId, null); - } - } + if (mId < 0) + { + mId = db.insert(TaskDatabaseHelper.Tables.LISTS, null, mValues); + return mId > 0 ? 1 : 0; + } + else + { + return db.update(TaskDatabaseHelper.Tables.LISTS, mValues, TaskContract.TaskListColumns._ID + "=" + mId, null); + } + } - @Override - public ListAdapter duplicate() - { - return new ContentValuesListAdapter(new ContentValues(mValues)); - } + @Override + public ListAdapter duplicate() + { + return new ContentValuesListAdapter(new ContentValues(mValues)); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesTaskAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesTaskAdapter.java index 23466fd2..e0f32fd8 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesTaskAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesTaskAdapter.java @@ -17,117 +17,117 @@ package org.dmfs.provider.tasks.model; +import android.content.ContentValues; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.adapters.FieldAdapter; -import android.content.ContentValues; -import android.database.sqlite.SQLiteDatabase; - /** * A {@link TaskAdapter} for tasks that are stored in a {@link ContentValues}. - * + * * @author Marten Gajda */ public class ContentValuesTaskAdapter extends AbstractTaskAdapter { - private long mId; - private final ContentValues mValues; + private long mId; + private final ContentValues mValues; - public ContentValuesTaskAdapter(ContentValues values) - { - this(-1L, values); - } + public ContentValuesTaskAdapter(ContentValues values) + { + this(-1L, values); + } - public ContentValuesTaskAdapter(long id, ContentValues values) - { - mId = id; - mValues = values; - } + public ContentValuesTaskAdapter(long id, ContentValues values) + { + mId = id; + mValues = values; + } - @Override - public long id() - { - return mId; - } + @Override + public long id() + { + return mId; + } - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mValues); - } + @Override + public T valueOf(FieldAdapter fieldAdapter) + { + return fieldAdapter.getFrom(mValues); + } - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return null; - } + @Override + public T oldValueOf(FieldAdapter fieldAdapter) + { + return null; + } - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return fieldAdapter.isSetIn(mValues); - } + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) + { + return fieldAdapter.isSetIn(mValues); + } - @Override - public boolean isWriteable() - { - return true; - } + @Override + public boolean isWriteable() + { + return true; + } - @Override - public boolean hasUpdates() - { - return mValues.size() > 0; - } + @Override + public boolean hasUpdates() + { + return mValues.size() > 0; + } - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } + @Override + public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException + { + fieldAdapter.setIn(mValues, value); + } - @Override - public void unset(FieldAdapter fieldAdapter) throws IllegalStateException - { - fieldAdapter.removeFrom(mValues); - } + @Override + public void unset(FieldAdapter fieldAdapter) throws IllegalStateException + { + fieldAdapter.removeFrom(mValues); + } - @Override - public int commit(SQLiteDatabase db) - { - if (mValues.size() == 0) - { - return 0; - } + @Override + public int commit(SQLiteDatabase db) + { + if (mValues.size() == 0) + { + return 0; + } - if (mId < 0) - { - mId = db.insert(TaskDatabaseHelper.Tables.TASKS, null, mValues); - return mId > 0 ? 1 : 0; - } - else - { - return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null); - } - } + if (mId < 0) + { + mId = db.insert(TaskDatabaseHelper.Tables.TASKS, null, mValues); + return mId > 0 ? 1 : 0; + } + else + { + return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null); + } + } - @Override - public TaskAdapter duplicate() - { - return new ContentValuesTaskAdapter(new ContentValues(mValues)); - } + @Override + public TaskAdapter duplicate() + { + return new ContentValuesTaskAdapter(new ContentValues(mValues)); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java index 0453de73..520fa911 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java @@ -17,117 +17,116 @@ package org.dmfs.provider.tasks.model; -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.TaskDatabaseHelper; -import org.dmfs.provider.tasks.model.adapters.FieldAdapter; - import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.TaskDatabaseHelper; +import org.dmfs.provider.tasks.model.adapters.FieldAdapter; + /** - * * @author Marten Gajda */ public class CursorContentValuesListAdapter extends AbstractListAdapter { - private final long mId; - private final Cursor mCursor; - private final ContentValues mValues; + private final long mId; + private final Cursor mCursor; + private final ContentValues mValues; - public CursorContentValuesListAdapter(long id, Cursor cursor, ContentValues values) - { - mId = id; - mCursor = cursor; - mValues = values; - } + public CursorContentValuesListAdapter(long id, Cursor cursor, ContentValues values) + { + mId = id; + mCursor = cursor; + mValues = values; + } - @Override - public long id() - { - return mId; - } + @Override + public long id() + { + return mId; + } - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor, mValues); - } + @Override + public T valueOf(FieldAdapter fieldAdapter) + { + return fieldAdapter.getFrom(mCursor, mValues); + } - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor); - } + @Override + public T oldValueOf(FieldAdapter fieldAdapter) + { + return fieldAdapter.getFrom(mCursor); + } - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return mValues != null && fieldAdapter.isSetIn(mValues); - } + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) + { + return mValues != null && fieldAdapter.isSetIn(mValues); + } - @Override - public boolean isWriteable() - { - return true; - } + @Override + public boolean isWriteable() + { + return true; + } - @Override - public boolean hasUpdates() - { - return mValues != null && mValues.size() > 0; - } + @Override + public boolean hasUpdates() + { + return mValues != null && mValues.size() > 0; + } - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } + @Override + public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException + { + fieldAdapter.setIn(mValues, value); + } - @Override - public void unset(FieldAdapter fieldAdapter) throws IllegalStateException - { - fieldAdapter.removeFrom(mValues); - } + @Override + public void unset(FieldAdapter fieldAdapter) throws IllegalStateException + { + fieldAdapter.removeFrom(mValues); + } - @Override - public int commit(SQLiteDatabase db) - { - if (mValues.size() == 0) - { - return 0; - } + @Override + public int commit(SQLiteDatabase db) + { + if (mValues.size() == 0) + { + return 0; + } - return db.update(TaskDatabaseHelper.Tables.LISTS, mValues, TaskContract.TaskListColumns._ID + "=" + mId, null); - } + return db.update(TaskDatabaseHelper.Tables.LISTS, mValues, TaskContract.TaskListColumns._ID + "=" + mId, null); + } - @Override - public ListAdapter duplicate() - { - ContentValues newValues = new ContentValues(mValues); + @Override + public ListAdapter duplicate() + { + ContentValues newValues = new ContentValues(mValues); - // copy all columns (except _ID) that are not in the values yet - for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i) - { - String column = mCursor.getColumnName(i); - if (!newValues.containsKey(column) && !TaskContract.Tasks._ID.equals(column)) - { - newValues.put(column, mCursor.getString(i)); - } - } + // copy all columns (except _ID) that are not in the values yet + for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i) + { + String column = mCursor.getColumnName(i); + if (!newValues.containsKey(column) && !TaskContract.Tasks._ID.equals(column)) + { + newValues.put(column, mCursor.getString(i)); + } + } - return new ContentValuesListAdapter(newValues); - } + return new ContentValuesListAdapter(newValues); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java index 41fc86a3..66355614 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java @@ -17,138 +17,138 @@ package org.dmfs.provider.tasks.model; -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.TaskDatabaseHelper; -import org.dmfs.provider.tasks.model.adapters.FieldAdapter; - import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.TaskDatabaseHelper; +import org.dmfs.provider.tasks.model.adapters.FieldAdapter; + /** * A {@link TaskAdapter} that adapts a {@link Cursor} and a {@link ContentValues} instance. All changes are written to the {@link ContentValues} and can be * stored in the database with {@link #commit(SQLiteDatabase)}. - * + * * @author Marten Gajda */ public class CursorContentValuesTaskAdapter extends AbstractTaskAdapter { - private final long mId; - private final Cursor mCursor; - private final ContentValues mValues; - + private final long mId; + private final Cursor mCursor; + private final ContentValues mValues; + - public CursorContentValuesTaskAdapter(Cursor cursor, ContentValues values) - { - if (cursor == null && !_ID.existsIn(values)) - { - mId = -1L; - } - else - { - mId = _ID.getFrom(cursor); - } - mCursor = cursor; - mValues = values; - } - - - public CursorContentValuesTaskAdapter(long id, Cursor cursor, ContentValues values) - { - mId = id; - mCursor = cursor; - mValues = values; - } - - - @Override - public long id() - { - return mId; - } - - - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - if (mValues == null) - { - return fieldAdapter.getFrom(mCursor); - } - return fieldAdapter.getFrom(mCursor, mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor); - } - - - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return mValues != null && fieldAdapter.isSetIn(mValues); - } - - - @Override - public boolean isWriteable() - { - return mValues != null; - } - - - @Override - public boolean hasUpdates() - { - return mValues != null && mValues.size() > 0; - } - - - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } - - - @Override - public void unset(FieldAdapter fieldAdapter) throws IllegalStateException - { - fieldAdapter.removeFrom(mValues); - } - - - @Override - public int commit(SQLiteDatabase db) - { - if (mValues.size() == 0) - { - return 0; - } - - return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null); - } - - - @Override - public TaskAdapter duplicate() - { - ContentValues newValues = new ContentValues(mValues); - - // copy all columns (except _ID) that are not in the values yet - for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i) - { - String column = mCursor.getColumnName(i); - if (!newValues.containsKey(column) && !TaskContract.Tasks._ID.equals(column)) - { - newValues.put(column, mCursor.getString(i)); - } - } + public CursorContentValuesTaskAdapter(Cursor cursor, ContentValues values) + { + if (cursor == null && !_ID.existsIn(values)) + { + mId = -1L; + } + else + { + mId = _ID.getFrom(cursor); + } + mCursor = cursor; + mValues = values; + } + + + public CursorContentValuesTaskAdapter(long id, Cursor cursor, ContentValues values) + { + mId = id; + mCursor = cursor; + mValues = values; + } + + + @Override + public long id() + { + return mId; + } + + + @Override + public T valueOf(FieldAdapter fieldAdapter) + { + if (mValues == null) + { + return fieldAdapter.getFrom(mCursor); + } + return fieldAdapter.getFrom(mCursor, mValues); + } + + + @Override + public T oldValueOf(FieldAdapter fieldAdapter) + { + return fieldAdapter.getFrom(mCursor); + } + + + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) + { + return mValues != null && fieldAdapter.isSetIn(mValues); + } + + + @Override + public boolean isWriteable() + { + return mValues != null; + } + + + @Override + public boolean hasUpdates() + { + return mValues != null && mValues.size() > 0; + } + + + @Override + public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException + { + fieldAdapter.setIn(mValues, value); + } + + + @Override + public void unset(FieldAdapter fieldAdapter) throws IllegalStateException + { + fieldAdapter.removeFrom(mValues); + } + + + @Override + public int commit(SQLiteDatabase db) + { + if (mValues.size() == 0) + { + return 0; + } + + return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null); + } + + + @Override + public TaskAdapter duplicate() + { + ContentValues newValues = new ContentValues(mValues); + + // copy all columns (except _ID) that are not in the values yet + for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i) + { + String column = mCursor.getColumnName(i); + if (!newValues.containsKey(column) && !TaskContract.Tasks._ID.equals(column)) + { + newValues.put(column, mCursor.getString(i)); + } + } - return new ContentValuesTaskAdapter(newValues); - } + return new ContentValuesTaskAdapter(newValues); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/EntityAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/EntityAdapter.java index ac3626a3..879168fa 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/EntityAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/EntityAdapter.java @@ -17,142 +17,136 @@ package org.dmfs.provider.tasks.model; -import org.dmfs.provider.tasks.model.adapters.FieldAdapter; - import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import org.dmfs.provider.tasks.model.adapters.FieldAdapter; + /** * Adapter to read values of a specific entity type from primitive data sets like {@link Cursor}s or {@link ContentValues}s. - * + * * @author Marten Gajda */ public interface EntityAdapter { - /** - * Returns the row id of the entity or -1 if the entity has not been stored yet. - * - * @return The entity row id or -1. - */ - public long id(); - - - /** - * Returns the {@link Uri} of the entity using the given authority. - * - * @param authority - * The authority of this provider. - * @return A {@link Uri} or null if this entity has not been stored yet. - */ - public Uri uri(String authority); - - - /** - * Returns the value identified by the given {@link FieldAdapter}. - * - * @param fieldAdapter - * The {@link FieldAdapter} of the value to return. - * @return The value, maybe be null. - */ - public T valueOf(FieldAdapter fieldAdapter); - - - /** - * Returns the old value identified by the given {@link FieldAdapter}. This will be equal to the value returned by {@link #valueOf(FieldAdapter)} unless it - * has been overridden, in which case this returns the former value. - * - * @param fieldAdapter - * The {@link FieldAdapter} of the value to return. - * @return The value, maybe be null. - */ - public T oldValueOf(FieldAdapter fieldAdapter); - - - /** - * Returns whether the given field has been overridden or not. - * - * @param fieldAdapter - * The {@link FieldAdapter} of the field to check. - * @return true if the field has been overridden, false otherwise. - */ - public boolean isUpdated(FieldAdapter fieldAdapter); - - - /** - * Returns whether this adapter supports modifying values. - * - * @return true if the task values can be changed by this adapter, false otherwise. - */ - public boolean isWriteable(); - - - /** - * Returns whether any value has been modified. - * - * @return true if there are modified values, false otherwise. - */ - public boolean hasUpdates(); - - - /** - * Sets a value of the adapted entity. The value is identified by a {@link FieldAdapter}. - * - * @param fieldAdapter - * The {@link FieldAdapter} of the value to set. - * @param value - * The new value. - */ - public void set(FieldAdapter fieldAdapter, T value); - - - /** - * Remove a value from the change set. In effect the respective field will keep it's old value. - * - * @param fieldAdapter - * The {@link FieldAdapter} of the field to un-set. - */ - public void unset(FieldAdapter fieldAdapter); - - - /** - * Commit all changes to the database. - * - * @param db - * A writable database. - * @return The number of entries affected. This may be 0 if no fields have been changed. - */ - public int commit(SQLiteDatabase db); - - - /** - * Return the value of a temporary state field. The state of an entity is not committed to the database, it's only bound to the instances of this - * {@link EntityAdapter} and will be lost once it gets garbage collected. - * - * @param stateFieldAdater - * The {@link FieldAdapter} of a state field. - * @return The value of the state field. - */ - public T getState(FieldAdapter stateFieldAdater); - - - /** - * Set the value of a state field. This value is not stored in the database. Instead it only exists as long as this {@link EntityAdapter} exists. - * - * @param stateFieldAdater - * The {@link FieldAdapter} of the state field to set. - * @param value - * The new state value. - */ - public void setState(FieldAdapter stateFieldAdater, T value); - - - /*** - * Creates a {@link EntityAdapter} for a new entity initialized with the values of this entity (except for _ID). - * - * @return A new {@link EntityAdapter} having the same values. - */ - public EntityAdapter duplicate(); + /** + * Returns the row id of the entity or -1 if the entity has not been stored yet. + * + * @return The entity row id or -1. + */ + public long id(); + + /** + * Returns the {@link Uri} of the entity using the given authority. + * + * @param authority + * The authority of this provider. + * + * @return A {@link Uri} or null if this entity has not been stored yet. + */ + public Uri uri(String authority); + + /** + * Returns the value identified by the given {@link FieldAdapter}. + * + * @param fieldAdapter + * The {@link FieldAdapter} of the value to return. + * + * @return The value, maybe be null. + */ + public T valueOf(FieldAdapter fieldAdapter); + + /** + * Returns the old value identified by the given {@link FieldAdapter}. This will be equal to the value returned by {@link #valueOf(FieldAdapter)} unless it + * has been overridden, in which case this returns the former value. + * + * @param fieldAdapter + * The {@link FieldAdapter} of the value to return. + * + * @return The value, maybe be null. + */ + public T oldValueOf(FieldAdapter fieldAdapter); + + /** + * Returns whether the given field has been overridden or not. + * + * @param fieldAdapter + * The {@link FieldAdapter} of the field to check. + * + * @return true if the field has been overridden, false otherwise. + */ + public boolean isUpdated(FieldAdapter fieldAdapter); + + /** + * Returns whether this adapter supports modifying values. + * + * @return true if the task values can be changed by this adapter, false otherwise. + */ + public boolean isWriteable(); + + /** + * Returns whether any value has been modified. + * + * @return true if there are modified values, false otherwise. + */ + public boolean hasUpdates(); + + /** + * Sets a value of the adapted entity. The value is identified by a {@link FieldAdapter}. + * + * @param fieldAdapter + * The {@link FieldAdapter} of the value to set. + * @param value + * The new value. + */ + public void set(FieldAdapter fieldAdapter, T value); + + /** + * Remove a value from the change set. In effect the respective field will keep it's old value. + * + * @param fieldAdapter + * The {@link FieldAdapter} of the field to un-set. + */ + public void unset(FieldAdapter fieldAdapter); + + /** + * Commit all changes to the database. + * + * @param db + * A writable database. + * + * @return The number of entries affected. This may be 0 if no fields have been changed. + */ + public int commit(SQLiteDatabase db); + + /** + * Return the value of a temporary state field. The state of an entity is not committed to the database, it's only bound to the instances of this + * {@link EntityAdapter} and will be lost once it gets garbage collected. + * + * @param stateFieldAdater + * The {@link FieldAdapter} of a state field. + * + * @return The value of the state field. + */ + public T getState(FieldAdapter stateFieldAdater); + + /** + * Set the value of a state field. This value is not stored in the database. Instead it only exists as long as this {@link EntityAdapter} exists. + * + * @param stateFieldAdater + * The {@link FieldAdapter} of the state field to set. + * @param value + * The new state value. + */ + public void setState(FieldAdapter stateFieldAdater, T value); + + /*** + * Creates a {@link EntityAdapter} for a new entity initialized with the values of this entity (except for _ID). + * + * @return A new {@link EntityAdapter} having the same values. + */ + public EntityAdapter duplicate(); } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ListAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ListAdapter.java index e34b8857..66c1cfec 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ListAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ListAdapter.java @@ -17,68 +17,67 @@ package org.dmfs.provider.tasks.model; +import android.content.ContentValues; +import android.database.Cursor; + import org.dmfs.provider.tasks.TaskContract.TaskLists; import org.dmfs.provider.tasks.model.adapters.IntegerFieldAdapter; import org.dmfs.provider.tasks.model.adapters.LongFieldAdapter; import org.dmfs.provider.tasks.model.adapters.StringFieldAdapter; -import android.content.ContentValues; -import android.database.Cursor; - /** * Adapter to read list values from primitive data sets like {@link Cursor}s or {@link ContentValues}s. - * + * * @author Marten Gajda */ public interface ListAdapter extends EntityAdapter { - /** - * Adapter for the row id of a task list. - */ - public final static LongFieldAdapter _ID = new LongFieldAdapter(TaskLists._ID); - - /** - * Adapter for the _sync_id of a list. - */ - public final static StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskLists._SYNC_ID); + /** + * Adapter for the row id of a task list. + */ + public final static LongFieldAdapter _ID = new LongFieldAdapter(TaskLists._ID); - /** - * Adapter for the sync version of a list. - */ - public final static StringFieldAdapter SYNC_VERSION = new StringFieldAdapter(TaskLists.SYNC_VERSION); + /** + * Adapter for the _sync_id of a list. + */ + public final static StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskLists._SYNC_ID); - /** - * Adapter for the account name of a list. - */ - public final static StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(TaskLists.ACCOUNT_NAME); + /** + * Adapter for the sync version of a list. + */ + public final static StringFieldAdapter SYNC_VERSION = new StringFieldAdapter(TaskLists.SYNC_VERSION); - /** - * Adapter for the account type of a list. - */ - public final static StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(TaskLists.ACCOUNT_TYPE); + /** + * Adapter for the account name of a list. + */ + public final static StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(TaskLists.ACCOUNT_NAME); - /** - * Adapter for the owner of a list. - */ - public final static StringFieldAdapter OWNER = new StringFieldAdapter(TaskLists.OWNER); + /** + * Adapter for the account type of a list. + */ + public final static StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(TaskLists.ACCOUNT_TYPE); - /** - * Adapter for the name of a list. - */ - public final static StringFieldAdapter LIST_NAME = new StringFieldAdapter(TaskLists.LIST_NAME); + /** + * Adapter for the owner of a list. + */ + public final static StringFieldAdapter OWNER = new StringFieldAdapter(TaskLists.OWNER); - /** - * Adapter for the color of a list. - */ - public final static IntegerFieldAdapter LIST_COLOR = new IntegerFieldAdapter(TaskLists.LIST_COLOR); + /** + * Adapter for the name of a list. + */ + public final static StringFieldAdapter LIST_NAME = new StringFieldAdapter(TaskLists.LIST_NAME); + /** + * Adapter for the color of a list. + */ + public final static IntegerFieldAdapter LIST_COLOR = new IntegerFieldAdapter(TaskLists.LIST_COLOR); - /*** - * Creates a {@link ListAdapter} for a new task initialized with the values of this task (except for _ID). - * - * @return A new task having the same values. - */ - @Override - public ListAdapter duplicate(); + /*** + * Creates a {@link ListAdapter} for a new task initialized with the values of this task (except for _ID). + * + * @return A new task having the same values. + */ + @Override + public ListAdapter duplicate(); } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java index 5eea0e8b..76cb06bc 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java @@ -17,6 +17,9 @@ package org.dmfs.provider.tasks.model; +import android.content.ContentValues; +import android.database.Cursor; + import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskContract.Instances; import org.dmfs.provider.tasks.TaskContract.Tasks; @@ -31,315 +34,309 @@ import org.dmfs.provider.tasks.model.adapters.RRuleFieldAdapter; import org.dmfs.provider.tasks.model.adapters.StringFieldAdapter; import org.dmfs.provider.tasks.model.adapters.UrlFieldAdapter; -import android.content.ContentValues; -import android.database.Cursor; - /** * Adapter to read task values from primitive data sets like {@link Cursor}s or {@link ContentValues}s. - * + * * @author Marten Gajda */ public interface TaskAdapter extends EntityAdapter { - /** - * Adapter for the row id of a task. - */ - public final static LongFieldAdapter _ID = new LongFieldAdapter(Tasks._ID); - - /** - * Adapter for the task row id of as instance. - */ - public final static LongFieldAdapter INSTANCE_TASK_ID = new LongFieldAdapter(Instances.TASK_ID); - - /** - * Adapter for the row id of the list of a task. - */ - public final static LongFieldAdapter LIST_ID = new LongFieldAdapter(Tasks.LIST_ID); - - /** - * Adapter for the owner of the list of a task. - */ - public final static StringFieldAdapter LIST_OWNER = new StringFieldAdapter(Tasks.LIST_OWNER); - - /** - * Adapter for the row id of original instance of a task. - */ - public final static LongFieldAdapter ORIGINAL_INSTANCE_ID = new LongFieldAdapter(Tasks.ORIGINAL_INSTANCE_ID); - - /** - * Adapter for the sync_id of original instance of a task. - */ - public final static StringFieldAdapter ORIGINAL_INSTANCE_SYNC_ID = new StringFieldAdapter(Tasks.ORIGINAL_INSTANCE_SYNC_ID); - - /** - * Adapter for the all day flag of a task. - */ - public final static BooleanFieldAdapter IS_ALLDAY = new BooleanFieldAdapter(Tasks.IS_ALLDAY); - - /** - * Adapter for the percent complete value of a task. - */ - public final static IntegerFieldAdapter PERCENT_COMPLETE = new IntegerFieldAdapter(Tasks.PERCENT_COMPLETE); - - /** - * Adapter for the status of a task. - */ - public final static IntegerFieldAdapter STATUS = new IntegerFieldAdapter(Tasks.STATUS); - - /** - * Adapter for the priority value of a task. - */ - public final static IntegerFieldAdapter PRIORITY = new IntegerFieldAdapter(Tasks.PRIORITY); - - /** - * Adapter for the classification value of a task. - */ - public final static IntegerFieldAdapter CLASSIFICATION = new IntegerFieldAdapter(Tasks.CLASSIFICATION); - - /** - * Adapter for the list name of a task. - */ - public final static StringFieldAdapter LIST_NAME = new StringFieldAdapter(Tasks.LIST_NAME); - - /** - * Adapter for the account name of a task. - */ - public final static StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(Tasks.ACCOUNT_NAME); - - /** - * Adapter for the account type of a task. - */ - public final static StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(Tasks.ACCOUNT_TYPE); - - /** - * Adapter for the title of a task. - */ - public final static StringFieldAdapter TITLE = new StringFieldAdapter(Tasks.TITLE); - - /** - * Adapter for the location of a task. - */ - public final static StringFieldAdapter LOCATION = new StringFieldAdapter(Tasks.LOCATION); - - /** - * Adapter for the description of a task. - */ - public final static StringFieldAdapter DESCRIPTION = new StringFieldAdapter(Tasks.DESCRIPTION); - - /** - * Adapter for the start date of a task. - */ - public final static DateTimeFieldAdapter DTSTART = new DateTimeFieldAdapter(Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the raw start date timestamp of a task. - */ - public final static LongFieldAdapter DTSTART_RAW = new LongFieldAdapter(Tasks.DTSTART); - - /** - * Adapter for the due date of a task. - */ - public final static DateTimeFieldAdapter DUE = new DateTimeFieldAdapter(Tasks.DUE, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the raw due date timestamp of a task. - */ - public final static LongFieldAdapter DUE_RAW = new LongFieldAdapter(Tasks.DUE); - - /** - * Adapter for the start date of a task. - */ - public final static DurationFieldAdapter DURATION = new DurationFieldAdapter(Tasks.DURATION); - - /** - * Adapter for the dirty flag of a task. - */ - public final static BooleanFieldAdapter _DIRTY = new BooleanFieldAdapter(Tasks._DIRTY); - - /** - * Adapter for the deleted flag of a task. - */ - public final static BooleanFieldAdapter _DELETED = new BooleanFieldAdapter(Tasks._DELETED); - - /** - * Adapter for the completed date of a task. - */ - public final static DateTimeFieldAdapter COMPLETED = new DateTimeFieldAdapter(Tasks.COMPLETED, null, null); - - /** - * Adapter for the created date of a task. - */ - public final static DateTimeFieldAdapter CREATED = new DateTimeFieldAdapter(Tasks.CREATED, null, null); - - /** - * Adapter for the last modified date of a task. - */ - public final static DateTimeFieldAdapter LAST_MODIFIED = new DateTimeFieldAdapter(Tasks.LAST_MODIFIED, null, null); - - /** - * Adapter for the URL of a task. - */ - public final static UrlFieldAdapter URL = new UrlFieldAdapter(TaskContract.Tasks.URL); - - /** - * Adapter for the UID of a task. - */ - public final static StringFieldAdapter _UID = new StringFieldAdapter(TaskContract.Tasks._UID); - - /** - * Adapter for the raw time zone of a task. - */ - public final static StringFieldAdapter TIMEZONE_RAW = new StringFieldAdapter(TaskContract.Tasks.TZ); - - /** - * Adapter for the Color of the task. - * */ - public final static IntegerFieldAdapter LIST_COLOR = new IntegerFieldAdapter(TaskContract.Tasks.LIST_COLOR); - - /** - * Adapter for the access level of the task list. - * */ - public final static IntegerFieldAdapter LIST_ACCESS_LEVEL = new IntegerFieldAdapter(TaskContract.Tasks.LIST_ACCESS_LEVEL); - - /** - * Adapter for the visibility setting of the task list. - * */ - public final static BooleanFieldAdapter LIST_VISIBLE = new BooleanFieldAdapter(TaskContract.Tasks.VISIBLE); - - /** - * Adpater for the ID of the task. - * */ - public static final IntegerFieldAdapter TASK_ID = new IntegerFieldAdapter(TaskContract.Tasks._ID); - - /** - * Adapter for the IS_CLOSED flag of a task. - * */ - public static final BooleanFieldAdapter IS_CLOSED = new BooleanFieldAdapter(TaskContract.Tasks.IS_CLOSED); - - /** - * Adapter for the IS_NEW flag of a task. - * */ - public static final BooleanFieldAdapter IS_NEW = new BooleanFieldAdapter(TaskContract.Tasks.IS_NEW); - - /** - * Adapter for the PINNED flag of a task. - * */ - public static final BooleanFieldAdapter PINNED = new BooleanFieldAdapter(TaskContract.Tasks.PINNED); - - /** - * Adapter for the HAS_ALARMS flag of a task. - * */ - public static final BooleanFieldAdapter HAS_ALARMS = new BooleanFieldAdapter(TaskContract.Tasks.HAS_ALARMS); - - /** - * Adapter for the HAS_PROPERTIES flag of a task. - * */ - public static final BooleanFieldAdapter HAS_PROPERTIES = new BooleanFieldAdapter(TaskContract.Tasks.HAS_PROPERTIES); - - /** - * Adapter for the RRULE of a task. - * */ - public static final RRuleFieldAdapter RRULE = new RRuleFieldAdapter(TaskContract.Tasks.RRULE); - - /** - * Adapter for the RDATE of a task. - * */ - public static final DateTimeArrayFieldAdapter RDATE = new DateTimeArrayFieldAdapter(TaskContract.Tasks.RDATE, - TaskContract.Tasks.TZ); - - /** - * Adapter for the EXDATE of a task. - * */ - public static final DateTimeArrayFieldAdapter EXDATE = new DateTimeArrayFieldAdapter(TaskContract.Tasks.EXDATE, - TaskContract.Tasks.TZ); - - /** - * Adapter for the SYNC1 field of a task. - * */ - public static final BinaryFieldAdapter SYNC1 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC1); - - /** - * Adapter for the SYNC2 field of a task. - * */ - public static final BinaryFieldAdapter SYNC2 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC2); - - /** - * Adapter for the SYNC3 field of a task. - * */ - public static final BinaryFieldAdapter SYNC3 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC3); - - /** - * Adapter for the SYNC4 field of a task. - * */ - public static final BinaryFieldAdapter SYNC4 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC4); - - /** - * Adapter for the SYNC5 field of a task. - * */ - public static final BinaryFieldAdapter SYNC5 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC5); - - /** - * Adapter for the SYNC6 field of a task. - * */ - public static final BinaryFieldAdapter SYNC6 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC6); - - /** - * Adapter for the SYNC7 field of a task. - * */ - public static final BinaryFieldAdapter SYNC7 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC7); - - /** - * Adapter for the SYNC8 field of a task. - * */ - public static final BinaryFieldAdapter SYNC8 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC8); - - /** - * Adapter for the SYNC_VERSION field of a task. - * */ - public static final BinaryFieldAdapter SYNC_VERSION = new BinaryFieldAdapter(TaskContract.Tasks.SYNC_VERSION); - - /** - * Adapter for the SYNC_ID field of a task. - * */ - public static final StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskContract.Tasks._SYNC_ID); - - /** - * Adapter for the due date of a task instance. - */ - public final static DateTimeFieldAdapter INSTANCE_DUE = new DateTimeFieldAdapter(Instances.INSTANCE_DUE, Tasks.TZ, - Tasks.IS_ALLDAY); - - /** - * Adapter for the start date of a task instance. - */ - public final static DateTimeFieldAdapter INSTANCE_START = new DateTimeFieldAdapter(Instances.INSTANCE_START, Tasks.TZ, - Tasks.IS_ALLDAY); - - - /** - * Returns whether the adapted task is recurring. - * - * @return true if the task is recurring, false otherwise. - */ - public boolean isRecurring(); - - - /** - * Returns whether any value that's relevant for recurrence has been modified thought this adapter. This returns true if any of - * {@link TaskContract.TaskColumns#DTSTART}, {@link TaskContract.TaskColumns#DUE},{@link TaskContract.TaskColumns#DURATION}, - * {@link TaskContract.TaskColumns#RRULE}, {@link TaskContract.TaskColumns#RDATE} or {@link TaskContract.TaskColumns#EXDATE} has been modified. - * - * @return true if the recurrence set has changed, false otherwise. - */ - public boolean recurrenceUpdated(); - - - /*** - * Creates a {@link TaskAdapter} for a new task initialized with the values of this task (except for _ID). - * - * @return A new task having the same values. - */ - @Override - public TaskAdapter duplicate(); + /** + * Adapter for the row id of a task. + */ + public final static LongFieldAdapter _ID = new LongFieldAdapter(Tasks._ID); + + /** + * Adapter for the task row id of as instance. + */ + public final static LongFieldAdapter INSTANCE_TASK_ID = new LongFieldAdapter(Instances.TASK_ID); + + /** + * Adapter for the row id of the list of a task. + */ + public final static LongFieldAdapter LIST_ID = new LongFieldAdapter(Tasks.LIST_ID); + + /** + * Adapter for the owner of the list of a task. + */ + public final static StringFieldAdapter LIST_OWNER = new StringFieldAdapter(Tasks.LIST_OWNER); + + /** + * Adapter for the row id of original instance of a task. + */ + public final static LongFieldAdapter ORIGINAL_INSTANCE_ID = new LongFieldAdapter(Tasks.ORIGINAL_INSTANCE_ID); + + /** + * Adapter for the sync_id of original instance of a task. + */ + public final static StringFieldAdapter ORIGINAL_INSTANCE_SYNC_ID = new StringFieldAdapter(Tasks.ORIGINAL_INSTANCE_SYNC_ID); + + /** + * Adapter for the all day flag of a task. + */ + public final static BooleanFieldAdapter IS_ALLDAY = new BooleanFieldAdapter(Tasks.IS_ALLDAY); + + /** + * Adapter for the percent complete value of a task. + */ + public final static IntegerFieldAdapter PERCENT_COMPLETE = new IntegerFieldAdapter(Tasks.PERCENT_COMPLETE); + + /** + * Adapter for the status of a task. + */ + public final static IntegerFieldAdapter STATUS = new IntegerFieldAdapter(Tasks.STATUS); + + /** + * Adapter for the priority value of a task. + */ + public final static IntegerFieldAdapter PRIORITY = new IntegerFieldAdapter(Tasks.PRIORITY); + + /** + * Adapter for the classification value of a task. + */ + public final static IntegerFieldAdapter CLASSIFICATION = new IntegerFieldAdapter(Tasks.CLASSIFICATION); + + /** + * Adapter for the list name of a task. + */ + public final static StringFieldAdapter LIST_NAME = new StringFieldAdapter(Tasks.LIST_NAME); + + /** + * Adapter for the account name of a task. + */ + public final static StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(Tasks.ACCOUNT_NAME); + + /** + * Adapter for the account type of a task. + */ + public final static StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(Tasks.ACCOUNT_TYPE); + + /** + * Adapter for the title of a task. + */ + public final static StringFieldAdapter TITLE = new StringFieldAdapter(Tasks.TITLE); + + /** + * Adapter for the location of a task. + */ + public final static StringFieldAdapter LOCATION = new StringFieldAdapter(Tasks.LOCATION); + + /** + * Adapter for the description of a task. + */ + public final static StringFieldAdapter DESCRIPTION = new StringFieldAdapter(Tasks.DESCRIPTION); + + /** + * Adapter for the start date of a task. + */ + public final static DateTimeFieldAdapter DTSTART = new DateTimeFieldAdapter(Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY); + + /** + * Adapter for the raw start date timestamp of a task. + */ + public final static LongFieldAdapter DTSTART_RAW = new LongFieldAdapter(Tasks.DTSTART); + + /** + * Adapter for the due date of a task. + */ + public final static DateTimeFieldAdapter DUE = new DateTimeFieldAdapter(Tasks.DUE, Tasks.TZ, Tasks.IS_ALLDAY); + + /** + * Adapter for the raw due date timestamp of a task. + */ + public final static LongFieldAdapter DUE_RAW = new LongFieldAdapter(Tasks.DUE); + + /** + * Adapter for the start date of a task. + */ + public final static DurationFieldAdapter DURATION = new DurationFieldAdapter(Tasks.DURATION); + + /** + * Adapter for the dirty flag of a task. + */ + public final static BooleanFieldAdapter _DIRTY = new BooleanFieldAdapter(Tasks._DIRTY); + + /** + * Adapter for the deleted flag of a task. + */ + public final static BooleanFieldAdapter _DELETED = new BooleanFieldAdapter(Tasks._DELETED); + + /** + * Adapter for the completed date of a task. + */ + public final static DateTimeFieldAdapter COMPLETED = new DateTimeFieldAdapter(Tasks.COMPLETED, null, null); + + /** + * Adapter for the created date of a task. + */ + public final static DateTimeFieldAdapter CREATED = new DateTimeFieldAdapter(Tasks.CREATED, null, null); + + /** + * Adapter for the last modified date of a task. + */ + public final static DateTimeFieldAdapter LAST_MODIFIED = new DateTimeFieldAdapter(Tasks.LAST_MODIFIED, null, null); + + /** + * Adapter for the URL of a task. + */ + public final static UrlFieldAdapter URL = new UrlFieldAdapter(TaskContract.Tasks.URL); + + /** + * Adapter for the UID of a task. + */ + public final static StringFieldAdapter _UID = new StringFieldAdapter(TaskContract.Tasks._UID); + + /** + * Adapter for the raw time zone of a task. + */ + public final static StringFieldAdapter TIMEZONE_RAW = new StringFieldAdapter(TaskContract.Tasks.TZ); + + /** + * Adapter for the Color of the task. + */ + public final static IntegerFieldAdapter LIST_COLOR = new IntegerFieldAdapter(TaskContract.Tasks.LIST_COLOR); + + /** + * Adapter for the access level of the task list. + */ + public final static IntegerFieldAdapter LIST_ACCESS_LEVEL = new IntegerFieldAdapter(TaskContract.Tasks.LIST_ACCESS_LEVEL); + + /** + * Adapter for the visibility setting of the task list. + */ + public final static BooleanFieldAdapter LIST_VISIBLE = new BooleanFieldAdapter(TaskContract.Tasks.VISIBLE); + + /** + * Adpater for the ID of the task. + */ + public static final IntegerFieldAdapter TASK_ID = new IntegerFieldAdapter(TaskContract.Tasks._ID); + + /** + * Adapter for the IS_CLOSED flag of a task. + */ + public static final BooleanFieldAdapter IS_CLOSED = new BooleanFieldAdapter(TaskContract.Tasks.IS_CLOSED); + + /** + * Adapter for the IS_NEW flag of a task. + */ + public static final BooleanFieldAdapter IS_NEW = new BooleanFieldAdapter(TaskContract.Tasks.IS_NEW); + + /** + * Adapter for the PINNED flag of a task. + */ + public static final BooleanFieldAdapter PINNED = new BooleanFieldAdapter(TaskContract.Tasks.PINNED); + + /** + * Adapter for the HAS_ALARMS flag of a task. + */ + public static final BooleanFieldAdapter HAS_ALARMS = new BooleanFieldAdapter(TaskContract.Tasks.HAS_ALARMS); + + /** + * Adapter for the HAS_PROPERTIES flag of a task. + */ + public static final BooleanFieldAdapter HAS_PROPERTIES = new BooleanFieldAdapter(TaskContract.Tasks.HAS_PROPERTIES); + + /** + * Adapter for the RRULE of a task. + */ + public static final RRuleFieldAdapter RRULE = new RRuleFieldAdapter(TaskContract.Tasks.RRULE); + + /** + * Adapter for the RDATE of a task. + */ + public static final DateTimeArrayFieldAdapter RDATE = new DateTimeArrayFieldAdapter(TaskContract.Tasks.RDATE, + TaskContract.Tasks.TZ); + + /** + * Adapter for the EXDATE of a task. + */ + public static final DateTimeArrayFieldAdapter EXDATE = new DateTimeArrayFieldAdapter(TaskContract.Tasks.EXDATE, + TaskContract.Tasks.TZ); + + /** + * Adapter for the SYNC1 field of a task. + */ + public static final BinaryFieldAdapter SYNC1 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC1); + + /** + * Adapter for the SYNC2 field of a task. + */ + public static final BinaryFieldAdapter SYNC2 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC2); + + /** + * Adapter for the SYNC3 field of a task. + */ + public static final BinaryFieldAdapter SYNC3 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC3); + + /** + * Adapter for the SYNC4 field of a task. + */ + public static final BinaryFieldAdapter SYNC4 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC4); + + /** + * Adapter for the SYNC5 field of a task. + */ + public static final BinaryFieldAdapter SYNC5 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC5); + + /** + * Adapter for the SYNC6 field of a task. + */ + public static final BinaryFieldAdapter SYNC6 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC6); + + /** + * Adapter for the SYNC7 field of a task. + */ + public static final BinaryFieldAdapter SYNC7 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC7); + + /** + * Adapter for the SYNC8 field of a task. + */ + public static final BinaryFieldAdapter SYNC8 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC8); + + /** + * Adapter for the SYNC_VERSION field of a task. + */ + public static final BinaryFieldAdapter SYNC_VERSION = new BinaryFieldAdapter(TaskContract.Tasks.SYNC_VERSION); + + /** + * Adapter for the SYNC_ID field of a task. + */ + public static final StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskContract.Tasks._SYNC_ID); + + /** + * Adapter for the due date of a task instance. + */ + public final static DateTimeFieldAdapter INSTANCE_DUE = new DateTimeFieldAdapter(Instances.INSTANCE_DUE, Tasks.TZ, + Tasks.IS_ALLDAY); + + /** + * Adapter for the start date of a task instance. + */ + public final static DateTimeFieldAdapter INSTANCE_START = new DateTimeFieldAdapter(Instances.INSTANCE_START, Tasks.TZ, + Tasks.IS_ALLDAY); + + /** + * Returns whether the adapted task is recurring. + * + * @return true if the task is recurring, false otherwise. + */ + public boolean isRecurring(); + + /** + * Returns whether any value that's relevant for recurrence has been modified thought this adapter. This returns true if any of + * {@link TaskContract.TaskColumns#DTSTART}, {@link TaskContract.TaskColumns#DUE},{@link TaskContract.TaskColumns#DURATION}, + * {@link TaskContract.TaskColumns#RRULE}, {@link TaskContract.TaskColumns#RDATE} or {@link TaskContract.TaskColumns#EXDATE} has been modified. + * + * @return true if the recurrence set has changed, false otherwise. + */ + public boolean recurrenceUpdated(); + + /*** + * Creates a {@link TaskAdapter} for a new task initialized with the values of this task (except for _ID). + * + * @return A new task having the same values. + */ + @Override + public TaskAdapter duplicate(); } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BinaryFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BinaryFieldAdapter.java index 3cfb8622..1c63ed3f 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BinaryFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BinaryFieldAdapter.java @@ -23,74 +23,74 @@ import android.database.Cursor; /** * Knows how to load and store a binary value from a {@link Cursor} or {@link ContentValues}. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class BinaryFieldAdapter extends SimpleFieldAdapter { - /** - * The field name this adapter uses to store the values. - */ - private final String mFieldName; - - - /** - * Constructor for a new {@link BinaryFieldAdapter}. - * - * @param fieldName - * The name of the field to use when loading or storing the value. - */ - public BinaryFieldAdapter(String fieldName) - { - if (fieldName == null) - { - throw new IllegalArgumentException("fieldName must not be null"); - } - mFieldName = fieldName; - } - - - @Override - String fieldName() - { - return mFieldName; - } - - - @Override - public byte[] getFrom(ContentValues values) - { - return values.getAsByteArray(mFieldName); - } - - - @Override - public byte[] getFrom(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(mFieldName); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); - } - return cursor.isNull(columnIdx) ? null : cursor.getBlob(columnIdx); - } - - - @Override - public void setIn(ContentValues values, byte[] value) - { - if (value != null) - { - values.put(mFieldName, value); - } - else - { - values.putNull(mFieldName); - } - } + /** + * The field name this adapter uses to store the values. + */ + private final String mFieldName; + + + /** + * Constructor for a new {@link BinaryFieldAdapter}. + * + * @param fieldName + * The name of the field to use when loading or storing the value. + */ + public BinaryFieldAdapter(String fieldName) + { + if (fieldName == null) + { + throw new IllegalArgumentException("fieldName must not be null"); + } + mFieldName = fieldName; + } + + + @Override + String fieldName() + { + return mFieldName; + } + + + @Override + public byte[] getFrom(ContentValues values) + { + return values.getAsByteArray(mFieldName); + } + + + @Override + public byte[] getFrom(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); + } + return cursor.isNull(columnIdx) ? null : cursor.getBlob(columnIdx); + } + + + @Override + public void setIn(ContentValues values, byte[] value) + { + if (value != null) + { + values.put(mFieldName, value); + } + else + { + values.putNull(mFieldName); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BooleanFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BooleanFieldAdapter.java index 427cb731..9fb346f7 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BooleanFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BooleanFieldAdapter.java @@ -23,73 +23,73 @@ import android.database.Cursor; /** * Knows how to load and store a {@link Boolean} value from a {@link Cursor} or {@link ContentValues}. - *

+ *

* Implementation detail: - *

+ *

* The values are loaded and stored as 0 (for false) and 1 (for true). - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class BooleanFieldAdapter extends SimpleFieldAdapter { - /** - * The field name this adapter uses to store the values. - */ - private final String mFieldName; - - - /** - * Constructor for a new {@link BooleanFieldAdapter}. - * - * @param fieldName - * The name of the field to use when loading or storing the value. - */ - public BooleanFieldAdapter(String fieldName) - { - if (fieldName == null) - { - throw new IllegalArgumentException("fieldName must not be null"); - } - mFieldName = fieldName; - } - - - @Override - String fieldName() - { - return mFieldName; - } - - - @Override - public Boolean getFrom(ContentValues values) - { - Integer value = values.getAsInteger(mFieldName); - - return value != null && value > 0; - } - - - @Override - public Boolean getFrom(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(mFieldName); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); - } - return !cursor.isNull(columnIdx) && cursor.getInt(columnIdx) > 0; - } - - - @Override - public void setIn(ContentValues values, Boolean value) - { - values.put(mFieldName, value ? 1 : 0); - } + /** + * The field name this adapter uses to store the values. + */ + private final String mFieldName; + + + /** + * Constructor for a new {@link BooleanFieldAdapter}. + * + * @param fieldName + * The name of the field to use when loading or storing the value. + */ + public BooleanFieldAdapter(String fieldName) + { + if (fieldName == null) + { + throw new IllegalArgumentException("fieldName must not be null"); + } + mFieldName = fieldName; + } + + + @Override + String fieldName() + { + return mFieldName; + } + + + @Override + public Boolean getFrom(ContentValues values) + { + Integer value = values.getAsInteger(mFieldName); + + return value != null && value > 0; + } + + + @Override + public Boolean getFrom(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); + } + return !cursor.isNull(columnIdx) && cursor.getInt(columnIdx) > 0; + } + + + @Override + public void setIn(ContentValues values, Boolean value) + { + values.put(mFieldName, value ? 1 : 0); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeArrayFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeArrayFieldAdapter.java index db80036a..81f260d0 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeArrayFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeArrayFieldAdapter.java @@ -17,249 +17,249 @@ package org.dmfs.provider.tasks.model.adapters; -import java.io.IOException; -import java.util.TimeZone; -import java.util.regex.Pattern; +import android.content.ContentValues; +import android.database.Cursor; import org.dmfs.rfc5545.DateTime; -import android.content.ContentValues; -import android.database.Cursor; +import java.io.IOException; +import java.util.TimeZone; +import java.util.regex.Pattern; /** * Knows how to load and store arrays of {@link DateTime} values from a {@link Cursor} or {@link ContentValues}. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class DateTimeArrayFieldAdapter extends SimpleFieldAdapter { - private final static Pattern SEPARATOR_PATTERN = Pattern.compile(","); - - private final String mDateTimeListFieldName; - private final String mTimeZoneFieldName; - - - /** - * Constructor for a new {@link DateTimeArrayFieldAdapter}. - * - * @param datetimeListFieldName - * The name of the field that holds the {@link DateTime} list. - * @param timezoneFieldName - * The name of the field that holds the time zone name. - */ - public DateTimeArrayFieldAdapter(String datetimeListFieldName, String timezoneFieldName) - { - if (datetimeListFieldName == null) - { - throw new IllegalArgumentException("datetimeListFieldName must not be null"); - } - mDateTimeListFieldName = datetimeListFieldName; - mTimeZoneFieldName = timezoneFieldName; - } - - - @Override - String fieldName() - { - return mDateTimeListFieldName; - } - - - @Override - public DateTime[] getFrom(ContentValues values) - { - String datetimeList = values.getAsString(mDateTimeListFieldName); - if (datetimeList == null) - { - // no list, return null - return null; - } - - // create a new TimeZone for the given time zone string - String timezoneString = mTimeZoneFieldName == null ? null : values.getAsString(mTimeZoneFieldName); - TimeZone timeZone = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString); - - String[] datetimes = SEPARATOR_PATTERN.split(datetimeList); - - DateTime[] result = new DateTime[datetimes.length]; - for (int i = 0, count = datetimes.length; i < count; ++i) - { - DateTime value = DateTime.parse(timeZone, datetimes[i]); - - if (!value.isAllDay() && value.isFloating()) - { - throw new IllegalArgumentException("DateTime values must not be floating, unless they are all-day."); - } - - result[i] = value; - if (i > 0 && result[0].isAllDay() != value.isAllDay()) - { - throw new IllegalArgumentException("DateTime values must all be of the same type."); - } - } - - return result; - } - - - @Override - public DateTime[] getFrom(Cursor cursor) - { - int tdLIdx = cursor.getColumnIndex(mDateTimeListFieldName); - int tzIdx = mTimeZoneFieldName == null ? -1 : cursor.getColumnIndex(mTimeZoneFieldName); - - if (tdLIdx < 0 || (mTimeZoneFieldName != null && tzIdx < 0)) - { - throw new IllegalArgumentException("At least one column is missing in cursor."); - } - - if (cursor.isNull(tdLIdx)) - { - // if the time stamp list is null we return null - return null; - } - - String datetimeList = cursor.getString(tdLIdx); - - // create a new TimeZone for the given time zone string - String timezoneString = mTimeZoneFieldName == null ? null : cursor.getString(tzIdx); - TimeZone timeZone = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString); - - String[] datetimes = SEPARATOR_PATTERN.split(datetimeList); - - DateTime[] result = new DateTime[datetimes.length]; - for (int i = 0, count = datetimes.length; i < count; ++i) - { - DateTime value = DateTime.parse(timeZone, datetimes[i]); - - if (!value.isAllDay() && value.isFloating()) - { - throw new IllegalArgumentException("DateTime values must not be floating, unless they are all-day."); - } - - result[i] = value; - if (i > 0 && result[0].isAllDay() != value.isAllDay()) - { - throw new IllegalArgumentException("DateTime values must all be of the same type."); - } - } - - return result; - } - - - @Override - public DateTime[] getFrom(Cursor cursor, ContentValues values) - { - int tsIdx; - int tzIdx; - String datetimeList; - String timeZoneId = null; - - if (values != null && values.containsKey(mDateTimeListFieldName)) - { - if (values.getAsLong(mDateTimeListFieldName) == null) - { - // the date times are null, so we return null - return null; - } - datetimeList = values.getAsString(mDateTimeListFieldName); - } - else if (cursor != null && (tsIdx = cursor.getColumnIndex(mDateTimeListFieldName)) >= 0) - { - if (cursor.isNull(tsIdx)) - { - // the date times are null, so we return null - return null; - } - datetimeList = cursor.getString(tsIdx); - } - else - { - throw new IllegalArgumentException("Missing date time list column."); - } - - if (mTimeZoneFieldName != null) - { - if (values != null && values.containsKey(mTimeZoneFieldName)) - { - timeZoneId = values.getAsString(mTimeZoneFieldName); - } - else if (cursor != null && (tzIdx = cursor.getColumnIndex(mTimeZoneFieldName)) >= 0) - { - timeZoneId = cursor.getString(tzIdx); - } - else - { - throw new IllegalArgumentException("Missing timezone column."); - } - } - - // create a new TimeZone for the given time zone string - TimeZone timeZone = timeZoneId == null ? null : TimeZone.getTimeZone(timeZoneId); - - String[] datetimes = SEPARATOR_PATTERN.split(datetimeList); - - DateTime[] result = new DateTime[datetimes.length]; - for (int i = 0, count = datetimes.length; i < count; ++i) - { - DateTime value = DateTime.parse(timeZone, datetimes[i]); - - if (!value.isAllDay() && value.isFloating()) - { - throw new IllegalArgumentException("DateTime values must not be floating, unless they are all-day."); - } - - result[i] = value; - if (i > 0 && result[0].isAllDay() != value.isAllDay()) - { - throw new IllegalArgumentException("DateTime values must all be of the same type."); - } - } - - return result; - } - - - @Override - public void setIn(ContentValues values, DateTime[] value) - { - if (value != null && value.length > 0) - { - try - { - // Note: we only store the datetime strings, not the timezone - StringBuilder result = new StringBuilder(value.length * 17 /* this is the maximum length */); - - boolean first = true; - for (DateTime datetime : value) - { - if (first) - { - first = false; - } - else - { - result.append(','); - } - DateTime outvalue = datetime.isFloating() ? datetime : datetime.shiftTimeZone(DateTime.UTC); - outvalue.writeTo(result); - } - values.put(mDateTimeListFieldName, result.toString()); - } - catch (IOException e) - { - throw new RuntimeException("Can not serialize datetime list."); - } - - } - else - { - values.put(mDateTimeListFieldName, (Long) null); - } - } + private final static Pattern SEPARATOR_PATTERN = Pattern.compile(","); + + private final String mDateTimeListFieldName; + private final String mTimeZoneFieldName; + + + /** + * Constructor for a new {@link DateTimeArrayFieldAdapter}. + * + * @param datetimeListFieldName + * The name of the field that holds the {@link DateTime} list. + * @param timezoneFieldName + * The name of the field that holds the time zone name. + */ + public DateTimeArrayFieldAdapter(String datetimeListFieldName, String timezoneFieldName) + { + if (datetimeListFieldName == null) + { + throw new IllegalArgumentException("datetimeListFieldName must not be null"); + } + mDateTimeListFieldName = datetimeListFieldName; + mTimeZoneFieldName = timezoneFieldName; + } + + + @Override + String fieldName() + { + return mDateTimeListFieldName; + } + + + @Override + public DateTime[] getFrom(ContentValues values) + { + String datetimeList = values.getAsString(mDateTimeListFieldName); + if (datetimeList == null) + { + // no list, return null + return null; + } + + // create a new TimeZone for the given time zone string + String timezoneString = mTimeZoneFieldName == null ? null : values.getAsString(mTimeZoneFieldName); + TimeZone timeZone = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString); + + String[] datetimes = SEPARATOR_PATTERN.split(datetimeList); + + DateTime[] result = new DateTime[datetimes.length]; + for (int i = 0, count = datetimes.length; i < count; ++i) + { + DateTime value = DateTime.parse(timeZone, datetimes[i]); + + if (!value.isAllDay() && value.isFloating()) + { + throw new IllegalArgumentException("DateTime values must not be floating, unless they are all-day."); + } + + result[i] = value; + if (i > 0 && result[0].isAllDay() != value.isAllDay()) + { + throw new IllegalArgumentException("DateTime values must all be of the same type."); + } + } + + return result; + } + + + @Override + public DateTime[] getFrom(Cursor cursor) + { + int tdLIdx = cursor.getColumnIndex(mDateTimeListFieldName); + int tzIdx = mTimeZoneFieldName == null ? -1 : cursor.getColumnIndex(mTimeZoneFieldName); + + if (tdLIdx < 0 || (mTimeZoneFieldName != null && tzIdx < 0)) + { + throw new IllegalArgumentException("At least one column is missing in cursor."); + } + + if (cursor.isNull(tdLIdx)) + { + // if the time stamp list is null we return null + return null; + } + + String datetimeList = cursor.getString(tdLIdx); + + // create a new TimeZone for the given time zone string + String timezoneString = mTimeZoneFieldName == null ? null : cursor.getString(tzIdx); + TimeZone timeZone = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString); + + String[] datetimes = SEPARATOR_PATTERN.split(datetimeList); + + DateTime[] result = new DateTime[datetimes.length]; + for (int i = 0, count = datetimes.length; i < count; ++i) + { + DateTime value = DateTime.parse(timeZone, datetimes[i]); + + if (!value.isAllDay() && value.isFloating()) + { + throw new IllegalArgumentException("DateTime values must not be floating, unless they are all-day."); + } + + result[i] = value; + if (i > 0 && result[0].isAllDay() != value.isAllDay()) + { + throw new IllegalArgumentException("DateTime values must all be of the same type."); + } + } + + return result; + } + + + @Override + public DateTime[] getFrom(Cursor cursor, ContentValues values) + { + int tsIdx; + int tzIdx; + String datetimeList; + String timeZoneId = null; + + if (values != null && values.containsKey(mDateTimeListFieldName)) + { + if (values.getAsLong(mDateTimeListFieldName) == null) + { + // the date times are null, so we return null + return null; + } + datetimeList = values.getAsString(mDateTimeListFieldName); + } + else if (cursor != null && (tsIdx = cursor.getColumnIndex(mDateTimeListFieldName)) >= 0) + { + if (cursor.isNull(tsIdx)) + { + // the date times are null, so we return null + return null; + } + datetimeList = cursor.getString(tsIdx); + } + else + { + throw new IllegalArgumentException("Missing date time list column."); + } + + if (mTimeZoneFieldName != null) + { + if (values != null && values.containsKey(mTimeZoneFieldName)) + { + timeZoneId = values.getAsString(mTimeZoneFieldName); + } + else if (cursor != null && (tzIdx = cursor.getColumnIndex(mTimeZoneFieldName)) >= 0) + { + timeZoneId = cursor.getString(tzIdx); + } + else + { + throw new IllegalArgumentException("Missing timezone column."); + } + } + + // create a new TimeZone for the given time zone string + TimeZone timeZone = timeZoneId == null ? null : TimeZone.getTimeZone(timeZoneId); + + String[] datetimes = SEPARATOR_PATTERN.split(datetimeList); + + DateTime[] result = new DateTime[datetimes.length]; + for (int i = 0, count = datetimes.length; i < count; ++i) + { + DateTime value = DateTime.parse(timeZone, datetimes[i]); + + if (!value.isAllDay() && value.isFloating()) + { + throw new IllegalArgumentException("DateTime values must not be floating, unless they are all-day."); + } + + result[i] = value; + if (i > 0 && result[0].isAllDay() != value.isAllDay()) + { + throw new IllegalArgumentException("DateTime values must all be of the same type."); + } + } + + return result; + } + + + @Override + public void setIn(ContentValues values, DateTime[] value) + { + if (value != null && value.length > 0) + { + try + { + // Note: we only store the datetime strings, not the timezone + StringBuilder result = new StringBuilder(value.length * 17 /* this is the maximum length */); + + boolean first = true; + for (DateTime datetime : value) + { + if (first) + { + first = false; + } + else + { + result.append(','); + } + DateTime outvalue = datetime.isFloating() ? datetime : datetime.shiftTimeZone(DateTime.UTC); + outvalue.writeTo(result); + } + values.put(mDateTimeListFieldName, result.toString()); + } + catch (IOException e) + { + throw new RuntimeException("Can not serialize datetime list."); + } + + } + else + { + values.put(mDateTimeListFieldName, (Long) null); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java index 80e55831..3ed253ea 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java @@ -17,231 +17,231 @@ package org.dmfs.provider.tasks.model.adapters; -import java.util.TimeZone; +import android.content.ContentValues; +import android.database.Cursor; import org.dmfs.rfc5545.DateTime; -import android.content.ContentValues; -import android.database.Cursor; +import java.util.TimeZone; /** * Knows how to load and store {@link DateTime} values from a {@link Cursor} or {@link ContentValues}. - * + *

* {@link DateTime} values are stored as three separate values: *

    *
  • a timestamp in milliseconds since the epoch
  • *
  • a time zone
  • *
  • an allday flag
  • *
- * + *

* This adapter combines those three fields to a {@link DateTime} value. If the time zone field is null the time zone is always set to UTC. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class DateTimeFieldAdapter extends SimpleFieldAdapter { - private final String mTimestampField; - private final String mTzField; - private final String mAllDayField; - private final boolean mAllDayDefault; - - - /** - * Constructor for a new {@link DateTimeFieldAdapter}. - * - * @param timestampField - * The name of the field that holds the time stamp in milliseconds. - * @param tzField - * The name of the field that holds the time zone (as Olson ID). If the field name is null the time is always set to UTC. - * @param alldayField - * The name of the field that indicated that this time is a date not a date-time. If this fieldName is null all loaded values are - * non-allday. - */ - public DateTimeFieldAdapter(String timestampField, String tzField, String alldayField) - { - if (timestampField == null) - { - throw new IllegalArgumentException("timestampField must not be null"); - } - mTimestampField = timestampField; - mTzField = tzField; - mAllDayField = alldayField; - mAllDayDefault = false; - } - - - @Override - String fieldName() - { - return mTimestampField; - } - - - @Override - public DateTime getFrom(ContentValues values) - { - Long timestamp = values.getAsLong(mTimestampField); - if (timestamp == null) - { - // if the time stamp is null we return null - return null; - } - // create a new Time for the given time zone, falling back to UTC if none is given - String timezone = mTzField == null ? null : values.getAsString(mTzField); - DateTime value = new DateTime(timezone == null ? DateTime.UTC : TimeZone.getTimeZone(timezone), timestamp); - - // cache mAlldayField locally - String allDayField = mAllDayField; - - // set the allday flag appropriately - Integer allDayInt = allDayField == null ? null : values.getAsInteger(allDayField); - - if ((allDayInt != null && allDayInt != 0) || (allDayField == null && mAllDayDefault)) - { - value = value.toAllDay(); - } - - return value; - } - - - @Override - public DateTime getFrom(Cursor cursor) - { - int tsIdx = cursor.getColumnIndex(mTimestampField); - int tzIdx = mTzField == null ? -1 : cursor.getColumnIndex(mTzField); - int adIdx = mAllDayField == null ? -1 : cursor.getColumnIndex(mAllDayField); - - if (tsIdx < 0 || (mTzField != null && tzIdx < 0) || (mAllDayField != null && adIdx < 0)) - { - throw new IllegalArgumentException("At least one column is missing in cursor."); - } - - if (cursor.isNull(tsIdx)) - { - // if the time stamp is null we return null - return null; - } - - Long timestamp = cursor.getLong(tsIdx); - - // create a new Time for the given time zone, falling back to UTC if none is given - String timezone = mTzField == null ? null : cursor.getString(tzIdx); - DateTime value = new DateTime(timezone == null ? DateTime.UTC : TimeZone.getTimeZone(timezone), timestamp); - - // set the allday flag appropriately - Integer allDayInt = adIdx < 0 ? null : cursor.getInt(adIdx); - - if ((allDayInt != null && allDayInt != 0) || (mAllDayField == null && mAllDayDefault)) - { - value = value.toAllDay(); - } - return value; - } - - - @Override - public DateTime getFrom(Cursor cursor, ContentValues values) - { - int tsIdx; - int tzIdx; - int adIdx; - long timestamp; - String timeZoneId = null; - Integer allDay = 0; - - if (values != null && values.containsKey(mTimestampField)) - { - if (values.getAsLong(mTimestampField) == null) - { - // if the time stamp is null we return null - return null; - } - timestamp = values.getAsLong(mTimestampField); - } - else if (cursor != null && (tsIdx = cursor.getColumnIndex(mTimestampField)) >= 0) - { - if (cursor.isNull(tsIdx)) - { - // if the time stamp is null we return null - return null; - } - timestamp = cursor.getLong(tsIdx); - } - else - { - throw new IllegalArgumentException("Missing timestamp column."); - } - - if (mTzField != null) - { - if (values != null && values.containsKey(mTzField)) - { - timeZoneId = values.getAsString(mTzField); - } - else if (cursor != null && (tzIdx = cursor.getColumnIndex(mTzField)) >= 0) - { - timeZoneId = cursor.getString(tzIdx); - } - else - { - throw new IllegalArgumentException("Missing timezone column."); - } - } - - if (mAllDayField != null) - { - if (values != null && values.containsKey(mAllDayField)) - { - allDay = values.getAsInteger(mAllDayField); - } - else if (cursor != null && (adIdx = cursor.getColumnIndex(mAllDayField)) >= 0) - { - allDay = cursor.getInt(adIdx); - } - else - { - throw new IllegalArgumentException("Missing timezone column."); - } - } - - // create a new Time for the given time zone, falling back to UTC if none is given - DateTime value = new DateTime(timeZoneId == null ? DateTime.UTC : TimeZone.getTimeZone(timeZoneId), timestamp); - - if (allDay != 0) - { - value = value.toAllDay(); - } - return value; - } - - - @Override - public void setIn(ContentValues values, DateTime value) - { - if (value != null) - { - // just store all three parts separately - values.put(mTimestampField, value.getTimestamp()); - - if (mTzField != null) - { - TimeZone timezone = value.getTimeZone(); - values.put(mTzField, timezone == null ? null : timezone.getID()); - } - if (mAllDayField != null) - { - values.put(mAllDayField, value.isAllDay() ? 1 : 0); - } - } - else - { - // write timestamp only, other fields may still use allday and timezone - values.put(mTimestampField, (Long) null); - } - } + private final String mTimestampField; + private final String mTzField; + private final String mAllDayField; + private final boolean mAllDayDefault; + + + /** + * Constructor for a new {@link DateTimeFieldAdapter}. + * + * @param timestampField + * The name of the field that holds the time stamp in milliseconds. + * @param tzField + * The name of the field that holds the time zone (as Olson ID). If the field name is null the time is always set to UTC. + * @param alldayField + * The name of the field that indicated that this time is a date not a date-time. If this fieldName is null all loaded values are + * non-allday. + */ + public DateTimeFieldAdapter(String timestampField, String tzField, String alldayField) + { + if (timestampField == null) + { + throw new IllegalArgumentException("timestampField must not be null"); + } + mTimestampField = timestampField; + mTzField = tzField; + mAllDayField = alldayField; + mAllDayDefault = false; + } + + + @Override + String fieldName() + { + return mTimestampField; + } + + + @Override + public DateTime getFrom(ContentValues values) + { + Long timestamp = values.getAsLong(mTimestampField); + if (timestamp == null) + { + // if the time stamp is null we return null + return null; + } + // create a new Time for the given time zone, falling back to UTC if none is given + String timezone = mTzField == null ? null : values.getAsString(mTzField); + DateTime value = new DateTime(timezone == null ? DateTime.UTC : TimeZone.getTimeZone(timezone), timestamp); + + // cache mAlldayField locally + String allDayField = mAllDayField; + + // set the allday flag appropriately + Integer allDayInt = allDayField == null ? null : values.getAsInteger(allDayField); + + if ((allDayInt != null && allDayInt != 0) || (allDayField == null && mAllDayDefault)) + { + value = value.toAllDay(); + } + + return value; + } + + + @Override + public DateTime getFrom(Cursor cursor) + { + int tsIdx = cursor.getColumnIndex(mTimestampField); + int tzIdx = mTzField == null ? -1 : cursor.getColumnIndex(mTzField); + int adIdx = mAllDayField == null ? -1 : cursor.getColumnIndex(mAllDayField); + + if (tsIdx < 0 || (mTzField != null && tzIdx < 0) || (mAllDayField != null && adIdx < 0)) + { + throw new IllegalArgumentException("At least one column is missing in cursor."); + } + + if (cursor.isNull(tsIdx)) + { + // if the time stamp is null we return null + return null; + } + + Long timestamp = cursor.getLong(tsIdx); + + // create a new Time for the given time zone, falling back to UTC if none is given + String timezone = mTzField == null ? null : cursor.getString(tzIdx); + DateTime value = new DateTime(timezone == null ? DateTime.UTC : TimeZone.getTimeZone(timezone), timestamp); + + // set the allday flag appropriately + Integer allDayInt = adIdx < 0 ? null : cursor.getInt(adIdx); + + if ((allDayInt != null && allDayInt != 0) || (mAllDayField == null && mAllDayDefault)) + { + value = value.toAllDay(); + } + return value; + } + + + @Override + public DateTime getFrom(Cursor cursor, ContentValues values) + { + int tsIdx; + int tzIdx; + int adIdx; + long timestamp; + String timeZoneId = null; + Integer allDay = 0; + + if (values != null && values.containsKey(mTimestampField)) + { + if (values.getAsLong(mTimestampField) == null) + { + // if the time stamp is null we return null + return null; + } + timestamp = values.getAsLong(mTimestampField); + } + else if (cursor != null && (tsIdx = cursor.getColumnIndex(mTimestampField)) >= 0) + { + if (cursor.isNull(tsIdx)) + { + // if the time stamp is null we return null + return null; + } + timestamp = cursor.getLong(tsIdx); + } + else + { + throw new IllegalArgumentException("Missing timestamp column."); + } + + if (mTzField != null) + { + if (values != null && values.containsKey(mTzField)) + { + timeZoneId = values.getAsString(mTzField); + } + else if (cursor != null && (tzIdx = cursor.getColumnIndex(mTzField)) >= 0) + { + timeZoneId = cursor.getString(tzIdx); + } + else + { + throw new IllegalArgumentException("Missing timezone column."); + } + } + + if (mAllDayField != null) + { + if (values != null && values.containsKey(mAllDayField)) + { + allDay = values.getAsInteger(mAllDayField); + } + else if (cursor != null && (adIdx = cursor.getColumnIndex(mAllDayField)) >= 0) + { + allDay = cursor.getInt(adIdx); + } + else + { + throw new IllegalArgumentException("Missing timezone column."); + } + } + + // create a new Time for the given time zone, falling back to UTC if none is given + DateTime value = new DateTime(timeZoneId == null ? DateTime.UTC : TimeZone.getTimeZone(timeZoneId), timestamp); + + if (allDay != 0) + { + value = value.toAllDay(); + } + return value; + } + + + @Override + public void setIn(ContentValues values, DateTime value) + { + if (value != null) + { + // just store all three parts separately + values.put(mTimestampField, value.getTimestamp()); + + if (mTzField != null) + { + TimeZone timezone = value.getTimeZone(); + values.put(mTzField, timezone == null ? null : timezone.getID()); + } + if (mAllDayField != null) + { + values.put(mAllDayField, value.isAllDay() ? 1 : 0); + } + } + else + { + // write timestamp only, other fields may still use allday and timezone + values.put(mTimestampField, (Long) null); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DurationFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DurationFieldAdapter.java index 5be867fe..2f72419f 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DurationFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DurationFieldAdapter.java @@ -17,91 +17,91 @@ package org.dmfs.provider.tasks.model.adapters; -import org.dmfs.rfc5545.Duration; - import android.content.ContentValues; import android.database.Cursor; +import org.dmfs.rfc5545.Duration; + /** * Knows how to load and store {@link Duration} values from a {@link Cursor} or {@link ContentValues}. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class DurationFieldAdapter extends SimpleFieldAdapter { - private final String mFieldName; - - - /** - * Constructor for a new {@link DurationFieldAdapter}. - * - * @param urlField - * The field name that holds the {@link Duration}. - */ - public DurationFieldAdapter(String urlField) - { - if (urlField == null) - { - throw new IllegalArgumentException("urlField must not be null"); - } - mFieldName = urlField; - } - - - @Override - String fieldName() - { - return mFieldName; - } - - - @Override - public Duration getFrom(ContentValues values) - { - String rawValue = values.getAsString(mFieldName); - if (rawValue == null) - { - return null; - } - - return Duration.parse(rawValue); - } - - - @Override - public Duration getFrom(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(mFieldName); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); - } - - if (cursor.isNull(columnIdx)) - { - return null; - } - - return Duration.parse(cursor.getString(columnIdx)); - } - - - @Override - public void setIn(ContentValues values, Duration value) - { - if (value != null) - { - values.put(mFieldName, value.toString()); - } - else - { - values.putNull(mFieldName); - } - } + private final String mFieldName; + + + /** + * Constructor for a new {@link DurationFieldAdapter}. + * + * @param urlField + * The field name that holds the {@link Duration}. + */ + public DurationFieldAdapter(String urlField) + { + if (urlField == null) + { + throw new IllegalArgumentException("urlField must not be null"); + } + mFieldName = urlField; + } + + + @Override + String fieldName() + { + return mFieldName; + } + + + @Override + public Duration getFrom(ContentValues values) + { + String rawValue = values.getAsString(mFieldName); + if (rawValue == null) + { + return null; + } + + return Duration.parse(rawValue); + } + + + @Override + public Duration getFrom(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); + } + + if (cursor.isNull(columnIdx)) + { + return null; + } + + return Duration.parse(cursor.getString(columnIdx)); + } + + + @Override + public void setIn(ContentValues values, Duration value) + { + if (value != null) + { + values.put(mFieldName, value.toString()); + } + else + { + values.putNull(mFieldName); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FieldAdapter.java index 3c7bf517..265338a0 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FieldAdapter.java @@ -23,130 +23,127 @@ import android.database.Cursor; /** * Knows how to load and store a specific field from or to {@link ContentValues} or from {@link Cursor}s. - * - * @author Marten Gajda - * + * * @param - * The type of the value this adapter stores. + * The type of the value this adapter stores. * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public interface FieldAdapter { - /** - * Check if a value is present and non-null in the given {@link ContentValues}. - * - * @param values - * The {@link ContentValues} to check. - * @return - */ - public boolean existsIn(ContentValues values); - - - /** - * Check if a value is present (may be null) in the given {@link ContentValues}. - * - * @param values - * The {@link ContentValues} to check. - * @return - */ - public boolean isSetIn(ContentValues values); - - - /** - * Get the value from the given {@link ContentValues} - * - * @param values - * The {@link ContentValues} that contain the value to return. - * @return The value. - */ - public FieldType getFrom(ContentValues values); - - - /** - * Check if a value is present and non-null in the given {@link Cursor}. - * - * @param cursor - * The {@link Cursor} that contains the value to check. - * @return - */ - public boolean existsIn(Cursor cursor); - - - /** - * Get the value from the given {@link Cursor} - * - * @param cursor - * The {@link Cursor} that contain the value to return. - * @return The value. - */ - public FieldType getFrom(Cursor cursor); - - - /** - * Check if a value is present and non-null in the given {@link Cursor} or {@link ContentValues}. - * - * @param cursor - * The {@link Cursor} that contains the value to check. - * @param values - * The {@link ContentValues} that contains the value to check. - * @return - */ - public boolean existsIn(Cursor cursor, ContentValues values); - - - /** - * Get the value from the given {@link Cursor} or {@link ContentValues}, with the {@link ContentValues} taking precedence over the cursor values. - * - * @param cursor - * The {@link Cursor} that contains the value to return. - * @param values - * The {@link ContentValues} that contains the value to return. - * @return The value. - */ - public FieldType getFrom(Cursor cursor, ContentValues values); - - - /** - * Set a value in the given {@link ContentValues}. - * - * @param values - * The {@link ContentValues} to store the new value in. - * @param value - * The new value to store. - */ - public void setIn(ContentValues values, FieldType value); - - - /** - * Remove a value from the given {@link ContentValues}. - * - * @param values - * The {@link ContentValues} from which to remove the value. - */ - public void removeFrom(ContentValues values); - - - /** - * Copy the value from a {@link Cursor} to the given {@link ContentValues}. - * - * @param source - * The {@link Cursor} that contains the value to copy. - * @param dest - * The {@link ContentValues} to receive the value. - */ - public void copyValue(Cursor source, ContentValues dest); - - - /** - * Copy the value from {@link ContentValues} to another {@link ContentValues} object. - * - * @param source - * The {@link ContentValues} that contains the value to copy. - * @param dest - * The {@link ContentValues} to receive the value. - */ - public void copyValue(ContentValues source, ContentValues dest); + /** + * Check if a value is present and non-null in the given {@link ContentValues}. + * + * @param values + * The {@link ContentValues} to check. + * + * @return + */ + public boolean existsIn(ContentValues values); + + /** + * Check if a value is present (may be null) in the given {@link ContentValues}. + * + * @param values + * The {@link ContentValues} to check. + * + * @return + */ + public boolean isSetIn(ContentValues values); + + /** + * Get the value from the given {@link ContentValues} + * + * @param values + * The {@link ContentValues} that contain the value to return. + * + * @return The value. + */ + public FieldType getFrom(ContentValues values); + + /** + * Check if a value is present and non-null in the given {@link Cursor}. + * + * @param cursor + * The {@link Cursor} that contains the value to check. + * + * @return + */ + public boolean existsIn(Cursor cursor); + + /** + * Get the value from the given {@link Cursor} + * + * @param cursor + * The {@link Cursor} that contain the value to return. + * + * @return The value. + */ + public FieldType getFrom(Cursor cursor); + + /** + * Check if a value is present and non-null in the given {@link Cursor} or {@link ContentValues}. + * + * @param cursor + * The {@link Cursor} that contains the value to check. + * @param values + * The {@link ContentValues} that contains the value to check. + * + * @return + */ + public boolean existsIn(Cursor cursor, ContentValues values); + + /** + * Get the value from the given {@link Cursor} or {@link ContentValues}, with the {@link ContentValues} taking precedence over the cursor values. + * + * @param cursor + * The {@link Cursor} that contains the value to return. + * @param values + * The {@link ContentValues} that contains the value to return. + * + * @return The value. + */ + public FieldType getFrom(Cursor cursor, ContentValues values); + + /** + * Set a value in the given {@link ContentValues}. + * + * @param values + * The {@link ContentValues} to store the new value in. + * @param value + * The new value to store. + */ + public void setIn(ContentValues values, FieldType value); + + /** + * Remove a value from the given {@link ContentValues}. + * + * @param values + * The {@link ContentValues} from which to remove the value. + */ + public void removeFrom(ContentValues values); + + /** + * Copy the value from a {@link Cursor} to the given {@link ContentValues}. + * + * @param source + * The {@link Cursor} that contains the value to copy. + * @param dest + * The {@link ContentValues} to receive the value. + */ + public void copyValue(Cursor source, ContentValues dest); + + /** + * Copy the value from {@link ContentValues} to another {@link ContentValues} object. + * + * @param source + * The {@link ContentValues} that contains the value to copy. + * @param dest + * The {@link ContentValues} to receive the value. + */ + public void copyValue(ContentValues source, ContentValues dest); } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FloatFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FloatFieldAdapter.java index a61d4243..f43d5bee 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FloatFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FloatFieldAdapter.java @@ -23,74 +23,74 @@ import android.database.Cursor; /** * Knows how to load and store a {@link Float} value from a {@link Cursor} or {@link ContentValues}. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class FloatFieldAdapter extends SimpleFieldAdapter { - /** - * The field name this adapter uses to store the values. - */ - private final String mFieldName; - - - /** - * Constructor for a new {@link FloatFieldAdapter}. - * - * @param fieldName - * The name of the field to use when loading or storing the value. - */ - public FloatFieldAdapter(String fieldName) - { - if (fieldName == null) - { - throw new IllegalArgumentException("fieldName must not be null"); - } - mFieldName = fieldName; - } - - - @Override - String fieldName() - { - return mFieldName; - } - - - @Override - public Float getFrom(ContentValues values) - { - return values.getAsFloat(mFieldName); - } - - - @Override - public Float getFrom(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(mFieldName); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); - } - return cursor.isNull(columnIdx) ? null : cursor.getFloat(columnIdx); - } - - - @Override - public void setIn(ContentValues values, Float value) - { - if (value != null) - { - values.put(mFieldName, value); - } - else - { - values.putNull(mFieldName); - } - } + /** + * The field name this adapter uses to store the values. + */ + private final String mFieldName; + + + /** + * Constructor for a new {@link FloatFieldAdapter}. + * + * @param fieldName + * The name of the field to use when loading or storing the value. + */ + public FloatFieldAdapter(String fieldName) + { + if (fieldName == null) + { + throw new IllegalArgumentException("fieldName must not be null"); + } + mFieldName = fieldName; + } + + + @Override + String fieldName() + { + return mFieldName; + } + + + @Override + public Float getFrom(ContentValues values) + { + return values.getAsFloat(mFieldName); + } + + + @Override + public Float getFrom(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); + } + return cursor.isNull(columnIdx) ? null : cursor.getFloat(columnIdx); + } + + + @Override + public void setIn(ContentValues values, Float value) + { + if (value != null) + { + values.put(mFieldName, value); + } + else + { + values.putNull(mFieldName); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/IntegerFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/IntegerFieldAdapter.java index fec28fbd..456612b4 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/IntegerFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/IntegerFieldAdapter.java @@ -23,75 +23,75 @@ import android.database.Cursor; /** * Knows how to load and store an {@link Integer} from a {@link Cursor} or {@link ContentValues}. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class IntegerFieldAdapter extends SimpleFieldAdapter { - /** - * The field name this adapter uses to store the values. - */ - private final String mFieldName; - - - /** - * Constructor for a new {@link IntegerFieldAdapter}. - * - * @param fieldName - * The name of the field to use when loading or storing the value. - */ - public IntegerFieldAdapter(String fieldName) - { - if (fieldName == null) - { - throw new IllegalArgumentException("fieldName must not be null"); - } - mFieldName = fieldName; - } - - - @Override - String fieldName() - { - return mFieldName; - } - - - @Override - public Integer getFrom(ContentValues values) - { - // return the value as Integer - return values.getAsInteger(mFieldName); - } - - - @Override - public Integer getFrom(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(mFieldName); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); - } - return cursor.isNull(columnIdx) ? null : cursor.getInt(columnIdx); - } - - - @Override - public void setIn(ContentValues values, Integer value) - { - if (value != null) - { - values.put(mFieldName, value); - } - else - { - values.putNull(mFieldName); - } - } + /** + * The field name this adapter uses to store the values. + */ + private final String mFieldName; + + + /** + * Constructor for a new {@link IntegerFieldAdapter}. + * + * @param fieldName + * The name of the field to use when loading or storing the value. + */ + public IntegerFieldAdapter(String fieldName) + { + if (fieldName == null) + { + throw new IllegalArgumentException("fieldName must not be null"); + } + mFieldName = fieldName; + } + + + @Override + String fieldName() + { + return mFieldName; + } + + + @Override + public Integer getFrom(ContentValues values) + { + // return the value as Integer + return values.getAsInteger(mFieldName); + } + + + @Override + public Integer getFrom(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); + } + return cursor.isNull(columnIdx) ? null : cursor.getInt(columnIdx); + } + + + @Override + public void setIn(ContentValues values, Integer value) + { + if (value != null) + { + values.put(mFieldName, value); + } + else + { + values.putNull(mFieldName); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/LongFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/LongFieldAdapter.java index 22d255b2..9f34ca6a 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/LongFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/LongFieldAdapter.java @@ -23,73 +23,73 @@ import android.database.Cursor; /** * Knows how to load and store a {@link Long} value from a {@link Cursor} or {@link ContentValues}. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class LongFieldAdapter extends SimpleFieldAdapter { - /** - * The field name this adapter uses to store the values. - */ - private final String mFieldName; + /** + * The field name this adapter uses to store the values. + */ + private final String mFieldName; - /** - * Constructor for a new {@link LongFieldAdapter}. - * - * @param fieldName - * The name of the field to use when loading or storing the value. - */ - public LongFieldAdapter(String fieldName) - { - if (fieldName == null) - { - throw new IllegalArgumentException("fieldName must not be null"); - } - mFieldName = fieldName; - } + /** + * Constructor for a new {@link LongFieldAdapter}. + * + * @param fieldName + * The name of the field to use when loading or storing the value. + */ + public LongFieldAdapter(String fieldName) + { + if (fieldName == null) + { + throw new IllegalArgumentException("fieldName must not be null"); + } + mFieldName = fieldName; + } - @Override - String fieldName() - { - return mFieldName; - } + @Override + String fieldName() + { + return mFieldName; + } - @Override - public Long getFrom(ContentValues values) - { - return values.getAsLong(mFieldName); - } + @Override + public Long getFrom(ContentValues values) + { + return values.getAsLong(mFieldName); + } - @Override - public Long getFrom(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(mFieldName); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); - } - return cursor.isNull(columnIdx) ? null : cursor.getLong(columnIdx); - } + @Override + public Long getFrom(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); + } + return cursor.isNull(columnIdx) ? null : cursor.getLong(columnIdx); + } - @Override - public void setIn(ContentValues values, Long value) - { - if (value != null) - { - values.put(mFieldName, value); - } - else - { - values.putNull(mFieldName); - } - } + @Override + public void setIn(ContentValues values, Long value) + { + if (value != null) + { + values.put(mFieldName, value); + } + else + { + values.putNull(mFieldName); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/RRuleFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/RRuleFieldAdapter.java index a6836833..32584750 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/RRuleFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/RRuleFieldAdapter.java @@ -17,107 +17,107 @@ package org.dmfs.provider.tasks.model.adapters; -import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; -import org.dmfs.rfc5545.recur.RecurrenceRule; - import android.content.ContentValues; import android.database.Cursor; +import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; +import org.dmfs.rfc5545.recur.RecurrenceRule; + /** * Knows how to load and store a {@link RecurrenceRule} from a {@link Cursor} or {@link ContentValues}. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class RRuleFieldAdapter extends SimpleFieldAdapter { - /** - * The field name this adapter uses to store the values. - */ - private final String mFieldName; - - - /** - * Constructor for a new {@link RRuleFieldAdapter}. - * - * @param fieldName - * The name of the field to use when loading or storing the value. - */ - public RRuleFieldAdapter(String fieldName) - { - if (fieldName == null) - { - throw new IllegalArgumentException("fieldName must not be null"); - } - mFieldName = fieldName; - } - - - @Override - String fieldName() - { - return mFieldName; - } - - - @Override - public RecurrenceRule getFrom(ContentValues values) - { - String rrule = values.getAsString(mFieldName); - if (rrule == null) - { - return null; - } - try - { - return new RecurrenceRule(rrule); - } - catch (InvalidRecurrenceRuleException e) - { - throw new IllegalArgumentException("can not parse RRULE '" + rrule + "'", e); - } - } - - - @Override - public RecurrenceRule getFrom(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(mFieldName); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); - } - if (cursor.isNull(columnIdx)) - { - return null; - } - - try - { - return new RecurrenceRule(cursor.getString(columnIdx)); - } - catch (InvalidRecurrenceRuleException e) - { - throw new IllegalArgumentException("can not parse RRULE '" + cursor.getString(columnIdx) + "'", e); - } - } - - - @Override - public void setIn(ContentValues values, RecurrenceRule value) - { - if (value != null) - { - values.put(mFieldName, value.toString()); - } - else - { - values.putNull(mFieldName); - } - } + /** + * The field name this adapter uses to store the values. + */ + private final String mFieldName; + + + /** + * Constructor for a new {@link RRuleFieldAdapter}. + * + * @param fieldName + * The name of the field to use when loading or storing the value. + */ + public RRuleFieldAdapter(String fieldName) + { + if (fieldName == null) + { + throw new IllegalArgumentException("fieldName must not be null"); + } + mFieldName = fieldName; + } + + + @Override + String fieldName() + { + return mFieldName; + } + + + @Override + public RecurrenceRule getFrom(ContentValues values) + { + String rrule = values.getAsString(mFieldName); + if (rrule == null) + { + return null; + } + try + { + return new RecurrenceRule(rrule); + } + catch (InvalidRecurrenceRuleException e) + { + throw new IllegalArgumentException("can not parse RRULE '" + rrule + "'", e); + } + } + + + @Override + public RecurrenceRule getFrom(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); + } + if (cursor.isNull(columnIdx)) + { + return null; + } + + try + { + return new RecurrenceRule(cursor.getString(columnIdx)); + } + catch (InvalidRecurrenceRuleException e) + { + throw new IllegalArgumentException("can not parse RRULE '" + cursor.getString(columnIdx) + "'", e); + } + } + + + @Override + public void setIn(ContentValues values, RecurrenceRule value) + { + if (value != null) + { + values.put(mFieldName, value.toString()); + } + else + { + values.putNull(mFieldName); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/SimpleFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/SimpleFieldAdapter.java index a5c0005c..b0d10821 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/SimpleFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/SimpleFieldAdapter.java @@ -23,85 +23,84 @@ import android.database.Cursor; /** * An abstract {@link FieldAdapter} that implements a couple of methods as used by most simple FieldAdapters. - * - * @author Marten Gajda - * + * * @param - * The Type of the field this adapter handles. - * + * The Type of the field this adapter handles. * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public abstract class SimpleFieldAdapter implements FieldAdapter { - /** - * Returns the sole field name of this adapter. - * - * @return - */ - abstract String fieldName(); + /** + * Returns the sole field name of this adapter. + * + * @return + */ + abstract String fieldName(); - @Override - public boolean existsIn(ContentValues values) - { - return values.get(fieldName()) != null; - } + @Override + public boolean existsIn(ContentValues values) + { + return values.get(fieldName()) != null; + } - @Override - public boolean isSetIn(ContentValues values) - { - return values.containsKey(fieldName()); - } + @Override + public boolean isSetIn(ContentValues values) + { + return values.containsKey(fieldName()); + } - @Override - public boolean existsIn(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(fieldName()); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + fieldName() + "' is missing in cursor."); - } + @Override + public boolean existsIn(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(fieldName()); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + fieldName() + "' is missing in cursor."); + } - return !cursor.isNull(columnIdx); - } + return !cursor.isNull(columnIdx); + } - @Override - public FieldType getFrom(Cursor cursor, ContentValues values) - { - return values.containsKey(fieldName()) ? getFrom(values) : getFrom(cursor); - } + @Override + public FieldType getFrom(Cursor cursor, ContentValues values) + { + return values.containsKey(fieldName()) ? getFrom(values) : getFrom(cursor); + } - @Override - public boolean existsIn(Cursor cursor, ContentValues values) - { - return existsIn(values) || existsIn(cursor); - } + @Override + public boolean existsIn(Cursor cursor, ContentValues values) + { + return existsIn(values) || existsIn(cursor); + } - @Override - public void removeFrom(ContentValues values) - { - values.remove(fieldName()); - } + @Override + public void removeFrom(ContentValues values) + { + values.remove(fieldName()); + } - @Override - public void copyValue(Cursor cursor, ContentValues values) - { - setIn(values, getFrom(cursor)); - } + @Override + public void copyValue(Cursor cursor, ContentValues values) + { + setIn(values, getFrom(cursor)); + } - @Override - public void copyValue(ContentValues oldValues, ContentValues newValues) - { - setIn(newValues, getFrom(oldValues)); - } + @Override + public void copyValue(ContentValues oldValues, ContentValues newValues) + { + setIn(newValues, getFrom(oldValues)); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/StringFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/StringFieldAdapter.java index 8e1ed3dc..2a972be5 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/StringFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/StringFieldAdapter.java @@ -23,74 +23,74 @@ import android.database.Cursor; /** * Knows how to load and store a {@link String} value from a {@link Cursor} or {@link ContentValues}. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class StringFieldAdapter extends SimpleFieldAdapter { - /** - * The field name this adapter uses to store the values. - */ - private final String mFieldName; + /** + * The field name this adapter uses to store the values. + */ + private final String mFieldName; - /** - * Constructor for a new {@link StringFieldAdapter}. - * - * @param fieldName - * The name of the field to use when loading or storing the value. - */ - public StringFieldAdapter(String fieldName) - { - if (fieldName == null) - { - throw new IllegalArgumentException("fieldName must not be null"); - } - mFieldName = fieldName; - } + /** + * Constructor for a new {@link StringFieldAdapter}. + * + * @param fieldName + * The name of the field to use when loading or storing the value. + */ + public StringFieldAdapter(String fieldName) + { + if (fieldName == null) + { + throw new IllegalArgumentException("fieldName must not be null"); + } + mFieldName = fieldName; + } - @Override - String fieldName() - { - return mFieldName; - } + @Override + String fieldName() + { + return mFieldName; + } - @Override - public String getFrom(ContentValues values) - { - // return the value as String - return values.getAsString(mFieldName); - } + @Override + public String getFrom(ContentValues values) + { + // return the value as String + return values.getAsString(mFieldName); + } - @Override - public String getFrom(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(mFieldName); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); - } - return cursor.getString(columnIdx); - } + @Override + public String getFrom(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); + } + return cursor.getString(columnIdx); + } - @Override - public void setIn(ContentValues values, String value) - { - if (value != null) - { - values.put(mFieldName, value); - } - else - { - values.putNull(mFieldName); - } - } + @Override + public void setIn(ContentValues values, String value) + { + if (value != null) + { + values.put(mFieldName, value); + } + else + { + values.putNull(mFieldName); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/UrlFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/UrlFieldAdapter.java index 0b21101d..16e8d10c 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/UrlFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/UrlFieldAdapter.java @@ -17,80 +17,80 @@ package org.dmfs.provider.tasks.model.adapters; -import java.net.URI; -import java.net.URL; - import android.content.ContentValues; import android.database.Cursor; +import java.net.URI; +import java.net.URL; + /** * Knows how to load and store {@link URL} values from a {@link Cursor} or {@link ContentValues}. - * - * @author Marten Gajda - * + * * @param - * The type of the entity the field belongs to. + * The type of the entity the field belongs to. + * + * @author Marten Gajda */ public final class UrlFieldAdapter extends SimpleFieldAdapter { - private final String mFieldName; - - - /** - * Constructor for a new {@link UrlFieldAdapter}. - * - * @param urlField - * The field name that holds the URL. - */ - public UrlFieldAdapter(String urlField) - { - if (urlField == null) - { - throw new IllegalArgumentException("urlField must not be null"); - } - mFieldName = urlField; - } - - - @Override - String fieldName() - { - return mFieldName; - } - - - @Override - public URI getFrom(ContentValues values) - { - return values.get(mFieldName) == null ? null : URI.create(values.getAsString(mFieldName)); - } - - - @Override - public URI getFrom(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(mFieldName); - if (columnIdx < 0) - { - throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); - } - - return cursor.isNull(columnIdx) ? null : URI.create(cursor.getString(columnIdx)); - } - - - @Override - public void setIn(ContentValues values, URI value) - { - if (value != null) - { - values.put(mFieldName, value.toASCIIString()); - } - else - { - values.putNull(mFieldName); - } - } + private final String mFieldName; + + + /** + * Constructor for a new {@link UrlFieldAdapter}. + * + * @param urlField + * The field name that holds the URL. + */ + public UrlFieldAdapter(String urlField) + { + if (urlField == null) + { + throw new IllegalArgumentException("urlField must not be null"); + } + mFieldName = urlField; + } + + + @Override + String fieldName() + { + return mFieldName; + } + + + @Override + public URI getFrom(ContentValues values) + { + return values.get(mFieldName) == null ? null : URI.create(values.getAsString(mFieldName)); + } + + + @Override + public URI getFrom(Cursor cursor) + { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) + { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); + } + + return cursor.isNull(columnIdx) ? null : URI.create(cursor.getString(columnIdx)); + } + + + @Override + public void setIn(ContentValues values, URI value) + { + if (value != null) + { + values.put(mFieldName, value.toASCIIString()); + } + else + { + values.putNull(mFieldName); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/AbstractEntityProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/AbstractEntityProcessor.java index dbd3eaf9..5cbfda4f 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/AbstractEntityProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/AbstractEntityProcessor.java @@ -17,58 +17,58 @@ package org.dmfs.provider.tasks.processors; -import org.dmfs.provider.tasks.model.EntityAdapter; - import android.database.sqlite.SQLiteDatabase; +import org.dmfs.provider.tasks.model.EntityAdapter; + /** * A default implementation of {@link EntityProcessor} that does nothing. It can be used as the basis of concrete {@link EntityProcessor}s without having to * override all the methods. - * + * * @author Marten Gajda */ public abstract class AbstractEntityProcessor> implements EntityProcessor { - @Override - public void beforeInsert(SQLiteDatabase db, T list, boolean isSyncAdapter) - { - // the default implementation doesn't do anything - } + @Override + public void beforeInsert(SQLiteDatabase db, T list, boolean isSyncAdapter) + { + // the default implementation doesn't do anything + } - @Override - public void afterInsert(SQLiteDatabase db, T list, boolean isSyncAdapter) - { - // the default implementation doesn't do anything - } + @Override + public void afterInsert(SQLiteDatabase db, T list, boolean isSyncAdapter) + { + // the default implementation doesn't do anything + } - @Override - public void beforeUpdate(SQLiteDatabase db, T list, boolean isSyncAdapter) - { - // the default implementation doesn't do anything - } + @Override + public void beforeUpdate(SQLiteDatabase db, T list, boolean isSyncAdapter) + { + // the default implementation doesn't do anything + } - @Override - public void afterUpdate(SQLiteDatabase db, T list, boolean isSyncAdapter) - { - // the default implementation doesn't do anything - } + @Override + public void afterUpdate(SQLiteDatabase db, T list, boolean isSyncAdapter) + { + // the default implementation doesn't do anything + } - @Override - public void beforeDelete(SQLiteDatabase db, T list, boolean isSyncAdapter) - { - // the default implementation doesn't do anything - } + @Override + public void beforeDelete(SQLiteDatabase db, T list, boolean isSyncAdapter) + { + // the default implementation doesn't do anything + } - @Override - public void afterDelete(SQLiteDatabase db, T list, boolean isSyncAdapter) - { - // the default implementation doesn't do anything - } + @Override + public void afterDelete(SQLiteDatabase db, T list, boolean isSyncAdapter) + { + // the default implementation doesn't do anything + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/EntityProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/EntityProcessor.java index 3e9ff908..ebcf6bc5 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/EntityProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/EntityProcessor.java @@ -17,106 +17,102 @@ package org.dmfs.provider.tasks.processors; -import org.dmfs.provider.tasks.model.EntityAdapter; - import android.database.sqlite.SQLiteDatabase; +import org.dmfs.provider.tasks.model.EntityAdapter; + /** * EntityProcessors are called before and after any operation on an entity. They can be used to perform additional operations for each entity. - * + * * @param - * The type of the entity adapter. + * The type of the entity adapter. + * * @author Marten Gajda */ public interface EntityProcessor> { - /** - * Called before an entity is inserted. - * - * @param db - * A writable database. - * @param entityAdapter - * The {@link EntityAdapter} that's about to be inserted. You can modify the entity at this stage. {@link EntityAdapter#id()} will return an - * invalid value. - * @param isSyncAdapter - */ - public void beforeInsert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - - - /** - * Called after an entity has been inserted. - * - * @param db - * A writable database. - * @param entityAdapter - * The {@link EntityAdapter} that's has been inserted. Modifying the entity has no effect. - * @param isSyncAdapter - */ - public void afterInsert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - - - /** - * Called before an entity is updated. - * - * @param db - * A writable database. - * @param entityAdapter - * The {@link EntityAdapter} that's about to be updated. You can modify the entity at this stage. - * @param isSyncAdapter - */ - public void beforeUpdate(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - + /** + * Called before an entity is inserted. + * + * @param db + * A writable database. + * @param entityAdapter + * The {@link EntityAdapter} that's about to be inserted. You can modify the entity at this stage. {@link EntityAdapter#id()} will return an invalid + * value. + * @param isSyncAdapter + */ + public void beforeInsert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - /** - * Called after an entity has been updated. - * - * @param db - * A writable database. - * @param entityAdapter - * The {@link EntityAdapter} that's has been updated. Modifying the entity has no effect. - * @param isSyncAdapter - */ - public void afterUpdate(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); + /** + * Called after an entity has been inserted. + * + * @param db + * A writable database. + * @param entityAdapter + * The {@link EntityAdapter} that's has been inserted. Modifying the entity has no effect. + * @param isSyncAdapter + */ + public void afterInsert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); + /** + * Called before an entity is updated. + * + * @param db + * A writable database. + * @param entityAdapter + * The {@link EntityAdapter} that's about to be updated. You can modify the entity at this stage. + * @param isSyncAdapter + */ + public void beforeUpdate(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - /** - * Called before an entity is deleted. - *

- * Note that may be called twice for each entity. Once when the entity is marked deleted by the UI and once when it's actually removed by the sync adapter. - * Both cases can be distinguished by the isSyncAdapter parameter. If an entity is removed because it was deleted on the server, this will be called only - * once with isSyncAdapter == true. - *

- *

- * Also note that no processor is called when an entity is removed automatically by a database trigger (e.g. when an entire task list is removed). - *

- * - * @param db - * A writable database. - * @param entityAdapter - * The {@link EntityAdapter} that's about to be deleted. Modifying the entity has no effect. - * @param isSyncAdapter - */ - public void beforeDelete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); + /** + * Called after an entity has been updated. + * + * @param db + * A writable database. + * @param entityAdapter + * The {@link EntityAdapter} that's has been updated. Modifying the entity has no effect. + * @param isSyncAdapter + */ + public void afterUpdate(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); + /** + * Called before an entity is deleted. + *

+ * Note that may be called twice for each entity. Once when the entity is marked deleted by the UI and once when it's actually removed by the sync adapter. + * Both cases can be distinguished by the isSyncAdapter parameter. If an entity is removed because it was deleted on the server, this will be called only + * once with isSyncAdapter == true. + *

+ *

+ * Also note that no processor is called when an entity is removed automatically by a database trigger (e.g. when an entire task list is removed). + *

+ * + * @param db + * A writable database. + * @param entityAdapter + * The {@link EntityAdapter} that's about to be deleted. Modifying the entity has no effect. + * @param isSyncAdapter + */ + public void beforeDelete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - /** - * Called after an entity is deleted. - *

- * Note that may be called twice for each entity. Once when the entity is marked deleted by the UI and once when it's actually removed by the sync adapter. - * Both cases can be distinguished by the isSyncAdapter parameter. If an entity is removed because it was deleted on the server, this will be called only - * once with isSyncAdapter == true. - *

- *

- * Also note that no processor is called when an entity is removed automatically by a database trigger (e.g. when an entire task list is removed). - *

- * - * @param db - * A writable database. - * @param entityAdapter - * The {@link EntityAdapter} that was deleted. The value of {@link EntityAdapter#id()} contains the id of the deleted entity. Modifying the - * entity has no effect. - * @param isSyncAdapter - */ - public void afterDelete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); + /** + * Called after an entity is deleted. + *

+ * Note that may be called twice for each entity. Once when the entity is marked deleted by the UI and once when it's actually removed by the sync adapter. + * Both cases can be distinguished by the isSyncAdapter parameter. If an entity is removed because it was deleted on the server, this will be called only + * once with isSyncAdapter == true. + *

+ *

+ * Also note that no processor is called when an entity is removed automatically by a database trigger (e.g. when an entire task list is removed). + *

+ * + * @param db + * A writable database. + * @param entityAdapter + * The {@link EntityAdapter} that was deleted. The value of {@link EntityAdapter#id()} contains the id of the deleted entity. Modifying the entity + * has no effect. + * @param isSyncAdapter + */ + public void afterDelete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListExecutionProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListExecutionProcessor.java index 9d2507b1..90fe6978 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListExecutionProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListExecutionProcessor.java @@ -17,39 +17,39 @@ package org.dmfs.provider.tasks.processors.lists; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; import org.dmfs.provider.tasks.model.ListAdapter; import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; -import android.database.sqlite.SQLiteDatabase; - /** * A processor that performs the actual operations on task lists. - * + * * @author Marten Gajda */ public class ListExecutionProcessor extends AbstractEntityProcessor { - @Override - public void beforeInsert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) - { - list.commit(db); - } + @Override + public void beforeInsert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) + { + list.commit(db); + } - @Override - public void beforeUpdate(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) - { - list.commit(db); - } + @Override + public void beforeUpdate(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) + { + list.commit(db); + } - @Override - public void beforeDelete(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) - { - db.delete(Tables.LISTS, TaskContract.TaskLists._ID + "=" + list.id(), null); - } + @Override + public void beforeDelete(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) + { + db.delete(Tables.LISTS, TaskContract.TaskLists._ID + "=" + list.id(), null); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListValidatorProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListValidatorProcessor.java index 5db06b7d..0b9d722e 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListValidatorProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListValidatorProcessor.java @@ -17,110 +17,110 @@ package org.dmfs.provider.tasks.processors.lists; -import org.dmfs.provider.tasks.model.ListAdapter; -import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; - import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; +import org.dmfs.provider.tasks.model.ListAdapter; +import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; + /** * A processor to validate the values of a task list. - * + * * @author Marten Gajda */ public class ListValidatorProcessor extends AbstractEntityProcessor { - @Override - public void beforeInsert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) - { - if (!isSyncAdapter) - { - throw new UnsupportedOperationException("Caller must be a sync adapter to create task lists"); - } - - if (TextUtils.isEmpty(list.valueOf(ListAdapter.ACCOUNT_NAME))) - { - throw new IllegalArgumentException("ACCOUNT_NAME is required on INSERT"); - } - - if (TextUtils.isEmpty(list.valueOf(ListAdapter.ACCOUNT_TYPE))) - { - throw new IllegalArgumentException("ACCOUNT_TYPE is required on INSERT"); - } - - verifyCommon(list, isSyncAdapter); - } - - - @Override - public void beforeUpdate(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) - { - if (list.isUpdated(ListAdapter.ACCOUNT_NAME)) - { - throw new IllegalArgumentException("ACCOUNT_NAME is write-once"); - } - - if (list.isUpdated(ListAdapter.ACCOUNT_TYPE)) - { - throw new IllegalArgumentException("ACCOUNT_TYPE is write-once"); - } - - verifyCommon(list, isSyncAdapter); - } - - - @Override - public void beforeDelete(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) - { - if (!isSyncAdapter) - { - throw new UnsupportedOperationException("Caller must be a sync adapter to delete task lists"); - } - } - - - /** - * Performs tests that are common to insert an update operations. - * - * @param list - * The {@link ListAdapter} to verify. - * @param isSyncAdapter - * true if the caller is a sync adapter, false otherwise. - */ - private void verifyCommon(ListAdapter list, boolean isSyncAdapter) - { - // row id can not be changed or set manually - if (list.isUpdated(ListAdapter._ID)) - { - throw new IllegalArgumentException("_ID can not be set manually"); - } - - if (isSyncAdapter) - { - // sync adapters may do all the stuff below - return; - } - - if (list.isUpdated(ListAdapter.LIST_COLOR)) - { - throw new IllegalArgumentException("Only sync adapters can change the LIST_COLOR."); - } - if (list.isUpdated(ListAdapter.LIST_NAME)) - { - throw new IllegalArgumentException("Only sync adapters can change the LIST_NAME."); - } - if (list.isUpdated(ListAdapter.SYNC_ID)) - { - throw new IllegalArgumentException("Only sync adapters can change the _SYNC_ID."); - } - if (list.isUpdated(ListAdapter.SYNC_VERSION)) - { - throw new IllegalArgumentException("Only sync adapters can change SYNC_VERSION."); - } - if (list.isUpdated(ListAdapter.OWNER)) - { - throw new IllegalArgumentException("Only sync adapters can change the list OWNER."); - } - } + @Override + public void beforeInsert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) + { + if (!isSyncAdapter) + { + throw new UnsupportedOperationException("Caller must be a sync adapter to create task lists"); + } + + if (TextUtils.isEmpty(list.valueOf(ListAdapter.ACCOUNT_NAME))) + { + throw new IllegalArgumentException("ACCOUNT_NAME is required on INSERT"); + } + + if (TextUtils.isEmpty(list.valueOf(ListAdapter.ACCOUNT_TYPE))) + { + throw new IllegalArgumentException("ACCOUNT_TYPE is required on INSERT"); + } + + verifyCommon(list, isSyncAdapter); + } + + + @Override + public void beforeUpdate(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) + { + if (list.isUpdated(ListAdapter.ACCOUNT_NAME)) + { + throw new IllegalArgumentException("ACCOUNT_NAME is write-once"); + } + + if (list.isUpdated(ListAdapter.ACCOUNT_TYPE)) + { + throw new IllegalArgumentException("ACCOUNT_TYPE is write-once"); + } + + verifyCommon(list, isSyncAdapter); + } + + + @Override + public void beforeDelete(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) + { + if (!isSyncAdapter) + { + throw new UnsupportedOperationException("Caller must be a sync adapter to delete task lists"); + } + } + + + /** + * Performs tests that are common to insert an update operations. + * + * @param list + * The {@link ListAdapter} to verify. + * @param isSyncAdapter + * true if the caller is a sync adapter, false otherwise. + */ + private void verifyCommon(ListAdapter list, boolean isSyncAdapter) + { + // row id can not be changed or set manually + if (list.isUpdated(ListAdapter._ID)) + { + throw new IllegalArgumentException("_ID can not be set manually"); + } + + if (isSyncAdapter) + { + // sync adapters may do all the stuff below + return; + } + + if (list.isUpdated(ListAdapter.LIST_COLOR)) + { + throw new IllegalArgumentException("Only sync adapters can change the LIST_COLOR."); + } + if (list.isUpdated(ListAdapter.LIST_NAME)) + { + throw new IllegalArgumentException("Only sync adapters can change the LIST_NAME."); + } + if (list.isUpdated(ListAdapter.SYNC_ID)) + { + throw new IllegalArgumentException("Only sync adapters can change the _SYNC_ID."); + } + if (list.isUpdated(ListAdapter.SYNC_VERSION)) + { + throw new IllegalArgumentException("Only sync adapters can change SYNC_VERSION."); + } + if (list.isUpdated(ListAdapter.OWNER)) + { + throw new IllegalArgumentException("Only sync adapters can change the list OWNER."); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoUpdateProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoUpdateProcessor.java index 18b16bd7..2e8bc9a8 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoUpdateProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoUpdateProcessor.java @@ -17,6 +17,10 @@ package org.dmfs.provider.tasks.processors.tasks; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskContract.Tasks; import org.dmfs.provider.tasks.TaskDatabaseHelper; @@ -25,196 +29,192 @@ import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; import org.dmfs.rfc5545.DateTime; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - /** * A processor to adjust some task values automatically. - *

+ *

* Other then recurrence exceptions no relations are handled by this code. Relation specific changes go to {@link RelationProcessor}. - * + * * @author Marten Gajda */ public class AutoUpdateProcessor extends AbstractEntityProcessor { - private static final String[] TASK_ID_PROJECTION = { Tasks._ID }; - private static final String[] TASK_SYNC_ID_PROJECTION = { Tasks._SYNC_ID }; - - private static final String SYNC_ID_SELECTION = Tasks._SYNC_ID + "=?"; - private static final String TASK_ID_SELECTION = Tasks._ID + "=?"; - - - @Override - public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - updateFields(db, task, isSyncAdapter); - - if (!isSyncAdapter) - { - // set created date for tasks created on the device - task.set(TaskAdapter.CREATED, new DateTime(System.currentTimeMillis())); - } - } - - - @Override - public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - if (isSyncAdapter && task.isRecurring()) - { - // task is recurring, update ORIGINAL_INSTANCE_ID of all exceptions that may already exists - ContentValues values = new ContentValues(1); - TaskAdapter.ORIGINAL_INSTANCE_ID.setIn(values, task.id()); - db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID + "=? and " - + TaskContract.Tasks.ORIGINAL_INSTANCE_ID + " is null", new String[] { task.valueOf(TaskAdapter.SYNC_ID) }); - } - } - - - @Override - public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - updateFields(db, task, isSyncAdapter); - } - - - @Override - public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - if (isSyncAdapter && task.isRecurring() && task.isUpdated(TaskAdapter.SYNC_ID)) - { - // task is recurring, update ORIGINAL_INSTANCE_SYNC_ID of all exceptions that may already exists - ContentValues values = new ContentValues(1); - TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID.setIn(values, task.valueOf(TaskAdapter.SYNC_ID)); - db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + task.id(), null); - } - } - - - private void updateFields(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - if (!isSyncAdapter) - { - task.set(TaskAdapter._DIRTY, true); - task.set(TaskAdapter.LAST_MODIFIED, new DateTime(System.currentTimeMillis())); - - // set proper STATUS if task has been completed - if (task.valueOf(TaskAdapter.COMPLETED) != null && !task.isUpdated(TaskAdapter.STATUS)) - { - task.set(TaskAdapter.STATUS, Tasks.STATUS_COMPLETED); - } - } - - if (task.isUpdated(TaskAdapter.PRIORITY)) - { - Integer priority = task.valueOf(TaskAdapter.PRIORITY); - if (priority != null && priority == 0) - { - // replace priority 0 by null, it's the default and we need that for proper sorting - task.set(TaskAdapter.PRIORITY, null); - } - } - - // Find corresponding ORIGINAL_INSTANCE_ID - if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID)) - { - String[] syncId = { task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) }; - Cursor cursor = db.query(Tables.TASKS, TASK_ID_PROJECTION, SYNC_ID_SELECTION, syncId, null, null, null); - try - { - if (cursor.moveToNext()) - { - Long originalId = cursor.getLong(0); - task.set(TaskAdapter.ORIGINAL_INSTANCE_ID, originalId); - } - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } - } - else if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID)) // Find corresponding ORIGINAL_INSTANCE_SYNC_ID - { - String[] id = { Long.toString(task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID)) }; - Cursor cursor = db.query(Tables.TASKS, TASK_SYNC_ID_PROJECTION, TASK_ID_SELECTION, id, null, null, null); - try - { - if (cursor.moveToNext()) - { - String originalSyncId = cursor.getString(0); - task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, originalSyncId); - } - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } - } - - // check that PERCENT_COMPLETE is an Integer between 0 and 100 if supplied also update status and completed accordingly - if (task.isUpdated(TaskAdapter.PERCENT_COMPLETE)) - { - Integer percent = task.valueOf(TaskAdapter.PERCENT_COMPLETE); - - if (!isSyncAdapter && percent != null && percent == 100) - { - if (!task.isUpdated(TaskAdapter.STATUS)) - { - task.set(TaskAdapter.STATUS, Tasks.STATUS_COMPLETED); - } - - if (!task.isUpdated(TaskAdapter.COMPLETED)) - { - task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis())); - } - } - else if (!isSyncAdapter && percent != null) - { - if (!task.isUpdated(TaskAdapter.COMPLETED)) - { - task.set(TaskAdapter.COMPLETED, null); - } - } - } - - // validate STATUS and set IS_NEW and IS_CLOSED accordingly - if (task.isUpdated(TaskAdapter.STATUS) || task.id() < 0 /* this is true when the task is new */) - { - Integer status = task.valueOf(TaskAdapter.STATUS); - if (status == null) - { - status = Tasks.STATUS_DEFAULT; - task.set(TaskAdapter.STATUS, status); - } - - task.set(TaskAdapter.IS_NEW, status == null || status == Tasks.STATUS_NEEDS_ACTION); - task.set(TaskAdapter.IS_CLOSED, status != null && (status == Tasks.STATUS_COMPLETED || status == Tasks.STATUS_CANCELLED)); + private static final String[] TASK_ID_PROJECTION = { Tasks._ID }; + private static final String[] TASK_SYNC_ID_PROJECTION = { Tasks._SYNC_ID }; + + private static final String SYNC_ID_SELECTION = Tasks._SYNC_ID + "=?"; + private static final String TASK_ID_SELECTION = Tasks._ID + "=?"; + + + @Override + public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + updateFields(db, task, isSyncAdapter); + + if (!isSyncAdapter) + { + // set created date for tasks created on the device + task.set(TaskAdapter.CREATED, new DateTime(System.currentTimeMillis())); + } + } + + + @Override + public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + if (isSyncAdapter && task.isRecurring()) + { + // task is recurring, update ORIGINAL_INSTANCE_ID of all exceptions that may already exists + ContentValues values = new ContentValues(1); + TaskAdapter.ORIGINAL_INSTANCE_ID.setIn(values, task.id()); + db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID + "=? and " + + TaskContract.Tasks.ORIGINAL_INSTANCE_ID + " is null", new String[] { task.valueOf(TaskAdapter.SYNC_ID) }); + } + } + + + @Override + public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + updateFields(db, task, isSyncAdapter); + } + + + @Override + public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + if (isSyncAdapter && task.isRecurring() && task.isUpdated(TaskAdapter.SYNC_ID)) + { + // task is recurring, update ORIGINAL_INSTANCE_SYNC_ID of all exceptions that may already exists + ContentValues values = new ContentValues(1); + TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID.setIn(values, task.valueOf(TaskAdapter.SYNC_ID)); + db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + task.id(), null); + } + } + + + private void updateFields(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + if (!isSyncAdapter) + { + task.set(TaskAdapter._DIRTY, true); + task.set(TaskAdapter.LAST_MODIFIED, new DateTime(System.currentTimeMillis())); + + // set proper STATUS if task has been completed + if (task.valueOf(TaskAdapter.COMPLETED) != null && !task.isUpdated(TaskAdapter.STATUS)) + { + task.set(TaskAdapter.STATUS, Tasks.STATUS_COMPLETED); + } + } + + if (task.isUpdated(TaskAdapter.PRIORITY)) + { + Integer priority = task.valueOf(TaskAdapter.PRIORITY); + if (priority != null && priority == 0) + { + // replace priority 0 by null, it's the default and we need that for proper sorting + task.set(TaskAdapter.PRIORITY, null); + } + } + + // Find corresponding ORIGINAL_INSTANCE_ID + if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID)) + { + String[] syncId = { task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) }; + Cursor cursor = db.query(Tables.TASKS, TASK_ID_PROJECTION, SYNC_ID_SELECTION, syncId, null, null, null); + try + { + if (cursor.moveToNext()) + { + Long originalId = cursor.getLong(0); + task.set(TaskAdapter.ORIGINAL_INSTANCE_ID, originalId); + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + } + else if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID)) // Find corresponding ORIGINAL_INSTANCE_SYNC_ID + { + String[] id = { Long.toString(task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID)) }; + Cursor cursor = db.query(Tables.TASKS, TASK_SYNC_ID_PROJECTION, TASK_ID_SELECTION, id, null, null, null); + try + { + if (cursor.moveToNext()) + { + String originalSyncId = cursor.getString(0); + task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, originalSyncId); + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + } + + // check that PERCENT_COMPLETE is an Integer between 0 and 100 if supplied also update status and completed accordingly + if (task.isUpdated(TaskAdapter.PERCENT_COMPLETE)) + { + Integer percent = task.valueOf(TaskAdapter.PERCENT_COMPLETE); + + if (!isSyncAdapter && percent != null && percent == 100) + { + if (!task.isUpdated(TaskAdapter.STATUS)) + { + task.set(TaskAdapter.STATUS, Tasks.STATUS_COMPLETED); + } + + if (!task.isUpdated(TaskAdapter.COMPLETED)) + { + task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis())); + } + } + else if (!isSyncAdapter && percent != null) + { + if (!task.isUpdated(TaskAdapter.COMPLETED)) + { + task.set(TaskAdapter.COMPLETED, null); + } + } + } + + // validate STATUS and set IS_NEW and IS_CLOSED accordingly + if (task.isUpdated(TaskAdapter.STATUS) || task.id() < 0 /* this is true when the task is new */) + { + Integer status = task.valueOf(TaskAdapter.STATUS); + if (status == null) + { + status = Tasks.STATUS_DEFAULT; + task.set(TaskAdapter.STATUS, status); + } + + task.set(TaskAdapter.IS_NEW, status == null || status == Tasks.STATUS_NEEDS_ACTION); + task.set(TaskAdapter.IS_CLOSED, status != null && (status == Tasks.STATUS_COMPLETED || status == Tasks.STATUS_CANCELLED)); /* - * Update PERCENT_COMPLETE and COMPLETED (if not given). Sync adapters should know what they're doing, so don't update anything if caller is a sync + * Update PERCENT_COMPLETE and COMPLETED (if not given). Sync adapters should know what they're doing, so don't update anything if caller is a sync * adapter. */ - if (status == Tasks.STATUS_COMPLETED && !isSyncAdapter) - { - task.set(TaskAdapter.PERCENT_COMPLETE, 100); - if (!task.isUpdated(TaskAdapter.COMPLETED)) - { - task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis())); - } - } - else if (!isSyncAdapter) - { - task.set(TaskAdapter.COMPLETED, null); - } - } - } + if (status == Tasks.STATUS_COMPLETED && !isSyncAdapter) + { + task.set(TaskAdapter.PERCENT_COMPLETE, 100); + if (!task.isUpdated(TaskAdapter.COMPLETED)) + { + task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis())); + } + } + else if (!isSyncAdapter) + { + task.set(TaskAdapter.COMPLETED, null); + } + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/ChangeListProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/ChangeListProcessor.java index ed94d6c8..5e111b02 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/ChangeListProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/ChangeListProcessor.java @@ -17,114 +17,114 @@ package org.dmfs.provider.tasks.processors.tasks; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - /** * This processor makes sure that changing the list a task belongs is properly handled by sync adapters. This is achieved by emulating an atomic copy & delete * operation. - *

+ *

* TODO: at present we only move recurrence exceptions based on the original row id. We should consider to move exceptions based on the original SYNC_ID as well * to support moving exception sets of tasks without known master instance. - * + * * @author Marten Gajda */ public class ChangeListProcessor extends AbstractEntityProcessor { - @Override - public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - if (isSyncAdapter) - { - // sync-adapters have to implement the move logic themselves - return; - } - - if (!task.isUpdated(TaskAdapter.LIST_ID)) - { - // list has not been changed - return; - } - - long oldList = task.oldValueOf(TaskAdapter.LIST_ID); - long newList = task.valueOf(TaskAdapter.LIST_ID); - - if (oldList == newList) - { - // list has not been changed - return; - } - - Long newMasterId; - Long deletedMasterId = null; - - if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) != null) - { - // this is an exception, move the master first - newMasterId = task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID); - if (newMasterId != null) - { - // find the master task - Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null, TaskContract.Tasks._ID + "=" + newMasterId, null, null, null, null); - try - { - if (c.moveToFirst()) - { - // move the master task - deletedMasterId = moveTask(db, new CursorContentValuesTaskAdapter(c, new ContentValues(16)), oldList, newList, null, true); - } - - } - finally - { - c.close(); - } - } - - // now move this exception, make sure we link the deleted exception to the deleted master - moveTask(db, task, oldList, newList, deletedMasterId, false); - } - else - { - newMasterId = task.id(); - // move the task to the new list - deletedMasterId = moveTask(db, task, oldList, newList, null, false); - } - - if (task.isRecurring() || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) - { - // This task is recurring and may have exceptions or it's an exception itself. Move all (other) exceptions to the new list. - Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null, TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + newMasterId + " and " - + TaskContract.Tasks._ID + "!=" + task.id(), null, null, null, null); - try - { - while (c.moveToNext()) - { - moveTask(db, new CursorContentValuesTaskAdapter(c, new ContentValues(16)), oldList, newList, deletedMasterId, true); - } - } - finally - { - c.close(); - } - } - - } - - - private Long moveTask(SQLiteDatabase db, TaskAdapter task, long oldList, long newList, Long deletedOriginalId, boolean commitTask) - { - /* - * The task has been moved to a different list. Sync adapters are not expected to support this (especially since the new list may belong to a completely + @Override + public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + if (isSyncAdapter) + { + // sync-adapters have to implement the move logic themselves + return; + } + + if (!task.isUpdated(TaskAdapter.LIST_ID)) + { + // list has not been changed + return; + } + + long oldList = task.oldValueOf(TaskAdapter.LIST_ID); + long newList = task.valueOf(TaskAdapter.LIST_ID); + + if (oldList == newList) + { + // list has not been changed + return; + } + + Long newMasterId; + Long deletedMasterId = null; + + if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) != null) + { + // this is an exception, move the master first + newMasterId = task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID); + if (newMasterId != null) + { + // find the master task + Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null, TaskContract.Tasks._ID + "=" + newMasterId, null, null, null, null); + try + { + if (c.moveToFirst()) + { + // move the master task + deletedMasterId = moveTask(db, new CursorContentValuesTaskAdapter(c, new ContentValues(16)), oldList, newList, null, true); + } + + } + finally + { + c.close(); + } + } + + // now move this exception, make sure we link the deleted exception to the deleted master + moveTask(db, task, oldList, newList, deletedMasterId, false); + } + else + { + newMasterId = task.id(); + // move the task to the new list + deletedMasterId = moveTask(db, task, oldList, newList, null, false); + } + + if (task.isRecurring() || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) + { + // This task is recurring and may have exceptions or it's an exception itself. Move all (other) exceptions to the new list. + Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null, TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + newMasterId + " and " + + TaskContract.Tasks._ID + "!=" + task.id(), null, null, null, null); + try + { + while (c.moveToNext()) + { + moveTask(db, new CursorContentValuesTaskAdapter(c, new ContentValues(16)), oldList, newList, deletedMasterId, true); + } + } + finally + { + c.close(); + } + } + + } + + + private Long moveTask(SQLiteDatabase db, TaskAdapter task, long oldList, long newList, Long deletedOriginalId, boolean commitTask) + { + /* + * The task has been moved to a different list. Sync adapters are not expected to support this (especially since the new list may belong to a completely * different account or even account-type), so we emulate a copy & delete operation. * * All sync adapter fields of the task are cleared, so it looks like a new task. In addition we create a new deleted task in the old list having the old @@ -132,52 +132,52 @@ public class ChangeListProcessor extends AbstractEntityProcessor * handle that correctly. */ - Long result = null; - - // create a deleted task for the old one, unless the task has not been synced yet (which is always true for tasks in the local account) - if (task.valueOf(TaskAdapter.SYNC_ID) != null || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) != null - || task.valueOf(TaskAdapter.SYNC_VERSION) != null) - { - TaskAdapter deletedTask = task.duplicate(); - deletedTask.set(TaskAdapter.LIST_ID, oldList); - deletedTask.set(TaskAdapter.ORIGINAL_INSTANCE_ID, deletedOriginalId); - deletedTask.set(TaskAdapter._DELETED, true); - - // make sure we unset any values that do not exist in the tasks table - deletedTask.unset(TaskAdapter.LIST_COLOR); - deletedTask.unset(TaskAdapter.LIST_NAME); - deletedTask.unset(TaskAdapter.ACCOUNT_NAME); - deletedTask.unset(TaskAdapter.ACCOUNT_TYPE); - deletedTask.unset(TaskAdapter.LIST_OWNER); - deletedTask.unset(TaskAdapter.LIST_ACCESS_LEVEL); - deletedTask.unset(TaskAdapter.LIST_VISIBLE); - - // create the deleted task - deletedTask.commit(db); - - result = deletedTask.id(); - } - - // clear all sync fields to convert the existing task to a new task - task.set(TaskAdapter.LIST_ID, newList); - task.set(TaskAdapter._DIRTY, true); - task.set(TaskAdapter.SYNC1, null); - task.set(TaskAdapter.SYNC2, null); - task.set(TaskAdapter.SYNC3, null); - task.set(TaskAdapter.SYNC4, null); - task.set(TaskAdapter.SYNC5, null); - task.set(TaskAdapter.SYNC6, null); - task.set(TaskAdapter.SYNC7, null); - task.set(TaskAdapter.SYNC8, null); - task.set(TaskAdapter.SYNC_ID, null); - task.set(TaskAdapter.SYNC_VERSION, null); - task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, null); - if (commitTask) - { - task.commit(db); - } - - return result; - } + Long result = null; + + // create a deleted task for the old one, unless the task has not been synced yet (which is always true for tasks in the local account) + if (task.valueOf(TaskAdapter.SYNC_ID) != null || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) != null + || task.valueOf(TaskAdapter.SYNC_VERSION) != null) + { + TaskAdapter deletedTask = task.duplicate(); + deletedTask.set(TaskAdapter.LIST_ID, oldList); + deletedTask.set(TaskAdapter.ORIGINAL_INSTANCE_ID, deletedOriginalId); + deletedTask.set(TaskAdapter._DELETED, true); + + // make sure we unset any values that do not exist in the tasks table + deletedTask.unset(TaskAdapter.LIST_COLOR); + deletedTask.unset(TaskAdapter.LIST_NAME); + deletedTask.unset(TaskAdapter.ACCOUNT_NAME); + deletedTask.unset(TaskAdapter.ACCOUNT_TYPE); + deletedTask.unset(TaskAdapter.LIST_OWNER); + deletedTask.unset(TaskAdapter.LIST_ACCESS_LEVEL); + deletedTask.unset(TaskAdapter.LIST_VISIBLE); + + // create the deleted task + deletedTask.commit(db); + + result = deletedTask.id(); + } + + // clear all sync fields to convert the existing task to a new task + task.set(TaskAdapter.LIST_ID, newList); + task.set(TaskAdapter._DIRTY, true); + task.set(TaskAdapter.SYNC1, null); + task.set(TaskAdapter.SYNC2, null); + task.set(TaskAdapter.SYNC3, null); + task.set(TaskAdapter.SYNC4, null); + task.set(TaskAdapter.SYNC5, null); + task.set(TaskAdapter.SYNC6, null); + task.set(TaskAdapter.SYNC7, null); + task.set(TaskAdapter.SYNC8, null); + task.set(TaskAdapter.SYNC_ID, null); + task.set(TaskAdapter.SYNC_VERSION, null); + task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, null); + if (commitTask) + { + task.commit(db); + } + + return result; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/FtsProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/FtsProcessor.java index ca09d672..121efa63 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/FtsProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/FtsProcessor.java @@ -17,31 +17,31 @@ package org.dmfs.provider.tasks.processors.tasks; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.FTSDatabaseHelper; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; -import android.database.sqlite.SQLiteDatabase; - /** * A {@link TaskProcessor} to update the fast text search table when inserting or updating a task. - * + * * @author Marten Gajda */ public class FtsProcessor extends AbstractEntityProcessor { - @Override - public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - FTSDatabaseHelper.updateTaskFTSEntries(db, task); - } + @Override + public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + FTSDatabaseHelper.updateTaskFTSEntries(db, task); + } - @Override - public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - FTSDatabaseHelper.updateTaskFTSEntries(db, task); - } + @Override + public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + FTSDatabaseHelper.updateTaskFTSEntries(db, task); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/RelationProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/RelationProcessor.java index bfcd0139..8445fffb 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/RelationProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/RelationProcessor.java @@ -17,15 +17,15 @@ package org.dmfs.provider.tasks.processors.tasks; +import android.content.ContentValues; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract.Property.Relation; import org.dmfs.provider.tasks.TaskContract.Tasks; import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; -import android.content.ContentValues; -import android.database.sqlite.SQLiteDatabase; - /** * A processor that updates relations for new tasks. @@ -37,70 +37,71 @@ import android.database.sqlite.SQLiteDatabase; * It also updates {@link Relation#RELATED_UID} when a tasks is synced the first time and a UID has been set. *

* TODO: update {@link Tasks#PARENT_ID} of related tasks. - * + * * @author Marten Gajda */ public class RelationProcessor extends AbstractEntityProcessor { - @Override - public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - // A new task has been inserted by the sync adapter. Update all relations that point to this task. - - if (!isSyncAdapter) - { - // the task was created on the device, so it doesn't have a UID - return; - } - - String uid = task.valueOf(TaskAdapter._UID); - - if (uid != null) - { - ContentValues v = new ContentValues(1); - v.put(Relation.RELATED_ID, task.id()); - - db.update(TaskDatabaseHelper.Tables.PROPERTIES, v, Relation.MIMETYPE + "= ? AND " + Relation.RELATED_UID + "=?", new String[] { - Relation.CONTENT_ITEM_TYPE, uid }); - } - } - - - @Override - public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - // A task has been updated and may have received a UID by the sync adapter. Update all by-id references to this task. - - if (!isSyncAdapter) - { - // only sync adapters may assign a UID - return; - } - - String uid = task.valueOf(TaskAdapter._UID); - - if (uid != null) - { - ContentValues v = new ContentValues(1); - v.put(Relation.RELATED_UID, uid); - - db.update(TaskDatabaseHelper.Tables.PROPERTIES, v, Relation.MIMETYPE + "= ? AND " + Relation.RELATED_ID + "=?", new String[] { - Relation.CONTENT_ITEM_TYPE, Long.toString(task.id()) }); - } - } - - - @Override - public void afterDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - if (!isSyncAdapter) - { - // remove once the deletion is final, which is when the sync adapter removes it - return; - } - - db.delete(TaskDatabaseHelper.Tables.PROPERTIES, Relation.MIMETYPE + "= ? AND " + Relation.RELATED_ID + "=?", new String[] { Relation.CONTENT_ITEM_TYPE, - Long.toString(task.id()) }); - } + @Override + public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + // A new task has been inserted by the sync adapter. Update all relations that point to this task. + + if (!isSyncAdapter) + { + // the task was created on the device, so it doesn't have a UID + return; + } + + String uid = task.valueOf(TaskAdapter._UID); + + if (uid != null) + { + ContentValues v = new ContentValues(1); + v.put(Relation.RELATED_ID, task.id()); + + db.update(TaskDatabaseHelper.Tables.PROPERTIES, v, Relation.MIMETYPE + "= ? AND " + Relation.RELATED_UID + "=?", new String[] { + Relation.CONTENT_ITEM_TYPE, uid }); + } + } + + + @Override + public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + // A task has been updated and may have received a UID by the sync adapter. Update all by-id references to this task. + + if (!isSyncAdapter) + { + // only sync adapters may assign a UID + return; + } + + String uid = task.valueOf(TaskAdapter._UID); + + if (uid != null) + { + ContentValues v = new ContentValues(1); + v.put(Relation.RELATED_UID, uid); + + db.update(TaskDatabaseHelper.Tables.PROPERTIES, v, Relation.MIMETYPE + "= ? AND " + Relation.RELATED_ID + "=?", new String[] { + Relation.CONTENT_ITEM_TYPE, Long.toString(task.id()) }); + } + } + + + @Override + public void afterDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + if (!isSyncAdapter) + { + // remove once the deletion is final, which is when the sync adapter removes it + return; + } + + db.delete(TaskDatabaseHelper.Tables.PROPERTIES, Relation.MIMETYPE + "= ? AND " + Relation.RELATED_ID + "=?", new String[] { + Relation.CONTENT_ITEM_TYPE, + Long.toString(task.id()) }); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskExecutionProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskExecutionProcessor.java index 70440174..9e47b2ff 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskExecutionProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskExecutionProcessor.java @@ -17,52 +17,52 @@ package org.dmfs.provider.tasks.processors.tasks; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskContract.TaskColumns; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; -import android.database.sqlite.SQLiteDatabase; - /** * A processor that perfomrs the actual operations on tasks. - * + * * @author Marten Gajda */ public class TaskExecutionProcessor extends AbstractEntityProcessor { - @Override - public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - task.commit(db); - } + @Override + public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + task.commit(db); + } - @Override - public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - task.commit(db); - } + @Override + public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + task.commit(db); + } - @Override - public void beforeDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - String accountType = task.valueOf(TaskAdapter.ACCOUNT_TYPE); + @Override + public void beforeDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + String accountType = task.valueOf(TaskAdapter.ACCOUNT_TYPE); - if (isSyncAdapter || TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType)) - { - // this is a local task or it' removed by a sync adapter, in either case we delete it right away - db.delete(Tables.TASKS, TaskColumns._ID + "=" + task.id(), null); - } - else - { - // just set the deleted flag otherwise - task.set(TaskAdapter._DELETED, true); - task.commit(db); - } - } + if (isSyncAdapter || TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType)) + { + // this is a local task or it' removed by a sync adapter, in either case we delete it right away + db.delete(Tables.TASKS, TaskColumns._ID + "=" + task.id(), null); + } + else + { + // just set the deleted flag otherwise + task.set(TaskAdapter._DELETED, true); + task.commit(db); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskInstancesProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskInstancesProcessor.java index b8a23600..7f1e5bfe 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskInstancesProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskInstancesProcessor.java @@ -17,8 +17,9 @@ package org.dmfs.provider.tasks.processors.tasks; -import java.sql.RowId; -import java.util.TimeZone; +import android.content.ContentValues; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskContract.Instances; @@ -29,173 +30,173 @@ import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; import org.dmfs.rfc5545.DateTime; import org.dmfs.rfc5545.Duration; -import android.content.ContentValues; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; +import java.sql.RowId; +import java.util.TimeZone; /** * A processor that creates or updates any instance values for a task. *

* TODO: At present this does not support recurrence. - * + * * @author Marten Gajda */ public class TaskInstancesProcessor extends AbstractEntityProcessor { - /** - * This is a field adapter for a pseudo column to indicate that the instances may need an update, even if no relevant value has changed. This is useful to - * force an update of the sorting values when the local timezone has been changed. - */ - private final static BooleanFieldAdapter UPDATE_REQUESTED = new BooleanFieldAdapter( - "org.dmfs.tasks.TaskInstanceProcessor.UPDATE_REQUESTED"); - - - /** - * Add a pseudo column to the given {@link ContentValues} to request an instances update, even if no time value has changed. - * - * @param values - * The {@link ContentValues} to add the pseudo column to. - */ - public static void addUpdateRequest(ContentValues values) - { - UPDATE_REQUESTED.setIn(values, true); - } - - - @Override - public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - createInstances(db, task); - } - - - @Override - public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - // move the UPDATE requested value to the state - if (task.isUpdated(UPDATE_REQUESTED)) - { - task.setState(UPDATE_REQUESTED, task.valueOf(UPDATE_REQUESTED)); - task.unset(UPDATE_REQUESTED); - } - } - - - @Override - public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - if (!task.isUpdated(TaskAdapter.DTSTART) && !task.isUpdated(TaskAdapter.DUE) && !task.isUpdated(TaskAdapter.DURATION) - && !task.getState(UPDATE_REQUESTED)) - { - // date values didn't change and update not requested - return; - } - updateInstances(db, task); - } - - - /** - * Create new {@link ContentValues} for insertion into the instances table. - * - * @param task - * The {@link TaskAdapter} of the task that's about to be inserted. - * @return {@link ContentValues} of the instance of this task. - */ - private ContentValues generateInstanceValues(TaskAdapter task) - { - ContentValues instanceValues = new ContentValues(); - - // get the relevant values from values - DateTime dtstart = task.valueOf(TaskAdapter.DTSTART); - DateTime due = task.valueOf(TaskAdapter.DUE); - Duration duration = task.valueOf(TaskAdapter.DURATION); - - TimeZone localTz = TimeZone.getDefault(); - - if (dtstart != null) - { - // copy dtstart as is - instanceValues.put(Instances.INSTANCE_START, dtstart.getTimestamp()); - instanceValues.put(Instances.INSTANCE_START_SORTING, dtstart.isAllDay() ? dtstart.getInstance() : dtstart.shiftTimeZone(localTz).getInstance()); - } - else - { - instanceValues.putNull(Instances.INSTANCE_START); - instanceValues.putNull(Instances.INSTANCE_START_SORTING); - } - - if (due != null) - { - // copy due and calculate the actual duration, if any - instanceValues.put(Instances.INSTANCE_DUE, due.getTimestamp()); - instanceValues.put(Instances.INSTANCE_DUE_SORTING, due.isAllDay() ? due.getInstance() : due.shiftTimeZone(localTz).getInstance()); - if (dtstart != null) - { - instanceValues.put(Instances.INSTANCE_DURATION, due.getTimestamp() - dtstart.getTimestamp()); - } - else - { - instanceValues.putNull(Instances.INSTANCE_DURATION); - } - } - else if (duration != null) - { - if (dtstart != null) - { - // calculate the actual due value from dtstart and the duration string - due = dtstart.addDuration(duration); - instanceValues.put(Instances.INSTANCE_DUE, due.getTimestamp()); - instanceValues.put(Instances.INSTANCE_DUE_SORTING, due.isAllDay() ? due.getInstance() : due.shiftTimeZone(localTz).getInstance()); - instanceValues.put(Instances.INSTANCE_DURATION, due.getTimestamp() - dtstart.getTimestamp()); - } - else - { - // this case should be filtered by TaskValidatorProcessor, since setting a DURATION without DTSTART is invalid - instanceValues.putNull(Instances.INSTANCE_DURATION); - instanceValues.putNull(Instances.INSTANCE_DUE); - instanceValues.putNull(Instances.INSTANCE_DUE_SORTING); - } - } - else - { - instanceValues.putNull(Instances.INSTANCE_DURATION); - instanceValues.putNull(Instances.INSTANCE_DUE); - instanceValues.putNull(Instances.INSTANCE_DUE_SORTING); - } - return instanceValues; - } - - - /** - * Creates new instances for the given task {@link ContentValues}. - *

- * TODO: expand recurrence - *

- * - * @param uri - * The {@link Uri} used when inserting the task. - * @param values - * The {@link ContentValues} of the task. - * @param rowId - * The new {@link RowId} of the task. - */ - private void createInstances(SQLiteDatabase db, TaskAdapter task) - { - ContentValues instanceValues = generateInstanceValues(task); - - // set rowID of current Task - instanceValues.put(Instances.TASK_ID, task.id()); - - db.insert(Tables.INSTANCES, null, instanceValues); - } - - - private void updateInstances(SQLiteDatabase db, TaskAdapter task) - { - ContentValues instanceValues = generateInstanceValues(task); - - db.update(Tables.INSTANCES, instanceValues, TaskContract.Instances.TASK_ID + " = " + task.id(), null); - } + /** + * This is a field adapter for a pseudo column to indicate that the instances may need an update, even if no relevant value has changed. This is useful to + * force an update of the sorting values when the local timezone has been changed. + */ + private final static BooleanFieldAdapter UPDATE_REQUESTED = new BooleanFieldAdapter( + "org.dmfs.tasks.TaskInstanceProcessor.UPDATE_REQUESTED"); + + + /** + * Add a pseudo column to the given {@link ContentValues} to request an instances update, even if no time value has changed. + * + * @param values + * The {@link ContentValues} to add the pseudo column to. + */ + public static void addUpdateRequest(ContentValues values) + { + UPDATE_REQUESTED.setIn(values, true); + } + + + @Override + public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + createInstances(db, task); + } + + + @Override + public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + // move the UPDATE requested value to the state + if (task.isUpdated(UPDATE_REQUESTED)) + { + task.setState(UPDATE_REQUESTED, task.valueOf(UPDATE_REQUESTED)); + task.unset(UPDATE_REQUESTED); + } + } + + + @Override + public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + if (!task.isUpdated(TaskAdapter.DTSTART) && !task.isUpdated(TaskAdapter.DUE) && !task.isUpdated(TaskAdapter.DURATION) + && !task.getState(UPDATE_REQUESTED)) + { + // date values didn't change and update not requested + return; + } + updateInstances(db, task); + } + + + /** + * Create new {@link ContentValues} for insertion into the instances table. + * + * @param task + * The {@link TaskAdapter} of the task that's about to be inserted. + * + * @return {@link ContentValues} of the instance of this task. + */ + private ContentValues generateInstanceValues(TaskAdapter task) + { + ContentValues instanceValues = new ContentValues(); + + // get the relevant values from values + DateTime dtstart = task.valueOf(TaskAdapter.DTSTART); + DateTime due = task.valueOf(TaskAdapter.DUE); + Duration duration = task.valueOf(TaskAdapter.DURATION); + + TimeZone localTz = TimeZone.getDefault(); + + if (dtstart != null) + { + // copy dtstart as is + instanceValues.put(Instances.INSTANCE_START, dtstart.getTimestamp()); + instanceValues.put(Instances.INSTANCE_START_SORTING, dtstart.isAllDay() ? dtstart.getInstance() : dtstart.shiftTimeZone(localTz).getInstance()); + } + else + { + instanceValues.putNull(Instances.INSTANCE_START); + instanceValues.putNull(Instances.INSTANCE_START_SORTING); + } + + if (due != null) + { + // copy due and calculate the actual duration, if any + instanceValues.put(Instances.INSTANCE_DUE, due.getTimestamp()); + instanceValues.put(Instances.INSTANCE_DUE_SORTING, due.isAllDay() ? due.getInstance() : due.shiftTimeZone(localTz).getInstance()); + if (dtstart != null) + { + instanceValues.put(Instances.INSTANCE_DURATION, due.getTimestamp() - dtstart.getTimestamp()); + } + else + { + instanceValues.putNull(Instances.INSTANCE_DURATION); + } + } + else if (duration != null) + { + if (dtstart != null) + { + // calculate the actual due value from dtstart and the duration string + due = dtstart.addDuration(duration); + instanceValues.put(Instances.INSTANCE_DUE, due.getTimestamp()); + instanceValues.put(Instances.INSTANCE_DUE_SORTING, due.isAllDay() ? due.getInstance() : due.shiftTimeZone(localTz).getInstance()); + instanceValues.put(Instances.INSTANCE_DURATION, due.getTimestamp() - dtstart.getTimestamp()); + } + else + { + // this case should be filtered by TaskValidatorProcessor, since setting a DURATION without DTSTART is invalid + instanceValues.putNull(Instances.INSTANCE_DURATION); + instanceValues.putNull(Instances.INSTANCE_DUE); + instanceValues.putNull(Instances.INSTANCE_DUE_SORTING); + } + } + else + { + instanceValues.putNull(Instances.INSTANCE_DURATION); + instanceValues.putNull(Instances.INSTANCE_DUE); + instanceValues.putNull(Instances.INSTANCE_DUE_SORTING); + } + return instanceValues; + } + + + /** + * Creates new instances for the given task {@link ContentValues}. + *

+ * TODO: expand recurrence + *

+ * + * @param uri + * The {@link Uri} used when inserting the task. + * @param values + * The {@link ContentValues} of the task. + * @param rowId + * The new {@link RowId} of the task. + */ + private void createInstances(SQLiteDatabase db, TaskAdapter task) + { + ContentValues instanceValues = generateInstanceValues(task); + + // set rowID of current Task + instanceValues.put(Instances.TASK_ID, task.id()); + + db.insert(Tables.INSTANCES, null, instanceValues); + } + + + private void updateInstances(SQLiteDatabase db, TaskAdapter task) + { + ContentValues instanceValues = generateInstanceValues(task); + + db.update(Tables.INSTANCES, instanceValues, TaskContract.Instances.TASK_ID + " = " + task.id(), null); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskValidatorProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskValidatorProcessor.java index 89db4c7d..bb8f2f07 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskValidatorProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskValidatorProcessor.java @@ -17,6 +17,9 @@ package org.dmfs.provider.tasks.processors.tasks; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + import org.dmfs.provider.tasks.TaskContract.TaskLists; import org.dmfs.provider.tasks.TaskContract.Tasks; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; @@ -24,236 +27,233 @@ import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; import org.dmfs.rfc5545.Duration; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - /** * A processor that validates the values of a task. - * + * * @author Marten Gajda */ public class TaskValidatorProcessor extends AbstractEntityProcessor { - private static final String[] TASKLIST_ID_PROJECTION = { TaskLists._ID }; - private static final String TASKLISTS_ID_SELECTION = TaskLists._ID + "="; - - - @Override - public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - verifyCommon(task, isSyncAdapter); - - // LIST_ID must be present and refer to an existing TaskList row id - Long listId = task.valueOf(TaskAdapter.LIST_ID); - if (listId == null) - { - throw new IllegalArgumentException("LIST_ID is required on INSERT"); - } - - // TODO: get rid of this query and use a cache instead - // TODO: ensure that the list is writable unless the caller is a sync adapter - Cursor cursor = db.query(Tables.LISTS, TASKLIST_ID_PROJECTION, TASKLISTS_ID_SELECTION + listId, null, null, null, null); - try - { - if (cursor == null || cursor.getCount() != 1) - { - throw new IllegalArgumentException("LIST_ID must refer to an existing TaskList"); - } - } - finally - { - if (cursor != null) - { - cursor.close(); - } - } - - } - - - @Override - public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - verifyCommon(task, isSyncAdapter); - - // only sync adapters can modify original sync id and original instance id of an existing task - if (!isSyncAdapter && (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID) || task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID))) - { - throw new IllegalArgumentException("ORIGINAL_INSTANCE_SYNC_ID and ORIGINAL_INSTANCE_ID can be modified by sync adapters only"); - } - } - - - /** - * Performs tests that are common to insert an update operations. - * - * @param task - * The {@link TaskAdapter} to verify. - * @param isSyncAdapter - * true if the caller is a sync adapter, false otherwise. - */ - private void verifyCommon(TaskAdapter task, boolean isSyncAdapter) - { - // row id can not be changed or set manually - if (task.isUpdated(TaskAdapter._ID)) - { - throw new IllegalArgumentException("_ID can not be set manually"); - } - - // account name can not be set on a tasks - if (task.isUpdated(TaskAdapter.ACCOUNT_NAME)) - { - throw new IllegalArgumentException("ACCOUNT_NAME can not be set on a tasks"); - } - - // account type can not be set on a tasks - if (task.isUpdated(TaskAdapter.ACCOUNT_TYPE)) - { - throw new IllegalArgumentException("ACCOUNT_TYPE can not be set on a tasks"); - } - - // list color is read only for tasks - if (task.isUpdated(TaskAdapter.LIST_COLOR)) - { - throw new IllegalArgumentException("LIST_COLOR can not be set on a tasks"); - } - - // no one can undelete a task! - if (task.isUpdated(TaskAdapter._DELETED)) - { - throw new IllegalArgumentException("modification of _DELETE is not allowed"); - } - - // only sync adapters are allowed to change the UID - if (!isSyncAdapter && task.isUpdated(TaskAdapter._UID)) - { - throw new IllegalArgumentException("modification of _UID is not allowed"); - } - - // only sync adapters are allowed to remove the dirty flag - if (!isSyncAdapter && task.isUpdated(TaskAdapter._DIRTY)) - { - throw new IllegalArgumentException("modification of _DIRTY is not allowed"); - } - - // only sync adapters are allowed to set creation time - if (!isSyncAdapter && task.isUpdated(TaskAdapter.CREATED)) - { - throw new IllegalArgumentException("modification of CREATED is not allowed"); - } - - // IS_NEW is set automatically - if (task.isUpdated(TaskAdapter.IS_NEW)) - { - throw new IllegalArgumentException("modification of IS_NEW is not allowed"); - } - - // IS_CLOSED is set automatically - if (task.isUpdated(TaskAdapter.IS_CLOSED)) - { - throw new IllegalArgumentException("modification of IS_CLOSED is not allowed"); - } - - // HAS_PROPERTIES is set automatically - if (task.isUpdated(TaskAdapter.HAS_PROPERTIES)) - { - throw new IllegalArgumentException("modification of HAS_PROPERTIES is not allowed"); - } - - // HAS_ALARMS is set automatically - if (task.isUpdated(TaskAdapter.HAS_ALARMS)) - { - throw new IllegalArgumentException("modification of HAS_ALARMS is not allowed"); - } - - // only sync adapters are allowed to set modification time - if (!isSyncAdapter && task.isUpdated(TaskAdapter.LAST_MODIFIED)) - { - throw new IllegalArgumentException("modification of MODIFICATION_TIME is not allowed"); - } - - if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) && task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID)) - { - throw new IllegalArgumentException("ORIGINAL_INSTANCE_SYNC_ID and ORIGINAL_INSTANCE_ID must not be specified at the same time"); - } - - // check that CLASSIFICATION is an Integer between 0 and 2 if given - if (task.isUpdated(TaskAdapter.CLASSIFICATION)) - { - Integer classification = task.valueOf(TaskAdapter.CLASSIFICATION); - if (classification != null && (classification < 0 || classification > 2)) - { - throw new IllegalArgumentException("CLASSIFICATION must be an integer between 0 and 2"); - } - } - - // check that PRIORITY is an Integer between 0 and 9 if given - if (task.isUpdated(TaskAdapter.PRIORITY)) - { - Integer priority = task.valueOf(TaskAdapter.PRIORITY); - if (priority != null && (priority < 0 || priority > 9)) - { - throw new IllegalArgumentException("PRIORITY must be an integer between 0 and 9"); - } - } - - // check that PERCENT_COMPLETE is an Integer between 0 and 100 - if (task.isUpdated(TaskAdapter.PERCENT_COMPLETE)) - { - Integer percent = task.valueOf(TaskAdapter.PERCENT_COMPLETE); - if (percent != null && (percent < 0 || percent > 100)) - { - throw new IllegalArgumentException("PERCENT_COMPLETE must be null or an integer between 0 and 100"); - } - } - - // validate STATUS - if (task.isUpdated(TaskAdapter.STATUS)) - { - Integer status = task.valueOf(TaskAdapter.STATUS); - if (status != null && (status < Tasks.STATUS_NEEDS_ACTION || status > Tasks.STATUS_CANCELLED)) - { - throw new IllegalArgumentException("invalid STATUS: " + status); - } - } - - // ensure that DUE and DURATION are set properly if DTSTART is given - Long dtStart = task.valueOf(TaskAdapter.DTSTART_RAW); - Long due = task.valueOf(TaskAdapter.DUE_RAW); - Duration duration = task.valueOf(TaskAdapter.DURATION); - - if (dtStart != null) - { - if (due != null && duration != null) - { - throw new IllegalArgumentException("Only one of DUE or DURATION must be supplied."); - } - else if (due != null) - { - if (due < dtStart) - { - throw new IllegalArgumentException("DUE must not be < DTSTART"); - } - } - else if (duration != null) - { - if (duration.getSign() == -1) - { - throw new IllegalArgumentException("DURATION must not be negative"); - } - } - } - else if (duration != null) - { - throw new IllegalArgumentException("DURATION must not be supplied without DTSTART"); - } - - // if one of DTSTART or DUE is given, TZ must not be null unless it's an all-day task - if ((dtStart != null || due != null) && !task.valueOf(TaskAdapter.IS_ALLDAY) && task.valueOf(TaskAdapter.TIMEZONE_RAW) == null) - { - throw new IllegalArgumentException("TIMEZONE must be supplied if one of DTSTART or DUE is not null and not all-day"); - } - } + private static final String[] TASKLIST_ID_PROJECTION = { TaskLists._ID }; + private static final String TASKLISTS_ID_SELECTION = TaskLists._ID + "="; + + + @Override + public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + verifyCommon(task, isSyncAdapter); + + // LIST_ID must be present and refer to an existing TaskList row id + Long listId = task.valueOf(TaskAdapter.LIST_ID); + if (listId == null) + { + throw new IllegalArgumentException("LIST_ID is required on INSERT"); + } + + // TODO: get rid of this query and use a cache instead + // TODO: ensure that the list is writable unless the caller is a sync adapter + Cursor cursor = db.query(Tables.LISTS, TASKLIST_ID_PROJECTION, TASKLISTS_ID_SELECTION + listId, null, null, null, null); + try + { + if (cursor == null || cursor.getCount() != 1) + { + throw new IllegalArgumentException("LIST_ID must refer to an existing TaskList"); + } + } + finally + { + if (cursor != null) + { + cursor.close(); + } + } + + } + + + @Override + public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + verifyCommon(task, isSyncAdapter); + + // only sync adapters can modify original sync id and original instance id of an existing task + if (!isSyncAdapter && (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID) || task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID))) + { + throw new IllegalArgumentException("ORIGINAL_INSTANCE_SYNC_ID and ORIGINAL_INSTANCE_ID can be modified by sync adapters only"); + } + } + + + /** + * Performs tests that are common to insert an update operations. + * + * @param task + * The {@link TaskAdapter} to verify. + * @param isSyncAdapter + * true if the caller is a sync adapter, false otherwise. + */ + private void verifyCommon(TaskAdapter task, boolean isSyncAdapter) + { + // row id can not be changed or set manually + if (task.isUpdated(TaskAdapter._ID)) + { + throw new IllegalArgumentException("_ID can not be set manually"); + } + + // account name can not be set on a tasks + if (task.isUpdated(TaskAdapter.ACCOUNT_NAME)) + { + throw new IllegalArgumentException("ACCOUNT_NAME can not be set on a tasks"); + } + + // account type can not be set on a tasks + if (task.isUpdated(TaskAdapter.ACCOUNT_TYPE)) + { + throw new IllegalArgumentException("ACCOUNT_TYPE can not be set on a tasks"); + } + + // list color is read only for tasks + if (task.isUpdated(TaskAdapter.LIST_COLOR)) + { + throw new IllegalArgumentException("LIST_COLOR can not be set on a tasks"); + } + + // no one can undelete a task! + if (task.isUpdated(TaskAdapter._DELETED)) + { + throw new IllegalArgumentException("modification of _DELETE is not allowed"); + } + + // only sync adapters are allowed to change the UID + if (!isSyncAdapter && task.isUpdated(TaskAdapter._UID)) + { + throw new IllegalArgumentException("modification of _UID is not allowed"); + } + + // only sync adapters are allowed to remove the dirty flag + if (!isSyncAdapter && task.isUpdated(TaskAdapter._DIRTY)) + { + throw new IllegalArgumentException("modification of _DIRTY is not allowed"); + } + + // only sync adapters are allowed to set creation time + if (!isSyncAdapter && task.isUpdated(TaskAdapter.CREATED)) + { + throw new IllegalArgumentException("modification of CREATED is not allowed"); + } + + // IS_NEW is set automatically + if (task.isUpdated(TaskAdapter.IS_NEW)) + { + throw new IllegalArgumentException("modification of IS_NEW is not allowed"); + } + + // IS_CLOSED is set automatically + if (task.isUpdated(TaskAdapter.IS_CLOSED)) + { + throw new IllegalArgumentException("modification of IS_CLOSED is not allowed"); + } + + // HAS_PROPERTIES is set automatically + if (task.isUpdated(TaskAdapter.HAS_PROPERTIES)) + { + throw new IllegalArgumentException("modification of HAS_PROPERTIES is not allowed"); + } + + // HAS_ALARMS is set automatically + if (task.isUpdated(TaskAdapter.HAS_ALARMS)) + { + throw new IllegalArgumentException("modification of HAS_ALARMS is not allowed"); + } + + // only sync adapters are allowed to set modification time + if (!isSyncAdapter && task.isUpdated(TaskAdapter.LAST_MODIFIED)) + { + throw new IllegalArgumentException("modification of MODIFICATION_TIME is not allowed"); + } + + if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) && task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID)) + { + throw new IllegalArgumentException("ORIGINAL_INSTANCE_SYNC_ID and ORIGINAL_INSTANCE_ID must not be specified at the same time"); + } + + // check that CLASSIFICATION is an Integer between 0 and 2 if given + if (task.isUpdated(TaskAdapter.CLASSIFICATION)) + { + Integer classification = task.valueOf(TaskAdapter.CLASSIFICATION); + if (classification != null && (classification < 0 || classification > 2)) + { + throw new IllegalArgumentException("CLASSIFICATION must be an integer between 0 and 2"); + } + } + + // check that PRIORITY is an Integer between 0 and 9 if given + if (task.isUpdated(TaskAdapter.PRIORITY)) + { + Integer priority = task.valueOf(TaskAdapter.PRIORITY); + if (priority != null && (priority < 0 || priority > 9)) + { + throw new IllegalArgumentException("PRIORITY must be an integer between 0 and 9"); + } + } + + // check that PERCENT_COMPLETE is an Integer between 0 and 100 + if (task.isUpdated(TaskAdapter.PERCENT_COMPLETE)) + { + Integer percent = task.valueOf(TaskAdapter.PERCENT_COMPLETE); + if (percent != null && (percent < 0 || percent > 100)) + { + throw new IllegalArgumentException("PERCENT_COMPLETE must be null or an integer between 0 and 100"); + } + } + + // validate STATUS + if (task.isUpdated(TaskAdapter.STATUS)) + { + Integer status = task.valueOf(TaskAdapter.STATUS); + if (status != null && (status < Tasks.STATUS_NEEDS_ACTION || status > Tasks.STATUS_CANCELLED)) + { + throw new IllegalArgumentException("invalid STATUS: " + status); + } + } + + // ensure that DUE and DURATION are set properly if DTSTART is given + Long dtStart = task.valueOf(TaskAdapter.DTSTART_RAW); + Long due = task.valueOf(TaskAdapter.DUE_RAW); + Duration duration = task.valueOf(TaskAdapter.DURATION); + + if (dtStart != null) + { + if (due != null && duration != null) + { + throw new IllegalArgumentException("Only one of DUE or DURATION must be supplied."); + } + else if (due != null) + { + if (due < dtStart) + { + throw new IllegalArgumentException("DUE must not be < DTSTART"); + } + } + else if (duration != null) + { + if (duration.getSign() == -1) + { + throw new IllegalArgumentException("DURATION must not be negative"); + } + } + } + else if (duration != null) + { + throw new IllegalArgumentException("DURATION must not be supplied without DTSTART"); + } + + // if one of DTSTART or DUE is given, TZ must not be null unless it's an all-day task + if ((dtStart != null || due != null) && !task.valueOf(TaskAdapter.IS_ALLDAY) && task.valueOf(TaskAdapter.TIMEZONE_RAW) == null) + { + throw new IllegalArgumentException("TIMEZONE must be supplied if one of DTSTART or DUE is not null and not all-day"); + } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TestProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TestProcessor.java index 38f089ed..2e3c2684 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TestProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TestProcessor.java @@ -17,59 +17,59 @@ package org.dmfs.provider.tasks.processors.tasks; -import org.dmfs.provider.tasks.model.TaskAdapter; -import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; - import android.database.sqlite.SQLiteDatabase; import android.util.Log; +import org.dmfs.provider.tasks.model.TaskAdapter; +import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; + /** * A simple debugging processor. It just logs every operation. - * + * * @author Marten Gajda */ public class TestProcessor extends AbstractEntityProcessor { - @Override - public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - Log.d("TestProcessor", "before insert processor called"); - } + @Override + public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + Log.d("TestProcessor", "before insert processor called"); + } - @Override - public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - Log.d("TestProcessor", "after insert processor called for " + task.id()); - } + @Override + public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + Log.d("TestProcessor", "after insert processor called for " + task.id()); + } - @Override - public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - Log.d("TestProcessor", "before update processor called for " + task.id()); - } + @Override + public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + Log.d("TestProcessor", "before update processor called for " + task.id()); + } - @Override - public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - Log.d("TestProcessor", "after update processor called for " + task.id()); - } + @Override + public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + Log.d("TestProcessor", "after update processor called for " + task.id()); + } - @Override - public void beforeDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - Log.d("TestProcessor", "before delete processor called for " + task.id()); - } + @Override + public void beforeDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + Log.d("TestProcessor", "before delete processor called for " + task.id()); + } - @Override - public void afterDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - Log.i("TestProcessor", "after delete processor called for " + task.id()); - } + @Override + public void afterDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + { + Log.i("TestProcessor", "after delete processor called for " + task.id()); + } } diff --git a/opentasks-provider/src/main/res/values/opentasks_defaults.xml b/opentasks-provider/src/main/res/values/opentasks_defaults.xml index b7e58054..00835955 100644 --- a/opentasks-provider/src/main/res/values/opentasks_defaults.xml +++ b/opentasks-provider/src/main/res/values/opentasks_defaults.xml @@ -2,6 +2,7 @@ - org.dmfs.tasks + org.dmfs.tasks \ No newline at end of file diff --git a/opentasks/lint.xml b/opentasks/lint.xml index 8423c0ef..c70207fb 100644 --- a/opentasks/lint.xml +++ b/opentasks/lint.xml @@ -1,3 +1,2 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/opentasks/src/main/AndroidManifest.xml b/opentasks/src/main/AndroidManifest.xml index f706db50..6173a353 100644 --- a/opentasks/src/main/AndroidManifest.xml +++ b/opentasks/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ - + @@ -10,20 +9,19 @@ - - + + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:taskAffinity="org.dmfs.tasks.TaskListActivity" + android:theme="@style/OpenTasksAppTheme"> + android:name="org.dmfs.tasks.TaskListActivity" + android:label="@string/title_task_list" + android:launchMode="singleTask"> @@ -33,33 +31,33 @@ + android:name="org.dmfs.tasks.ViewTaskActivity" + android:label="@string/title_task_detail" + android:parentActivityName="org.dmfs.tasks.TaskListActivity" + android:theme="@style/OpenTasksAppTheme" + android:windowSoftInputMode="adjustResize"> + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> + android:name="org.dmfs.tasks.EditTaskActivity" + android:label="@string/activity_add_task_title" + android:parentActivityName="org.dmfs.tasks.TaskListActivity" + android:theme="@style/DetailsTheme" + android:windowSoftInputMode="stateHidden|adjustResize"> + android:name="android.support.PARENT_ACTIVITY" + android:value="org.dmfs.tasks.TaskListActivity"/> @@ -67,9 +65,9 @@ + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> @@ -95,9 +93,9 @@ + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.dir/org.dmfs.tasks.tasks"/> @@ -105,45 +103,44 @@ + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.dir/org.dmfs.tasks.tasks"/> + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> + android:name="org.dmfs.tasks.SyncSettingsActivity" + android:label="@string/title_activity_settings" + android:parentActivityName="org.dmfs.tasks.TaskListActivity" + android:theme="@style/AppTheme"> + android:name="android.support.PARENT_ACTIVITY" + android:value="org.dmfs.tasks.TaskListActivity"/> + android:name="org.dmfs.tasks.homescreen.TaskListWidgetSettingsActivity" + android:label="@string/task_list_selection_title" + android:theme="@style/Theme.MaterialDialog"> - + android:name="org.dmfs.tasks.homescreen.TaskListWidgetUpdaterService" + android:permission="android.permission.BIND_REMOTEVIEWS"> + android:name="org.dmfs.tasks.homescreen.TaskListWidgetProviderLegacy" + android:enabled="@bool/preHoneycomb" + android:label="@string/task_list_widget_title"> @@ -154,26 +151,26 @@ + android:host="@string/opentasks_authority" + android:scheme="content"/> + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> + android:name="android.appwidget.provider" + android:resource="@xml/task_widget_info"/> + android:name="org.dmfs.tasks.homescreen.TaskListWidgetProviderLargeLegacy" + android:enabled="@bool/preHoneycomb" + android:label="@string/task_list_widget_title_4x4"> @@ -184,27 +181,27 @@ + android:host="@string/opentasks_authority" + android:scheme="content"/> + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> + android:name="android.appwidget.provider" + android:resource="@xml/task_widget_info_large"/> + android:name="org.dmfs.tasks.homescreen.TaskListWidgetProvider" + android:enabled="@bool/postHoneycomb" + android:label="@string/task_list_widget_title"> @@ -215,26 +212,26 @@ + android:host="@string/opentasks_authority" + android:scheme="content"/> + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> + android:name="android.appwidget.provider" + android:resource="@xml/task_widget_info"/> + android:name="org.dmfs.tasks.homescreen.TaskListWidgetProviderLarge" + android:enabled="@bool/postHoneycomb" + android:label="@string/task_list_widget_title_4x4"> @@ -245,21 +242,21 @@ + android:host="@string/opentasks_authority" + android:scheme="content"/> + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> + android:name="android.appwidget.provider" + android:resource="@xml/task_widget_info_large"/> @@ -268,56 +265,55 @@ + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> + android:name="org.dmfs.tasks.dashclock.TasksExtension" + android:icon="@drawable/ic_dashboard" + android:label="@string/dashclock_extension_title" + android:permission="com.google.android.apps.dashclock.permission.READ_EXTENSION_DATA"> + android:name="protocolVersion" + android:value="2"/> + android:name="worldReadable" + android:value="true"/> + android:name="description" + android:value="@string/dashclock_extension_description"/> + android:name="settingsActivity" + android:value="org.dmfs.tasks.dashclock.DashClockPreferenceActivity"/> - + android:name="org.dmfs.tasks.dashclock.DashClockPreferenceActivity" + android:exported="true" + android:label="@string/title_task_list" + android:theme="@style/AppTheme"> + android:name="org.dmfs.android.colorpicker.ColorPickerActivity" + android:exported="false" + android:theme="@style/Theme.AppCompat.Light.Dialog"> @@ -326,8 +322,8 @@ + android:name="org.dmfs.tasks.ManageListActivity" + android:theme="@style/AppThemeDialog"> @@ -335,9 +331,9 @@ + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.dir/org.dmfs.tasks.tasklists" + android:scheme="content"/> @@ -346,22 +342,22 @@ + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasklists" + android:scheme="content"/> + android:name="org.dmfs.tasks.notification.TaskNotificationHandler" + android:exported="true"> + android:host="@string/opentasks_authority" + android:scheme="content"/> @@ -376,8 +372,8 @@ + android:path="org.dmfs.tasks" + android:scheme="package"/> @@ -386,9 +382,9 @@ + android:scheme="content" + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.item/org.dmfs.tasks.tasks"/> @@ -398,22 +394,22 @@ + android:name="org.dmfs.tasks.utils.DatabaseInitializedReceiver" + android:enabled="@bool/opentasks_support_local_lists" + android:exported="false"> + android:host="@string/opentasks_authority" + android:mimeType="vnd.android.cursor.dir/vnd.org.dmfs.authority.mimetype" + android:scheme="content"/> + android:name="org.dmfs.tasks.notification.NotificationUpdaterService" + android:exported="false"/> diff --git a/opentasks/src/main/java/com/jmedeisis/draglinearlayout/DragLinearLayout.java b/opentasks/src/main/java/com/jmedeisis/draglinearlayout/DragLinearLayout.java index 0c639230..12ebcda0 100644 --- a/opentasks/src/main/java/com/jmedeisis/draglinearlayout/DragLinearLayout.java +++ b/opentasks/src/main/java/com/jmedeisis/draglinearlayout/DragLinearLayout.java @@ -44,217 +44,232 @@ import android.widget.ScrollView; @SuppressLint("NewApi") public class DragLinearLayout extends LinearLayout { - private static final String LOG_TAG = DragLinearLayout.class.getSimpleName(); - private static final long NOMINAL_SWITCH_DURATION = 150; - private static final long MIN_SWITCH_DURATION = NOMINAL_SWITCH_DURATION; - private static final long MAX_SWITCH_DURATION = NOMINAL_SWITCH_DURATION * 2; - private static final float NOMINAL_DISTANCE = 20; - private final float nominalDistanceScaled; - - /** The the vibration duration in milliseconds for drag start */ - public static final int VIBRATION_DURATION = 12; - - /** - * Use with - * {@link com.jmedeisis.draglinearlayout.DragLinearLayout#setOnViewSwapListener(com.jmedeisis.draglinearlayout.DragLinearLayout.OnViewSwapListener)} to - * listen for draggable view swaps. - */ - public interface OnViewSwapListener - { - /** - * Invoked right before the two items are swapped due to a drag event. After the swap, the firstView will be in the secondPosition, and vice versa. - *

- * No guarantee is made as to which of the two has a lesser/greater position. - */ - void onSwap(View firstView, int firstPosition, View secondView, int secondPosition); - } - - private OnViewSwapListener swapListener; - - private LayoutTransition layoutTransition; - - /** - * Mapping from child index to drag-related info container. Presence of mapping implies the child can be dragged, and is considered for swaps with the - * currently dragged item. - */ - private final SparseArray draggableChildren; - - private class DraggableChild - { - /** - * If non-null, a reference to an on-going position animation. - */ - private ValueAnimator swapAnimation; - - - public void endExistingAnimation() - { - if (null != swapAnimation) - swapAnimation.end(); - } - - - public void cancelExistingAnimation() - { - if (null != swapAnimation) - swapAnimation.cancel(); - } - } - - /** - * Holds state information about the currently dragged item. - *

- * Rough lifecycle: - *

  • #startDetectingOnPossibleDrag - #detecting == true
  • - *
  • if drag is recognised, #onDragStart - #dragging == true
  • - *
  • if drag ends, #onDragStop - #dragging == false, #settling == true
  • - *
  • if gesture ends without drag, or settling finishes, #stopDetecting - #detecting == false
  • - */ - private class DragItem - { - private View view; - private int startVisibility; - private BitmapDrawable viewDrawable; - private int position; - private int startTop; - private int height; - private int totalDragOffset; - private int targetTopOffset; - private ValueAnimator settleAnimation; - - private boolean detecting; - private boolean dragging; - - - public DragItem() - { - stopDetecting(); - } - - - public void startDetectingOnPossibleDrag(final View view, final int position) - { - this.view = view; - this.startVisibility = view.getVisibility(); - this.viewDrawable = getDragDrawable(view); - this.position = position; - this.startTop = view.getTop(); - this.height = view.getHeight(); - this.totalDragOffset = 0; - this.targetTopOffset = 0; - this.settleAnimation = null; - - this.detecting = true; - } - - - public void onDragStart() - { - view.setVisibility(View.INVISIBLE); - this.dragging = true; - } - - - public void setTotalOffset(int offset) - { - totalDragOffset = offset; - updateTargetTop(); - } - - - public void updateTargetTop() - { - targetTopOffset = startTop - view.getTop() + totalDragOffset; - } - - - public void onDragStop() - { - this.dragging = false; - } - - - public boolean settling() - { - return null != settleAnimation; - } - - - public void stopDetecting() - { - this.detecting = false; - if (null != view) - view.setVisibility(startVisibility); - view = null; - startVisibility = -1; - viewDrawable = null; - position = -1; - startTop = -1; - height = -1; - totalDragOffset = 0; - targetTopOffset = 0; - if (null != settleAnimation) - settleAnimation.end(); - settleAnimation = null; - } - } - - /** - * The currently dragged item, if {@link com.jmedeisis.draglinearlayout.DragLinearLayout.DragItem#detecting}. - */ - private final DragItem draggedItem; - private final int slop; - - private static final int INVALID_POINTER_ID = -1; - private int downY = -1; - private int activePointerId = INVALID_POINTER_ID; - - /** - * The shadow to be drawn above the {@link #draggedItem}. - */ + private static final String LOG_TAG = DragLinearLayout.class.getSimpleName(); + private static final long NOMINAL_SWITCH_DURATION = 150; + private static final long MIN_SWITCH_DURATION = NOMINAL_SWITCH_DURATION; + private static final long MAX_SWITCH_DURATION = NOMINAL_SWITCH_DURATION * 2; + private static final float NOMINAL_DISTANCE = 20; + private final float nominalDistanceScaled; + + /** + * The the vibration duration in milliseconds for drag start + */ + public static final int VIBRATION_DURATION = 12; + + + /** + * Use with + * {@link com.jmedeisis.draglinearlayout.DragLinearLayout#setOnViewSwapListener(com.jmedeisis.draglinearlayout.DragLinearLayout.OnViewSwapListener)} to + * listen for draggable view swaps. + */ + public interface OnViewSwapListener + { + /** + * Invoked right before the two items are swapped due to a drag event. After the swap, the firstView will be in the secondPosition, and vice versa. + *

    + * No guarantee is made as to which of the two has a lesser/greater position. + */ + void onSwap(View firstView, int firstPosition, View secondView, int secondPosition); + } + + + private OnViewSwapListener swapListener; + + private LayoutTransition layoutTransition; + + /** + * Mapping from child index to drag-related info container. Presence of mapping implies the child can be dragged, and is considered for swaps with the + * currently dragged item. + */ + private final SparseArray draggableChildren; + + + private class DraggableChild + { + /** + * If non-null, a reference to an on-going position animation. + */ + private ValueAnimator swapAnimation; + + + public void endExistingAnimation() + { + if (null != swapAnimation) + { + swapAnimation.end(); + } + } + + + public void cancelExistingAnimation() + { + if (null != swapAnimation) + { + swapAnimation.cancel(); + } + } + } + + + /** + * Holds state information about the currently dragged item. + *

    + * Rough lifecycle: + *

  • #startDetectingOnPossibleDrag - #detecting == true
  • + *
  • if drag is recognised, #onDragStart - #dragging == true
  • + *
  • if drag ends, #onDragStop - #dragging == false, #settling == true
  • + *
  • if gesture ends without drag, or settling finishes, #stopDetecting - #detecting == false
  • + */ + private class DragItem + { + private View view; + private int startVisibility; + private BitmapDrawable viewDrawable; + private int position; + private int startTop; + private int height; + private int totalDragOffset; + private int targetTopOffset; + private ValueAnimator settleAnimation; + + private boolean detecting; + private boolean dragging; + + + public DragItem() + { + stopDetecting(); + } + + + public void startDetectingOnPossibleDrag(final View view, final int position) + { + this.view = view; + this.startVisibility = view.getVisibility(); + this.viewDrawable = getDragDrawable(view); + this.position = position; + this.startTop = view.getTop(); + this.height = view.getHeight(); + this.totalDragOffset = 0; + this.targetTopOffset = 0; + this.settleAnimation = null; + + this.detecting = true; + } + + + public void onDragStart() + { + view.setVisibility(View.INVISIBLE); + this.dragging = true; + } + + + public void setTotalOffset(int offset) + { + totalDragOffset = offset; + updateTargetTop(); + } + + + public void updateTargetTop() + { + targetTopOffset = startTop - view.getTop() + totalDragOffset; + } + + + public void onDragStop() + { + this.dragging = false; + } + + + public boolean settling() + { + return null != settleAnimation; + } + + + public void stopDetecting() + { + this.detecting = false; + if (null != view) + { + view.setVisibility(startVisibility); + } + view = null; + startVisibility = -1; + viewDrawable = null; + position = -1; + startTop = -1; + height = -1; + totalDragOffset = 0; + targetTopOffset = 0; + if (null != settleAnimation) + { + settleAnimation.end(); + } + settleAnimation = null; + } + } + + + /** + * The currently dragged item, if {@link com.jmedeisis.draglinearlayout.DragLinearLayout.DragItem#detecting}. + */ + private final DragItem draggedItem; + private final int slop; + + private static final int INVALID_POINTER_ID = -1; + private int downY = -1; + private int activePointerId = INVALID_POINTER_ID; + + /** + * The shadow to be drawn above the {@link #draggedItem}. + */ // private final Drawable dragTopShadowDrawable; - /** - * The shadow to be drawn below the {@link #draggedItem}. - */ + /** + * The shadow to be drawn below the {@link #draggedItem}. + */ // private final Drawable dragBottomShadowDrawable; // private final int dragShadowHeight; - /** - * See {@link #setContainerScrollView(android.widget.ScrollView)}. - */ - private ScrollView containerScrollView; - private int scrollSensitiveAreaHeight; - private static final int DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP = 48; - private static final int MAX_DRAG_SCROLL_SPEED = 16; + /** + * See {@link #setContainerScrollView(android.widget.ScrollView)}. + */ + private ScrollView containerScrollView; + private int scrollSensitiveAreaHeight; + private static final int DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP = 48; + private static final int MAX_DRAG_SCROLL_SPEED = 16; - public DragLinearLayout(Context context) - { - this(context, null); - } + public DragLinearLayout(Context context) + { + this(context, null); + } - public DragLinearLayout(Context context, AttributeSet attrs) - { - super(context, attrs); + public DragLinearLayout(Context context, AttributeSet attrs) + { + super(context, attrs); - setOrientation(LinearLayout.VERTICAL); + setOrientation(LinearLayout.VERTICAL); - draggableChildren = new SparseArray<>(); + draggableChildren = new SparseArray<>(); - draggedItem = new DragItem(); - ViewConfiguration vc = ViewConfiguration.get(context); - slop = vc.getScaledTouchSlop(); + draggedItem = new DragItem(); + ViewConfiguration vc = ViewConfiguration.get(context); + slop = vc.getScaledTouchSlop(); - final Resources resources = getResources(); - // changed by dmfs: don't use shadows + final Resources resources = getResources(); + // changed by dmfs: don't use shadows // dragTopShadowDrawable = ContextCompat.getDrawable(context, R.drawable.ab_solid_shadow_holo_flipped); // dragBottomShadowDrawable = ContextCompat.getDrawable(context, R.drawable.ab_solid_shadow_holo); // dragShadowHeight = resources.getDimensionPixelSize(R.dimen.downwards_drop_shadow_height); - // changed by dmfs: don't use DragLinearLayout_scrollSensitiveHeight from resources, use default - scrollSensitiveAreaHeight = - (int) (DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP * resources.getDisplayMetrics().density + 0.5f); + // changed by dmfs: don't use DragLinearLayout_scrollSensitiveHeight from resources, use default + scrollSensitiveAreaHeight = + (int) (DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP * resources.getDisplayMetrics().density + 0.5f); // TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DragLinearLayout, 0, 0); // try @@ -267,463 +282,472 @@ public class DragLinearLayout extends LinearLayout // a.recycle(); // } - nominalDistanceScaled = (int) (NOMINAL_DISTANCE * resources.getDisplayMetrics().density + 0.5f); - } - - - @Override - public void setOrientation(int orientation) - { - // enforce VERTICAL orientation; remove if HORIZONTAL support is ever added - if (LinearLayout.HORIZONTAL == orientation) - { - throw new IllegalArgumentException("DragLinearLayout must be VERTICAL."); - } - super.setOrientation(orientation); - } - - - /** - * Calls {@link #addView(android.view.View)} followed by {@link #setViewDraggable(android.view.View, android.view.View)}. - */ - public void addDragView(View child, View dragHandle) - { - addView(child); - setViewDraggable(child, dragHandle); - } - - - /** - * Calls {@link #addView(android.view.View, int)} followed by {@link #setViewDraggable(android.view.View, android.view.View)} and correctly updates the - * drag-ability state of all existing views. - */ - public void addDragView(View child, View dragHandle, int index) - { - addView(child, index); - - // update drag-able children mappings - final int numMappings = draggableChildren.size(); - for (int i = numMappings - 1; i >= 0; i--) - { - final int key = draggableChildren.keyAt(i); - if (key >= index) - { - draggableChildren.put(key + 1, draggableChildren.get(key)); - } - } - - setViewDraggable(child, dragHandle); - } - - - /** - * Makes the child a candidate for dragging. Must be an existing child of this layout. - */ - public void setViewDraggable(View child, View dragHandle) - { - if (null == child || null == dragHandle) - { - throw new IllegalArgumentException("Draggable children and their drag handles must not be null."); - } - - if (this == child.getParent()) - { - dragHandle.setOnTouchListener(new DragHandleOnTouchListener(child)); - draggableChildren.put(indexOfChild(child), new DraggableChild()); - } - else - { - Log.e(LOG_TAG, child + " is not a child, cannot make draggable."); - } - } - - - /** - * Calls {@link #removeView(android.view.View)} and correctly updates the drag-ability state of all remaining views. - */ - @SuppressWarnings("UnusedDeclaration") - public void removeDragView(View child) - { - if (this == child.getParent()) - { - final int index = indexOfChild(child); - removeView(child); - - // update drag-able children mappings - final int mappings = draggableChildren.size(); - for (int i = 0; i < mappings; i++) - { - final int key = draggableChildren.keyAt(i); - if (key >= index) - { - DraggableChild next = draggableChildren.get(key + 1); - if (null == next) - { - draggableChildren.delete(key); - } - else - { - draggableChildren.put(key, next); - } - } - } - } - } - - - @Override - public void removeAllViews() - { - super.removeAllViews(); - draggableChildren.clear(); - } - - - /** - * If this layout is within a {@link android.widget.ScrollView}, register it here so that it can be scrolled during item drags. - */ - public void setContainerScrollView(ScrollView scrollView) - { - this.containerScrollView = scrollView; - } - - - /** - * Sets the height from upper / lower edge at which a container {@link android.widget.ScrollView}, if one is registered via - * {@link #setContainerScrollView(android.widget.ScrollView)}, is scrolled. - */ - @SuppressWarnings("UnusedDeclaration") - public void setScrollSensitiveHeight(int height) - { - this.scrollSensitiveAreaHeight = height; - } - - - @SuppressWarnings("UnusedDeclaration") - public int getScrollSensitiveHeight() - { - return scrollSensitiveAreaHeight; - } - - - /** - * See {@link com.jmedeisis.draglinearlayout.DragLinearLayout.OnViewSwapListener}. - */ - public void setOnViewSwapListener(OnViewSwapListener swapListener) - { - this.swapListener = swapListener; - } - - - /** - * A linear relationship b/w distance and duration, bounded. - */ - private long getTranslateAnimationDuration(float distance) - { - return Math.min(MAX_SWITCH_DURATION, Math.max(MIN_SWITCH_DURATION, (long) (NOMINAL_SWITCH_DURATION * Math.abs(distance) / nominalDistanceScaled))); - } - - - /** - * Initiates a new {@link #draggedItem} unless the current one is still {@link com.jmedeisis.draglinearlayout.DragLinearLayout.DragItem#detecting}. - */ - private void startDetectingDrag(View child) - { - if (draggedItem.detecting) - return; // existing drag in process, only one at a time is allowed - - final int position = indexOfChild(child); - - // complete any existing animations, both for the newly selected child and the previous dragged one - draggableChildren.get(position).endExistingAnimation(); - - draggedItem.startDetectingOnPossibleDrag(child, position); - } - - - private void startDrag() - { - // remove layout transition, it conflicts with drag animation - // we will restore it after drag animation end, see onDragStop() - layoutTransition = getLayoutTransition(); - if (layoutTransition != null) - { - setLayoutTransition(null); - } - - draggedItem.onDragStart(); - requestDisallowInterceptTouchEvent(true); - - try - { - Vibrator vibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); - vibrator.vibrate(VIBRATION_DURATION); - } - catch (Exception e) - { - - } - } - - - /** - * Animates the dragged item to its final resting position. - */ - private void onDragStop() - { - draggedItem.settleAnimation = ValueAnimator.ofFloat(draggedItem.totalDragOffset, draggedItem.totalDragOffset - draggedItem.targetTopOffset) - .setDuration(getTranslateAnimationDuration(draggedItem.targetTopOffset)); - draggedItem.settleAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() - { - @Override - public void onAnimationUpdate(ValueAnimator animation) - { - if (!draggedItem.detecting) - return; // already stopped - - draggedItem.setTotalOffset(((Float) animation.getAnimatedValue()).intValue()); - - final int shadowAlpha = (int) ((1 - animation.getAnimatedFraction()) * 255); + nominalDistanceScaled = (int) (NOMINAL_DISTANCE * resources.getDisplayMetrics().density + 0.5f); + } + + + @Override + public void setOrientation(int orientation) + { + // enforce VERTICAL orientation; remove if HORIZONTAL support is ever added + if (LinearLayout.HORIZONTAL == orientation) + { + throw new IllegalArgumentException("DragLinearLayout must be VERTICAL."); + } + super.setOrientation(orientation); + } + + + /** + * Calls {@link #addView(android.view.View)} followed by {@link #setViewDraggable(android.view.View, android.view.View)}. + */ + public void addDragView(View child, View dragHandle) + { + addView(child); + setViewDraggable(child, dragHandle); + } + + + /** + * Calls {@link #addView(android.view.View, int)} followed by {@link #setViewDraggable(android.view.View, android.view.View)} and correctly updates the + * drag-ability state of all existing views. + */ + public void addDragView(View child, View dragHandle, int index) + { + addView(child, index); + + // update drag-able children mappings + final int numMappings = draggableChildren.size(); + for (int i = numMappings - 1; i >= 0; i--) + { + final int key = draggableChildren.keyAt(i); + if (key >= index) + { + draggableChildren.put(key + 1, draggableChildren.get(key)); + } + } + + setViewDraggable(child, dragHandle); + } + + + /** + * Makes the child a candidate for dragging. Must be an existing child of this layout. + */ + public void setViewDraggable(View child, View dragHandle) + { + if (null == child || null == dragHandle) + { + throw new IllegalArgumentException("Draggable children and their drag handles must not be null."); + } + + if (this == child.getParent()) + { + dragHandle.setOnTouchListener(new DragHandleOnTouchListener(child)); + draggableChildren.put(indexOfChild(child), new DraggableChild()); + } + else + { + Log.e(LOG_TAG, child + " is not a child, cannot make draggable."); + } + } + + + /** + * Calls {@link #removeView(android.view.View)} and correctly updates the drag-ability state of all remaining views. + */ + @SuppressWarnings("UnusedDeclaration") + public void removeDragView(View child) + { + if (this == child.getParent()) + { + final int index = indexOfChild(child); + removeView(child); + + // update drag-able children mappings + final int mappings = draggableChildren.size(); + for (int i = 0; i < mappings; i++) + { + final int key = draggableChildren.keyAt(i); + if (key >= index) + { + DraggableChild next = draggableChildren.get(key + 1); + if (null == next) + { + draggableChildren.delete(key); + } + else + { + draggableChildren.put(key, next); + } + } + } + } + } + + + @Override + public void removeAllViews() + { + super.removeAllViews(); + draggableChildren.clear(); + } + + + /** + * If this layout is within a {@link android.widget.ScrollView}, register it here so that it can be scrolled during item drags. + */ + public void setContainerScrollView(ScrollView scrollView) + { + this.containerScrollView = scrollView; + } + + + /** + * Sets the height from upper / lower edge at which a container {@link android.widget.ScrollView}, if one is registered via + * {@link #setContainerScrollView(android.widget.ScrollView)}, is scrolled. + */ + @SuppressWarnings("UnusedDeclaration") + public void setScrollSensitiveHeight(int height) + { + this.scrollSensitiveAreaHeight = height; + } + + + @SuppressWarnings("UnusedDeclaration") + public int getScrollSensitiveHeight() + { + return scrollSensitiveAreaHeight; + } + + + /** + * See {@link com.jmedeisis.draglinearlayout.DragLinearLayout.OnViewSwapListener}. + */ + public void setOnViewSwapListener(OnViewSwapListener swapListener) + { + this.swapListener = swapListener; + } + + + /** + * A linear relationship b/w distance and duration, bounded. + */ + private long getTranslateAnimationDuration(float distance) + { + return Math.min(MAX_SWITCH_DURATION, Math.max(MIN_SWITCH_DURATION, (long) (NOMINAL_SWITCH_DURATION * Math.abs(distance) / nominalDistanceScaled))); + } + + + /** + * Initiates a new {@link #draggedItem} unless the current one is still {@link com.jmedeisis.draglinearlayout.DragLinearLayout.DragItem#detecting}. + */ + private void startDetectingDrag(View child) + { + if (draggedItem.detecting) + { + return; // existing drag in process, only one at a time is allowed + } + + final int position = indexOfChild(child); + + // complete any existing animations, both for the newly selected child and the previous dragged one + draggableChildren.get(position).endExistingAnimation(); + + draggedItem.startDetectingOnPossibleDrag(child, position); + } + + + private void startDrag() + { + // remove layout transition, it conflicts with drag animation + // we will restore it after drag animation end, see onDragStop() + layoutTransition = getLayoutTransition(); + if (layoutTransition != null) + { + setLayoutTransition(null); + } + + draggedItem.onDragStart(); + requestDisallowInterceptTouchEvent(true); + + try + { + Vibrator vibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); + vibrator.vibrate(VIBRATION_DURATION); + } + catch (Exception e) + { + + } + } + + + /** + * Animates the dragged item to its final resting position. + */ + private void onDragStop() + { + draggedItem.settleAnimation = ValueAnimator.ofFloat(draggedItem.totalDragOffset, draggedItem.totalDragOffset - draggedItem.targetTopOffset) + .setDuration(getTranslateAnimationDuration(draggedItem.targetTopOffset)); + draggedItem.settleAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() + { + @Override + public void onAnimationUpdate(ValueAnimator animation) + { + if (!draggedItem.detecting) + { + return; // already stopped + } + + draggedItem.setTotalOffset(((Float) animation.getAnimatedValue()).intValue()); + + final int shadowAlpha = (int) ((1 - animation.getAnimatedFraction()) * 255); // if (null != dragTopShadowDrawable) // dragTopShadowDrawable.setAlpha(shadowAlpha); // dragBottomShadowDrawable.setAlpha(shadowAlpha); - invalidate(); - } - }); - draggedItem.settleAnimation.addListener(new AnimatorListenerAdapter() - { - @Override - public void onAnimationStart(Animator animation) - { - draggedItem.onDragStop(); - } - - - @Override - public void onAnimationEnd(Animator animation) - { - if (!draggedItem.detecting) - { - return; // already stopped - } - - draggedItem.settleAnimation = null; - draggedItem.stopDetecting(); + invalidate(); + } + }); + draggedItem.settleAnimation.addListener(new AnimatorListenerAdapter() + { + @Override + public void onAnimationStart(Animator animation) + { + draggedItem.onDragStop(); + } + + + @Override + public void onAnimationEnd(Animator animation) + { + if (!draggedItem.detecting) + { + return; // already stopped + } + + draggedItem.settleAnimation = null; + draggedItem.stopDetecting(); // if (null != dragTopShadowDrawable) // dragTopShadowDrawable.setAlpha(255); // dragBottomShadowDrawable.setAlpha(255); - // restore layout transition - if (layoutTransition != null && getLayoutTransition() == null) - { - setLayoutTransition(layoutTransition); - } - } - }); - draggedItem.settleAnimation.start(); - } - - - /** - * Updates the dragged item with the given total offset from its starting position. Evaluates and executes draggable view swaps. - */ - private void onDrag(final int offset) - { - draggedItem.setTotalOffset(Math.min(Math.max(offset, -draggedItem.startTop), this.getHeight() - draggedItem.startTop - draggedItem.height)); - invalidate(); - - int currentTop = draggedItem.startTop + draggedItem.totalDragOffset; - - handleContainerScroll(currentTop); - - int belowPosition = nextDraggablePosition(draggedItem.position); - int abovePosition = previousDraggablePosition(draggedItem.position); - - View belowView = getChildAt(belowPosition); - View aboveView = getChildAt(abovePosition); - - final boolean isBelow = (belowView != null) && (currentTop + draggedItem.height > belowView.getTop() + belowView.getHeight() / 2); - final boolean isAbove = (aboveView != null) && (currentTop < aboveView.getTop() + aboveView.getHeight() / 2); - - if (isBelow || isAbove) - { - final View switchView = isBelow ? belowView : aboveView; - - // swap elements - final int originalPosition = draggedItem.position; - final int switchPosition = isBelow ? belowPosition : abovePosition; - - draggableChildren.get(switchPosition).cancelExistingAnimation(); - final float switchViewStartY = switchView.getY(); - - if (null != swapListener) - { - swapListener.onSwap(draggedItem.view, draggedItem.position, switchView, switchPosition); - } - - if (isBelow) - { - removeViewAt(originalPosition); - removeViewAt(switchPosition - 1); - - addView(belowView, originalPosition); - addView(draggedItem.view, switchPosition); - } - else - { - removeViewAt(switchPosition); - removeViewAt(originalPosition - 1); - - addView(draggedItem.view, switchPosition); - addView(aboveView, originalPosition); - } - draggedItem.position = switchPosition; - - final ViewTreeObserver switchViewObserver = switchView.getViewTreeObserver(); - switchViewObserver.addOnPreDrawListener(new OnPreDrawListener() - { - @Override - public boolean onPreDraw() - { - switchViewObserver.removeOnPreDrawListener(this); - - final ObjectAnimator switchAnimator = ObjectAnimator.ofFloat(switchView, "y", switchViewStartY, switchView.getTop()).setDuration( - getTranslateAnimationDuration(switchView.getTop() - switchViewStartY)); - switchAnimator.addListener(new AnimatorListenerAdapter() - { - @Override - public void onAnimationStart(Animator animation) - { - draggableChildren.get(originalPosition).swapAnimation = switchAnimator; - } - - - @Override - public void onAnimationEnd(Animator animation) - { - draggableChildren.get(originalPosition).swapAnimation = null; - } - }); - switchAnimator.start(); - - return true; - } - }); - - final ViewTreeObserver observer = draggedItem.view.getViewTreeObserver(); - observer.addOnPreDrawListener(new OnPreDrawListener() - { - @Override - public boolean onPreDraw() - { - observer.removeOnPreDrawListener(this); - draggedItem.updateTargetTop(); - - // TODO test if still necessary.. - // because draggedItem#view#getTop() is only up-to-date NOW - // (and not right after the #addView() swaps above) - // we may need to update an ongoing settle animation - if (draggedItem.settling()) - { - Log.d(LOG_TAG, "Updating settle animation"); - draggedItem.settleAnimation.removeAllListeners(); - draggedItem.settleAnimation.cancel(); - onDragStop(); - } - return true; - } - }); - } - } - - - private int previousDraggablePosition(int position) - { - int startIndex = draggableChildren.indexOfKey(position); - if (startIndex < 1 || startIndex > draggableChildren.size()) - return -1; - return draggableChildren.keyAt(startIndex - 1); - } - - - private int nextDraggablePosition(int position) - { - int startIndex = draggableChildren.indexOfKey(position); - if (startIndex < -1 || startIndex > draggableChildren.size() - 2) - return -1; - return draggableChildren.keyAt(startIndex + 1); - } - - private Runnable dragUpdater; - - - private void handleContainerScroll(final int currentTop) - { - if (null != containerScrollView) - { - final int startScrollY = containerScrollView.getScrollY(); - final int absTop = getTop() - startScrollY + currentTop; - final int height = containerScrollView.getHeight(); - - final int delta; - - if (absTop < scrollSensitiveAreaHeight) - { - delta = (int) (-MAX_DRAG_SCROLL_SPEED * smootherStep(scrollSensitiveAreaHeight, 0, absTop)); - } - else if (absTop > height - scrollSensitiveAreaHeight) - { - delta = (int) (MAX_DRAG_SCROLL_SPEED * smootherStep(height - scrollSensitiveAreaHeight, height, absTop)); - } - else - { - delta = 0; - } - - containerScrollView.removeCallbacks(dragUpdater); - containerScrollView.smoothScrollBy(0, delta); - dragUpdater = new Runnable() - { - @Override - public void run() - { - if (draggedItem.dragging && startScrollY != containerScrollView.getScrollY()) - { - onDrag(draggedItem.totalDragOffset + delta); - } - } - }; - containerScrollView.post(dragUpdater); - } - } - - - /** - * By Ken Perlin. See Smoothstep - Wikipedia. - */ - private static float smootherStep(float edge1, float edge2, float val) - { - val = Math.max(0, Math.min((val - edge1) / (edge2 - edge1), 1)); - return val * val * val * (val * (val * 6 - 15) + 10); - } - - - @Override - protected void dispatchDraw(@NonNull Canvas canvas) - { - super.dispatchDraw(canvas); - - if (draggedItem.detecting && (draggedItem.dragging || draggedItem.settling())) - { - canvas.save(); - canvas.translate(0, draggedItem.totalDragOffset); - draggedItem.viewDrawable.draw(canvas); - - final int left = draggedItem.viewDrawable.getBounds().left; - final int right = draggedItem.viewDrawable.getBounds().right; - final int top = draggedItem.viewDrawable.getBounds().top; - final int bottom = draggedItem.viewDrawable.getBounds().bottom; + // restore layout transition + if (layoutTransition != null && getLayoutTransition() == null) + { + setLayoutTransition(layoutTransition); + } + } + }); + draggedItem.settleAnimation.start(); + } + + + /** + * Updates the dragged item with the given total offset from its starting position. Evaluates and executes draggable view swaps. + */ + private void onDrag(final int offset) + { + draggedItem.setTotalOffset(Math.min(Math.max(offset, -draggedItem.startTop), this.getHeight() - draggedItem.startTop - draggedItem.height)); + invalidate(); + + int currentTop = draggedItem.startTop + draggedItem.totalDragOffset; + + handleContainerScroll(currentTop); + + int belowPosition = nextDraggablePosition(draggedItem.position); + int abovePosition = previousDraggablePosition(draggedItem.position); + + View belowView = getChildAt(belowPosition); + View aboveView = getChildAt(abovePosition); + + final boolean isBelow = (belowView != null) && (currentTop + draggedItem.height > belowView.getTop() + belowView.getHeight() / 2); + final boolean isAbove = (aboveView != null) && (currentTop < aboveView.getTop() + aboveView.getHeight() / 2); + + if (isBelow || isAbove) + { + final View switchView = isBelow ? belowView : aboveView; + + // swap elements + final int originalPosition = draggedItem.position; + final int switchPosition = isBelow ? belowPosition : abovePosition; + + draggableChildren.get(switchPosition).cancelExistingAnimation(); + final float switchViewStartY = switchView.getY(); + + if (null != swapListener) + { + swapListener.onSwap(draggedItem.view, draggedItem.position, switchView, switchPosition); + } + + if (isBelow) + { + removeViewAt(originalPosition); + removeViewAt(switchPosition - 1); + + addView(belowView, originalPosition); + addView(draggedItem.view, switchPosition); + } + else + { + removeViewAt(switchPosition); + removeViewAt(originalPosition - 1); + + addView(draggedItem.view, switchPosition); + addView(aboveView, originalPosition); + } + draggedItem.position = switchPosition; + + final ViewTreeObserver switchViewObserver = switchView.getViewTreeObserver(); + switchViewObserver.addOnPreDrawListener(new OnPreDrawListener() + { + @Override + public boolean onPreDraw() + { + switchViewObserver.removeOnPreDrawListener(this); + + final ObjectAnimator switchAnimator = ObjectAnimator.ofFloat(switchView, "y", switchViewStartY, switchView.getTop()).setDuration( + getTranslateAnimationDuration(switchView.getTop() - switchViewStartY)); + switchAnimator.addListener(new AnimatorListenerAdapter() + { + @Override + public void onAnimationStart(Animator animation) + { + draggableChildren.get(originalPosition).swapAnimation = switchAnimator; + } + + + @Override + public void onAnimationEnd(Animator animation) + { + draggableChildren.get(originalPosition).swapAnimation = null; + } + }); + switchAnimator.start(); + + return true; + } + }); + + final ViewTreeObserver observer = draggedItem.view.getViewTreeObserver(); + observer.addOnPreDrawListener(new OnPreDrawListener() + { + @Override + public boolean onPreDraw() + { + observer.removeOnPreDrawListener(this); + draggedItem.updateTargetTop(); + + // TODO test if still necessary.. + // because draggedItem#view#getTop() is only up-to-date NOW + // (and not right after the #addView() swaps above) + // we may need to update an ongoing settle animation + if (draggedItem.settling()) + { + Log.d(LOG_TAG, "Updating settle animation"); + draggedItem.settleAnimation.removeAllListeners(); + draggedItem.settleAnimation.cancel(); + onDragStop(); + } + return true; + } + }); + } + } + + + private int previousDraggablePosition(int position) + { + int startIndex = draggableChildren.indexOfKey(position); + if (startIndex < 1 || startIndex > draggableChildren.size()) + { + return -1; + } + return draggableChildren.keyAt(startIndex - 1); + } + + + private int nextDraggablePosition(int position) + { + int startIndex = draggableChildren.indexOfKey(position); + if (startIndex < -1 || startIndex > draggableChildren.size() - 2) + { + return -1; + } + return draggableChildren.keyAt(startIndex + 1); + } + + + private Runnable dragUpdater; + + + private void handleContainerScroll(final int currentTop) + { + if (null != containerScrollView) + { + final int startScrollY = containerScrollView.getScrollY(); + final int absTop = getTop() - startScrollY + currentTop; + final int height = containerScrollView.getHeight(); + + final int delta; + + if (absTop < scrollSensitiveAreaHeight) + { + delta = (int) (-MAX_DRAG_SCROLL_SPEED * smootherStep(scrollSensitiveAreaHeight, 0, absTop)); + } + else if (absTop > height - scrollSensitiveAreaHeight) + { + delta = (int) (MAX_DRAG_SCROLL_SPEED * smootherStep(height - scrollSensitiveAreaHeight, height, absTop)); + } + else + { + delta = 0; + } + + containerScrollView.removeCallbacks(dragUpdater); + containerScrollView.smoothScrollBy(0, delta); + dragUpdater = new Runnable() + { + @Override + public void run() + { + if (draggedItem.dragging && startScrollY != containerScrollView.getScrollY()) + { + onDrag(draggedItem.totalDragOffset + delta); + } + } + }; + containerScrollView.post(dragUpdater); + } + } + + + /** + * By Ken Perlin. See Smoothstep - Wikipedia. + */ + private static float smootherStep(float edge1, float edge2, float val) + { + val = Math.max(0, Math.min((val - edge1) / (edge2 - edge1), 1)); + return val * val * val * (val * (val * 6 - 15) + 10); + } + + + @Override + protected void dispatchDraw(@NonNull Canvas canvas) + { + super.dispatchDraw(canvas); + + if (draggedItem.detecting && (draggedItem.dragging || draggedItem.settling())) + { + canvas.save(); + canvas.translate(0, draggedItem.totalDragOffset); + draggedItem.viewDrawable.draw(canvas); + + final int left = draggedItem.viewDrawable.getBounds().left; + final int right = draggedItem.viewDrawable.getBounds().right; + final int top = draggedItem.viewDrawable.getBounds().top; + final int bottom = draggedItem.viewDrawable.getBounds().bottom; // dragBottomShadowDrawable.setBounds(left, bottom, right, bottom + dragShadowHeight); // dragBottomShadowDrawable.draw(canvas); @@ -734,13 +758,13 @@ public class DragLinearLayout extends LinearLayout // dragTopShadowDrawable.draw(canvas); // } - canvas.restore(); - } - } + canvas.restore(); + } + } /* - * Note regarding touch handling: In general, we have three cases - 1) User taps outside any children. #onInterceptTouchEvent receives DOWN #onTouchEvent + * Note regarding touch handling: In general, we have three cases - 1) User taps outside any children. #onInterceptTouchEvent receives DOWN #onTouchEvent * receives DOWN draggedItem.detecting == false, we return false and no further events are received 2) User taps on non-interactive drag handle / child, * e.g. TextView or ImageView. #onInterceptTouchEvent receives DOWN DragHandleOnTouchListener (attached to each draggable child) #onTouch receives DOWN * #startDetectingDrag is called, draggedItem is now detecting view does not handle touch, so our #onTouchEvent receives DOWN draggedItem.detecting == true, @@ -753,168 +777,188 @@ public class DragLinearLayout extends LinearLayout * #stopDetecting. */ - @Override - public boolean onInterceptTouchEvent(MotionEvent event) - { - switch (MotionEventCompat.getActionMasked(event)) - { - case MotionEvent.ACTION_DOWN: - { - if (draggedItem.detecting) - return false; // an existing item is (likely) settling - downY = (int) MotionEventCompat.getY(event, 0); - activePointerId = MotionEventCompat.getPointerId(event, 0); - break; - } - case MotionEvent.ACTION_MOVE: - { - if (!draggedItem.detecting) - return false; - if (INVALID_POINTER_ID == activePointerId) - break; - final int pointerIndex = event.findPointerIndex(activePointerId); - final float y = MotionEventCompat.getY(event, pointerIndex); - final float dy = y - downY; - if (Math.abs(dy) > slop) - { - startDrag(); - return true; - } - return false; - } - case MotionEvent.ACTION_POINTER_UP: - { - final int pointerIndex = MotionEventCompat.getActionIndex(event); - final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex); - - if (pointerId != activePointerId) - break; // if active pointer, fall through and cancel! - } - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - { - onTouchEnd(); - - if (draggedItem.detecting) - draggedItem.stopDetecting(); - break; - } - default: - break; - } - - return false; - } - - - @Override - public boolean onTouchEvent(@NonNull MotionEvent event) - { - switch (MotionEventCompat.getActionMasked(event)) - { - case MotionEvent.ACTION_DOWN: - { - if (!draggedItem.detecting || draggedItem.settling()) - return false; - startDrag(); - return true; - } - case MotionEvent.ACTION_MOVE: - { - if (!draggedItem.dragging) - break; - if (INVALID_POINTER_ID == activePointerId) - break; - - int pointerIndex = event.findPointerIndex(activePointerId); - int lastEventY = (int) MotionEventCompat.getY(event, pointerIndex); - int deltaY = lastEventY - downY; - - onDrag(deltaY); - return true; - } - case MotionEvent.ACTION_POINTER_UP: - { - final int pointerIndex = MotionEventCompat.getActionIndex(event); - final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex); - - if (pointerId != activePointerId) - break; // if active pointer, fall through and cancel! - } - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - { - onTouchEnd(); - - if (draggedItem.dragging) - { - onDragStop(); - } - else if (draggedItem.detecting) - { - draggedItem.stopDetecting(); - } - return true; - } - default: - break; - } - return false; - } - - - private void onTouchEnd() - { - downY = -1; - activePointerId = INVALID_POINTER_ID; - } - - private class DragHandleOnTouchListener implements OnTouchListener - { - private final View view; - - - public DragHandleOnTouchListener(final View view) - { - this.view = view; - } - - - @Override - public boolean onTouch(View v, MotionEvent event) - { - if (MotionEvent.ACTION_DOWN == MotionEventCompat.getActionMasked(event)) - { - startDetectingDrag(view); - } - return false; - } - } - - - private BitmapDrawable getDragDrawable(View view) - { - int top = view.getTop(); - int left = view.getLeft(); - - Bitmap bitmap = getBitmapFromView(view); - - BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); - - drawable.setBounds(new Rect(left, top, left + view.getWidth(), top + view.getHeight())); - - return drawable; - } - - - /** - * @return a bitmap showing a screenshot of the view passed in. - */ - private static Bitmap getBitmapFromView(View view) - { - Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - view.draw(canvas); - return bitmap; - } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) + { + switch (MotionEventCompat.getActionMasked(event)) + { + case MotionEvent.ACTION_DOWN: + { + if (draggedItem.detecting) + { + return false; // an existing item is (likely) settling + } + downY = (int) MotionEventCompat.getY(event, 0); + activePointerId = MotionEventCompat.getPointerId(event, 0); + break; + } + case MotionEvent.ACTION_MOVE: + { + if (!draggedItem.detecting) + { + return false; + } + if (INVALID_POINTER_ID == activePointerId) + { + break; + } + final int pointerIndex = event.findPointerIndex(activePointerId); + final float y = MotionEventCompat.getY(event, pointerIndex); + final float dy = y - downY; + if (Math.abs(dy) > slop) + { + startDrag(); + return true; + } + return false; + } + case MotionEvent.ACTION_POINTER_UP: + { + final int pointerIndex = MotionEventCompat.getActionIndex(event); + final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex); + + if (pointerId != activePointerId) + { + break; // if active pointer, fall through and cancel! + } + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + { + onTouchEnd(); + + if (draggedItem.detecting) + { + draggedItem.stopDetecting(); + } + break; + } + default: + break; + } + + return false; + } + + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) + { + switch (MotionEventCompat.getActionMasked(event)) + { + case MotionEvent.ACTION_DOWN: + { + if (!draggedItem.detecting || draggedItem.settling()) + { + return false; + } + startDrag(); + return true; + } + case MotionEvent.ACTION_MOVE: + { + if (!draggedItem.dragging) + { + break; + } + if (INVALID_POINTER_ID == activePointerId) + { + break; + } + + int pointerIndex = event.findPointerIndex(activePointerId); + int lastEventY = (int) MotionEventCompat.getY(event, pointerIndex); + int deltaY = lastEventY - downY; + + onDrag(deltaY); + return true; + } + case MotionEvent.ACTION_POINTER_UP: + { + final int pointerIndex = MotionEventCompat.getActionIndex(event); + final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex); + + if (pointerId != activePointerId) + { + break; // if active pointer, fall through and cancel! + } + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + { + onTouchEnd(); + + if (draggedItem.dragging) + { + onDragStop(); + } + else if (draggedItem.detecting) + { + draggedItem.stopDetecting(); + } + return true; + } + default: + break; + } + return false; + } + + + private void onTouchEnd() + { + downY = -1; + activePointerId = INVALID_POINTER_ID; + } + + + private class DragHandleOnTouchListener implements OnTouchListener + { + private final View view; + + + public DragHandleOnTouchListener(final View view) + { + this.view = view; + } + + + @Override + public boolean onTouch(View v, MotionEvent event) + { + if (MotionEvent.ACTION_DOWN == MotionEventCompat.getActionMasked(event)) + { + startDetectingDrag(view); + } + return false; + } + } + + + private BitmapDrawable getDragDrawable(View view) + { + int top = view.getTop(); + int left = view.getLeft(); + + Bitmap bitmap = getBitmapFromView(view); + + BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); + + drawable.setBounds(new Rect(left, top, left + view.getWidth(), top + view.getHeight())); + + return drawable; + } + + + /** + * @return a bitmap showing a screenshot of the view passed in. + */ + private static Bitmap getBitmapFromView(View view) + { + Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + view.draw(canvas); + return bitmap; + } } diff --git a/opentasks/src/main/java/org/dmfs/android/widgets/ColoredShapeCheckBox.java b/opentasks/src/main/java/org/dmfs/android/widgets/ColoredShapeCheckBox.java index 0e3aa12a..a10d2a7f 100644 --- a/opentasks/src/main/java/org/dmfs/android/widgets/ColoredShapeCheckBox.java +++ b/opentasks/src/main/java/org/dmfs/android/widgets/ColoredShapeCheckBox.java @@ -1,7 +1,5 @@ package org.dmfs.android.widgets; -import org.dmfs.tasks.R; - import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -13,10 +11,12 @@ import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; import android.widget.CheckBox; +import org.dmfs.tasks.R; + /** * A checkbox with a colored shape in the background and a check mark (if checked). The check mark is chosen by the lightness of the current color of the shape. - * + * * @author Marten Gajda */ public class ColoredShapeCheckBox extends CheckBox diff --git a/opentasks/src/main/java/org/dmfs/tasks/EditTaskActivity.java b/opentasks/src/main/java/org/dmfs/tasks/EditTaskActivity.java index cc948a4e..ea070144 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/EditTaskActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/EditTaskActivity.java @@ -36,196 +36,196 @@ import java.util.TimeZone; /** * Activity to edit a task. - * + * * @author Arjun Naik * @author Marten Gajda * @author Tobias Reinsch */ public class EditTaskActivity extends ActionBarActivity { - private static final String ACTION_NOTE_TO_SELF = "com.google.android.gm.action.AUTO_SEND"; - - public final static String EXTRA_DATA_BUNDLE = "org.dmfs.extra.BUNDLE"; - - public final static String EXTRA_DATA_CONTENT_SET = "org.dmfs.DATA"; - - public final static String EXTRA_DATA_ACCOUNT_TYPE = "org.dmfs.ACCOUNT_TYPE"; - - private EditTaskFragment mEditFragment; - - private String mAuthority; - - - @TargetApi(11) - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_task_editor); - - mAuthority = TaskContract.taskAuthority(this); - - if (android.os.Build.VERSION.SDK_INT >= 11) - { - // hide up button in action bar - ActionBar actionBar = getSupportActionBar(); - actionBar.setDisplayShowHomeEnabled(false); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setHomeAsUpIndicator(R.drawable.content_remove_light); - // actionBar.setDisplayShowTitleEnabled(false); - } - - if (savedInstanceState == null) - { - - Bundle arguments = new Bundle(); - Intent intent = getIntent(); - String action = intent.getAction(); - - setActivityTitle(action); - - if (Intent.ACTION_SEND.equals(action)) - { - - // load data from incoming share intent - ContentSet sharedContentSet = new ContentSet(Tasks.getContentUri(mAuthority)); - if (intent.hasExtra(Intent.EXTRA_SUBJECT)) - { - sharedContentSet.put(Tasks.TITLE, intent.getStringExtra(Intent.EXTRA_SUBJECT)); - } - if (intent.hasExtra(Intent.EXTRA_TITLE)) - { - sharedContentSet.put(Tasks.TITLE, intent.getStringExtra(Intent.EXTRA_TITLE)); - } - if (intent.hasExtra(Intent.EXTRA_TEXT)) - { - String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); - sharedContentSet.put(Tasks.DESCRIPTION, extraText); - // check if supplied text is a URL - if (extraText.startsWith("http://") && !extraText.contains(" ")) - { - sharedContentSet.put(Tasks.URL, extraText); - } - - } - // hand over shared information to EditTaskFragment - arguments.putParcelable(EditTaskFragment.PARAM_CONTENT_SET, sharedContentSet); - - } - else if (ACTION_NOTE_TO_SELF.equals(action)) - { - // process the note to self intent - ContentSet sharedContentSet = new ContentSet(Tasks.getContentUri(mAuthority)); - - if (intent.hasExtra(Intent.EXTRA_SUBJECT)) - { - sharedContentSet.put(Tasks.DESCRIPTION, intent.getStringExtra(Intent.EXTRA_SUBJECT)); - } - - if (intent.hasExtra(Intent.EXTRA_TEXT)) - { - String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); - sharedContentSet.put(Tasks.TITLE, extraText); - - } - - // add start time stamp - sharedContentSet.put(Tasks.DTSTART, System.currentTimeMillis()); - sharedContentSet.put(Tasks.TZ, TimeZone.getDefault().getID()); - - // hand over shared information to EditTaskFragment - arguments.putParcelable(EditTaskFragment.PARAM_CONTENT_SET, sharedContentSet); - - } - else - { - // hand over task URI for editing / creating empty task - arguments.putParcelable(EditTaskFragment.PARAM_TASK_URI, getIntent().getData()); - Bundle extraBundle = getIntent().getBundleExtra(EXTRA_DATA_BUNDLE); - if (extraBundle != null) - { - ContentSet data = extraBundle.getParcelable(EXTRA_DATA_CONTENT_SET); - if (data != null) - { - arguments.putParcelable(EditTaskFragment.PARAM_CONTENT_SET, data); - } - } - String accountType = getIntent().getStringExtra(EXTRA_DATA_ACCOUNT_TYPE); - if (accountType != null) - { - arguments.putString(EditTaskFragment.PARAM_ACCOUNT_TYPE, accountType); - } - } - - EditTaskFragment fragment = new EditTaskFragment(); - fragment.setArguments(arguments); - getSupportFragmentManager().beginTransaction().add(R.id.add_task_container, fragment).commit(); - - } - - } - - - @Override - public void onAttachFragment(Fragment fragment) - { - super.onAttachFragment(fragment); - if (fragment instanceof EditTaskFragment) - { - mEditFragment = (EditTaskFragment) fragment; - } - else - { - throw new IllegalArgumentException("Invalid fragment attached: " + fragment.getClass().getCanonicalName()); - } - } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) - { - getMenuInflater().inflate(R.menu.edit_task_activity_menu, menu); - return true; - } - - - @Override - public void onBackPressed() - { - super.onBackPressed(); - - if (mEditFragment != null) - { - mEditFragment.saveAndExit(); - } - } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - switch (item.getItemId()) - { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - default: - break; - } - return super.onOptionsItemSelected(item); - } - - - private void setActivityTitle(String action) - { - if (Intent.ACTION_EDIT.equals(action)) - { - setTitle(R.string.activity_edit_task_title); - } - else - { - setTitle(R.string.activity_add_task_title); - } - } + private static final String ACTION_NOTE_TO_SELF = "com.google.android.gm.action.AUTO_SEND"; + + public final static String EXTRA_DATA_BUNDLE = "org.dmfs.extra.BUNDLE"; + + public final static String EXTRA_DATA_CONTENT_SET = "org.dmfs.DATA"; + + public final static String EXTRA_DATA_ACCOUNT_TYPE = "org.dmfs.ACCOUNT_TYPE"; + + private EditTaskFragment mEditFragment; + + private String mAuthority; + + + @TargetApi(11) + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_task_editor); + + mAuthority = TaskContract.taskAuthority(this); + + if (android.os.Build.VERSION.SDK_INT >= 11) + { + // hide up button in action bar + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowHomeEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeAsUpIndicator(R.drawable.content_remove_light); + // actionBar.setDisplayShowTitleEnabled(false); + } + + if (savedInstanceState == null) + { + + Bundle arguments = new Bundle(); + Intent intent = getIntent(); + String action = intent.getAction(); + + setActivityTitle(action); + + if (Intent.ACTION_SEND.equals(action)) + { + + // load data from incoming share intent + ContentSet sharedContentSet = new ContentSet(Tasks.getContentUri(mAuthority)); + if (intent.hasExtra(Intent.EXTRA_SUBJECT)) + { + sharedContentSet.put(Tasks.TITLE, intent.getStringExtra(Intent.EXTRA_SUBJECT)); + } + if (intent.hasExtra(Intent.EXTRA_TITLE)) + { + sharedContentSet.put(Tasks.TITLE, intent.getStringExtra(Intent.EXTRA_TITLE)); + } + if (intent.hasExtra(Intent.EXTRA_TEXT)) + { + String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); + sharedContentSet.put(Tasks.DESCRIPTION, extraText); + // check if supplied text is a URL + if (extraText.startsWith("http://") && !extraText.contains(" ")) + { + sharedContentSet.put(Tasks.URL, extraText); + } + + } + // hand over shared information to EditTaskFragment + arguments.putParcelable(EditTaskFragment.PARAM_CONTENT_SET, sharedContentSet); + + } + else if (ACTION_NOTE_TO_SELF.equals(action)) + { + // process the note to self intent + ContentSet sharedContentSet = new ContentSet(Tasks.getContentUri(mAuthority)); + + if (intent.hasExtra(Intent.EXTRA_SUBJECT)) + { + sharedContentSet.put(Tasks.DESCRIPTION, intent.getStringExtra(Intent.EXTRA_SUBJECT)); + } + + if (intent.hasExtra(Intent.EXTRA_TEXT)) + { + String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); + sharedContentSet.put(Tasks.TITLE, extraText); + + } + + // add start time stamp + sharedContentSet.put(Tasks.DTSTART, System.currentTimeMillis()); + sharedContentSet.put(Tasks.TZ, TimeZone.getDefault().getID()); + + // hand over shared information to EditTaskFragment + arguments.putParcelable(EditTaskFragment.PARAM_CONTENT_SET, sharedContentSet); + + } + else + { + // hand over task URI for editing / creating empty task + arguments.putParcelable(EditTaskFragment.PARAM_TASK_URI, getIntent().getData()); + Bundle extraBundle = getIntent().getBundleExtra(EXTRA_DATA_BUNDLE); + if (extraBundle != null) + { + ContentSet data = extraBundle.getParcelable(EXTRA_DATA_CONTENT_SET); + if (data != null) + { + arguments.putParcelable(EditTaskFragment.PARAM_CONTENT_SET, data); + } + } + String accountType = getIntent().getStringExtra(EXTRA_DATA_ACCOUNT_TYPE); + if (accountType != null) + { + arguments.putString(EditTaskFragment.PARAM_ACCOUNT_TYPE, accountType); + } + } + + EditTaskFragment fragment = new EditTaskFragment(); + fragment.setArguments(arguments); + getSupportFragmentManager().beginTransaction().add(R.id.add_task_container, fragment).commit(); + + } + + } + + + @Override + public void onAttachFragment(Fragment fragment) + { + super.onAttachFragment(fragment); + if (fragment instanceof EditTaskFragment) + { + mEditFragment = (EditTaskFragment) fragment; + } + else + { + throw new IllegalArgumentException("Invalid fragment attached: " + fragment.getClass().getCanonicalName()); + } + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + getMenuInflater().inflate(R.menu.edit_task_activity_menu, menu); + return true; + } + + + @Override + public void onBackPressed() + { + super.onBackPressed(); + + if (mEditFragment != null) + { + mEditFragment.saveAndExit(); + } + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + switch (item.getItemId()) + { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + default: + break; + } + return super.onOptionsItemSelected(item); + } + + + private void setActivityTitle(String action) + { + if (Intent.ACTION_EDIT.equals(action)) + { + setTitle(R.string.activity_edit_task_title); + } + else + { + setTitle(R.string.activity_add_task_title); + } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java index 4206f12d..e7ffad2c 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java @@ -16,32 +16,6 @@ */ package org.dmfs.tasks; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.TimeZone; - -import org.dmfs.android.retentionmagic.SupportFragment; -import org.dmfs.android.retentionmagic.annotations.Parameter; -import org.dmfs.android.retentionmagic.annotations.Retain; -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.TaskContract.TaskLists; -import org.dmfs.provider.tasks.TaskContract.Tasks; -import org.dmfs.tasks.model.CheckListItem; -import org.dmfs.tasks.model.ContentSet; -import org.dmfs.tasks.model.Model; -import org.dmfs.tasks.model.OnContentChangeListener; -import org.dmfs.tasks.model.Sources; -import org.dmfs.tasks.model.TaskFieldAdapters; -import org.dmfs.tasks.utils.ContentValueMapper; -import org.dmfs.tasks.utils.OnModelLoadedListener; -import org.dmfs.tasks.utils.RecentlyUsedLists; -import org.dmfs.tasks.utils.TasksListCursorSpinnerAdapter; -import org.dmfs.tasks.widget.ListenableScrollView; -import org.dmfs.tasks.widget.ListenableScrollView.OnScrollListener; -import org.dmfs.tasks.widget.TaskEdit; - import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; @@ -74,662 +48,696 @@ import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.Toast; +import org.dmfs.android.retentionmagic.SupportFragment; +import org.dmfs.android.retentionmagic.annotations.Parameter; +import org.dmfs.android.retentionmagic.annotations.Retain; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.TaskContract.TaskLists; +import org.dmfs.provider.tasks.TaskContract.Tasks; +import org.dmfs.tasks.model.CheckListItem; +import org.dmfs.tasks.model.ContentSet; +import org.dmfs.tasks.model.Model; +import org.dmfs.tasks.model.OnContentChangeListener; +import org.dmfs.tasks.model.Sources; +import org.dmfs.tasks.model.TaskFieldAdapters; +import org.dmfs.tasks.utils.ContentValueMapper; +import org.dmfs.tasks.utils.OnModelLoadedListener; +import org.dmfs.tasks.utils.RecentlyUsedLists; +import org.dmfs.tasks.utils.TasksListCursorSpinnerAdapter; +import org.dmfs.tasks.widget.ListenableScrollView; +import org.dmfs.tasks.widget.ListenableScrollView.OnScrollListener; +import org.dmfs.tasks.widget.TaskEdit; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TimeZone; + /** * Fragment to edit task details. - * + * * @author Arjun Naik * @author Marten Gajda * @author Tobias Reinsch */ public class EditTaskFragment extends SupportFragment implements LoaderManager.LoaderCallbacks, OnModelLoadedListener, OnContentChangeListener, - OnItemSelectedListener + OnItemSelectedListener { - private static final String TAG = "TaskEditDetailFragment"; - - public static final String PARAM_TASK_URI = "task_uri"; - public static final String PARAM_CONTENT_SET = "task_content_set"; - public static final String PARAM_ACCOUNT_TYPE = "task_account_type"; - - public static final String LIST_LOADER_URI = "uri"; - public static final String LIST_LOADER_FILTER = "filter"; - - public static final String LIST_LOADER_VISIBLE_LISTS_FILTER = TaskLists.SYNC_ENABLED + "=1"; - - public static final String PREFERENCE_LAST_LIST = "pref_last_list_used_for_new_event"; - public static final String PREFERENCE_LAST_ACCOUNT_TYPE = "pref_last_account_type_used_for_new_event"; - - /** - * A set of values that may affect the recurrence set of a task. If one of these values changes we have to submit all of them. - */ - private final static Set RECURRENCE_VALUES = new HashSet(Arrays.asList(new String[] { Tasks.DUE, Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY, - Tasks.RRULE, Tasks.RDATE, Tasks.EXDATE })); - - /** - * Projection into the task list. - */ - private final static String[] TASK_LIST_PROJECTION = new String[] { TaskContract.TaskListColumns._ID, TaskContract.TaskListColumns.LIST_NAME, - TaskContract.TaskListSyncColumns.ACCOUNT_TYPE, TaskContract.TaskListSyncColumns.ACCOUNT_NAME, TaskContract.TaskListColumns.LIST_COLOR }; - - /** - * This interface provides a convenient way to get column indices of {@link #TASK_LIST_PROJECTION} without any overhead. - */ - private interface TASK_LIST_PROJECTION_VALUES - { - public final static int id = 0; - @SuppressWarnings("unused") - public final static int list_name = 1; - public final static int account_type = 2; - @SuppressWarnings("unused") - public final static int account_name = 3; - @SuppressWarnings("unused") - public final static int list_color = 4; - } - - private static final String KEY_VALUES = "key_values"; - - static final ContentValueMapper CONTENT_VALUE_MAPPER = new ContentValueMapper() - .addString(Tasks.ACCOUNT_TYPE, Tasks.ACCOUNT_NAME, Tasks.TITLE, Tasks.LOCATION, Tasks.DESCRIPTION, Tasks.GEO, Tasks.URL, Tasks.TZ, Tasks.DURATION, - Tasks.LIST_NAME) - .addInteger(Tasks.PRIORITY, Tasks.LIST_COLOR, Tasks.TASK_COLOR, Tasks.STATUS, Tasks.CLASSIFICATION, Tasks.PERCENT_COMPLETE, Tasks.IS_ALLDAY, - Tasks.IS_CLOSED, Tasks.PINNED).addLong(Tasks.LIST_ID, Tasks.DTSTART, Tasks.DUE, Tasks.COMPLETED, Tasks._ID); - - private boolean mAppForEdit = true; - private TasksListCursorSpinnerAdapter mTaskListAdapter; - - private Uri mTaskUri; - - private ContentSet mValues; - private ViewGroup mContent; - private ViewGroup mHeader; - private Model mModel; - private Context mAppContext; - private TaskEdit mEditor; - private LinearLayout mTaskListBar; - private Spinner mListSpinner; - private String mAuthority; - private View mColorBar; - - private boolean mRestored; - - private int mListColor = -1; - private ListenableScrollView mRootView; - - @Parameter(key = PARAM_ACCOUNT_TYPE) - private String mAccountType; - - /** - * The id of the list that was selected when we created the last task. - */ - @Retain(key = PREFERENCE_LAST_LIST, classNS = "", permanent = true) - private long mSelectedList = -1; - - /** - * The last account type we added a task to. - */ - @Retain(key = PREFERENCE_LAST_ACCOUNT_TYPE, classNS = "", permanent = true) - private String mLastAccountType = null; - - /** - * A Runnable that updates the view. - */ - private Runnable mUpdateViewRunnable = new Runnable() - { - @Override - public void run() - { - updateView(); - } - }; - - - /** - * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon screen orientation changes). - */ - public EditTaskFragment() - { - } - - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - mAuthority = TaskContract.taskAuthority(activity); - Bundle bundle = getArguments(); - - // check for supplied task information from intent - if (bundle.containsKey(PARAM_CONTENT_SET)) - { - mValues = bundle.getParcelable(PARAM_CONTENT_SET); - if (!mValues.isInsert()) - { - mTaskUri = mValues.getUri(); - } - } - else - { - mTaskUri = bundle.getParcelable(PARAM_TASK_URI); - } - mAppContext = activity.getApplicationContext(); - - } - - - @SuppressWarnings("deprecation") - @TargetApi(16) - @Override - public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { - ListenableScrollView rootView = mRootView = (ListenableScrollView) inflater.inflate(R.layout.fragment_task_edit_detail, container, false); - mContent = (ViewGroup) rootView.findViewById(R.id.content); - mHeader = (ViewGroup) rootView.findViewById(R.id.header); - mColorBar = rootView.findViewById(R.id.headercolorbar); - - mRestored = savedInstanceState != null; - - if (mColorBar != null) - { - mRootView.setOnScrollListener(new OnScrollListener() - { - - @Override - public void onScroll(int oldScrollY, int newScrollY) - { - int headerHeight = mTaskListBar.getMeasuredHeight(); - if (newScrollY <= headerHeight || oldScrollY <= headerHeight) - { - updateColor((float) newScrollY / headerHeight); - } - } - }); - } - mAppForEdit = !Tasks.getContentUri(mAuthority).equals(mTaskUri) && mTaskUri != null; - - mTaskListBar = (LinearLayout) inflater.inflate(R.layout.task_list_provider_bar, mHeader); - mListSpinner = (Spinner) mTaskListBar.findViewById(R.id.task_list_spinner); - - mTaskListAdapter = new TasksListCursorSpinnerAdapter(mAppContext); - mListSpinner.setAdapter(mTaskListAdapter); - - mListSpinner.setOnItemSelectedListener(this); - - if (android.os.Build.VERSION.SDK_INT < 11) - { - mListSpinner.setBackgroundDrawable(null); - } - - if (mAppForEdit) - { - if (mTaskUri != null) - { - if (savedInstanceState == null && mValues == null) - { - mValues = new ContentSet(mTaskUri); - mValues.addOnChangeListener(this, null, false); - - mValues.update(mAppContext, CONTENT_VALUE_MAPPER); - } - else - { - if (savedInstanceState != null) - { - mValues = savedInstanceState.getParcelable(KEY_VALUES); - Sources.loadModelAsync(mAppContext, mValues.getAsString(Tasks.ACCOUNT_TYPE), this); - } - else - { - Sources.loadModelAsync(mAppContext, mValues.getAsString(Tasks.ACCOUNT_TYPE), this); - // ensure we're using the latest values - mValues.update(mAppContext, CONTENT_VALUE_MAPPER); - } - mListColor = TaskFieldAdapters.LIST_COLOR.get(mValues); - // update the color of the action bar as soon as possible - updateColor(0); - setListUri(TaskLists.getContentUri(mAuthority), LIST_LOADER_VISIBLE_LISTS_FILTER); - } - } - } - else - { - if (savedInstanceState == null) - { - // create empty ContentSet if there was no ContentSet supplied - if (mValues == null) - { - mValues = new ContentSet(Tasks.getContentUri(mAuthority)); - // ensure we start with the current time zone - TaskFieldAdapters.TIMEZONE.set(mValues, TimeZone.getDefault()); - } - else - { - // check id the provided content set contains a list and update the selected list if so - Long listId = mValues.getAsLong(Tasks.LIST_ID); - if (listId != null) - { - mSelectedList = listId; - } - } - - if (mLastAccountType != null) - { - Sources.loadModelAsync(mAppContext, mLastAccountType, this); - } - } - else - { - mValues = savedInstanceState.getParcelable(KEY_VALUES); - Sources.loadModelAsync(mAppContext, mLastAccountType, this); - } - setListUri(TaskLists.getContentUri(mAuthority), LIST_LOADER_VISIBLE_LISTS_FILTER); - } - - return rootView; - } - - - @Override - public void onPause() - { - // save values on rotation - if (mEditor != null) - { - mEditor.updateValues(); - } - - super.onPause(); - }; - - - @Override - public void onDestroyView() - { - super.onDestroyView(); - if (mEditor != null) - { - // remove values, to ensure all listeners get released - mEditor.setValues(null); - } - if (mContent != null) - { - mContent.removeAllViews(); - } - - final Spinner listSpinner = (Spinner) mTaskListBar.findViewById(R.id.task_list_spinner); - listSpinner.setOnItemSelectedListener(null); - if (mValues != null) - { - mValues.removeOnChangeListener(this, null); - } - } - - - private void updateView() - { - /* - * If the model loads very slowly then this function may be called after onDetach. In this case check if Activity is null and return if + private static final String TAG = "TaskEditDetailFragment"; + + public static final String PARAM_TASK_URI = "task_uri"; + public static final String PARAM_CONTENT_SET = "task_content_set"; + public static final String PARAM_ACCOUNT_TYPE = "task_account_type"; + + public static final String LIST_LOADER_URI = "uri"; + public static final String LIST_LOADER_FILTER = "filter"; + + public static final String LIST_LOADER_VISIBLE_LISTS_FILTER = TaskLists.SYNC_ENABLED + "=1"; + + public static final String PREFERENCE_LAST_LIST = "pref_last_list_used_for_new_event"; + public static final String PREFERENCE_LAST_ACCOUNT_TYPE = "pref_last_account_type_used_for_new_event"; + + /** + * A set of values that may affect the recurrence set of a task. If one of these values changes we have to submit all of them. + */ + private final static Set RECURRENCE_VALUES = new HashSet(Arrays.asList(new String[] { + Tasks.DUE, Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY, + Tasks.RRULE, Tasks.RDATE, Tasks.EXDATE })); + + /** + * Projection into the task list. + */ + private final static String[] TASK_LIST_PROJECTION = new String[] { + TaskContract.TaskListColumns._ID, TaskContract.TaskListColumns.LIST_NAME, + TaskContract.TaskListSyncColumns.ACCOUNT_TYPE, TaskContract.TaskListSyncColumns.ACCOUNT_NAME, TaskContract.TaskListColumns.LIST_COLOR }; + + + /** + * This interface provides a convenient way to get column indices of {@link #TASK_LIST_PROJECTION} without any overhead. + */ + private interface TASK_LIST_PROJECTION_VALUES + { + public final static int id = 0; + @SuppressWarnings("unused") + public final static int list_name = 1; + public final static int account_type = 2; + @SuppressWarnings("unused") + public final static int account_name = 3; + @SuppressWarnings("unused") + public final static int list_color = 4; + } + + + private static final String KEY_VALUES = "key_values"; + + static final ContentValueMapper CONTENT_VALUE_MAPPER = new ContentValueMapper() + .addString(Tasks.ACCOUNT_TYPE, Tasks.ACCOUNT_NAME, Tasks.TITLE, Tasks.LOCATION, Tasks.DESCRIPTION, Tasks.GEO, Tasks.URL, Tasks.TZ, Tasks.DURATION, + Tasks.LIST_NAME) + .addInteger(Tasks.PRIORITY, Tasks.LIST_COLOR, Tasks.TASK_COLOR, Tasks.STATUS, Tasks.CLASSIFICATION, Tasks.PERCENT_COMPLETE, Tasks.IS_ALLDAY, + Tasks.IS_CLOSED, Tasks.PINNED).addLong(Tasks.LIST_ID, Tasks.DTSTART, Tasks.DUE, Tasks.COMPLETED, Tasks._ID); + + private boolean mAppForEdit = true; + private TasksListCursorSpinnerAdapter mTaskListAdapter; + + private Uri mTaskUri; + + private ContentSet mValues; + private ViewGroup mContent; + private ViewGroup mHeader; + private Model mModel; + private Context mAppContext; + private TaskEdit mEditor; + private LinearLayout mTaskListBar; + private Spinner mListSpinner; + private String mAuthority; + private View mColorBar; + + private boolean mRestored; + + private int mListColor = -1; + private ListenableScrollView mRootView; + + @Parameter(key = PARAM_ACCOUNT_TYPE) + private String mAccountType; + + /** + * The id of the list that was selected when we created the last task. + */ + @Retain(key = PREFERENCE_LAST_LIST, classNS = "", permanent = true) + private long mSelectedList = -1; + + /** + * The last account type we added a task to. + */ + @Retain(key = PREFERENCE_LAST_ACCOUNT_TYPE, classNS = "", permanent = true) + private String mLastAccountType = null; + + /** + * A Runnable that updates the view. + */ + private Runnable mUpdateViewRunnable = new Runnable() + { + @Override + public void run() + { + updateView(); + } + }; + + + /** + * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon screen orientation changes). + */ + public EditTaskFragment() + { + } + + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + + @Override + public void onAttach(Activity activity) + { + super.onAttach(activity); + mAuthority = TaskContract.taskAuthority(activity); + Bundle bundle = getArguments(); + + // check for supplied task information from intent + if (bundle.containsKey(PARAM_CONTENT_SET)) + { + mValues = bundle.getParcelable(PARAM_CONTENT_SET); + if (!mValues.isInsert()) + { + mTaskUri = mValues.getUri(); + } + } + else + { + mTaskUri = bundle.getParcelable(PARAM_TASK_URI); + } + mAppContext = activity.getApplicationContext(); + + } + + + @SuppressWarnings("deprecation") + @TargetApi(16) + @Override + public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + ListenableScrollView rootView = mRootView = (ListenableScrollView) inflater.inflate(R.layout.fragment_task_edit_detail, container, false); + mContent = (ViewGroup) rootView.findViewById(R.id.content); + mHeader = (ViewGroup) rootView.findViewById(R.id.header); + mColorBar = rootView.findViewById(R.id.headercolorbar); + + mRestored = savedInstanceState != null; + + if (mColorBar != null) + { + mRootView.setOnScrollListener(new OnScrollListener() + { + + @Override + public void onScroll(int oldScrollY, int newScrollY) + { + int headerHeight = mTaskListBar.getMeasuredHeight(); + if (newScrollY <= headerHeight || oldScrollY <= headerHeight) + { + updateColor((float) newScrollY / headerHeight); + } + } + }); + } + mAppForEdit = !Tasks.getContentUri(mAuthority).equals(mTaskUri) && mTaskUri != null; + + mTaskListBar = (LinearLayout) inflater.inflate(R.layout.task_list_provider_bar, mHeader); + mListSpinner = (Spinner) mTaskListBar.findViewById(R.id.task_list_spinner); + + mTaskListAdapter = new TasksListCursorSpinnerAdapter(mAppContext); + mListSpinner.setAdapter(mTaskListAdapter); + + mListSpinner.setOnItemSelectedListener(this); + + if (android.os.Build.VERSION.SDK_INT < 11) + { + mListSpinner.setBackgroundDrawable(null); + } + + if (mAppForEdit) + { + if (mTaskUri != null) + { + if (savedInstanceState == null && mValues == null) + { + mValues = new ContentSet(mTaskUri); + mValues.addOnChangeListener(this, null, false); + + mValues.update(mAppContext, CONTENT_VALUE_MAPPER); + } + else + { + if (savedInstanceState != null) + { + mValues = savedInstanceState.getParcelable(KEY_VALUES); + Sources.loadModelAsync(mAppContext, mValues.getAsString(Tasks.ACCOUNT_TYPE), this); + } + else + { + Sources.loadModelAsync(mAppContext, mValues.getAsString(Tasks.ACCOUNT_TYPE), this); + // ensure we're using the latest values + mValues.update(mAppContext, CONTENT_VALUE_MAPPER); + } + mListColor = TaskFieldAdapters.LIST_COLOR.get(mValues); + // update the color of the action bar as soon as possible + updateColor(0); + setListUri(TaskLists.getContentUri(mAuthority), LIST_LOADER_VISIBLE_LISTS_FILTER); + } + } + } + else + { + if (savedInstanceState == null) + { + // create empty ContentSet if there was no ContentSet supplied + if (mValues == null) + { + mValues = new ContentSet(Tasks.getContentUri(mAuthority)); + // ensure we start with the current time zone + TaskFieldAdapters.TIMEZONE.set(mValues, TimeZone.getDefault()); + } + else + { + // check id the provided content set contains a list and update the selected list if so + Long listId = mValues.getAsLong(Tasks.LIST_ID); + if (listId != null) + { + mSelectedList = listId; + } + } + + if (mLastAccountType != null) + { + Sources.loadModelAsync(mAppContext, mLastAccountType, this); + } + } + else + { + mValues = savedInstanceState.getParcelable(KEY_VALUES); + Sources.loadModelAsync(mAppContext, mLastAccountType, this); + } + setListUri(TaskLists.getContentUri(mAuthority), LIST_LOADER_VISIBLE_LISTS_FILTER); + } + + return rootView; + } + + + @Override + public void onPause() + { + // save values on rotation + if (mEditor != null) + { + mEditor.updateValues(); + } + + super.onPause(); + } + + + ; + + + @Override + public void onDestroyView() + { + super.onDestroyView(); + if (mEditor != null) + { + // remove values, to ensure all listeners get released + mEditor.setValues(null); + } + if (mContent != null) + { + mContent.removeAllViews(); + } + + final Spinner listSpinner = (Spinner) mTaskListBar.findViewById(R.id.task_list_spinner); + listSpinner.setOnItemSelectedListener(null); + if (mValues != null) + { + mValues.removeOnChangeListener(this, null); + } + } + + + private void updateView() + { + /* + * If the model loads very slowly then this function may be called after onDetach. In this case check if Activity is null and return if * true. Also return if we don't have values or the values are still loading. */ - Activity activity = getActivity(); - if (activity == null || mValues == null || mValues.isLoading()) - { - return; - } - - final LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - if (mEditor != null) - { - // remove values, to ensure all listeners get released - mEditor.setValues(null); - } - mContent.removeAllViews(); - - mEditor = (TaskEdit) inflater.inflate(R.layout.task_edit, mContent, false); - mEditor.setModel(mModel); - mEditor.setValues(mValues); - mContent.addView(mEditor); - - // update focus to title - String title = mValues.getAsString(Tasks.TITLE); - - // set focus to first element of the editor - if (mEditor != null) - { - mEditor.requestFocus(); - if (title == null || title.length() == 0) - { - // open soft input as there is no title - InputMethodManager imm = (InputMethodManager) this.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) - { - imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); - } - } - } - - updateColor((float) mRootView.getScrollY() / mTaskListBar.getMeasuredHeight()); - } - - - /** - * Update the view. This doesn't call {@link #updateView()} right away, instead it posts it. - */ - private void postUpdateView() - { - if (mContent != null) - { - mContent.post(mUpdateViewRunnable); - } - } - - - @Override - public void onModelLoaded(Model model) - { - if (model == null) - { - Toast.makeText(getActivity(), "Could not load Model", Toast.LENGTH_LONG).show(); - return; - } - if (mModel == null || !mModel.equals(model)) - { - mModel = model; - if (mRestored) - { - // The fragment has been restored from a saved state - // We need to wait until all views are ready, otherwise the new data might get lost and all widgets show their default state (and no data). - postUpdateView(); - } - else - { - // This is the initial update. Just go ahead and update the view right away to ensure the activity comes up with a filled form. - updateView(); - } - } - } - - - @Override - public void onSaveInstanceState(Bundle outState) - { - super.onSaveInstanceState(outState); - outState.putParcelable(KEY_VALUES, mValues); - } - - - @Override - public Loader onCreateLoader(int id, Bundle bundle) - { - return new CursorLoader(mAppContext, (Uri) bundle.getParcelable(LIST_LOADER_URI), TASK_LIST_PROJECTION, bundle.getString(LIST_LOADER_FILTER), null, - null); - } - - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) - { - if (cursor == null || cursor.getCount() == 0) - { - showNoListMessageAndFinish(); - return; - } + Activity activity = getActivity(); + if (activity == null || mValues == null || mValues.isLoading()) + { + return; + } + + final LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + if (mEditor != null) + { + // remove values, to ensure all listeners get released + mEditor.setValues(null); + } + mContent.removeAllViews(); + + mEditor = (TaskEdit) inflater.inflate(R.layout.task_edit, mContent, false); + mEditor.setModel(mModel); + mEditor.setValues(mValues); + mContent.addView(mEditor); + + // update focus to title + String title = mValues.getAsString(Tasks.TITLE); + + // set focus to first element of the editor + if (mEditor != null) + { + mEditor.requestFocus(); + if (title == null || title.length() == 0) + { + // open soft input as there is no title + InputMethodManager imm = (InputMethodManager) this.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) + { + imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); + } + } + } + + updateColor((float) mRootView.getScrollY() / mTaskListBar.getMeasuredHeight()); + } + + + /** + * Update the view. This doesn't call {@link #updateView()} right away, instead it posts it. + */ + private void postUpdateView() + { + if (mContent != null) + { + mContent.post(mUpdateViewRunnable); + } + } + + + @Override + public void onModelLoaded(Model model) + { + if (model == null) + { + Toast.makeText(getActivity(), "Could not load Model", Toast.LENGTH_LONG).show(); + return; + } + if (mModel == null || !mModel.equals(model)) + { + mModel = model; + if (mRestored) + { + // The fragment has been restored from a saved state + // We need to wait until all views are ready, otherwise the new data might get lost and all widgets show their default state (and no data). + postUpdateView(); + } + else + { + // This is the initial update. Just go ahead and update the view right away to ensure the activity comes up with a filled form. + updateView(); + } + } + } + + + @Override + public void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + outState.putParcelable(KEY_VALUES, mValues); + } + + + @Override + public Loader onCreateLoader(int id, Bundle bundle) + { + return new CursorLoader(mAppContext, (Uri) bundle.getParcelable(LIST_LOADER_URI), TASK_LIST_PROJECTION, bundle.getString(LIST_LOADER_FILTER), null, + null); + } + + + @Override + public void onLoadFinished(Loader loader, Cursor cursor) + { + if (cursor == null || cursor.getCount() == 0) + { + showNoListMessageAndFinish(); + return; + } mTaskListAdapter.changeCursor(cursor); - if (cursor != null) - { - if (mAppForEdit) - { - mSelectedList = mValues.getAsLong(Tasks.LIST_ID); - } - // set the list that was used the last time the user created an event - if (mSelectedList != -1) - { - // iterate over all lists and select the one that matches the given id - cursor.moveToFirst(); - while (!cursor.isAfterLast()) - { - Long listId = cursor.getLong(TASK_LIST_PROJECTION_VALUES.id); - if (listId != null && listId == mSelectedList) - { - mListSpinner.setSelection(cursor.getPosition()); - break; - } - cursor.moveToNext(); - } - } - } - } - - private void showNoListMessageAndFinish() - { - Toast.makeText(getContext(), R.string.task_list_selection_empty, Toast.LENGTH_LONG).show(); - FragmentActivity activity = getActivity(); - if (activity != null) - { - activity.finish(); - } - } - - - @Override - public void onLoaderReset(Loader loader) - { - mTaskListAdapter.changeCursor(null); - } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - final int menuId = item.getItemId(); - Activity activity = getActivity(); - if (menuId == R.id.editor_action_save) - { - saveAndExit(); - return true; - } - else if (menuId == R.id.editor_action_cancel) - { - activity.setResult(Activity.RESULT_CANCELED); - activity.finish(); - return true; - } - return false; - } - - - @Override - public void onContentLoaded(ContentSet contentSet) - { - if (contentSet.containsKey(Tasks.ACCOUNT_TYPE)) - { - mListColor = TaskFieldAdapters.LIST_COLOR.get(contentSet); - updateColor((float) mRootView.getScrollY() / mTaskListBar.getMeasuredHeight()); - - if (mAppForEdit) - { - Sources.loadModelAsync(mAppContext, contentSet.getAsString(Tasks.ACCOUNT_TYPE), EditTaskFragment.this); - } + if (cursor != null) + { + if (mAppForEdit) + { + mSelectedList = mValues.getAsLong(Tasks.LIST_ID); + } + // set the list that was used the last time the user created an event + if (mSelectedList != -1) + { + // iterate over all lists and select the one that matches the given id + cursor.moveToFirst(); + while (!cursor.isAfterLast()) + { + Long listId = cursor.getLong(TASK_LIST_PROJECTION_VALUES.id); + if (listId != null && listId == mSelectedList) + { + mListSpinner.setSelection(cursor.getPosition()); + break; + } + cursor.moveToNext(); + } + } + } + } + + + private void showNoListMessageAndFinish() + { + Toast.makeText(getContext(), R.string.task_list_selection_empty, Toast.LENGTH_LONG).show(); + FragmentActivity activity = getActivity(); + if (activity != null) + { + activity.finish(); + } + } + + + @Override + public void onLoaderReset(Loader loader) + { + mTaskListAdapter.changeCursor(null); + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + final int menuId = item.getItemId(); + Activity activity = getActivity(); + if (menuId == R.id.editor_action_save) + { + saveAndExit(); + return true; + } + else if (menuId == R.id.editor_action_cancel) + { + activity.setResult(Activity.RESULT_CANCELED); + activity.finish(); + return true; + } + return false; + } + + + @Override + public void onContentLoaded(ContentSet contentSet) + { + if (contentSet.containsKey(Tasks.ACCOUNT_TYPE)) + { + mListColor = TaskFieldAdapters.LIST_COLOR.get(contentSet); + updateColor((float) mRootView.getScrollY() / mTaskListBar.getMeasuredHeight()); + + if (mAppForEdit) + { + Sources.loadModelAsync(mAppContext, contentSet.getAsString(Tasks.ACCOUNT_TYPE), EditTaskFragment.this); + } /* * Don't start the model loader here, let onItemSelected do that. */ - setListUri(TaskLists.getContentUri(mAuthority), LIST_LOADER_VISIBLE_LISTS_FILTER); - } - - } + setListUri(TaskLists.getContentUri(mAuthority), LIST_LOADER_VISIBLE_LISTS_FILTER); + } + + } + + + private void setListUri(Uri uri, String filter) + { + if (this.isAdded()) + { + Bundle bundle = new Bundle(); + bundle.putParcelable(LIST_LOADER_URI, uri); + bundle.putString(LIST_LOADER_FILTER, filter); + + getLoaderManager().restartLoader(-2, bundle, this); + } + } + + + @Override + public void onContentChanged(ContentSet contentSet) + { + // nothing to do + } + + + @Override + public void onItemSelected(AdapterView arg0, View arg1, int pos, long itemId) + { + Cursor c = (Cursor) arg0.getItemAtPosition(pos); + + String accountType = c.getString(TASK_LIST_PROJECTION_VALUES.account_type); + mListColor = TaskFieldAdapters.LIST_COLOR.get(c); + updateColor((float) mRootView.getScrollY() / mTaskListBar.getMeasuredHeight()); + + if (mEditor != null) + { + mEditor.updateValues(); + } + + long listId = c.getLong(TASK_LIST_PROJECTION_VALUES.id); + mValues.put(Tasks.LIST_ID, listId); + mSelectedList = itemId; + mLastAccountType = c.getString(TASK_LIST_PROJECTION_VALUES.account_type); + + if (mModel == null || !mModel.getAccountType().equals(accountType)) + { + // the model changed, load the new model + Sources.loadModelAsync(mAppContext, accountType, this); + } + else + { + postUpdateView(); + } + } + + + private static int darkenColor(int color) + { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] = hsv[2] * 0.75f; + color = Color.HSVToColor(hsv); + return color; + } - private void setListUri(Uri uri, String filter) - { - if (this.isAdded()) - { - Bundle bundle = new Bundle(); - bundle.putParcelable(LIST_LOADER_URI, uri); - bundle.putString(LIST_LOADER_FILTER, filter); + public int mixColors(int col1, int col2) + { + int r1, g1, b1, r2, g2, b2; + + int a1 = Color.alpha(col1); + + r1 = Color.red(col1); + g1 = Color.green(col1); + b1 = Color.blue(col1); + + r2 = Color.red(col2); + g2 = Color.green(col2); + b2 = Color.blue(col2); + + int r3 = (r1 * a1 + r2 * (255 - a1)) / 255; + int g3 = (g1 * a1 + g2 * (255 - a1)) / 255; + int b3 = (b1 * a1 + b2 * (255 - a1)) / 255; + + return Color.rgb(r3, g3, b3); + } + + + private int getBlendColor(int baseColor, int targetColor, float alpha) + { + int r1, g1, b1, r3, g3, b3; + + if (alpha <= 0) + { + return targetColor; + } + else if (alpha > 254) + { + return targetColor; + } + + r1 = Color.red(baseColor); + g1 = Color.green(baseColor); + b1 = Color.blue(baseColor); - getLoaderManager().restartLoader(-2, bundle, this); - } - } + r3 = Color.red(targetColor); + g3 = Color.green(targetColor); + b3 = Color.blue(targetColor); + int r2 = (int) Math.ceil((Math.max(0, r3 * 255 - r1 * (255 - alpha))) / alpha); + int g2 = (int) Math.ceil((Math.max(0, g3 * 255 - g1 * (255 - alpha))) / alpha); + int b2 = (int) Math.ceil((Math.max(0, b3 * 255 - b1 * (255 - alpha))) / alpha); - @Override - public void onContentChanged(ContentSet contentSet) - { - // nothing to do - } - - - @Override - public void onItemSelected(AdapterView arg0, View arg1, int pos, long itemId) - { - Cursor c = (Cursor) arg0.getItemAtPosition(pos); - - String accountType = c.getString(TASK_LIST_PROJECTION_VALUES.account_type); - mListColor = TaskFieldAdapters.LIST_COLOR.get(c); - updateColor((float) mRootView.getScrollY() / mTaskListBar.getMeasuredHeight()); - - if (mEditor != null) - { - mEditor.updateValues(); - } - - long listId = c.getLong(TASK_LIST_PROJECTION_VALUES.id); - mValues.put(Tasks.LIST_ID, listId); - mSelectedList = itemId; - mLastAccountType = c.getString(TASK_LIST_PROJECTION_VALUES.account_type); - - if (mModel == null || !mModel.getAccountType().equals(accountType)) - { - // the model changed, load the new model - Sources.loadModelAsync(mAppContext, accountType, this); - } - else - { - postUpdateView(); - } - } - - - private static int darkenColor(int color) - { - float[] hsv = new float[3]; - Color.colorToHSV(color, hsv); - hsv[2] = hsv[2] * 0.75f; - color = Color.HSVToColor(hsv); - return color; - } - - - public int mixColors(int col1, int col2) - { - int r1, g1, b1, r2, g2, b2; - - int a1 = Color.alpha(col1); - - r1 = Color.red(col1); - g1 = Color.green(col1); - b1 = Color.blue(col1); - - r2 = Color.red(col2); - g2 = Color.green(col2); - b2 = Color.blue(col2); - - int r3 = (r1 * a1 + r2 * (255 - a1)) / 255; - int g3 = (g1 * a1 + g2 * (255 - a1)) / 255; - int b3 = (b1 * a1 + b2 * (255 - a1)) / 255; - - return Color.rgb(r3, g3, b3); - } - - - private int getBlendColor(int baseColor, int targetColor, float alpha) - { - int r1, g1, b1, r3, g3, b3; - - if (alpha <= 0) - { - return targetColor; - } - else if (alpha > 254) - { - return targetColor; - } - - r1 = Color.red(baseColor); - g1 = Color.green(baseColor); - b1 = Color.blue(baseColor); - - r3 = Color.red(targetColor); - g3 = Color.green(targetColor); - b3 = Color.blue(targetColor); - - int r2 = (int) Math.ceil((Math.max(0, r3 * 255 - r1 * (255 - alpha))) / alpha); - int g2 = (int) Math.ceil((Math.max(0, g3 * 255 - g1 * (255 - alpha))) / alpha); - int b2 = (int) Math.ceil((Math.max(0, b3 * 255 - b1 * (255 - alpha))) / alpha); - - return Color.argb((int) alpha, r2, g2, b2); - } - - - @SuppressLint("NewApi") - private void updateColor(float percentage) - { - if (VERSION.SDK_INT >= 11) - { - if (mColorBar == null) - { - percentage = 1; - } - else - { - percentage = Math.max(0, Math.min(Float.isNaN(percentage) ? 0 : percentage, 1)); - } - - int newColor = getBlendColor(mListColor, darkenColor(mListColor), (int) ((0.5 + 0.5 * percentage) * 255)); - ActionBar actionBar = ((ActionBarActivity) getActivity()).getSupportActionBar(); - actionBar.setBackgroundDrawable(new ColorDrawable(newColor)); - - // this is a workaround to ensure the new color is applied on all devices, some devices show a transparent ActionBar if we don't do that. - actionBar.setDisplayShowTitleEnabled(false); - actionBar.setDisplayShowTitleEnabled(true); - - if (VERSION.SDK_INT >= 21) - { - Window window = getActivity().getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - window.setStatusBarColor(mixColors(newColor, mListColor)); - // window.setNavigationBarColor(mixColors(newColor, mListColor)); - } - } - mTaskListBar.setBackgroundColor(mListColor); - if (mColorBar != null) - { - mColorBar.setBackgroundColor(mListColor); - } - } - - - @Override - public void onNothingSelected(AdapterView arg0) - { - // nothing to do here - } - - - /** - * Persist the current task (if anything has been edited) and close the editor. - */ + return Color.argb((int) alpha, r2, g2, b2); + } + + + @SuppressLint("NewApi") + private void updateColor(float percentage) + { + if (VERSION.SDK_INT >= 11) + { + if (mColorBar == null) + { + percentage = 1; + } + else + { + percentage = Math.max(0, Math.min(Float.isNaN(percentage) ? 0 : percentage, 1)); + } + + int newColor = getBlendColor(mListColor, darkenColor(mListColor), (int) ((0.5 + 0.5 * percentage) * 255)); + ActionBar actionBar = ((ActionBarActivity) getActivity()).getSupportActionBar(); + actionBar.setBackgroundDrawable(new ColorDrawable(newColor)); + + // this is a workaround to ensure the new color is applied on all devices, some devices show a transparent ActionBar if we don't do that. + actionBar.setDisplayShowTitleEnabled(false); + actionBar.setDisplayShowTitleEnabled(true); + + if (VERSION.SDK_INT >= 21) + { + Window window = getActivity().getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(mixColors(newColor, mListColor)); + // window.setNavigationBarColor(mixColors(newColor, mListColor)); + } + } + mTaskListBar.setBackgroundColor(mListColor); + if (mColorBar != null) + { + mColorBar.setBackgroundColor(mListColor); + } + } + + + @Override + public void onNothingSelected(AdapterView arg0) + { + // nothing to do here + } + + + /** + * Persist the current task (if anything has been edited) and close the editor. + */ public void saveAndExit() { // TODO: put that in a background task @@ -786,10 +794,11 @@ public class EditTaskFragment extends SupportFragment implements LoaderManager.L mValues.ensureUpdates(RECURRENCE_VALUES); } - if(mValues.isInsert()) { - // update recently used lists - RecentlyUsedLists.use(getContext(), mValues.getAsLong(Tasks.LIST_ID)); - } + if (mValues.isInsert()) + { + // update recently used lists + RecentlyUsedLists.use(getContext(), mValues.getAsLong(Tasks.LIST_ID)); + } mTaskUri = mValues.persist(activity); diff --git a/opentasks/src/main/java/org/dmfs/tasks/EmptyTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/EmptyTaskFragment.java index 96a70c2e..e8183551 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/EmptyTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/EmptyTaskFragment.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; + import org.dmfs.android.retentionmagic.SupportFragment; diff --git a/opentasks/src/main/java/org/dmfs/tasks/InputTextDialogFragment.java b/opentasks/src/main/java/org/dmfs/tasks/InputTextDialogFragment.java index fc0f42a9..42eb7da5 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/InputTextDialogFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/InputTextDialogFragment.java @@ -17,9 +17,6 @@ package org.dmfs.tasks; -import org.dmfs.android.retentionmagic.SupportDialogFragment; -import org.dmfs.android.retentionmagic.annotations.Parameter; - import android.app.Activity; import android.app.Dialog; import android.content.Context; @@ -40,12 +37,15 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; +import org.dmfs.android.retentionmagic.SupportDialogFragment; +import org.dmfs.android.retentionmagic.annotations.Parameter; + /** * A simple prompt for text input. - *

    + *

    * TODO: Use the style from the support library - * + * * @author Marten Gajda * @author Tristan Heinig */ @@ -77,16 +77,16 @@ public class InputTextDialogFragment extends SupportDialogFragment implements On /** * Creates a {@link InputTextDialogFragment} with the given title and initial text value. - * + * * @param title - * The title of the dialog. + * The title of the dialog. * @param message - * The text of the message field. + * The text of the message field. * @param hint - * The hint of the input field. + * The hint of the input field. * @param initalText - * The initial text in the input field. - * + * The initial text in the input field. + * * @return A new {@link InputTextDialogFragment}. */ public static InputTextDialogFragment newInstance(String title, String hint, String initalText, String message) @@ -104,13 +104,14 @@ public class InputTextDialogFragment extends SupportDialogFragment implements On /** * Creates a {@link InputTextDialogFragment} with the given title and initial text value. - * + * * @param title - * The title of the dialog. + * The title of the dialog. * @param message - * The text of the message field. + * The text of the message field. * @param hint - * The hint of the input field. + * The hint of the input field. + * * @return A new {@link InputTextDialogFragment}. */ public static InputTextDialogFragment newInstance(String title, String hint, String initalText) @@ -121,11 +122,12 @@ public class InputTextDialogFragment extends SupportDialogFragment implements On /** * Creates a {@link InputTextDialogFragment} with the given title and initial text value. - * + * * @param title - * The title of the dialog. + * The title of the dialog. * @param message - * The text of the message field. + * The text of the message field. + * * @return A new {@link InputTextDialogFragment}. */ public static InputTextDialogFragment newInstance(String title, String hint) @@ -136,9 +138,10 @@ public class InputTextDialogFragment extends SupportDialogFragment implements On /** * Creates a {@link InputTextDialogFragment} with the given title and initial text value. - * + * * @param title - * The title of the dialog. + * The title of the dialog. + * * @return A new {@link InputTextDialogFragment}. */ public static InputTextDialogFragment newInstance(String title) @@ -150,10 +153,10 @@ public class InputTextDialogFragment extends SupportDialogFragment implements On @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - + final Context contextThemeWrapperLight = new ContextThemeWrapper(getActivity(), R.style.ThemeOverlay_AppCompat_Light); LayoutInflater localInflater = inflater.cloneInContext(contextThemeWrapperLight); - + View view = localInflater.inflate(R.layout.fragment_input_text_dialog, container); mEditText = (EditText) view.findViewById(android.R.id.input); @@ -266,6 +269,7 @@ public class InputTextDialogFragment extends SupportDialogFragment implements On handleCancel(); } + @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -306,9 +310,10 @@ public class InputTextDialogFragment extends SupportDialogFragment implements On /** * Validates the user input and returns true if the input is valid. - * + * * @param input - * the text of the {@link EditText} field. + * the text of the {@link EditText} field. + * * @return true, if there is user input, otherwise false. */ protected boolean validate(String input) @@ -323,23 +328,22 @@ public class InputTextDialogFragment extends SupportDialogFragment implements On return true; } + /** * Interface to listen to InputTextDialog events. - * + * * @author Tristan Heinig - * */ public interface InputTextListener { /** * Is Called, when the user wants to save his input. - * + * * @param inputText - * the user input. + * the user input. */ void onInputTextChanged(String inputText); - /** * Is Called, when the user want to cancel the input. */ diff --git a/opentasks/src/main/java/org/dmfs/tasks/ManageListActivity.java b/opentasks/src/main/java/org/dmfs/tasks/ManageListActivity.java index 2118b09f..44b24fd2 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ManageListActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ManageListActivity.java @@ -17,14 +17,6 @@ package org.dmfs.tasks; -import org.dmfs.android.colorpicker.ColorPickerActivity; -import org.dmfs.android.colorpicker.palettes.RandomPalette; -import org.dmfs.android.retentionmagic.annotations.Retain; -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.TaskContract.TaskLists; -import org.dmfs.tasks.InputTextDialogFragment.InputTextListener; -import org.dmfs.tasks.utils.ActionBarActivity; - import android.accounts.Account; import android.app.Activity; import android.content.ContentValues; @@ -41,12 +33,19 @@ import android.view.WindowManager.LayoutParams; import android.widget.TextView; import android.widget.Toast; +import org.dmfs.android.colorpicker.ColorPickerActivity; +import org.dmfs.android.colorpicker.palettes.RandomPalette; +import org.dmfs.android.retentionmagic.annotations.Retain; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.TaskContract.TaskLists; +import org.dmfs.tasks.InputTextDialogFragment.InputTextListener; +import org.dmfs.tasks.utils.ActionBarActivity; + /** * Activity to create and edit local task lists. This activity provides an interface to edit the name and the color of a local list. * * @author Tristan Heinig - * */ public class ManageListActivity extends ActionBarActivity implements OnClickListener, InputTextListener, android.content.DialogInterface.OnClickListener { @@ -121,7 +120,7 @@ public class ManageListActivity extends ActionBarActivity implements OnClickList * Initializes the user interface for editing tasks. * * @param savedInstanceState - * saved activity state from {@link #onCreate(Bundle)} + * saved activity state from {@link #onCreate(Bundle)} */ private void initEditing(Bundle savedInstanceState) { @@ -133,9 +132,10 @@ public class ManageListActivity extends ActionBarActivity implements OnClickList if (savedInstanceState == null) { Cursor cursor = getContentResolver().query( - mTaskListUri, - new String[] { TaskContract.TaskLists._ID, TaskContract.TaskLists.LIST_NAME, TaskContract.TaskLists.LIST_COLOR, - TaskContract.TaskLists.ACCOUNT_NAME }, null, null, null); + mTaskListUri, + new String[] { + TaskContract.TaskLists._ID, TaskContract.TaskLists.LIST_NAME, TaskContract.TaskLists.LIST_COLOR, + TaskContract.TaskLists.ACCOUNT_NAME }, null, null, null); if (cursor == null || cursor.getCount() < 1) { setResult(Activity.RESULT_CANCELED); @@ -161,7 +161,7 @@ public class ManageListActivity extends ActionBarActivity implements OnClickList * Initializes the user interface for creating tasks. * * @param savedInstanceState - * saved activity state from {@link #onCreate(Bundle)} + * saved activity state from {@link #onCreate(Bundle)} */ private void initInsert(Bundle savedInstanceState) { @@ -173,7 +173,7 @@ public class ManageListActivity extends ActionBarActivity implements OnClickList if (savedInstanceState == null) { InputTextDialogFragment dialog = InputTextDialogFragment.newInstance(getString(R.string.task_list_name_dialog_title), - getString(R.string.task_list_name_dialog_hint), null, getString(R.string.task_list_no_sync)); + getString(R.string.task_list_name_dialog_hint), null, getString(R.string.task_list_no_sync)); dialog.show(getSupportFragmentManager(), null); } if (mListColor == NO_COLOR) @@ -207,8 +207,8 @@ public class ManageListActivity extends ActionBarActivity implements OnClickList if (android.R.id.button2 == v.getId()) { final AlertDialog dialog = new AlertDialog.Builder(this).setTitle(getString(R.string.task_list_delete_dialog_title, mListName)) - .setMessage(R.string.task_list_delete_dialog_message).setPositiveButton(R.string.activity_manage_list_btn_delete, this) - .setNegativeButton(android.R.string.cancel, this).create(); + .setMessage(R.string.task_list_delete_dialog_message).setPositiveButton(R.string.activity_manage_list_btn_delete, this) + .setNegativeButton(android.R.string.cancel, this).create(); // changes the color of the delete list button to red dialog.setOnShowListener(new OnShowListener() { @@ -230,7 +230,7 @@ public class ManageListActivity extends ActionBarActivity implements OnClickList if (R.id.name_setting == v.getId()) { InputTextDialogFragment dialog = InputTextDialogFragment.newInstance(getString(R.string.task_list_name_dialog_title), - getString(R.string.task_list_name_dialog_hint), mNameView.getText().toString()); + getString(R.string.task_list_name_dialog_hint), mNameView.getText().toString()); dialog.show(getSupportFragmentManager(), null); return; } @@ -262,8 +262,9 @@ public class ManageListActivity extends ActionBarActivity implements OnClickList values.put(TaskLists.SYNC_ENABLED, 1); values.put(TaskLists.OWNER, ""); getContentResolver().insert( - mTaskListUri.buildUpon().appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "1") - .appendQueryParameter(TaskContract.ACCOUNT_TYPE, mAccount.type).appendQueryParameter(TaskContract.ACCOUNT_NAME, mAccount.name).build(), values); + mTaskListUri.buildUpon().appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "1") + .appendQueryParameter(TaskContract.ACCOUNT_TYPE, mAccount.type).appendQueryParameter(TaskContract.ACCOUNT_NAME, mAccount.name).build(), + values); setResult(Activity.RESULT_OK); finish(); } @@ -282,9 +283,10 @@ public class ManageListActivity extends ActionBarActivity implements OnClickList values.put(TaskLists.SYNC_ENABLED, 1); values.put(TaskLists.OWNER, ""); int count = getContentResolver().update( - mTaskListUri.buildUpon().appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "1") - .appendQueryParameter(TaskContract.ACCOUNT_TYPE, mAccount.type).appendQueryParameter(TaskContract.ACCOUNT_NAME, mAccount.name).build(), values, - null, null); + mTaskListUri.buildUpon().appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "1") + .appendQueryParameter(TaskContract.ACCOUNT_TYPE, mAccount.type).appendQueryParameter(TaskContract.ACCOUNT_NAME, mAccount.name).build(), + values, + null, null); if (count > 0) { setResult(Activity.RESULT_OK); @@ -303,9 +305,10 @@ public class ManageListActivity extends ActionBarActivity implements OnClickList private void deleteList() { int count = getContentResolver().delete( - mTaskListUri.buildUpon().appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "1") - .appendQueryParameter(TaskContract.ACCOUNT_TYPE, mAccount.type).appendQueryParameter(TaskContract.ACCOUNT_NAME, mAccount.name).build(), null, - null); + mTaskListUri.buildUpon().appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "1") + .appendQueryParameter(TaskContract.ACCOUNT_TYPE, mAccount.type).appendQueryParameter(TaskContract.ACCOUNT_NAME, mAccount.name).build(), + null, + null); if (count > 0) { setResult(Activity.RESULT_OK); diff --git a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java index 0fd0d937..41d469db 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java @@ -17,17 +17,6 @@ package org.dmfs.tasks; -import org.dmfs.android.retentionmagic.SupportDialogFragment; -import org.dmfs.android.retentionmagic.annotations.Parameter; -import org.dmfs.android.retentionmagic.annotations.Retain; -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.TaskContract.TaskLists; -import org.dmfs.provider.tasks.TaskContract.Tasks; -import org.dmfs.tasks.model.ContentSet; -import org.dmfs.tasks.model.TaskFieldAdapters; -import org.dmfs.tasks.utils.RecentlyUsedLists; -import org.dmfs.tasks.utils.TasksListCursorSpinnerAdapter; - import android.annotation.TargetApi; import android.app.Dialog; import android.content.Context; @@ -60,6 +49,17 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; +import org.dmfs.android.retentionmagic.SupportDialogFragment; +import org.dmfs.android.retentionmagic.annotations.Parameter; +import org.dmfs.android.retentionmagic.annotations.Retain; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.TaskContract.TaskLists; +import org.dmfs.provider.tasks.TaskContract.Tasks; +import org.dmfs.tasks.model.ContentSet; +import org.dmfs.tasks.model.TaskFieldAdapters; +import org.dmfs.tasks.utils.RecentlyUsedLists; +import org.dmfs.tasks.utils.TasksListCursorSpinnerAdapter; + /** * A quick add dialog. It allows the user to enter a new task without having to deal with the full blown editor interface. At present it support task with a @@ -68,465 +68,471 @@ import android.widget.TextView.OnEditorActionListener; * @author Marten Gajda */ public class QuickAddDialogFragment extends SupportDialogFragment - implements OnEditorActionListener, LoaderManager.LoaderCallbacks, OnItemSelectedListener, OnClickListener, TextWatcher + implements OnEditorActionListener, LoaderManager.LoaderCallbacks, OnItemSelectedListener, OnClickListener, TextWatcher { - /** - * The minimal duration for the "Task completed" info to be visible - */ - private final static int COMPLETION_DELAY_BASE = 500; // ms - - /** - * The maximum time to add for the first time the "Task completed" info is shown. - */ - private final static int COMPLETION_DELAY_MAX = 1500; // ms - - private final static String ARG_LIST_ID = "list_id"; - private final static String ARG_CONTENT = "content"; - - public static final String LIST_LOADER_URI = "uri"; - public static final String LIST_LOADER_FILTER = "filter"; - - public static final String LIST_LOADER_VISIBLE_LISTS_FILTER = TaskLists.SYNC_ENABLED + "=1"; - - /** - * Projection into the task list. - */ - private final static String[] TASK_LIST_PROJECTION = new String[] { TaskContract.TaskListColumns._ID, TaskContract.TaskListColumns.LIST_NAME, - TaskContract.TaskListSyncColumns.ACCOUNT_TYPE, TaskContract.TaskListSyncColumns.ACCOUNT_NAME, TaskContract.TaskListColumns.LIST_COLOR }; - - /** - * This interface provides a convenient way to get column indices of {@link #TASK_LIST_PROJECTION} without any overhead. - */ - private interface TASK_LIST_PROJECTION_VALUES - { - public final static int id = 0; - @SuppressWarnings("unused") - public final static int list_name = 1; - @SuppressWarnings("unused") - public final static int account_type = 2; - @SuppressWarnings("unused") - public final static int account_name = 3; - @SuppressWarnings("unused") - public final static int list_color = 4; - } - - public interface OnTextInputListener - { - void onTextInput(String inputText); - } - - @Parameter(key = ARG_LIST_ID) - private long mListId = -1; - - @Parameter(key = ARG_CONTENT) - private ContentSet mInitialContent; - - @Retain(permanent = true, key = "quick_add_list_id", classNS = "") - private long mSelectedListId = -1; - - @Retain - private int mLastColor = Color.WHITE; - - @Retain(permanent = true, key = "quick_add_save_count", classNS = "") - private int mSaveCounter = 0; - - private boolean mClosing; - - private View mColorBackground; - private Spinner mListSpinner; - - private EditText mEditText; - private View mConfirmation; - private View mContent; - - private View mSaveButton; - private View mSaveAndNextButton; - - private TasksListCursorSpinnerAdapter mTaskListAdapter; - - private String mAuthority; - - - /** - * Create a {@link QuickAddDialogFragment} with the given title and initial text value. - * - * @param titleId - * The resource id of the title. - * @param initalText - * The initial text in the input field. - * @return A new {@link QuickAddDialogFragment}. - */ - public static QuickAddDialogFragment newInstance(long listId) - { - QuickAddDialogFragment fragment = new QuickAddDialogFragment(); - Bundle args = new Bundle(); - args.putLong(ARG_LIST_ID, listId); - fragment.setArguments(args); - return fragment; - } - - - /** - * Create a {@link QuickAddDialogFragment} with the given title and initial text value. - * - * @param titleId - * The resource id of the title. - * @param initalText - * The initial text in the input field. - * @return A new {@link QuickAddDialogFragment}. - */ - public static QuickAddDialogFragment newInstance(ContentSet content) - { - QuickAddDialogFragment fragment = new QuickAddDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_CONTENT, content); - args.putLong(ARG_LIST_ID, -1); - fragment.setArguments(args); - return fragment; - } - - - public QuickAddDialogFragment() - { - } - - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) - { - Dialog dialog = super.onCreateDialog(savedInstanceState); - - // hide the actual dialog title, we have our own... - dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); - return dialog; - } - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { - - // create ContextThemeWrapper from the original Activity Context with the custom theme - final Context contextThemeWrapperLight = new ContextThemeWrapper(getActivity(), R.style.ThemeOverlay_AppCompat_Light); - final Context contextThemeWrapperDark = new ContextThemeWrapper(getActivity(), R.style.Base_Theme_AppCompat); - - LayoutInflater localInflater = inflater.cloneInContext(contextThemeWrapperLight); - View view = localInflater.inflate(R.layout.fragment_quick_add_dialog, container); - - ViewGroup headerContainer = (ViewGroup) view.findViewById(R.id.header_container); - localInflater = inflater.cloneInContext(contextThemeWrapperDark); - localInflater.inflate(R.layout.fragment_quick_add_dialog_header, headerContainer); - - if (savedInstanceState == null) - { - if (mListId >= 0) - { - mSelectedListId = mListId; - } - } - - mColorBackground = view.findViewById(R.id.color_background); - mColorBackground.setBackgroundColor(mLastColor); - - mListSpinner = (Spinner) view.findViewById(R.id.task_list_spinner); - mTaskListAdapter = new TasksListCursorSpinnerAdapter(getActivity(), R.layout.list_spinner_item_selected_quick_add, R.layout.list_spinner_item_dropdown); - mListSpinner.setAdapter(mTaskListAdapter); - mListSpinner.setOnItemSelectedListener(this); - - mEditText = (EditText) view.findViewById(android.R.id.input); - mEditText.requestFocus(); - getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE); - mEditText.setOnEditorActionListener(this); - mEditText.addTextChangedListener(this); - - mConfirmation = view.findViewById(R.id.created_confirmation); - mContent = view.findViewById(R.id.content); - - mSaveButton = view.findViewById(android.R.id.button1); - mSaveButton.setOnClickListener(this); - mSaveAndNextButton = view.findViewById(android.R.id.button2); - mSaveAndNextButton.setOnClickListener(this); - view.findViewById(android.R.id.edit).setOnClickListener(this); - - mAuthority = TaskContract.taskAuthority(getActivity()); - - afterTextChanged(mEditText.getEditableText()); - - setListUri(TaskLists.getContentUri(mAuthority), LIST_LOADER_VISIBLE_LISTS_FILTER); - - return view; - } - - - @Override - public Loader onCreateLoader(int id, Bundle bundle) - { - return new CursorLoader(getActivity(), (Uri) bundle.getParcelable(LIST_LOADER_URI), TASK_LIST_PROJECTION, bundle.getString(LIST_LOADER_FILTER), null, - null); - } - - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) - { - mTaskListAdapter.changeCursor(cursor); - if (cursor != null) - { - if (mSelectedListId == -1) - { - mSelectedListId = mListId; - } - // set the list that was used the last time the user created an event - // iterate over all lists and select the one that matches the given id - cursor.moveToFirst(); - while (!cursor.isAfterLast()) - { - long listId = cursor.getLong(TASK_LIST_PROJECTION_VALUES.id); - if (listId == mSelectedListId) - { - mListSpinner.setSelection(cursor.getPosition()); - break; - } - cursor.moveToNext(); - } - } - } - - - @Override - public void onLoaderReset(Loader loader) - { - mTaskListAdapter.changeCursor(null); - } - - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) - { - if (EditorInfo.IME_ACTION_DONE == actionId) - { - notifyUser(true /* close afterwards */); - createTask(); - return true; - } - return false; - } - - - private void setListUri(Uri uri, String filter) - { - if (this.isAdded()) - { - Bundle bundle = new Bundle(); - bundle.putParcelable(LIST_LOADER_URI, uri); - bundle.putString(LIST_LOADER_FILTER, filter); - - getLoaderManager().restartLoader(-2, bundle, this); - } - } - - - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) - { - Cursor c = (Cursor) parent.getItemAtPosition(position); - mLastColor = TaskFieldAdapters.LIST_COLOR.get(c); - mColorBackground.setBackgroundColor(mLastColor); - mSelectedListId = id; - } - - - @Override - public void onNothingSelected(AdapterView parent) - { - } - - - /** - * Launch the task editor activity. - */ - private void editTask() - { - Intent intent = new Intent(Intent.ACTION_INSERT); - intent.setData(Tasks.getContentUri(mAuthority)); - Bundle extraBundle = new Bundle(); - extraBundle.putParcelable(EditTaskActivity.EXTRA_DATA_CONTENT_SET, buildContentSet()); - intent.putExtra(EditTaskActivity.EXTRA_DATA_BUNDLE, extraBundle); - getActivity().startActivity(intent); - } - - - /** - * Store the task. - */ - private void createTask() - { - ContentSet content = buildContentSet(); - RecentlyUsedLists.use(getContext(), content.getAsLong(Tasks.LIST_ID)); // update recently used lists - content.persist(getActivity()); - } - - - private ContentSet buildContentSet() - { - ContentSet task; - if (mInitialContent != null) - { - task = new ContentSet(mInitialContent); - } - else - { - task = new ContentSet(Tasks.getContentUri(mAuthority)); - } - task.put(Tasks.LIST_ID, mListSpinner.getSelectedItemId()); - TaskFieldAdapters.TITLE.set(task, mEditText.getText().toString()); - return task; - } - - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - @Override - public void onClick(View v) - { - int id = v.getId(); - mSaveButton.setEnabled(false); - mSaveAndNextButton.setEnabled(false); - - if (id == android.R.id.button1) - { - // "save" pressed - notifyUser(true /* close afterwards */); - createTask(); - } - else if (id == android.R.id.button2) - { - // "save and continue" pressed - notifyUser(false /* reset view */); - createTask(); - } - else if (id == android.R.id.edit) - { - // "edit" pressed - editTask(); - dismiss(); - } - } - - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) - { - } - - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) - { - } - - - @Override - public void afterTextChanged(Editable s) - { - // disable buttons if there is no title - boolean enabled = s == null || s.length() != 0; - mSaveButton.setEnabled(enabled); - mSaveAndNextButton.setEnabled(enabled); - } - - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - private void notifyUser(boolean close) - { - if (VERSION.SDK_INT >= 14) - { - mContent.animate().alpha(0).setDuration(250).start(); - mConfirmation.setAlpha(0); - mConfirmation.setVisibility(View.VISIBLE); - mConfirmation.animate().alpha(1).setDuration(250).start(); - } - else - { - mContent.setVisibility(View.INVISIBLE); - mConfirmation.setVisibility(View.VISIBLE); - } - - if (close) - { - delayedDismiss(); - } - else - { - // We use a dynamic duration. When you hit "save & continue" for the very first time we use a rather long delay, that gets closer to - // COMPLETION_DELAY_BASE with every time you do that. - int duration = COMPLETION_DELAY_BASE + COMPLETION_DELAY_MAX / ++mSaveCounter; - mContent.postDelayed(mReset, duration); - } - } - - - private void delayedDismiss() - { - mContent.postDelayed(mDismiss, 1000); - mClosing = true; - } - - - @Override - public void onPause() - { - super.onPause(); - if (mClosing) - { - mContent.removeCallbacks(mDismiss); - dismiss(); - } - } - - - /** - * A runnable that closes the dialog. - */ - private final Runnable mDismiss = new Runnable() - { - @Override - public void run() - { - dismiss(); - } - }; - - /** - * A {@link Runnable} that resets the editor view. - */ - private final Runnable mReset = new Runnable() - { - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - @Override - public void run() - { - if (VERSION.SDK_INT >= 14) - { - mContent.animate().alpha(1).setDuration(250).start(); - mConfirmation.animate().alpha(0).setDuration(250).start(); - } - else - { - mContent.setVisibility(View.VISIBLE); - mConfirmation.setVisibility(View.INVISIBLE); - } - mSaveButton.setEnabled(true); - mSaveAndNextButton.setEnabled(true); - - // reset view - mEditText.selectAll(); - - // bring the keyboard up again - mEditText.requestFocus(); - InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.showSoftInput(mEditText, 0); - } - }; + /** + * The minimal duration for the "Task completed" info to be visible + */ + private final static int COMPLETION_DELAY_BASE = 500; // ms + + /** + * The maximum time to add for the first time the "Task completed" info is shown. + */ + private final static int COMPLETION_DELAY_MAX = 1500; // ms + + private final static String ARG_LIST_ID = "list_id"; + private final static String ARG_CONTENT = "content"; + + public static final String LIST_LOADER_URI = "uri"; + public static final String LIST_LOADER_FILTER = "filter"; + + public static final String LIST_LOADER_VISIBLE_LISTS_FILTER = TaskLists.SYNC_ENABLED + "=1"; + + /** + * Projection into the task list. + */ + private final static String[] TASK_LIST_PROJECTION = new String[] { + TaskContract.TaskListColumns._ID, TaskContract.TaskListColumns.LIST_NAME, + TaskContract.TaskListSyncColumns.ACCOUNT_TYPE, TaskContract.TaskListSyncColumns.ACCOUNT_NAME, TaskContract.TaskListColumns.LIST_COLOR }; + + + /** + * This interface provides a convenient way to get column indices of {@link #TASK_LIST_PROJECTION} without any overhead. + */ + private interface TASK_LIST_PROJECTION_VALUES + { + public final static int id = 0; + @SuppressWarnings("unused") + public final static int list_name = 1; + @SuppressWarnings("unused") + public final static int account_type = 2; + @SuppressWarnings("unused") + public final static int account_name = 3; + @SuppressWarnings("unused") + public final static int list_color = 4; + } + + + public interface OnTextInputListener + { + void onTextInput(String inputText); + } + + + @Parameter(key = ARG_LIST_ID) + private long mListId = -1; + + @Parameter(key = ARG_CONTENT) + private ContentSet mInitialContent; + + @Retain(permanent = true, key = "quick_add_list_id", classNS = "") + private long mSelectedListId = -1; + + @Retain + private int mLastColor = Color.WHITE; + + @Retain(permanent = true, key = "quick_add_save_count", classNS = "") + private int mSaveCounter = 0; + + private boolean mClosing; + + private View mColorBackground; + private Spinner mListSpinner; + + private EditText mEditText; + private View mConfirmation; + private View mContent; + + private View mSaveButton; + private View mSaveAndNextButton; + + private TasksListCursorSpinnerAdapter mTaskListAdapter; + + private String mAuthority; + + + /** + * Create a {@link QuickAddDialogFragment} with the given title and initial text value. + * + * @param titleId + * The resource id of the title. + * @param initalText + * The initial text in the input field. + * + * @return A new {@link QuickAddDialogFragment}. + */ + public static QuickAddDialogFragment newInstance(long listId) + { + QuickAddDialogFragment fragment = new QuickAddDialogFragment(); + Bundle args = new Bundle(); + args.putLong(ARG_LIST_ID, listId); + fragment.setArguments(args); + return fragment; + } + + + /** + * Create a {@link QuickAddDialogFragment} with the given title and initial text value. + * + * @param titleId + * The resource id of the title. + * @param initalText + * The initial text in the input field. + * + * @return A new {@link QuickAddDialogFragment}. + */ + public static QuickAddDialogFragment newInstance(ContentSet content) + { + QuickAddDialogFragment fragment = new QuickAddDialogFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_CONTENT, content); + args.putLong(ARG_LIST_ID, -1); + fragment.setArguments(args); + return fragment; + } + + + public QuickAddDialogFragment() + { + } + + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) + { + Dialog dialog = super.onCreateDialog(savedInstanceState); + + // hide the actual dialog title, we have our own... + dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); + return dialog; + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + + // create ContextThemeWrapper from the original Activity Context with the custom theme + final Context contextThemeWrapperLight = new ContextThemeWrapper(getActivity(), R.style.ThemeOverlay_AppCompat_Light); + final Context contextThemeWrapperDark = new ContextThemeWrapper(getActivity(), R.style.Base_Theme_AppCompat); + + LayoutInflater localInflater = inflater.cloneInContext(contextThemeWrapperLight); + View view = localInflater.inflate(R.layout.fragment_quick_add_dialog, container); + + ViewGroup headerContainer = (ViewGroup) view.findViewById(R.id.header_container); + localInflater = inflater.cloneInContext(contextThemeWrapperDark); + localInflater.inflate(R.layout.fragment_quick_add_dialog_header, headerContainer); + + if (savedInstanceState == null) + { + if (mListId >= 0) + { + mSelectedListId = mListId; + } + } + + mColorBackground = view.findViewById(R.id.color_background); + mColorBackground.setBackgroundColor(mLastColor); + + mListSpinner = (Spinner) view.findViewById(R.id.task_list_spinner); + mTaskListAdapter = new TasksListCursorSpinnerAdapter(getActivity(), R.layout.list_spinner_item_selected_quick_add, R.layout.list_spinner_item_dropdown); + mListSpinner.setAdapter(mTaskListAdapter); + mListSpinner.setOnItemSelectedListener(this); + + mEditText = (EditText) view.findViewById(android.R.id.input); + mEditText.requestFocus(); + getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE); + mEditText.setOnEditorActionListener(this); + mEditText.addTextChangedListener(this); + + mConfirmation = view.findViewById(R.id.created_confirmation); + mContent = view.findViewById(R.id.content); + + mSaveButton = view.findViewById(android.R.id.button1); + mSaveButton.setOnClickListener(this); + mSaveAndNextButton = view.findViewById(android.R.id.button2); + mSaveAndNextButton.setOnClickListener(this); + view.findViewById(android.R.id.edit).setOnClickListener(this); + + mAuthority = TaskContract.taskAuthority(getActivity()); + + afterTextChanged(mEditText.getEditableText()); + + setListUri(TaskLists.getContentUri(mAuthority), LIST_LOADER_VISIBLE_LISTS_FILTER); + + return view; + } + + + @Override + public Loader onCreateLoader(int id, Bundle bundle) + { + return new CursorLoader(getActivity(), (Uri) bundle.getParcelable(LIST_LOADER_URI), TASK_LIST_PROJECTION, bundle.getString(LIST_LOADER_FILTER), null, + null); + } + + + @Override + public void onLoadFinished(Loader loader, Cursor cursor) + { + mTaskListAdapter.changeCursor(cursor); + if (cursor != null) + { + if (mSelectedListId == -1) + { + mSelectedListId = mListId; + } + // set the list that was used the last time the user created an event + // iterate over all lists and select the one that matches the given id + cursor.moveToFirst(); + while (!cursor.isAfterLast()) + { + long listId = cursor.getLong(TASK_LIST_PROJECTION_VALUES.id); + if (listId == mSelectedListId) + { + mListSpinner.setSelection(cursor.getPosition()); + break; + } + cursor.moveToNext(); + } + } + } + + + @Override + public void onLoaderReset(Loader loader) + { + mTaskListAdapter.changeCursor(null); + } + + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) + { + if (EditorInfo.IME_ACTION_DONE == actionId) + { + notifyUser(true /* close afterwards */); + createTask(); + return true; + } + return false; + } + + + private void setListUri(Uri uri, String filter) + { + if (this.isAdded()) + { + Bundle bundle = new Bundle(); + bundle.putParcelable(LIST_LOADER_URI, uri); + bundle.putString(LIST_LOADER_FILTER, filter); + + getLoaderManager().restartLoader(-2, bundle, this); + } + } + + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) + { + Cursor c = (Cursor) parent.getItemAtPosition(position); + mLastColor = TaskFieldAdapters.LIST_COLOR.get(c); + mColorBackground.setBackgroundColor(mLastColor); + mSelectedListId = id; + } + + + @Override + public void onNothingSelected(AdapterView parent) + { + } + + + /** + * Launch the task editor activity. + */ + private void editTask() + { + Intent intent = new Intent(Intent.ACTION_INSERT); + intent.setData(Tasks.getContentUri(mAuthority)); + Bundle extraBundle = new Bundle(); + extraBundle.putParcelable(EditTaskActivity.EXTRA_DATA_CONTENT_SET, buildContentSet()); + intent.putExtra(EditTaskActivity.EXTRA_DATA_BUNDLE, extraBundle); + getActivity().startActivity(intent); + } + + + /** + * Store the task. + */ + private void createTask() + { + ContentSet content = buildContentSet(); + RecentlyUsedLists.use(getContext(), content.getAsLong(Tasks.LIST_ID)); // update recently used lists + content.persist(getActivity()); + } + + + private ContentSet buildContentSet() + { + ContentSet task; + if (mInitialContent != null) + { + task = new ContentSet(mInitialContent); + } + else + { + task = new ContentSet(Tasks.getContentUri(mAuthority)); + } + task.put(Tasks.LIST_ID, mListSpinner.getSelectedItemId()); + TaskFieldAdapters.TITLE.set(task, mEditText.getText().toString()); + return task; + } + + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onClick(View v) + { + int id = v.getId(); + mSaveButton.setEnabled(false); + mSaveAndNextButton.setEnabled(false); + + if (id == android.R.id.button1) + { + // "save" pressed + notifyUser(true /* close afterwards */); + createTask(); + } + else if (id == android.R.id.button2) + { + // "save and continue" pressed + notifyUser(false /* reset view */); + createTask(); + } + else if (id == android.R.id.edit) + { + // "edit" pressed + editTask(); + dismiss(); + } + } + + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) + { + } + + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) + { + } + + + @Override + public void afterTextChanged(Editable s) + { + // disable buttons if there is no title + boolean enabled = s == null || s.length() != 0; + mSaveButton.setEnabled(enabled); + mSaveAndNextButton.setEnabled(enabled); + } + + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private void notifyUser(boolean close) + { + if (VERSION.SDK_INT >= 14) + { + mContent.animate().alpha(0).setDuration(250).start(); + mConfirmation.setAlpha(0); + mConfirmation.setVisibility(View.VISIBLE); + mConfirmation.animate().alpha(1).setDuration(250).start(); + } + else + { + mContent.setVisibility(View.INVISIBLE); + mConfirmation.setVisibility(View.VISIBLE); + } + + if (close) + { + delayedDismiss(); + } + else + { + // We use a dynamic duration. When you hit "save & continue" for the very first time we use a rather long delay, that gets closer to + // COMPLETION_DELAY_BASE with every time you do that. + int duration = COMPLETION_DELAY_BASE + COMPLETION_DELAY_MAX / ++mSaveCounter; + mContent.postDelayed(mReset, duration); + } + } + + + private void delayedDismiss() + { + mContent.postDelayed(mDismiss, 1000); + mClosing = true; + } + + + @Override + public void onPause() + { + super.onPause(); + if (mClosing) + { + mContent.removeCallbacks(mDismiss); + dismiss(); + } + } + + + /** + * A runnable that closes the dialog. + */ + private final Runnable mDismiss = new Runnable() + { + @Override + public void run() + { + dismiss(); + } + }; + + /** + * A {@link Runnable} that resets the editor view. + */ + private final Runnable mReset = new Runnable() + { + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void run() + { + if (VERSION.SDK_INT >= 14) + { + mContent.animate().alpha(1).setDuration(250).start(); + mConfirmation.animate().alpha(0).setDuration(250).start(); + } + else + { + mContent.setVisibility(View.VISIBLE); + mConfirmation.setVisibility(View.INVISIBLE); + } + mSaveButton.setEnabled(true); + mSaveAndNextButton.setEnabled(true); + + // reset view + mEditText.selectAll(); + + // bring the keyboard up again + mEditText.requestFocus(); + InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.showSoftInput(mEditText, 0); + } + }; } diff --git a/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java b/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java index bbf258c9..0a089141 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java @@ -17,16 +17,6 @@ package org.dmfs.tasks; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - -import org.dmfs.android.widgets.ColoredShapeCheckBox; -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.tasks.model.Model; -import org.dmfs.tasks.model.Sources; - import android.accounts.Account; import android.app.Activity; import android.content.ActivityNotFoundException; @@ -57,533 +47,545 @@ import android.widget.BaseAdapter; import android.widget.TextView; import android.widget.Toast; +import org.dmfs.android.widgets.ColoredShapeCheckBox; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.tasks.model.Model; +import org.dmfs.tasks.model.Sources; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + /** * This fragment is used to display a list of task-providers. It show the task-providers which are visible in main {@link TaskListFragment} and also the * task-providers which are synced. The selection between the two lists is made by passing arguments to the fragment in a {@link Bundle} when it created in the * {@link SyncSettingsActivity}. *

    - * + * * @author Arjun Naik */ public class SettingsListFragment extends ListFragment implements AbsListView.OnItemClickListener, LoaderManager.LoaderCallbacks, - android.content.DialogInterface.OnClickListener + android.content.DialogInterface.OnClickListener { - public static final String LIST_SELECTION_ARGS = "list_selection_args"; - public static final String LIST_STRING_PARAMS = "list_string_params"; - public static final String LIST_FRAGMENT_LAYOUT = "list_fragment_layout"; - public static final String LIST_ONDETACH_SAVE = "list_ondetach_save"; - public static final String COMPARE_COLUMN_NAME = "column_name"; - - private Context mContext; - private VisibleListAdapter mAdapter; - - private String mListSelectionArguments; - private String[] mListSelectionParam; - private String mListCompareColumnName; - private boolean mSaveOnPause; - private int mFragmentLayout; - private String mAuthority; - - /** - * A dialog, that shows a list of accounts, which support the insert intent. - */ - private AlertDialog mChooseAccountToAddListDialog; - /** - * An adapter, that holds the accounts, which support the insert intent. - */ - private AccountAdapter mAccountAdapter; - - private Sources mSources; - - - public SettingsListFragment() - { - - } - - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - - /** - * The SQL selection condition used to select synced or visible list, the parameters for the select condition, the layout to be used and the column which is - * used for current selection is passed through a {@link Bundle}. The fragment layout is inflated and returned. - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { - Bundle args = getArguments(); - mListSelectionArguments = args.getString(LIST_SELECTION_ARGS); - mListSelectionParam = args.getStringArray(LIST_STRING_PARAMS); - mFragmentLayout = args.getInt(LIST_FRAGMENT_LAYOUT); - mSaveOnPause = args.getBoolean(LIST_ONDETACH_SAVE); - mListCompareColumnName = args.getString(COMPARE_COLUMN_NAME); - View view = inflater.inflate(mFragmentLayout, container, false); - return view; - } - - - @Override - public void onActivityCreated(Bundle savedInstanceState) - { - super.onActivityCreated(savedInstanceState); - getLoaderManager().restartLoader(-2, null, this); - mAdapter = new VisibleListAdapter(mContext, null, 0); - List accounts = mSources.getExistingAccounts(); - if (mContext.getResources().getBoolean(R.bool.opentasks_support_local_lists)) - { - accounts.add(new Account(TaskContract.LOCAL_ACCOUNT_NAME, TaskContract.LOCAL_ACCOUNT_TYPE)); - } - mAccountAdapter = new AccountAdapter(accounts); - setListAdapter(mAdapter); - getListView().setOnItemClickListener(this); - } - - - @Override - public void onResume() - { - super.onResume(); - // create a new dialog, that shows accounts for inserting task lists - mChooseAccountToAddListDialog = new AlertDialog.Builder(getActivity()).setTitle(R.string.task_list_settings_dialog_account_title) - .setAdapter(mAccountAdapter, this).create(); - } - - - /* - * Is called, when the user clicks on an account of the 'mChooseAccountToAddListDialog' dialog - */ - @Override - public void onClick(DialogInterface dialog, int which) - { - Account selectedAccount = mAccountAdapter.getItem(which); - if (selectedAccount == null) - { - return; - } - - Model model = mSources.getModel(selectedAccount.type); - if (model.hasInsertActivity()) - { - try - { - model.startInsertIntent(getActivity(), selectedAccount); - } - catch (ActivityNotFoundException e) - { - Toast.makeText(getActivity(), "No activity found to edit list", Toast.LENGTH_SHORT).show(); - } - } - } - - - /* - * Adds an action to the ActionBar to create local lists. - */ - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) - { - inflater.inflate(R.menu.list_settings_menu, menu); - } - - - /* - * Called, when the user clicks on an ActionBar item - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - if (R.id.action_add_local_list == item.getItemId()) - { - onAddListClick(); - return true; - } - return super.onOptionsItemSelected(item); - } - - - /** - * Is called, when the user clicks the action to create a local list. It will start the component, that handles the list creation. If there are more than - * one, it will show a list of assigned accounts before. - */ - private void onAddListClick() - { - try - { - if (mAccountAdapter.getCount() == 1) - { - Account account = mAccountAdapter.getItem(0); - Model model = mSources.getModel(account.type); - model.startInsertIntent(getActivity(), account); - } - else - { - mChooseAccountToAddListDialog.show(); - } - } - catch (ActivityNotFoundException e) - { - Toast.makeText(getActivity(), "No activity found to edit list", Toast.LENGTH_SHORT).show(); - } - } - - - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - mSources = Sources.getInstance(activity); - mContext = activity.getBaseContext(); - mAuthority = TaskContract.taskAuthority(activity); - } - - - @Override - public void onPause() - { - super.onPause(); - if (mSaveOnPause) - { - saveListState(); - doneSaveListState(); - } - } - - - @Override - public void onItemClick(AdapterView adapterView, View view, int position, long rowId) - { - VisibleListAdapter adapter = (VisibleListAdapter) adapterView.getAdapter(); - VisibleListAdapter.CheckableItem item = (VisibleListAdapter.CheckableItem) view.getTag(); - boolean checked = item.coloredCheckBox.isChecked(); - item.coloredCheckBox.setChecked(!checked); - adapter.addToState(rowId, !checked); - } - - - @Override - public Loader onCreateLoader(int arg0, Bundle arg1) - { - return new CursorLoader(mContext, TaskContract.TaskLists.getContentUri(mAuthority), new String[] { TaskContract.TaskLists._ID, - TaskContract.TaskLists.LIST_NAME, TaskContract.TaskLists.LIST_COLOR, TaskContract.TaskLists.SYNC_ENABLED, TaskContract.TaskLists.VISIBLE, - TaskContract.TaskLists.ACCOUNT_NAME, TaskContract.TaskLists.ACCOUNT_TYPE }, mListSelectionArguments, mListSelectionParam, - TaskContract.TaskLists.ACCOUNT_NAME + " COLLATE NOCASE ASC"); - } - - - @Override - public void onLoadFinished(Loader arg0, Cursor cursor) - { - mAdapter.swapCursor(cursor); - } - - - @Override - public void onLoaderReset(Loader arg0) - { - mAdapter.changeCursor(null); - - } - - /** - * This extends the {@link CursorAdapter}. The column index for the list name, list color and the current selection state is computed when the - * {@link Cursor} is swapped. It also maintains the changes made to the current selection state through a {@link HashMap} of ids and selection state. If the - * selection state is modified and then modified again then it is removed from the HashMap because it has reverted to the original state. - * - * @author Arjun Naik - * - */ - private class VisibleListAdapter extends CursorAdapter implements OnClickListener - { - LayoutInflater inflater; - private int listNameColumn, listColorColumn, compareColumn, accountNameColumn, accountTypeColumn; - private HashMap savedPositions = new HashMap(); - - - @Override - public Cursor swapCursor(Cursor c) - { - if (c != null) - { - listNameColumn = c.getColumnIndex(TaskContract.TaskLists.LIST_NAME); - listColorColumn = c.getColumnIndex(TaskContract.TaskLists.LIST_COLOR); - compareColumn = c.getColumnIndex(mListCompareColumnName); - accountNameColumn = c.getColumnIndex(TaskContract.TaskLists.ACCOUNT_NAME); - accountTypeColumn = c.getColumnIndex(TaskContract.TaskLists.ACCOUNT_TYPE); - } - else - { - listNameColumn = -1; - listColorColumn = -1; - compareColumn = -1; - accountNameColumn = -1; - accountTypeColumn = -1; - } - return super.swapCursor(c); - - } - - - public VisibleListAdapter(Context context, Cursor c, int flags) - { - super(context, c, flags); - inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - - @Override - public void bindView(View v, Context c, final Cursor cur) - { - String listName = cur.getString(listNameColumn); - CheckableItem item = (CheckableItem) v.getTag(); - String accountName = cur.getString(accountNameColumn); - String accountType = cur.getString(accountTypeColumn); - Model model = mSources.getModel(accountType); - if (model.hasEditActivity()) - { - item.btnSettings.setVisibility(View.VISIBLE); - item.btnSettings.setTag(cur.getPosition()); - item.btnSettings.setOnClickListener(this); - } - else - { - item.btnSettings.setVisibility(View.GONE); - item.btnSettings.setOnClickListener(null); - } - - item.text1.setText(listName); - item.text2.setText(accountName); - - int listColor = cur.getInt(listColorColumn); - item.coloredCheckBox.setColor(listColor); - - if (!cur.isNull(compareColumn)) - { - long id = cur.getLong(0); - boolean checkValue; - if (savedPositions.containsKey(id)) - { - checkValue = savedPositions.get(id); - } - else - { - checkValue = cur.getInt(compareColumn) == 1; - } - item.coloredCheckBox.setChecked(checkValue); - - } - } - - - @Override - public View newView(Context c, Cursor cur, ViewGroup vg) - { - View newInflatedView = inflater.inflate(R.layout.visible_task_list_item, vg, false); - CheckableItem item = new CheckableItem(); - item.text1 = (TextView) newInflatedView.findViewById(android.R.id.text1); - item.text2 = (TextView) newInflatedView.findViewById(android.R.id.text2); - item.btnSettings = newInflatedView.findViewById(R.id.btn_settings); - item.coloredCheckBox = (ColoredShapeCheckBox) newInflatedView.findViewById(R.id.visible_task_list_checked); - newInflatedView.setTag(item); - return newInflatedView; - } - - public class CheckableItem - { - TextView text1; - TextView text2; - View btnSettings; - ColoredShapeCheckBox coloredCheckBox; - } - - - private boolean addToState(long id, boolean val) - { - if (savedPositions.containsKey(Long.valueOf(id))) - { - savedPositions.remove(id); - return false; - } - else - { - savedPositions.put(id, val); - return true; - } - } - - - public void clearHashMap() - { - savedPositions.clear(); - } - - - public HashMap getState() - { - return savedPositions; - } - - - @Override - public void onClick(View v) - { - Cursor cursor = (Cursor) getItem((Integer) v.getTag()); - if (cursor != null) - { - onEditListClick(new Account(cursor.getString(accountNameColumn), cursor.getString(accountTypeColumn)), cursor.getLong(mRowIDColumn), - cursor.getString(listNameColumn), cursor.getInt(listColorColumn)); - } - } - - } - - - /** - * Is called, when the user click on the settings icon of a list item. This calls the assigned component to edit the list. - * - * @param account - * The account of the list. - * @param listId - * The id of the list. - * @param name - * The name of the list. - * @param color - * The color of the list. - */ - private void onEditListClick(Account account, long listId, String name, Integer color) - { - Model model = mSources.getModel(account.type); - - if (!model.hasEditActivity()) - { - return; - } - - try - { - model.startEditIntent(getActivity(), account, listId, name, color); - } - catch (ActivityNotFoundException e) - { - Toast.makeText(getActivity(), "No activity found to edit the list" + e.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); - } - } - - - /** - * This function is called to save the any modifications made to the displayed list. It retrieves the {@link HashMap} from the adapter of the list and uses - * it makes the changes persistent. For this it uses a batch operation provided by {@link ContentResolver}. The operations to be performed in the batch - * operation are stored in an {@link ArrayList} of {@link ContentProviderOperation}. - * - * @return true if the save operation was successful, false otherwise. - */ - public boolean saveListState() - { - HashMap savedPositions = ((VisibleListAdapter) getListAdapter()).getState(); - ArrayList ops = new ArrayList(); - - for (Long posInt : savedPositions.keySet()) - { - boolean val = savedPositions.get(posInt); - ContentProviderOperation op = ContentProviderOperation.newUpdate(TaskContract.TaskLists.getContentUri(mAuthority)) - .withSelection(TaskContract.TaskLists._ID + "=?", new String[] { posInt.toString() }).withValue(mListCompareColumnName, val ? "1" : "0") - .build(); - ops.add(op); - } - - try - { - mContext.getContentResolver().applyBatch(mAuthority, ops); - } - catch (RemoteException e) - { - e.printStackTrace(); - return false; - } - catch (OperationApplicationException e) - { - e.printStackTrace(); - return false; - } - return true; - } - - - public void doneSaveListState() - { - ((VisibleListAdapter) getListAdapter()).clearHashMap(); - } - - /** - * This class is used to display a list of accounts. The list can be modified by the {@link #addAccount(Account)} and {@link #clear()} method. The dialog is - * supposed to display accounts, which support the insert intent to create new task list. The selection must be done before. The adapter will show all - * accounts, which are added. - * - * @author Tristan Heinig - * - */ - private class AccountAdapter extends BaseAdapter - { - - private List mAccountList; - - - public AccountAdapter(List accountList) - { - mAccountList = accountList; - Iterator accountIterator = accountList.iterator(); - while (accountIterator.hasNext()) - { - Account account = accountIterator.next(); - if (!mSources.getModel(account.type).hasInsertActivity()) - { - accountIterator.remove(); - } - } - } - - - @Override - public int getCount() - { - return mAccountList.size(); - } - - - @Override - public Account getItem(int position) - { - return mAccountList.get(position); - } - - - @Override - public long getItemId(int position) - { - return position; - } - - - @Override - public View getView(int position, View convertView, ViewGroup parent) - { - if (convertView == null) - { - convertView = LayoutInflater.from(getActivity()).inflate(R.layout.account_list_item_dialog, parent, false); - } - Account account = getItem(position); - Model model = mSources.getModel(account.type); - ((TextView) convertView.findViewById(android.R.id.text1)).setText(account.name); - ((TextView) convertView.findViewById(android.R.id.text2)).setText(model.getAccountLabel()); - return convertView; - } - - } + public static final String LIST_SELECTION_ARGS = "list_selection_args"; + public static final String LIST_STRING_PARAMS = "list_string_params"; + public static final String LIST_FRAGMENT_LAYOUT = "list_fragment_layout"; + public static final String LIST_ONDETACH_SAVE = "list_ondetach_save"; + public static final String COMPARE_COLUMN_NAME = "column_name"; + + private Context mContext; + private VisibleListAdapter mAdapter; + + private String mListSelectionArguments; + private String[] mListSelectionParam; + private String mListCompareColumnName; + private boolean mSaveOnPause; + private int mFragmentLayout; + private String mAuthority; + + /** + * A dialog, that shows a list of accounts, which support the insert intent. + */ + private AlertDialog mChooseAccountToAddListDialog; + /** + * An adapter, that holds the accounts, which support the insert intent. + */ + private AccountAdapter mAccountAdapter; + + private Sources mSources; + + + public SettingsListFragment() + { + + } + + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + + /** + * The SQL selection condition used to select synced or visible list, the parameters for the select condition, the layout to be used and the column which is + * used for current selection is passed through a {@link Bundle}. The fragment layout is inflated and returned. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + Bundle args = getArguments(); + mListSelectionArguments = args.getString(LIST_SELECTION_ARGS); + mListSelectionParam = args.getStringArray(LIST_STRING_PARAMS); + mFragmentLayout = args.getInt(LIST_FRAGMENT_LAYOUT); + mSaveOnPause = args.getBoolean(LIST_ONDETACH_SAVE); + mListCompareColumnName = args.getString(COMPARE_COLUMN_NAME); + View view = inflater.inflate(mFragmentLayout, container, false); + return view; + } + + + @Override + public void onActivityCreated(Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + getLoaderManager().restartLoader(-2, null, this); + mAdapter = new VisibleListAdapter(mContext, null, 0); + List accounts = mSources.getExistingAccounts(); + if (mContext.getResources().getBoolean(R.bool.opentasks_support_local_lists)) + { + accounts.add(new Account(TaskContract.LOCAL_ACCOUNT_NAME, TaskContract.LOCAL_ACCOUNT_TYPE)); + } + mAccountAdapter = new AccountAdapter(accounts); + setListAdapter(mAdapter); + getListView().setOnItemClickListener(this); + } + + + @Override + public void onResume() + { + super.onResume(); + // create a new dialog, that shows accounts for inserting task lists + mChooseAccountToAddListDialog = new AlertDialog.Builder(getActivity()).setTitle(R.string.task_list_settings_dialog_account_title) + .setAdapter(mAccountAdapter, this).create(); + } + + + /* + * Is called, when the user clicks on an account of the 'mChooseAccountToAddListDialog' dialog + */ + @Override + public void onClick(DialogInterface dialog, int which) + { + Account selectedAccount = mAccountAdapter.getItem(which); + if (selectedAccount == null) + { + return; + } + + Model model = mSources.getModel(selectedAccount.type); + if (model.hasInsertActivity()) + { + try + { + model.startInsertIntent(getActivity(), selectedAccount); + } + catch (ActivityNotFoundException e) + { + Toast.makeText(getActivity(), "No activity found to edit list", Toast.LENGTH_SHORT).show(); + } + } + } + + + /* + * Adds an action to the ActionBar to create local lists. + */ + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) + { + inflater.inflate(R.menu.list_settings_menu, menu); + } + + + /* + * Called, when the user clicks on an ActionBar item + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + if (R.id.action_add_local_list == item.getItemId()) + { + onAddListClick(); + return true; + } + return super.onOptionsItemSelected(item); + } + + + /** + * Is called, when the user clicks the action to create a local list. It will start the component, that handles the list creation. If there are more than + * one, it will show a list of assigned accounts before. + */ + private void onAddListClick() + { + try + { + if (mAccountAdapter.getCount() == 1) + { + Account account = mAccountAdapter.getItem(0); + Model model = mSources.getModel(account.type); + model.startInsertIntent(getActivity(), account); + } + else + { + mChooseAccountToAddListDialog.show(); + } + } + catch (ActivityNotFoundException e) + { + Toast.makeText(getActivity(), "No activity found to edit list", Toast.LENGTH_SHORT).show(); + } + } + + + @Override + public void onAttach(Activity activity) + { + super.onAttach(activity); + mSources = Sources.getInstance(activity); + mContext = activity.getBaseContext(); + mAuthority = TaskContract.taskAuthority(activity); + } + + + @Override + public void onPause() + { + super.onPause(); + if (mSaveOnPause) + { + saveListState(); + doneSaveListState(); + } + } + + + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long rowId) + { + VisibleListAdapter adapter = (VisibleListAdapter) adapterView.getAdapter(); + VisibleListAdapter.CheckableItem item = (VisibleListAdapter.CheckableItem) view.getTag(); + boolean checked = item.coloredCheckBox.isChecked(); + item.coloredCheckBox.setChecked(!checked); + adapter.addToState(rowId, !checked); + } + + + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) + { + return new CursorLoader(mContext, TaskContract.TaskLists.getContentUri(mAuthority), new String[] { + TaskContract.TaskLists._ID, + TaskContract.TaskLists.LIST_NAME, TaskContract.TaskLists.LIST_COLOR, TaskContract.TaskLists.SYNC_ENABLED, TaskContract.TaskLists.VISIBLE, + TaskContract.TaskLists.ACCOUNT_NAME, TaskContract.TaskLists.ACCOUNT_TYPE }, mListSelectionArguments, mListSelectionParam, + TaskContract.TaskLists.ACCOUNT_NAME + " COLLATE NOCASE ASC"); + } + + + @Override + public void onLoadFinished(Loader arg0, Cursor cursor) + { + mAdapter.swapCursor(cursor); + } + + + @Override + public void onLoaderReset(Loader arg0) + { + mAdapter.changeCursor(null); + + } + + + /** + * This extends the {@link CursorAdapter}. The column index for the list name, list color and the current selection state is computed when the + * {@link Cursor} is swapped. It also maintains the changes made to the current selection state through a {@link HashMap} of ids and selection state. If the + * selection state is modified and then modified again then it is removed from the HashMap because it has reverted to the original state. + * + * @author Arjun Naik + */ + private class VisibleListAdapter extends CursorAdapter implements OnClickListener + { + LayoutInflater inflater; + private int listNameColumn, listColorColumn, compareColumn, accountNameColumn, accountTypeColumn; + private HashMap savedPositions = new HashMap(); + + + @Override + public Cursor swapCursor(Cursor c) + { + if (c != null) + { + listNameColumn = c.getColumnIndex(TaskContract.TaskLists.LIST_NAME); + listColorColumn = c.getColumnIndex(TaskContract.TaskLists.LIST_COLOR); + compareColumn = c.getColumnIndex(mListCompareColumnName); + accountNameColumn = c.getColumnIndex(TaskContract.TaskLists.ACCOUNT_NAME); + accountTypeColumn = c.getColumnIndex(TaskContract.TaskLists.ACCOUNT_TYPE); + } + else + { + listNameColumn = -1; + listColorColumn = -1; + compareColumn = -1; + accountNameColumn = -1; + accountTypeColumn = -1; + } + return super.swapCursor(c); + + } + + + public VisibleListAdapter(Context context, Cursor c, int flags) + { + super(context, c, flags); + inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + + @Override + public void bindView(View v, Context c, final Cursor cur) + { + String listName = cur.getString(listNameColumn); + CheckableItem item = (CheckableItem) v.getTag(); + String accountName = cur.getString(accountNameColumn); + String accountType = cur.getString(accountTypeColumn); + Model model = mSources.getModel(accountType); + if (model.hasEditActivity()) + { + item.btnSettings.setVisibility(View.VISIBLE); + item.btnSettings.setTag(cur.getPosition()); + item.btnSettings.setOnClickListener(this); + } + else + { + item.btnSettings.setVisibility(View.GONE); + item.btnSettings.setOnClickListener(null); + } + + item.text1.setText(listName); + item.text2.setText(accountName); + + int listColor = cur.getInt(listColorColumn); + item.coloredCheckBox.setColor(listColor); + + if (!cur.isNull(compareColumn)) + { + long id = cur.getLong(0); + boolean checkValue; + if (savedPositions.containsKey(id)) + { + checkValue = savedPositions.get(id); + } + else + { + checkValue = cur.getInt(compareColumn) == 1; + } + item.coloredCheckBox.setChecked(checkValue); + + } + } + + + @Override + public View newView(Context c, Cursor cur, ViewGroup vg) + { + View newInflatedView = inflater.inflate(R.layout.visible_task_list_item, vg, false); + CheckableItem item = new CheckableItem(); + item.text1 = (TextView) newInflatedView.findViewById(android.R.id.text1); + item.text2 = (TextView) newInflatedView.findViewById(android.R.id.text2); + item.btnSettings = newInflatedView.findViewById(R.id.btn_settings); + item.coloredCheckBox = (ColoredShapeCheckBox) newInflatedView.findViewById(R.id.visible_task_list_checked); + newInflatedView.setTag(item); + return newInflatedView; + } + + + public class CheckableItem + { + TextView text1; + TextView text2; + View btnSettings; + ColoredShapeCheckBox coloredCheckBox; + } + + + private boolean addToState(long id, boolean val) + { + if (savedPositions.containsKey(Long.valueOf(id))) + { + savedPositions.remove(id); + return false; + } + else + { + savedPositions.put(id, val); + return true; + } + } + + + public void clearHashMap() + { + savedPositions.clear(); + } + + + public HashMap getState() + { + return savedPositions; + } + + + @Override + public void onClick(View v) + { + Cursor cursor = (Cursor) getItem((Integer) v.getTag()); + if (cursor != null) + { + onEditListClick(new Account(cursor.getString(accountNameColumn), cursor.getString(accountTypeColumn)), cursor.getLong(mRowIDColumn), + cursor.getString(listNameColumn), cursor.getInt(listColorColumn)); + } + } + + } + + + /** + * Is called, when the user click on the settings icon of a list item. This calls the assigned component to edit the list. + * + * @param account + * The account of the list. + * @param listId + * The id of the list. + * @param name + * The name of the list. + * @param color + * The color of the list. + */ + private void onEditListClick(Account account, long listId, String name, Integer color) + { + Model model = mSources.getModel(account.type); + + if (!model.hasEditActivity()) + { + return; + } + + try + { + model.startEditIntent(getActivity(), account, listId, name, color); + } + catch (ActivityNotFoundException e) + { + Toast.makeText(getActivity(), "No activity found to edit the list" + e.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); + } + } + + + /** + * This function is called to save the any modifications made to the displayed list. It retrieves the {@link HashMap} from the adapter of the list and uses + * it makes the changes persistent. For this it uses a batch operation provided by {@link ContentResolver}. The operations to be performed in the batch + * operation are stored in an {@link ArrayList} of {@link ContentProviderOperation}. + * + * @return true if the save operation was successful, false otherwise. + */ + public boolean saveListState() + { + HashMap savedPositions = ((VisibleListAdapter) getListAdapter()).getState(); + ArrayList ops = new ArrayList(); + + for (Long posInt : savedPositions.keySet()) + { + boolean val = savedPositions.get(posInt); + ContentProviderOperation op = ContentProviderOperation.newUpdate(TaskContract.TaskLists.getContentUri(mAuthority)) + .withSelection(TaskContract.TaskLists._ID + "=?", new String[] { posInt.toString() }).withValue(mListCompareColumnName, val ? "1" : "0") + .build(); + ops.add(op); + } + + try + { + mContext.getContentResolver().applyBatch(mAuthority, ops); + } + catch (RemoteException e) + { + e.printStackTrace(); + return false; + } + catch (OperationApplicationException e) + { + e.printStackTrace(); + return false; + } + return true; + } + + + public void doneSaveListState() + { + ((VisibleListAdapter) getListAdapter()).clearHashMap(); + } + + + /** + * This class is used to display a list of accounts. The list can be modified by the {@link #addAccount(Account)} and {@link #clear()} method. The dialog is + * supposed to display accounts, which support the insert intent to create new task list. The selection must be done before. The adapter will show all + * accounts, which are added. + * + * @author Tristan Heinig + */ + private class AccountAdapter extends BaseAdapter + { + + private List mAccountList; + + + public AccountAdapter(List accountList) + { + mAccountList = accountList; + Iterator accountIterator = accountList.iterator(); + while (accountIterator.hasNext()) + { + Account account = accountIterator.next(); + if (!mSources.getModel(account.type).hasInsertActivity()) + { + accountIterator.remove(); + } + } + } + + + @Override + public int getCount() + { + return mAccountList.size(); + } + + + @Override + public Account getItem(int position) + { + return mAccountList.get(position); + } + + + @Override + public long getItemId(int position) + { + return position; + } + + + @Override + public View getView(int position, View convertView, ViewGroup parent) + { + if (convertView == null) + { + convertView = LayoutInflater.from(getActivity()).inflate(R.layout.account_list_item_dialog, parent, false); + } + Account account = getItem(position); + Model model = mSources.getModel(account.type); + ((TextView) convertView.findViewById(android.R.id.text1)).setText(account.name); + ((TextView) convertView.findViewById(android.R.id.text2)).setText(model.getAccountLabel()); + return convertView; + } + + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java b/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java index 39270f4b..06461fcc 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java @@ -16,8 +16,6 @@ */ package org.dmfs.tasks; -import org.dmfs.provider.tasks.TaskContract; - import android.annotation.TargetApi; import android.os.Build; import android.os.Bundle; @@ -27,136 +25,138 @@ import android.support.v7.app.ActionBarActivity; import android.view.View; import android.widget.Button; +import org.dmfs.provider.tasks.TaskContract; + /** * This extends the {@link FragmentActivity} for displaying the list of synced or visible task-providers. It displays the visible providers when it is created. - * + * * @author Arjun Naik */ public class SyncSettingsActivity extends ActionBarActivity { - private FragmentManager mManager; - private SettingsListFragment mCurrentFrag; - - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - // Show the Up button in the action bar. - setupActionBar(); - - mManager = getSupportFragmentManager(); - showVisibleListsFragment(); - - } - - - /** - * This function displays the list of providers which can be visible or hidden in {@link TaskListFragment}. - */ - public void showVisibleListsFragment() - { - SettingsListFragment syncedListFragment = new SettingsListFragment(); - Bundle args = new Bundle(); - args.putStringArray(SettingsListFragment.LIST_STRING_PARAMS, new String[] { "1" }); - args.putInt(SettingsListFragment.LIST_FRAGMENT_LAYOUT, R.layout.fragment_visiblelist); - args.putString(SettingsListFragment.LIST_SELECTION_ARGS, TaskContract.TaskLists.SYNC_ENABLED + "=?"); - args.putString(SettingsListFragment.COMPARE_COLUMN_NAME, TaskContract.TaskLists.VISIBLE); - args.putBoolean(SettingsListFragment.LIST_ONDETACH_SAVE, true); - syncedListFragment.setArguments(args); - mManager.beginTransaction().replace(R.id.visible_task_list_fragment, syncedListFragment).commit(); - mCurrentFrag = syncedListFragment; - showActionBarTitle(R.string.visible_task_lists); - } - - - /** - * This function displays the list of providers which can be synced. - */ - public void showSyncedListsFragment() - { - SettingsListFragment syncedListFragment = new SettingsListFragment(); - Bundle args = new Bundle(); - args.putStringArray(SettingsListFragment.LIST_STRING_PARAMS, null); - args.putInt(SettingsListFragment.LIST_FRAGMENT_LAYOUT, R.layout.fragment_synced_task_list); - args.putString(SettingsListFragment.LIST_SELECTION_ARGS, null); - args.putString(SettingsListFragment.COMPARE_COLUMN_NAME, TaskContract.TaskLists.SYNC_ENABLED); - args.putBoolean(SettingsListFragment.LIST_ONDETACH_SAVE, false); - syncedListFragment.setArguments(args); - mManager.beginTransaction().replace(R.id.visible_task_list_fragment, syncedListFragment).commit(); - mCurrentFrag = syncedListFragment; - showActionBarTitle(R.string.synced_task_lists); - } - - - /** - * Set up the {@link android.app.ActionBar}, if the API is available. - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - private void setupActionBar() - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) - { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - } - - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - private void showActionBarTitle(int titleRes) - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) - { - getSupportActionBar().setTitle(titleRes); - } - } - - - /** - * This function is a handler for the {@link Button} which is present in the layout loaded by {@link SettingsListFragment}. When this button is clicked the - * {@link SyncSettingsActivity} instructs the {@link SettingsListFragment} to save the current modification and then loads a {@link SettingsListFragment} - * which shows the list the syncable task-providers. - * - * @param v - * Reference to the {@link Button} which was clicked is passed as a {@link View} object. - */ - public void showSyncedList(View v) - { - mCurrentFrag.saveListState(); - // Call a function to indicate to the fragment that the state change to the list have been saved(clear the hashmap). - mCurrentFrag.doneSaveListState(); - showSyncedListsFragment(); - } - - - /** - * This function is a handler for the {@link Button} which is present in the layout loaded by {@link SettingsListFragment}. When this button is clicked the - * {@link SyncSettingsActivity} instructs the {@link SettingsListFragment} to save the current modification and then loads a {@link SettingsListFragment} - * which shows the list the displayable task-providers. - * - * @param v - * Reference to the {@link Button} which was clicked is passed as a {@link View} object. - */ - public void onSaveUpdated(View v) - { - mCurrentFrag.saveListState(); - showVisibleListsFragment(); - - } - - - /** - * This function is a handler for the {@link Button} which is present in the layout loaded by {@link SettingsListFragment}. When this button is clicked the - * list showing the syncable task-providers is displayed. - * - * @param v - * Reference to the {@link Button} which was clicked is passed as a {@link View} object. - */ - public void onCancelUpdated(View v) - { - showVisibleListsFragment(); - } + private FragmentManager mManager; + private SettingsListFragment mCurrentFrag; + + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + // Show the Up button in the action bar. + setupActionBar(); + + mManager = getSupportFragmentManager(); + showVisibleListsFragment(); + + } + + + /** + * This function displays the list of providers which can be visible or hidden in {@link TaskListFragment}. + */ + public void showVisibleListsFragment() + { + SettingsListFragment syncedListFragment = new SettingsListFragment(); + Bundle args = new Bundle(); + args.putStringArray(SettingsListFragment.LIST_STRING_PARAMS, new String[] { "1" }); + args.putInt(SettingsListFragment.LIST_FRAGMENT_LAYOUT, R.layout.fragment_visiblelist); + args.putString(SettingsListFragment.LIST_SELECTION_ARGS, TaskContract.TaskLists.SYNC_ENABLED + "=?"); + args.putString(SettingsListFragment.COMPARE_COLUMN_NAME, TaskContract.TaskLists.VISIBLE); + args.putBoolean(SettingsListFragment.LIST_ONDETACH_SAVE, true); + syncedListFragment.setArguments(args); + mManager.beginTransaction().replace(R.id.visible_task_list_fragment, syncedListFragment).commit(); + mCurrentFrag = syncedListFragment; + showActionBarTitle(R.string.visible_task_lists); + } + + + /** + * This function displays the list of providers which can be synced. + */ + public void showSyncedListsFragment() + { + SettingsListFragment syncedListFragment = new SettingsListFragment(); + Bundle args = new Bundle(); + args.putStringArray(SettingsListFragment.LIST_STRING_PARAMS, null); + args.putInt(SettingsListFragment.LIST_FRAGMENT_LAYOUT, R.layout.fragment_synced_task_list); + args.putString(SettingsListFragment.LIST_SELECTION_ARGS, null); + args.putString(SettingsListFragment.COMPARE_COLUMN_NAME, TaskContract.TaskLists.SYNC_ENABLED); + args.putBoolean(SettingsListFragment.LIST_ONDETACH_SAVE, false); + syncedListFragment.setArguments(args); + mManager.beginTransaction().replace(R.id.visible_task_list_fragment, syncedListFragment).commit(); + mCurrentFrag = syncedListFragment; + showActionBarTitle(R.string.synced_task_lists); + } + + + /** + * Set up the {@link android.app.ActionBar}, if the API is available. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private void setupActionBar() + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + } + + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private void showActionBarTitle(int titleRes) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + { + getSupportActionBar().setTitle(titleRes); + } + } + + + /** + * This function is a handler for the {@link Button} which is present in the layout loaded by {@link SettingsListFragment}. When this button is clicked the + * {@link SyncSettingsActivity} instructs the {@link SettingsListFragment} to save the current modification and then loads a {@link SettingsListFragment} + * which shows the list the syncable task-providers. + * + * @param v + * Reference to the {@link Button} which was clicked is passed as a {@link View} object. + */ + public void showSyncedList(View v) + { + mCurrentFrag.saveListState(); + // Call a function to indicate to the fragment that the state change to the list have been saved(clear the hashmap). + mCurrentFrag.doneSaveListState(); + showSyncedListsFragment(); + } + + + /** + * This function is a handler for the {@link Button} which is present in the layout loaded by {@link SettingsListFragment}. When this button is clicked the + * {@link SyncSettingsActivity} instructs the {@link SettingsListFragment} to save the current modification and then loads a {@link SettingsListFragment} + * which shows the list the displayable task-providers. + * + * @param v + * Reference to the {@link Button} which was clicked is passed as a {@link View} object. + */ + public void onSaveUpdated(View v) + { + mCurrentFrag.saveListState(); + showVisibleListsFragment(); + + } + + + /** + * This function is a handler for the {@link Button} which is present in the layout loaded by {@link SettingsListFragment}. When this button is clicked the + * list showing the syncable task-providers is displayed. + * + * @param v + * Reference to the {@link Button} which was clicked is passed as a {@link View} object. + */ + public void onCancelUpdated(View v) + { + showVisibleListsFragment(); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/TaskGroupPagerAdapter.java b/opentasks/src/main/java/org/dmfs/tasks/TaskGroupPagerAdapter.java index 58413ee0..c135f827 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/TaskGroupPagerAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/TaskGroupPagerAdapter.java @@ -17,25 +17,25 @@ package org.dmfs.tasks; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; import org.dmfs.tasks.groupings.AbstractGroupingFactory; import org.dmfs.tasks.groupings.TabConfig; import org.dmfs.xmlobjects.pull.XmlObjectPullParserException; import org.xmlpull.v1.XmlPullParserException; -import android.annotation.SuppressLint; -import android.content.Context; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentStatePagerAdapter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; /** * An adapter to populate the different views of grouped tasks for a ViewPager. - * + * * @author Tobias Reinsch * @author Marten Gajda */ @@ -43,128 +43,132 @@ import android.support.v4.app.FragmentStatePagerAdapter; public class TaskGroupPagerAdapter extends FragmentStatePagerAdapter { - @SuppressWarnings("unused") - private static final String TAG = "TaskGroupPager"; - private final Map mGroupingFactories = new HashMap(16); - private boolean mTwoPaneLayout; - private final TabConfig mTabConfig; - - - /** - * Create a new {@link TaskGroupPagerAdapter}. - * - * @param fm - * A {@link FragmentManager} - * @param groupingFactories - * An array of {@link AbstractGroupingFactory}. - * @param context - * A context to access resources - * @param tabRes - * The resource id of an XML resource that describes the items of the pager - * @throws XmlObjectPullParserException - * @throws IOException - * @throws XmlPullParserException - */ - @SuppressLint("NewApi") - public TaskGroupPagerAdapter(FragmentManager fm, AbstractGroupingFactory[] groupingFactories, Context context, int tabRes) throws XmlPullParserException, - IOException, XmlObjectPullParserException - { - super(fm); - - mTabConfig = TabConfig.load(context, tabRes); - - for (AbstractGroupingFactory factory : groupingFactories) - { - mGroupingFactories.put(factory.getId(), factory); - } - } - - - @Override - public CharSequence getPageTitle(int position) - { - // we don't want to show any title - return null; - } - - - @Override - public Fragment getItem(int position) - { - int pageId = mTabConfig.getVisibleItem(position).getId(); - AbstractGroupingFactory factory = getGroupingFactoryForId(pageId); - - TaskListFragment fragment = TaskListFragment.newInstance(position, mTwoPaneLayout); - fragment.setExpandableGroupDescriptor(factory.getExpandableGroupDescriptor()); - fragment.setPageId(pageId); - return fragment; - - } - - - /** - * Get the id of a specific page. - * - * @param position - * The position of the page. - * @return The id of the page. - */ - public int getPageId(int position) - { - return mTabConfig.getVisibleItem(position).getId(); - } - - - /** - * Returns the position of the page with the given id. - * - * @param id - * The id of the page. - * @return The position of the page or -1 if the page doesn't exist or is not visible. - */ - public int getPagePosition(int id) - { - TabConfig groupings = mTabConfig; - for (int i = 0, count = groupings.visibleSize(); i < count; ++i) - { - if (groupings.getVisibleItem(i).getId() == id) - { - return i; - } - } - return -1; - } - - - /** - * Get an {@link AbstractGroupingFactory} for the page with the given id. - * - * @param id - * The is of the page. - * @return The {@link AbstractGroupingFactory} that belongs to the id, if any, null otherwise. - */ - public AbstractGroupingFactory getGroupingFactoryForId(int id) - { - return mGroupingFactories.get(id); - } - - - @Override - public int getCount() - { - return mTabConfig.visibleSize(); - } - - - public void setTwoPaneLayout(boolean twoPane) - { - mTwoPaneLayout = twoPane; - } - - - public int getTabIcon(int position) - { - return mTabConfig.getVisibleItem(position).getIcon(); - } + @SuppressWarnings("unused") + private static final String TAG = "TaskGroupPager"; + private final Map mGroupingFactories = new HashMap(16); + private boolean mTwoPaneLayout; + private final TabConfig mTabConfig; + + + /** + * Create a new {@link TaskGroupPagerAdapter}. + * + * @param fm + * A {@link FragmentManager} + * @param groupingFactories + * An array of {@link AbstractGroupingFactory}. + * @param context + * A context to access resources + * @param tabRes + * The resource id of an XML resource that describes the items of the pager + * + * @throws XmlObjectPullParserException + * @throws IOException + * @throws XmlPullParserException + */ + @SuppressLint("NewApi") + public TaskGroupPagerAdapter(FragmentManager fm, AbstractGroupingFactory[] groupingFactories, Context context, int tabRes) throws XmlPullParserException, + IOException, XmlObjectPullParserException + { + super(fm); + + mTabConfig = TabConfig.load(context, tabRes); + + for (AbstractGroupingFactory factory : groupingFactories) + { + mGroupingFactories.put(factory.getId(), factory); + } + } + + + @Override + public CharSequence getPageTitle(int position) + { + // we don't want to show any title + return null; + } + + + @Override + public Fragment getItem(int position) + { + int pageId = mTabConfig.getVisibleItem(position).getId(); + AbstractGroupingFactory factory = getGroupingFactoryForId(pageId); + + TaskListFragment fragment = TaskListFragment.newInstance(position, mTwoPaneLayout); + fragment.setExpandableGroupDescriptor(factory.getExpandableGroupDescriptor()); + fragment.setPageId(pageId); + return fragment; + + } + + + /** + * Get the id of a specific page. + * + * @param position + * The position of the page. + * + * @return The id of the page. + */ + public int getPageId(int position) + { + return mTabConfig.getVisibleItem(position).getId(); + } + + + /** + * Returns the position of the page with the given id. + * + * @param id + * The id of the page. + * + * @return The position of the page or -1 if the page doesn't exist or is not visible. + */ + public int getPagePosition(int id) + { + TabConfig groupings = mTabConfig; + for (int i = 0, count = groupings.visibleSize(); i < count; ++i) + { + if (groupings.getVisibleItem(i).getId() == id) + { + return i; + } + } + return -1; + } + + + /** + * Get an {@link AbstractGroupingFactory} for the page with the given id. + * + * @param id + * The is of the page. + * + * @return The {@link AbstractGroupingFactory} that belongs to the id, if any, null otherwise. + */ + public AbstractGroupingFactory getGroupingFactoryForId(int id) + { + return mGroupingFactories.get(id); + } + + + @Override + public int getCount() + { + return mTabConfig.visibleSize(); + } + + + public void setTwoPaneLayout(boolean twoPane) + { + mTwoPaneLayout = twoPane; + } + + + public int getTabIcon(int position) + { + return mTabConfig.getVisibleItem(position).getIcon(); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java b/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java index 4b836f3a..6482eaa9 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java @@ -50,6 +50,7 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.view.WindowManager; + import org.dmfs.android.retentionmagic.annotations.Retain; import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskContract.Tasks; @@ -79,7 +80,7 @@ import java.io.IOException; * The activity makes heavy use of fragments. The list of items is a {@link TaskListFragment} and the item details (if present) is a {@link ViewTaskFragment}. *

    * This activity also implements the required {@link TaskListFragment.Callbacks} interface to listen for item selections. - * + *

    *

    * TODO: move the code to persist the expanded groups into a the GroupingDescriptor class *

    @@ -89,363 +90,384 @@ import java.io.IOException; public class TaskListActivity extends AppCompatActivity implements TaskListFragment.Callbacks, ViewTaskFragment.Callback { - /** Tells the activity to display the details of the task with the URI from the intent data. **/ - public static final String EXTRA_DISPLAY_TASK = "org.dmfs.tasks.DISPLAY_TASK"; + /** + * Tells the activity to display the details of the task with the URI from the intent data. + **/ + public static final String EXTRA_DISPLAY_TASK = "org.dmfs.tasks.DISPLAY_TASK"; + + /** + * Tells the activity to select the task in the list with the URI from the intent data. + **/ + public static final String EXTRA_FORCE_LIST_SELECTION = "org.dmfs.tasks.FORCE_LIST_SELECTION"; + + private static final String TAG = "TaskListActivity"; + + private final static int REQUEST_CODE_NEW_TASK = 2924; + + /** + * The time to wait for a new key before updating the search view. + */ + private final static int SEARCH_UPDATE_DELAY = 400; // ms + + private final static String DETAIL_FRAGMENT_TAG = "taskListActivity.ViewTaskFragment"; + + /** + * Array of {@link ExpandableGroupDescriptor}s. + */ + private AbstractGroupingFactory[] mGroupingFactories; + + /** + * Whether or not the activity is in two-pane mode, i.e. running on a tablet device. + */ + private boolean mTwoPane; + private ViewPager mViewPager; + private TaskGroupPagerAdapter mPagerAdapter; + + @Retain(permanent = true) + private int mCurrentPageId; + + /** + * The current pager position + **/ + private int mCurrentPagePosition = 0; - /** Tells the activity to select the task in the list with the URI from the intent data. **/ - public static final String EXTRA_FORCE_LIST_SELECTION = "org.dmfs.tasks.FORCE_LIST_SELECTION"; + private int mPreviousPagePosition = -1; - private static final String TAG = "TaskListActivity"; + private String mAuthority; - private final static int REQUEST_CODE_NEW_TASK = 2924; + private MenuItem mSearchItem; - /** - * The time to wait for a new key before updating the search view. - */ - private final static int SEARCH_UPDATE_DELAY = 400; // ms + private TabLayout mTabs; - private final static String DETAIL_FRAGMENT_TAG = "taskListActivity.ViewTaskFragment"; + private final Handler mHandler = new Handler(); - /** - * Array of {@link ExpandableGroupDescriptor}s. - */ - private AbstractGroupingFactory[] mGroupingFactories; + private SearchHistoryHelper mSearchHistoryHelper; - /** - * Whether or not the activity is in two-pane mode, i.e. running on a tablet device. - */ - private boolean mTwoPane; - private ViewPager mViewPager; - private TaskGroupPagerAdapter mPagerAdapter; + private boolean mAutoExpandSearchView = false; - @Retain(permanent = true) - private int mCurrentPageId; + /** + * Indicates that the activity switched to detail view due to rotation. + **/ + @Retain + private boolean mSwitchedToDetail = false; - /** The current pager position **/ - private int mCurrentPagePosition = 0; + /** + * The Uri of the task to display/highlight in the list view. + **/ + @Retain + private Uri mSelectedTaskUri; - private int mPreviousPagePosition = -1; + /** + * The Uri of the task to display/highlight in the list view coming from the widget. + **/ + private Uri mSelectedTaskUriOnLaunch; - private String mAuthority; + /** + * Indicates to display the two pane layout with details + **/ + @Retain + private boolean mShouldShowDetails = false; - private MenuItem mSearchItem; + /** + * Indicates to show ViewTaskActivity when rotating to single pane. + **/ + @Retain + private boolean mShouldSwitchToDetail = false; - private TabLayout mTabs; + /** + * Indicates the TaskListFragments to select/highlight the mSelectedTaskUri item + **/ + private boolean mShouldSelectTaskListItem = false; + + /** + * Indicates a transient state after rotation to redirect to the TaskViewActivtiy + **/ + private boolean mTransientState = false; + + private CollapsingToolbarLayout mToolbarLayout; + + private AppBarLayout mAppBarLayout; + + private FloatingActionButton mFloatingActionButton; + + + @SuppressLint("NewApi") + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + protected void onCreate(Bundle savedInstanceState) + { + Log.d(TAG, "onCreate called again"); + super.onCreate(savedInstanceState); + + // check for single pane activity change + mTwoPane = getResources().getBoolean(R.bool.has_two_panes); + + resolveIntentAction(getIntent()); + + if (mSelectedTaskUri != null) + { + if (mShouldShowDetails && mShouldSwitchToDetail) + { + Intent viewTaskIntent = new Intent(Intent.ACTION_VIEW); + viewTaskIntent.setData(mSelectedTaskUri); + startActivity(viewTaskIntent); + mSwitchedToDetail = true; + mShouldSwitchToDetail = false; + mTransientState = true; + } + } + else + { + mShouldShowDetails = false; + } + + setContentView(R.layout.activity_task_list); + mAppBarLayout = (AppBarLayout) findViewById(R.id.appbar); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + mAuthority = TaskContract.taskAuthority(this); + mSearchHistoryHelper = new SearchHistoryHelper(this); + + if (findViewById(R.id.task_detail_container) != null) + { + // In two-pane mode, list items should be given the + // 'activated' state when touched. + + // get list fragment + // mTaskListFrag = (TaskListFragment) getSupportFragmentManager().findFragmentById(R.id.task_list); + // mTaskListFrag.setListViewScrollbarPositionLeft(true); + + // mTaskListFrag.setActivateOnItemClick(true); + + loadTaskDetailFragment(mSelectedTaskUri); + } + else + { + FragmentManager fragmentManager = getSupportFragmentManager(); + Fragment detailFragment = fragmentManager.findFragmentByTag(DETAIL_FRAGMENT_TAG); + if (detailFragment != null) + { + fragmentManager.beginTransaction().remove(detailFragment).commit(); + } + } + + mGroupingFactories = new AbstractGroupingFactory[] { + new ByList(mAuthority), new ByDueDate(mAuthority), new ByStartDate(mAuthority), + new ByPriority(mAuthority), new ByProgress(mAuthority), new BySearch(mAuthority, mSearchHistoryHelper) }; + + // set up pager adapter + try + { + mPagerAdapter = new TaskGroupPagerAdapter(getSupportFragmentManager(), mGroupingFactories, this, R.xml.listview_tabs); + } + catch (XmlPullParserException e) + { + // TODO Automatisch generierter Erfassungsblock + e.printStackTrace(); + } + catch (IOException e) + { + // TODO Automatisch generierter Erfassungsblock + e.printStackTrace(); + } + catch (XmlObjectPullParserException e) + { + // TODO Automatisch generierter Erfassungsblock + e.printStackTrace(); + } + + // Setup ViewPager + mPagerAdapter.setTwoPaneLayout(mTwoPane); + mViewPager = (ViewPager) findViewById(R.id.pager); + mViewPager.setAdapter(mPagerAdapter); + + int currentPageIndex = mPagerAdapter.getPagePosition(mCurrentPageId); + + if (currentPageIndex >= 0) + { + mCurrentPagePosition = currentPageIndex; + mViewPager.setCurrentItem(currentPageIndex); + if (VERSION.SDK_INT >= 14 && mCurrentPageId == R.id.task_group_search) + { + if (mSearchItem != null) + { + // that's actually quite impossible to happen + MenuItemCompat.expandActionView(mSearchItem); + } + else + { + mAutoExpandSearchView = true; + } + } + } + updateTitle(currentPageIndex); + + // Bind the tabs to the ViewPager + mTabs = (TabLayout) findViewById(R.id.tabs); + mTabs.setupWithViewPager(mViewPager); + + // set up the tab icons + for (int i = 0, count = mPagerAdapter.getCount(); i < count; ++i) + { + mTabs.getTabAt(i).setIcon(mPagerAdapter.getTabIcon(i)); + } + + mViewPager.addOnPageChangeListener(new OnPageChangeListener() + { + + @Override + public void onPageSelected(int position) + { + mSelectedTaskUri = null; + mCurrentPagePosition = position; + + int newPageId = mPagerAdapter.getPageId(mCurrentPagePosition); + + if (newPageId == R.id.task_group_search) + { + int oldPageId = mCurrentPageId; + mCurrentPageId = newPageId; + + // store the page position we're coming from + mPreviousPagePosition = mPagerAdapter.getPagePosition(oldPageId); + } + else if (mCurrentPageId == R.id.task_group_search) + { + // we've been on the search page before, so commit the search and close the search view + mSearchHistoryHelper.commitSearch(); + mHandler.post(mSearchUpdater); + mCurrentPageId = newPageId; + hideSearchActionView(); + } + mCurrentPageId = newPageId; + updateTitle(mCurrentPageId); + } + + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) + { + } + + + @Override + public void onPageScrollStateChanged(int state) + { + if (state == ViewPager.SCROLL_STATE_IDLE && mCurrentPageId == R.id.task_group_search) + { + // the search page is selected now, expand the search view + mHandler.postDelayed(new Runnable() + { + @Override + public void run() + { + MenuItemCompat.expandActionView(mSearchItem); + } + }, 50); + } + } + + }); + + mFloatingActionButton = (FloatingActionButton) findViewById(R.id.floating_action_button); + if (mFloatingActionButton != null) + { + mFloatingActionButton.setOnClickListener(new OnClickListener() + { + + @Override + public void onClick(View v) + { + onAddNewTask(); + } + }); + } + } + + + @Override + protected void onResume() + { + updateTitle(mCurrentPageId); + super.onResume(); + } - private final Handler mHandler = new Handler(); - private SearchHistoryHelper mSearchHistoryHelper; + @Override + protected void onNewIntent(Intent intent) + { + resolveIntentAction(intent); + super.onNewIntent(intent); + } - private boolean mAutoExpandSearchView = false; - /** Indicates that the activity switched to detail view due to rotation. **/ - @Retain - private boolean mSwitchedToDetail = false; + @Override + protected void onDestroy() + { + super.onDestroy(); + mSearchHistoryHelper.close(); + } - /** The Uri of the task to display/highlight in the list view. **/ - @Retain - private Uri mSelectedTaskUri; - /** The Uri of the task to display/highlight in the list view coming from the widget. **/ - private Uri mSelectedTaskUriOnLaunch; + /** + * Callback method from {@link TaskListFragment.Callbacks} indicating that the item with the given ID was selected. + */ + @Override + public void onItemSelected(Uri uri, boolean forceReload, int pagePosition) + { + // only accept selections from the current visible task fragment or the activity itself + if (pagePosition == -1 || pagePosition == mCurrentPagePosition) + { + if (mTwoPane) + { + mShouldShowDetails = true; + if (forceReload) + { + mSelectedTaskUri = null; + mShouldSwitchToDetail = false; + } + loadTaskDetailFragment(uri); + } + else if (forceReload) + { + mSelectedTaskUri = uri; + + // In single-pane mode, simply start the detail activity + // for the selected item ID. + Intent detailIntent = new Intent(Intent.ACTION_VIEW); + detailIntent.setData(uri); + startActivity(detailIntent); + mSwitchedToDetail = true; + mShouldSwitchToDetail = false; + } + } + } - /** Indicates to display the two pane layout with details **/ - @Retain - private boolean mShouldShowDetails = false; - - /** Indicates to show ViewTaskActivity when rotating to single pane. **/ - @Retain - private boolean mShouldSwitchToDetail = false; - - /** Indicates the TaskListFragments to select/highlight the mSelectedTaskUri item **/ - private boolean mShouldSelectTaskListItem = false; - - /** Indicates a transient state after rotation to redirect to the TaskViewActivtiy **/ - private boolean mTransientState = false; - private CollapsingToolbarLayout mToolbarLayout; - - private AppBarLayout mAppBarLayout; - - private FloatingActionButton mFloatingActionButton; - - - @SuppressLint("NewApi") - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - @Override - protected void onCreate(Bundle savedInstanceState) - { - Log.d(TAG, "onCreate called again"); - super.onCreate(savedInstanceState); - - // check for single pane activity change - mTwoPane = getResources().getBoolean(R.bool.has_two_panes); - - resolveIntentAction(getIntent()); - - if (mSelectedTaskUri != null) - { - if (mShouldShowDetails && mShouldSwitchToDetail) - { - Intent viewTaskIntent = new Intent(Intent.ACTION_VIEW); - viewTaskIntent.setData(mSelectedTaskUri); - startActivity(viewTaskIntent); - mSwitchedToDetail = true; - mShouldSwitchToDetail = false; - mTransientState = true; - } - } - else - { - mShouldShowDetails = false; - } - - setContentView(R.layout.activity_task_list); - mAppBarLayout = (AppBarLayout) findViewById(R.id.appbar); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - mAuthority = TaskContract.taskAuthority(this); - mSearchHistoryHelper = new SearchHistoryHelper(this); - - if (findViewById(R.id.task_detail_container) != null) - { - // In two-pane mode, list items should be given the - // 'activated' state when touched. - - // get list fragment - // mTaskListFrag = (TaskListFragment) getSupportFragmentManager().findFragmentById(R.id.task_list); - // mTaskListFrag.setListViewScrollbarPositionLeft(true); - - // mTaskListFrag.setActivateOnItemClick(true); - - loadTaskDetailFragment(mSelectedTaskUri); - } - else - { - FragmentManager fragmentManager = getSupportFragmentManager(); - Fragment detailFragment = fragmentManager.findFragmentByTag(DETAIL_FRAGMENT_TAG); - if (detailFragment != null) - { - fragmentManager.beginTransaction().remove(detailFragment).commit(); - } - } - - mGroupingFactories = new AbstractGroupingFactory[] { new ByList(mAuthority), new ByDueDate(mAuthority), new ByStartDate(mAuthority), - new ByPriority(mAuthority), new ByProgress(mAuthority), new BySearch(mAuthority, mSearchHistoryHelper) }; - - // set up pager adapter - try - { - mPagerAdapter = new TaskGroupPagerAdapter(getSupportFragmentManager(), mGroupingFactories, this, R.xml.listview_tabs); - } - catch (XmlPullParserException e) - { - // TODO Automatisch generierter Erfassungsblock - e.printStackTrace(); - } - catch (IOException e) - { - // TODO Automatisch generierter Erfassungsblock - e.printStackTrace(); - } - catch (XmlObjectPullParserException e) - { - // TODO Automatisch generierter Erfassungsblock - e.printStackTrace(); - } - - // Setup ViewPager - mPagerAdapter.setTwoPaneLayout(mTwoPane); - mViewPager = (ViewPager) findViewById(R.id.pager); - mViewPager.setAdapter(mPagerAdapter); - - int currentPageIndex = mPagerAdapter.getPagePosition(mCurrentPageId); - - if (currentPageIndex >= 0) - { - mCurrentPagePosition = currentPageIndex; - mViewPager.setCurrentItem(currentPageIndex); - if (VERSION.SDK_INT >= 14 && mCurrentPageId == R.id.task_group_search) - { - if (mSearchItem != null) - { - // that's actually quite impossible to happen - MenuItemCompat.expandActionView(mSearchItem); - } - else - { - mAutoExpandSearchView = true; - } - } - } - updateTitle(currentPageIndex); - - // Bind the tabs to the ViewPager - mTabs = (TabLayout) findViewById(R.id.tabs); - mTabs.setupWithViewPager(mViewPager); - - // set up the tab icons - for (int i = 0, count = mPagerAdapter.getCount(); i < count; ++i) - { - mTabs.getTabAt(i).setIcon(mPagerAdapter.getTabIcon(i)); - } - - mViewPager.addOnPageChangeListener(new OnPageChangeListener() - { - - @Override - public void onPageSelected(int position) - { - mSelectedTaskUri = null; - mCurrentPagePosition = position; - - int newPageId = mPagerAdapter.getPageId(mCurrentPagePosition); - - if (newPageId == R.id.task_group_search) - { - int oldPageId = mCurrentPageId; - mCurrentPageId = newPageId; - - // store the page position we're coming from - mPreviousPagePosition = mPagerAdapter.getPagePosition(oldPageId); - } - else if (mCurrentPageId == R.id.task_group_search) - { - // we've been on the search page before, so commit the search and close the search view - mSearchHistoryHelper.commitSearch(); - mHandler.post(mSearchUpdater); - mCurrentPageId = newPageId; - hideSearchActionView(); - } - mCurrentPageId = newPageId; - updateTitle(mCurrentPageId); - } - - - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) - { - } - - - @Override - public void onPageScrollStateChanged(int state) - { - if (state == ViewPager.SCROLL_STATE_IDLE && mCurrentPageId == R.id.task_group_search) - { - // the search page is selected now, expand the search view - mHandler.postDelayed(new Runnable() - { - @Override - public void run() - { - MenuItemCompat.expandActionView(mSearchItem); - } - }, 50); - } - } - - }); - - mFloatingActionButton = (FloatingActionButton) findViewById(R.id.floating_action_button); - if (mFloatingActionButton != null) - { - mFloatingActionButton.setOnClickListener(new OnClickListener() - { - - @Override - public void onClick(View v) - { - onAddNewTask(); - } - }); - } - } - - - @Override - protected void onResume() - { - updateTitle(mCurrentPageId); - super.onResume(); - } - - - @Override - protected void onNewIntent(Intent intent) - { - resolveIntentAction(intent); - super.onNewIntent(intent); - } - - - @Override - protected void onDestroy() - { - super.onDestroy(); - mSearchHistoryHelper.close(); - } - - - /** - * Callback method from {@link TaskListFragment.Callbacks} indicating that the item with the given ID was selected. - */ - @Override - public void onItemSelected(Uri uri, boolean forceReload, int pagePosition) - { - // only accept selections from the current visible task fragment or the activity itself - if (pagePosition == -1 || pagePosition == mCurrentPagePosition) - { - if (mTwoPane) - { - mShouldShowDetails = true; - if (forceReload) - { - mSelectedTaskUri = null; - mShouldSwitchToDetail = false; - } - loadTaskDetailFragment(uri); - } - else if (forceReload) - { - mSelectedTaskUri = uri; - - // In single-pane mode, simply start the detail activity - // for the selected item ID. - Intent detailIntent = new Intent(Intent.ACTION_VIEW); - detailIntent.setData(uri); - startActivity(detailIntent); - mSwitchedToDetail = true; - mShouldSwitchToDetail = false; - } - } - } - - - private void loadTaskDetailFragment(Uri uri) - { - Fragment detailFragment = getSupportFragmentManager().findFragmentByTag(DETAIL_FRAGMENT_TAG); - - if (uri == null) - { - if (!(detailFragment instanceof EmptyTaskFragment)) - { - replaceDetailFragment(new EmptyTaskFragment()); - } - } - else - { - if (detailFragment instanceof ViewTaskFragment) - { - ((ViewTaskFragment) detailFragment).loadUri(uri); - } - else - { - replaceDetailFragment(ViewTaskFragment.newInstance(uri)); - } - } - } + private void loadTaskDetailFragment(Uri uri) + { + Fragment detailFragment = getSupportFragmentManager().findFragmentByTag(DETAIL_FRAGMENT_TAG); + + if (uri == null) + { + if (!(detailFragment instanceof EmptyTaskFragment)) + { + replaceDetailFragment(new EmptyTaskFragment()); + } + } + else + { + if (detailFragment instanceof ViewTaskFragment) + { + ((ViewTaskFragment) detailFragment).loadUri(uri); + } + else + { + replaceDetailFragment(ViewTaskFragment.newInstance(uri)); + } + } + } private void replaceDetailFragment(Fragment fragment) @@ -454,341 +476,342 @@ public class TaskListActivity extends AppCompatActivity implements TaskListFragm } - private void updateTitle(int pageId) - { - switch (pageId) - { - case R.id.task_group_by_list: - getSupportActionBar().setTitle(R.string.task_group_title_list); - break; - case R.id.task_group_by_start: - getSupportActionBar().setTitle(R.string.task_group_title_start); - break; - case R.id.task_group_by_due: - getSupportActionBar().setTitle(R.string.task_group_title_due); - break; - case R.id.task_group_by_priority: - getSupportActionBar().setTitle(R.string.task_group_title_priority); - break; - case R.id.task_group_by_progress: - getSupportActionBar().setTitle(R.string.task_group_title_progress); - break; - - default: - getSupportActionBar().setTitle(R.string.task_group_title_default); - break; - } - } - - - @Override - public void onEditTask(Uri taskUri, ContentSet data) - { - Intent editTaskIntent = new Intent(Intent.ACTION_EDIT); - editTaskIntent.setData(taskUri); - if (data != null) - { - Bundle extraBundle = new Bundle(); - extraBundle.putParcelable(EditTaskActivity.EXTRA_DATA_CONTENT_SET, data); - editTaskIntent.putExtra(EditTaskActivity.EXTRA_DATA_BUNDLE, extraBundle); - } - startActivity(editTaskIntent); - } - - - @Override - public void onAddNewTask() - { - Intent editTaskIntent = new Intent(Intent.ACTION_INSERT); - editTaskIntent.setData(Tasks.getContentUri(mAuthority)); - startActivityForResult(editTaskIntent, REQUEST_CODE_NEW_TASK); - } - - - private void resolveIntentAction(Intent intent) - { - // check which task should be selected - if (intent.hasExtra(EXTRA_DISPLAY_TASK)) - - { - mShouldSwitchToDetail = true; - mSelectedTaskUri = intent.getData(); - } - - if (intent != null && intent.hasExtra(EXTRA_DISPLAY_TASK) && intent.getBooleanExtra(EXTRA_FORCE_LIST_SELECTION, true) && mTwoPane) - { - mShouldSwitchToDetail = true; - Uri newSelection = intent.getData(); - mSelectedTaskUriOnLaunch = newSelection; - mShouldSelectTaskListItem = true; - if (mPagerAdapter != null) - { - mPagerAdapter.notifyDataSetChanged(); - } - } - else - { - mSelectedTaskUriOnLaunch = null; - mShouldSelectTaskListItem = false; - } - } - - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) - { - if (resultCode == RESULT_OK && intent != null && intent.getData() != null) - { - // Use the same flow to display the new task as if it was opened from the widget - Intent displayIntent = new Intent(this, TaskListActivity.class); - displayIntent.putExtra(TaskListActivity.EXTRA_DISPLAY_TASK, true); - displayIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, true); - Uri newTaskUri = intent.getData(); - displayIntent.setData(newTaskUri); - onNewIntent(displayIntent); - } - } - - - @Override - public void onDelete(Uri taskUri) - { - // nothing to do here, the loader will take care of reloading the list and the list view will take care of selecting the next element. - - // empty the detail fragment - if (mTwoPane) - { - loadTaskDetailFragment(null); - } - } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) - { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.task_list_activity_menu, menu); - - MenuItem addItem = menu.findItem(R.id.menu_add_task); - if (addItem != null && mFloatingActionButton != null) - { - // hide menu option to add a task if we have a floating action button - addItem.setVisible(false); - } - - // restore menu state - MenuItem item = menu.findItem(R.id.menu_alarms); - if (item != null) - { - item.setChecked(AlarmBroadcastReceiver.getAlarmPreference(this)); - } - - // search - setupSearch(menu); - - return true; - } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - int id = item.getItemId(); - if (id == R.id.menu_add_task) - { - onAddNewTask(); - return true; - } - else if (item.getItemId() == R.id.menu_visible_list) - { - Intent settingsIntent = new Intent(getBaseContext(), SyncSettingsActivity.class); - startActivity(settingsIntent); - return true; - } - else if (item.getItemId() == R.id.menu_alarms) - { - // set and save state - boolean activatedAlarms = !item.isChecked(); - item.setChecked(activatedAlarms); - AlarmBroadcastReceiver.setAlarmPreference(this, activatedAlarms); - return true; - } - else - { - return super.onOptionsItemSelected(item); - } - } - - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - private void hideSearchActionView() - { - MenuItemCompat.collapseActionView(mSearchItem); - } - - - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - public void setupSearch(Menu menu) - { - // bail early on unsupported devices - if (Build.VERSION.SDK_INT < 11) - { - return; - } - - mSearchItem = menu.findItem(R.id.search); - MenuItemCompat.setOnActionExpandListener(mSearchItem, new OnActionExpandListener() - { - - @Override - public boolean onMenuItemActionExpand(MenuItem item) - { - // always allow expansion of the search action view - return mCurrentPageId == R.id.task_group_search; - } - - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) - { - // return to previous view - if (mPreviousPagePosition >= 0 && mCurrentPageId == R.id.task_group_search) - { - mViewPager.setCurrentItem(mPreviousPagePosition); - mCurrentPageId = mPagerAdapter.getPageId(mPreviousPagePosition); - } - return mPreviousPagePosition >= 0 || mCurrentPageId != R.id.task_group_search; - } - }); - SearchView searchView = (SearchView) MenuItemCompat.getActionView(mSearchItem); - - SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); - if (null != searchManager) - { - searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); - } - - searchView.setQueryHint(getString(R.string.menu_search_hint)); - searchView.setIconified(true); - searchView.setOnQueryTextListener(new OnQueryTextListener() - { - - @Override - public boolean onQueryTextSubmit(String query) - { - // persist current search - mSearchHistoryHelper.commitSearch(); - mHandler.post(mSearchUpdater); - return true; - } - - - @Override - public boolean onQueryTextChange(String query) - { - if (mCurrentPageId != R.id.task_group_search) - { - return true; - } - - mHandler.removeCallbacks(mSearchUpdater); - if (query.length() > 0) - { - mSearchHistoryHelper.updateSearch(query); - mHandler.postDelayed(mSearchUpdater, SEARCH_UPDATE_DELAY); - } - else - { - mSearchHistoryHelper.removeCurrentSearch(); - mHandler.post(mSearchUpdater); - } - return true; - } - }); - - if (mAutoExpandSearchView) - { - mSearchItem.expandActionView(); - } - - } - - - @Override - public ExpandableGroupDescriptor getGroupDescriptor(int pageId) - { - for (AbstractGroupingFactory factory : mGroupingFactories) - { - if (factory.getId() == pageId) - { - return factory.getExpandableGroupDescriptor(); - } - } - return null; - } - - /** - * Notifies the search fragment of an update. - */ - private final Runnable mSearchUpdater = new Runnable() - { - - @Override - public void run() - { - TaskListFragment fragment = (TaskListFragment) mPagerAdapter.instantiateItem(mViewPager, mViewPager.getCurrentItem()); - fragment.notifyDataSetChanged(true); - fragment.expandCurrentSearchGroup(); - } - }; - - - private int darkenColor(int color) - { - float[] hsv = new float[3]; - Color.colorToHSV(color, hsv); - hsv[2] = hsv[2] * 0.75f; - color = Color.HSVToColor(hsv); - return color; - } - - - @SuppressLint("NewApi") - @Override - public void updateColor(int color) - { - if (mTwoPane) - { - getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color)); - mTabs.setBackgroundColor(color); - - if (mAppBarLayout != null) - { - mAppBarLayout.setBackgroundColor(color); - } - - if (VERSION.SDK_INT >= 21) - { - Window window = getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - window.setStatusBarColor(darkenColor(color)); - } - } - } - - - public Uri getSelectedTaskUri() - { - if (mShouldSelectTaskListItem) - { - return mSelectedTaskUriOnLaunch; - } - return null; - } - - - public boolean isInTransientState() - { - return mTransientState; - } + private void updateTitle(int pageId) + { + switch (pageId) + { + case R.id.task_group_by_list: + getSupportActionBar().setTitle(R.string.task_group_title_list); + break; + case R.id.task_group_by_start: + getSupportActionBar().setTitle(R.string.task_group_title_start); + break; + case R.id.task_group_by_due: + getSupportActionBar().setTitle(R.string.task_group_title_due); + break; + case R.id.task_group_by_priority: + getSupportActionBar().setTitle(R.string.task_group_title_priority); + break; + case R.id.task_group_by_progress: + getSupportActionBar().setTitle(R.string.task_group_title_progress); + break; + + default: + getSupportActionBar().setTitle(R.string.task_group_title_default); + break; + } + } + + + @Override + public void onEditTask(Uri taskUri, ContentSet data) + { + Intent editTaskIntent = new Intent(Intent.ACTION_EDIT); + editTaskIntent.setData(taskUri); + if (data != null) + { + Bundle extraBundle = new Bundle(); + extraBundle.putParcelable(EditTaskActivity.EXTRA_DATA_CONTENT_SET, data); + editTaskIntent.putExtra(EditTaskActivity.EXTRA_DATA_BUNDLE, extraBundle); + } + startActivity(editTaskIntent); + } + + + @Override + public void onAddNewTask() + { + Intent editTaskIntent = new Intent(Intent.ACTION_INSERT); + editTaskIntent.setData(Tasks.getContentUri(mAuthority)); + startActivityForResult(editTaskIntent, REQUEST_CODE_NEW_TASK); + } + + + private void resolveIntentAction(Intent intent) + { + // check which task should be selected + if (intent.hasExtra(EXTRA_DISPLAY_TASK)) + + { + mShouldSwitchToDetail = true; + mSelectedTaskUri = intent.getData(); + } + + if (intent != null && intent.hasExtra(EXTRA_DISPLAY_TASK) && intent.getBooleanExtra(EXTRA_FORCE_LIST_SELECTION, true) && mTwoPane) + { + mShouldSwitchToDetail = true; + Uri newSelection = intent.getData(); + mSelectedTaskUriOnLaunch = newSelection; + mShouldSelectTaskListItem = true; + if (mPagerAdapter != null) + { + mPagerAdapter.notifyDataSetChanged(); + } + } + else + { + mSelectedTaskUriOnLaunch = null; + mShouldSelectTaskListItem = false; + } + } + + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) + { + if (resultCode == RESULT_OK && intent != null && intent.getData() != null) + { + // Use the same flow to display the new task as if it was opened from the widget + Intent displayIntent = new Intent(this, TaskListActivity.class); + displayIntent.putExtra(TaskListActivity.EXTRA_DISPLAY_TASK, true); + displayIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, true); + Uri newTaskUri = intent.getData(); + displayIntent.setData(newTaskUri); + onNewIntent(displayIntent); + } + } + + + @Override + public void onDelete(Uri taskUri) + { + // nothing to do here, the loader will take care of reloading the list and the list view will take care of selecting the next element. + + // empty the detail fragment + if (mTwoPane) + { + loadTaskDetailFragment(null); + } + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.task_list_activity_menu, menu); + + MenuItem addItem = menu.findItem(R.id.menu_add_task); + if (addItem != null && mFloatingActionButton != null) + { + // hide menu option to add a task if we have a floating action button + addItem.setVisible(false); + } + + // restore menu state + MenuItem item = menu.findItem(R.id.menu_alarms); + if (item != null) + { + item.setChecked(AlarmBroadcastReceiver.getAlarmPreference(this)); + } + + // search + setupSearch(menu); + + return true; + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + int id = item.getItemId(); + if (id == R.id.menu_add_task) + { + onAddNewTask(); + return true; + } + else if (item.getItemId() == R.id.menu_visible_list) + { + Intent settingsIntent = new Intent(getBaseContext(), SyncSettingsActivity.class); + startActivity(settingsIntent); + return true; + } + else if (item.getItemId() == R.id.menu_alarms) + { + // set and save state + boolean activatedAlarms = !item.isChecked(); + item.setChecked(activatedAlarms); + AlarmBroadcastReceiver.setAlarmPreference(this, activatedAlarms); + return true; + } + else + { + return super.onOptionsItemSelected(item); + } + } + + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private void hideSearchActionView() + { + MenuItemCompat.collapseActionView(mSearchItem); + } + + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public void setupSearch(Menu menu) + { + // bail early on unsupported devices + if (Build.VERSION.SDK_INT < 11) + { + return; + } + + mSearchItem = menu.findItem(R.id.search); + MenuItemCompat.setOnActionExpandListener(mSearchItem, new OnActionExpandListener() + { + + @Override + public boolean onMenuItemActionExpand(MenuItem item) + { + // always allow expansion of the search action view + return mCurrentPageId == R.id.task_group_search; + } + + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) + { + // return to previous view + if (mPreviousPagePosition >= 0 && mCurrentPageId == R.id.task_group_search) + { + mViewPager.setCurrentItem(mPreviousPagePosition); + mCurrentPageId = mPagerAdapter.getPageId(mPreviousPagePosition); + } + return mPreviousPagePosition >= 0 || mCurrentPageId != R.id.task_group_search; + } + }); + SearchView searchView = (SearchView) MenuItemCompat.getActionView(mSearchItem); + + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + if (null != searchManager) + { + searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); + } + + searchView.setQueryHint(getString(R.string.menu_search_hint)); + searchView.setIconified(true); + searchView.setOnQueryTextListener(new OnQueryTextListener() + { + + @Override + public boolean onQueryTextSubmit(String query) + { + // persist current search + mSearchHistoryHelper.commitSearch(); + mHandler.post(mSearchUpdater); + return true; + } + + + @Override + public boolean onQueryTextChange(String query) + { + if (mCurrentPageId != R.id.task_group_search) + { + return true; + } + + mHandler.removeCallbacks(mSearchUpdater); + if (query.length() > 0) + { + mSearchHistoryHelper.updateSearch(query); + mHandler.postDelayed(mSearchUpdater, SEARCH_UPDATE_DELAY); + } + else + { + mSearchHistoryHelper.removeCurrentSearch(); + mHandler.post(mSearchUpdater); + } + return true; + } + }); + + if (mAutoExpandSearchView) + { + mSearchItem.expandActionView(); + } + + } + + + @Override + public ExpandableGroupDescriptor getGroupDescriptor(int pageId) + { + for (AbstractGroupingFactory factory : mGroupingFactories) + { + if (factory.getId() == pageId) + { + return factory.getExpandableGroupDescriptor(); + } + } + return null; + } + + + /** + * Notifies the search fragment of an update. + */ + private final Runnable mSearchUpdater = new Runnable() + { + + @Override + public void run() + { + TaskListFragment fragment = (TaskListFragment) mPagerAdapter.instantiateItem(mViewPager, mViewPager.getCurrentItem()); + fragment.notifyDataSetChanged(true); + fragment.expandCurrentSearchGroup(); + } + }; + + + private int darkenColor(int color) + { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] = hsv[2] * 0.75f; + color = Color.HSVToColor(hsv); + return color; + } + + + @SuppressLint("NewApi") + @Override + public void updateColor(int color) + { + if (mTwoPane) + { + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color)); + mTabs.setBackgroundColor(color); + + if (mAppBarLayout != null) + { + mAppBarLayout.setBackgroundColor(color); + } + + if (VERSION.SDK_INT >= 21) + { + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(darkenColor(color)); + } + } + } + + + public Uri getSelectedTaskUri() + { + if (mShouldSelectTaskListItem) + { + return mSelectedTaskUriOnLaunch; + } + return null; + } + + + public boolean isInTransientState() + { + return mTransientState; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java b/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java index 9f681c7c..7b84e6e7 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java @@ -17,27 +17,6 @@ package org.dmfs.tasks; -import org.dmfs.android.retentionmagic.SupportFragment; -import org.dmfs.android.retentionmagic.annotations.Parameter; -import org.dmfs.android.retentionmagic.annotations.Retain; -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.TaskContract.Instances; -import org.dmfs.provider.tasks.TaskContract.Tasks; -import org.dmfs.tasks.groupings.ByDueDate; -import org.dmfs.tasks.groupings.ByList; -import org.dmfs.tasks.groupings.filters.AbstractFilter; -import org.dmfs.tasks.groupings.filters.ConstantFilter; -import org.dmfs.tasks.model.Model; -import org.dmfs.tasks.model.Sources; -import org.dmfs.tasks.utils.ExpandableGroupDescriptor; -import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; -import org.dmfs.tasks.utils.FlingDetector; -import org.dmfs.tasks.utils.FlingDetector.OnFlingListener; -import org.dmfs.tasks.utils.OnChildLoadedListener; -import org.dmfs.tasks.utils.OnModelLoadedListener; -import org.dmfs.tasks.utils.RetainExpandableListView; -import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper.SearchHistoryColumns; - import android.accounts.Account; import android.accounts.AccountManager; import android.annotation.TargetApi; @@ -73,1001 +52,1030 @@ import android.widget.ExpandableListView.OnGroupCollapseListener; import android.widget.ListView; import android.widget.TextView; +import org.dmfs.android.retentionmagic.SupportFragment; +import org.dmfs.android.retentionmagic.annotations.Parameter; +import org.dmfs.android.retentionmagic.annotations.Retain; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.TaskContract.Instances; +import org.dmfs.provider.tasks.TaskContract.Tasks; +import org.dmfs.tasks.groupings.ByDueDate; +import org.dmfs.tasks.groupings.ByList; +import org.dmfs.tasks.groupings.filters.AbstractFilter; +import org.dmfs.tasks.groupings.filters.ConstantFilter; +import org.dmfs.tasks.model.Model; +import org.dmfs.tasks.model.Sources; +import org.dmfs.tasks.utils.ExpandableGroupDescriptor; +import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; +import org.dmfs.tasks.utils.FlingDetector; +import org.dmfs.tasks.utils.FlingDetector.OnFlingListener; +import org.dmfs.tasks.utils.OnChildLoadedListener; +import org.dmfs.tasks.utils.OnModelLoadedListener; +import org.dmfs.tasks.utils.RetainExpandableListView; +import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper.SearchHistoryColumns; + /** * A list fragment representing a list of Tasks. This fragment also supports tablet devices by allowing list items to be given an 'activated' state upon * selection. This helps indicate which item is currently being viewed in a {@link ViewTaskFragment}. *

    * Activities containing this fragment MUST implement the {@link Callbacks} interface - * + * * @author Tobias Reinsch */ public class TaskListFragment extends SupportFragment - implements LoaderManager.LoaderCallbacks, OnChildLoadedListener, OnModelLoadedListener, OnFlingListener + implements LoaderManager.LoaderCallbacks, OnChildLoadedListener, OnModelLoadedListener, OnFlingListener { - @SuppressWarnings("unused") - private static final String TAG = "org.dmfs.tasks.TaskListFragment"; - - private final static String ARG_INSTANCE_ID = "instance_id"; - private final static String ARG_TWO_PANE_LAYOUT = "two_pane_layout"; - - private static final long INTERVAL_LISTVIEW_REDRAW = 60000; - - /** - * A filter to hide completed tasks. - */ - private final static AbstractFilter COMPLETED_FILTER = new ConstantFilter(Tasks.IS_CLOSED + "=0"); - - /** - * The group descriptor to use. At present this can be either {@link ByDueDate#GROUP_DESCRIPTOR}, {@link ByCompleted#GROUP_DESCRIPTOR} or - * {@link ByList#GROUP_DESCRIPTOR}. - */ - private ExpandableGroupDescriptor mGroupDescriptor; - - /** - * The fragment's current callback object, which is notified of list item clicks. - */ - private Callbacks mCallbacks; - - @Retain(permanent = true, instanceNSField = "mInstancePosition") - private int mActivatedPositionGroup = ExpandableListView.INVALID_POSITION; - @Retain(permanent = true, instanceNSField = "mInstancePosition") - private int mActivatedPositionChild = ExpandableListView.INVALID_POSITION; - - private RetainExpandableListView mExpandableListView; - private Context mAppContext; - private ExpandableGroupDescriptorAdapter mAdapter; - private Handler mHandler; - @Retain(permanent = true, instanceNSField = "mInstancePosition") - private long[] mSavedExpandedGroups = null; - @Retain(permanent = true, instanceNSField = "mInstancePosition") - private boolean mSavedCompletedFilter; - - @Parameter(key = ARG_INSTANCE_ID) - private int mInstancePosition; - - @Parameter(key = ARG_TWO_PANE_LAYOUT) - private boolean mTwoPaneLayout; - - private Loader mCursorLoader; - private String mAuthority; - - private Uri mSelectedTaskUri; - - /** The child position to open when the fragment is displayed. **/ - private ListPosition mSelectedChildPosition; - - @Retain - private int mPageId = -1; - - private final OnChildClickListener mTaskItemClickListener = new OnChildClickListener() - { - - @Override - public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) - { - selectChildView(parent, groupPosition, childPosition, true); - if (mExpandableListView.getChoiceMode() == ExpandableListView.CHOICE_MODE_SINGLE) - { - mActivatedPositionGroup = groupPosition; - mActivatedPositionChild = childPosition; - } - /* - * In contrast to a ListView an ExpandableListView does not set the activated item on it's own. So we have to do that here. + @SuppressWarnings("unused") + private static final String TAG = "org.dmfs.tasks.TaskListFragment"; + + private final static String ARG_INSTANCE_ID = "instance_id"; + private final static String ARG_TWO_PANE_LAYOUT = "two_pane_layout"; + + private static final long INTERVAL_LISTVIEW_REDRAW = 60000; + + /** + * A filter to hide completed tasks. + */ + private final static AbstractFilter COMPLETED_FILTER = new ConstantFilter(Tasks.IS_CLOSED + "=0"); + + /** + * The group descriptor to use. At present this can be either {@link ByDueDate#GROUP_DESCRIPTOR}, {@link ByCompleted#GROUP_DESCRIPTOR} or + * {@link ByList#GROUP_DESCRIPTOR}. + */ + private ExpandableGroupDescriptor mGroupDescriptor; + + /** + * The fragment's current callback object, which is notified of list item clicks. + */ + private Callbacks mCallbacks; + + @Retain(permanent = true, instanceNSField = "mInstancePosition") + private int mActivatedPositionGroup = ExpandableListView.INVALID_POSITION; + @Retain(permanent = true, instanceNSField = "mInstancePosition") + private int mActivatedPositionChild = ExpandableListView.INVALID_POSITION; + + private RetainExpandableListView mExpandableListView; + private Context mAppContext; + private ExpandableGroupDescriptorAdapter mAdapter; + private Handler mHandler; + @Retain(permanent = true, instanceNSField = "mInstancePosition") + private long[] mSavedExpandedGroups = null; + @Retain(permanent = true, instanceNSField = "mInstancePosition") + private boolean mSavedCompletedFilter; + + @Parameter(key = ARG_INSTANCE_ID) + private int mInstancePosition; + + @Parameter(key = ARG_TWO_PANE_LAYOUT) + private boolean mTwoPaneLayout; + + private Loader mCursorLoader; + private String mAuthority; + + private Uri mSelectedTaskUri; + + /** + * The child position to open when the fragment is displayed. + **/ + private ListPosition mSelectedChildPosition; + + @Retain + private int mPageId = -1; + + private final OnChildClickListener mTaskItemClickListener = new OnChildClickListener() + { + + @Override + public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) + { + selectChildView(parent, groupPosition, childPosition, true); + if (mExpandableListView.getChoiceMode() == ExpandableListView.CHOICE_MODE_SINGLE) + { + mActivatedPositionGroup = groupPosition; + mActivatedPositionChild = childPosition; + } + /* + * In contrast to a ListView an ExpandableListView does not set the activated item on it's own. So we have to do that here. */ - setActivatedItem(groupPosition, childPosition); - return true; - } - - }; - - private final OnGroupCollapseListener mTaskListCollapseListener = new OnGroupCollapseListener() - { - - @Override - public void onGroupCollapse(int groupPosition) - { - if (groupPosition == mActivatedPositionGroup) - { - mActivatedPositionChild = ExpandableListView.INVALID_POSITION; - mActivatedPositionGroup = ExpandableListView.INVALID_POSITION; - } - - } - }; - - /** - * A callback interface that all activities containing this fragment must implement. This mechanism allows activities to be notified of item selections. - */ - public interface Callbacks - { - /** - * Callback for when an item has been selected. - * - * @param taskUri - * The {@link Uri} of the selected task. - * @param forceReload - * Whether to reload the task or not. - * @param sender - * The sender of the callback. - */ - public void onItemSelected(Uri taskUri, boolean forceReload, int pagePosition); - - - public ExpandableGroupDescriptor getGroupDescriptor(int position); - - - public void onAddNewTask(); - } - - /** - * A runnable that periodically updates the list. We need that to update relative dates & times. TODO: we probably should move that to the adapter to update - * only the date & times fields, not the entire list. - */ - private Runnable mListRedrawRunnable = new Runnable() - { - - @Override - public void run() - { - mExpandableListView.invalidateViews(); - mHandler.postDelayed(this, INTERVAL_LISTVIEW_REDRAW); - } - }; - - - public static TaskListFragment newInstance(int instancePosition, boolean twoPaneLayout) - { - TaskListFragment result = new TaskListFragment(); - Bundle args = new Bundle(); - args.putInt(ARG_INSTANCE_ID, instancePosition); - args.putBoolean(ARG_TWO_PANE_LAYOUT, twoPaneLayout); - result.setArguments(args); - return result; - } - - - /** - * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon screen orientation changes). - */ - public TaskListFragment() - { - } - - - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - mAuthority = TaskContract.taskAuthority(activity); - - mAppContext = activity.getBaseContext(); - - // Activities containing this fragment must implement its callbacks. - if (!(activity instanceof Callbacks)) - { - throw new IllegalStateException("Activity must implement fragment's callbacks."); - } - - mCallbacks = (Callbacks) activity; - - // load accounts early - Sources.loadModelAsync(activity, TaskContract.LOCAL_ACCOUNT_TYPE, this); - } - - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - mHandler = new Handler(); - setHasOptionsMenu(true); - } - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { - View rootView = inflater.inflate(R.layout.fragment_expandable_task_list, container, false); - mExpandableListView = (RetainExpandableListView) rootView.findViewById(android.R.id.list); - - if (!mTwoPaneLayout) - { - // Add a footer to make sure the floating action button doesn't hide anything. - mExpandableListView.addFooterView(inflater.inflate(R.layout.task_list_group, mExpandableListView, false)); - } - - if (mGroupDescriptor == null) - { - loadGroupDescriptor(); - } - - // setup the views - this.prepareReload(); - - // expand lists - if (mSavedExpandedGroups != null) - { - mExpandableListView.expandGroups(mSavedExpandedGroups); - } - - FlingDetector swiper = new FlingDetector(mExpandableListView, mGroupDescriptor.getElementViewDescriptor().getFlingContentViewId(), - getActivity().getApplicationContext()); - swiper.setOnFlingListener(this); - - return rootView; - } - - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) - { - super.onViewCreated(view, savedInstanceState); - } - - - @Override - public void onStart() - { - reloadCursor(); - super.onStart(); - } - - - @Override - public void onResume() - { - super.onResume(); - mExpandableListView.invalidateViews(); - startAutomaticRedraw(); - openSelectedChild(); - - if (mTwoPaneLayout) - { - setListViewScrollbarPositionLeft(true); - setActivateOnItemClick(true); - } - } - - - @Override - public void onPause() - { - // we can't rely on save instance state being called before onPause, so we get the expanded groups here again - if (!((TaskListActivity) getActivity()).isInTransientState()) - { - mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); - } - stopAutomaticRedraw(); - super.onPause(); - } - - - @Override - public void onDetach() - { - super.onDetach(); - - } - - - @Override - public void onSaveInstanceState(Bundle outState) - { - if (!((TaskListActivity) getActivity()).isInTransientState()) - { - mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); - } - super.onSaveInstanceState(outState); - } - - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) - { - // create menu - inflater.inflate(R.menu.task_list_fragment_menu, menu); - - // restore menu state - MenuItem item = menu.findItem(R.id.menu_show_completed); - if (item != null) - { - item.setChecked(mSavedCompletedFilter); - - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) - { - if (mSavedCompletedFilter) - { - item.setTitle(R.string.menu_hide_completed); - } - else - { - item.setTitle(R.string.menu_show_completed); - } - } - } - } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - int itemId = item.getItemId(); - if (itemId == R.id.menu_show_completed) - { - - mSavedCompletedFilter = !mSavedCompletedFilter; - item.setChecked(mSavedCompletedFilter); - mAdapter.setChildCursorFilter(mSavedCompletedFilter ? null : COMPLETED_FILTER); - - // reload the child cursors only - for (int i = 0; i < mAdapter.getGroupCount(); ++i) - { - mAdapter.reloadGroup(i); - } - return true; - } - else if (itemId == R.id.menu_sync_now) - { - doSyncNow(); - return true; - } - else - { - return super.onOptionsItemSelected(item); - } - } - - - @Override - public Loader onCreateLoader(int arg0, Bundle arg1) - { - - if (mGroupDescriptor != null) - { - mCursorLoader = mGroupDescriptor.getGroupCursorLoader(mAppContext); - } - return mCursorLoader; - - } - - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) - { - - if (mSavedExpandedGroups == null) - { - mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); - } - - mAdapter.setGroupCursor(cursor); - - if (mSavedExpandedGroups != null) - { - mExpandableListView.expandGroups(mSavedExpandedGroups); - if (!((TaskListActivity) getActivity()).isInTransientState()) - { - mSavedExpandedGroups = null; - } - } - - mHandler.post(new Runnable() - { - @Override - public void run() - { - mAdapter.reloadLoadedGroups(); - } - }); - } - - - @Override - public void onLoaderReset(Loader loader) - { - mAdapter.changeCursor(null); - } - - - @Override - public void onChildLoaded(final int pos, Cursor childCursor) - { - if (mActivatedPositionChild != ExpandableListView.INVALID_POSITION) - { - if (pos == mActivatedPositionGroup && mActivatedPositionChild != ExpandableListView.INVALID_POSITION) - { - mHandler.post(setOpenHandler); - } - } - // check for child to select - if (mTwoPaneLayout) - { - selectChild(pos, childCursor); - } - } - - - @Override - public void onModelLoaded(Model model) - { - // nothing to do, we've just loaded the default model to speed up loading the detail view and the editor view. - } - - - private void selectChildView(ExpandableListView expandLV, int groupPosition, int childPosition, boolean force) - { - if (groupPosition < mAdapter.getGroupCount() && childPosition < mAdapter.getChildrenCount(groupPosition)) - { - // a task instance element has been clicked, get it's instance id and notify the activity - ExpandableListAdapter listAdapter = expandLV.getExpandableListAdapter(); - Cursor cursor = (Cursor) listAdapter.getChild(groupPosition, childPosition); - - if (cursor == null) - { - return; - } - // TODO: for now we get the id of the task, not the instance, once we support recurrence we'll have to change that - Long selectTaskId = cursor.getLong(cursor.getColumnIndex(Instances.TASK_ID)); - - if (selectTaskId != null) - { - // Notify the active callbacks interface (the activity, if the fragment is attached to one) that an item has been selected. - - // TODO: use the instance URI one we support recurrence - Uri taskUri = ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), selectTaskId); - - mCallbacks.onItemSelected(taskUri, force, mInstancePosition); - } - } - } - - - /** - * prepares the update of the view after the group descriptor was changed - * - * - */ - public void prepareReload() - { - mAdapter = new ExpandableGroupDescriptorAdapter(getActivity(), getLoaderManager(), mGroupDescriptor); - mExpandableListView.setAdapter(mAdapter); - mExpandableListView.setOnChildClickListener((android.widget.ExpandableListView.OnChildClickListener) mTaskItemClickListener); - mExpandableListView.setOnGroupCollapseListener((android.widget.ExpandableListView.OnGroupCollapseListener) mTaskListCollapseListener); - mAdapter.setOnChildLoadedListener(this); - mAdapter.setChildCursorFilter(COMPLETED_FILTER); - restoreFilterState(); - - } - - - private void reloadCursor() - { - getLoaderManager().restartLoader(-1, null, this); - } - - - public void restoreFilterState() - { - if (mSavedCompletedFilter) - { - mAdapter.setChildCursorFilter(mSavedCompletedFilter ? null : COMPLETED_FILTER); - // reload the child cursors only - for (int i = 0; i < mAdapter.getGroupCount(); ++i) - { - mAdapter.reloadGroup(i); - } - } - - } - - - /** - * Trigger a synchronization for all accounts. - */ - private void doSyncNow() - { - AccountManager accountManager = AccountManager.get(mAppContext); - Account[] accounts = accountManager.getAccounts(); - for (Account account : accounts) - { - // TODO: do we need a new bundle for each account or can we reuse it? - Bundle extras = new Bundle(); - extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); - ContentResolver.requestSync(account, mAuthority, extras); - } - } - - - /** - * Remove the task with the given {@link Uri} and title, asking for confirmation first. - * - * @param taskUri - * The {@link Uri} of the atsk to remove. - * @param taskTitle - * the title of the task to remove. - * @return - */ - private void removeTask(final Uri taskUri, final String taskTitle) - { - new AlertDialog.Builder(getActivity()).setTitle(R.string.confirm_delete_title).setCancelable(true) - .setNegativeButton(android.R.string.cancel, new OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - // nothing to do here - } - }).setPositiveButton(android.R.string.ok, new OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - // TODO: remove the task in a background task - mAppContext.getContentResolver().delete(taskUri, null, null); - Snackbar.make(mExpandableListView, getString(R.string.toast_task_deleted, taskTitle), Snackbar.LENGTH_SHORT).show(); - } - }).setMessage(getString(R.string.confirm_delete_message_with_title, taskTitle)).create().show(); - } - - - /** - * Opens the task editor for the selected Task. - * - * @param taskUri - * The {@link Uri} of the task. - * @param taskTitle - * The name/title of the task. - */ - private void openTaskEditor(final Uri taskUri, final String accountType) - { - Intent editTaskIntent = new Intent(Intent.ACTION_EDIT); - editTaskIntent.setData(taskUri); - editTaskIntent.putExtra(EditTaskActivity.EXTRA_DATA_ACCOUNT_TYPE, accountType); - startActivity(editTaskIntent); - } - - - @Override - public int canFling(ListView v, int pos) - { - long packedPos = mExpandableListView.getExpandableListPosition(pos); - if (packedPos != ExpandableListView.PACKED_POSITION_VALUE_NULL - && ExpandableListView.getPackedPositionType(packedPos) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) - { - return FlingDetector.RIGHT_FLING | FlingDetector.LEFT_FLING; - } - else - { - return 0; - } - } - - - @Override - public void onFlingStart(ListView listView, View listElement, int position, int direction) - { - - // control the visibility of the views that reveal behind a flinging element regarding the fling direction - int rightFlingViewId = mGroupDescriptor.getElementViewDescriptor().getFlingRevealRightViewId(); - int leftFlingViewId = mGroupDescriptor.getElementViewDescriptor().getFlingRevealLeftViewId(); - TextView rightFlingView = null; - TextView leftFlingView = null; - - if (rightFlingViewId != -1) - { - rightFlingView = (TextView) listElement.findViewById(rightFlingViewId); - } - if (leftFlingViewId != -1) - { - leftFlingView = (TextView) listElement.findViewById(leftFlingViewId); - } - - Resources resources = getActivity().getResources(); - - // change title and icon regarding the task status - long packedPos = mExpandableListView.getExpandableListPosition(position); - if (ExpandableListView.getPackedPositionType(packedPos) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) - { - ExpandableListAdapter listAdapter = mExpandableListView.getExpandableListAdapter(); - Cursor cursor = (Cursor) listAdapter.getChild(ExpandableListView.getPackedPositionGroup(packedPos), - ExpandableListView.getPackedPositionChild(packedPos)); - - if (cursor != null) - { - int taskStatus = cursor.getInt(cursor.getColumnIndex(Instances.STATUS)); - if (leftFlingView != null && rightFlingView != null) - { - if (taskStatus == Instances.STATUS_COMPLETED) - { - leftFlingView.setText(R.string.fling_task_delete); - leftFlingView.setCompoundDrawablesWithIntrinsicBounds(resources.getDrawable(R.drawable.content_discard), null, null, null); - rightFlingView.setText(R.string.fling_task_uncomplete); - rightFlingView.setCompoundDrawablesWithIntrinsicBounds(null, null, resources.getDrawable(R.drawable.content_remove_light), null); - } - else - { - leftFlingView.setText(R.string.fling_task_complete); - leftFlingView.setCompoundDrawablesWithIntrinsicBounds(resources.getDrawable(R.drawable.ic_action_complete), null, null, null); - rightFlingView.setText(R.string.fling_task_edit); - rightFlingView.setCompoundDrawablesWithIntrinsicBounds(null, null, resources.getDrawable(R.drawable.content_edit), null); - } - } - } - } - - if (rightFlingView != null) - { - rightFlingView.setVisibility(direction != FlingDetector.LEFT_FLING ? View.GONE : View.VISIBLE); - } - if (leftFlingView != null) - { - leftFlingView.setVisibility(direction != FlingDetector.RIGHT_FLING ? View.GONE : View.VISIBLE); - } - - } - - - @Override - public boolean onFlingEnd(ListView v, View listElement, int pos, int direction) - { - long packedPos = mExpandableListView.getExpandableListPosition(pos); - if (ExpandableListView.getPackedPositionType(packedPos) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) - { - ExpandableListAdapter listAdapter = mExpandableListView.getExpandableListAdapter(); - Cursor cursor = (Cursor) listAdapter.getChild(ExpandableListView.getPackedPositionGroup(packedPos), - ExpandableListView.getPackedPositionChild(packedPos)); - - if (cursor != null) - { - // TODO: for now we get the id of the task, not the instance, once we support recurrence we'll have to change that - Long taskId = cursor.getLong(cursor.getColumnIndex(Instances.TASK_ID)); - - if (taskId != null) - { - boolean closed = cursor.getLong(cursor.getColumnIndex(Instances.IS_CLOSED)) > 0; - String title = cursor.getString(cursor.getColumnIndex(Instances.TITLE)); - // TODO: use the instance URI once we support recurrence - Uri taskUri = ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), taskId); - - if (direction == FlingDetector.RIGHT_FLING) - { - if (closed) - { - removeTask(taskUri, title); - // we do not know for sure if the task has been removed since the user is asked for confirmation first, so return false - - return false; - - } - else - { - return setCompleteTask(taskUri, title, true); - } - } - else if (direction == FlingDetector.LEFT_FLING) - { - if (closed) - { - return setCompleteTask(taskUri, title, false); - } - else - { - openTaskEditor(taskUri, cursor.getString(cursor.getColumnIndex(Instances.ACCOUNT_TYPE))); - return false; - } - } - } - } - } - - return false; - } - - - @Override - public void onFlingCancel(int direction) - { - // TODO Auto-generated method stub - - } - - - public void loadGroupDescriptor() - { - if (getActivity() != null) - { - TaskListActivity activity = (TaskListActivity) getActivity(); - if (activity != null) - { - mGroupDescriptor = activity.getGroupDescriptor(mPageId); - } - } - } - - - /** - * Starts the automatic list view redraw (e.g. to display changing time values) on the next minute. - */ - public void startAutomaticRedraw() - { - long now = System.currentTimeMillis(); - long millisToInterval = INTERVAL_LISTVIEW_REDRAW - (now % INTERVAL_LISTVIEW_REDRAW); - - mHandler.postDelayed(mListRedrawRunnable, millisToInterval); - } - - - /** - * Stops the automatic list view redraw. - * - */ - public void stopAutomaticRedraw() - { - mHandler.removeCallbacks(mListRedrawRunnable); - } - - - public int getOpenChildPosition() - { - return mActivatedPositionChild; - } - - - public int getOpenGroupPosition() - { - return mActivatedPositionGroup; - } - - - /** - * Turns on activate-on-click mode. When this mode is on, list items will be given the 'activated' state when touched. - *

    - * Note: this does not work 100% with {@link ExpandableListView}, it doesn't check touched items automatically. - *

    - * - * @param activateOnItemClick - * Whether to enable single choice mode or not. - */ - public void setActivateOnItemClick(boolean activateOnItemClick) - { - mExpandableListView.setChoiceMode(activateOnItemClick ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE); - } - - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public void setListViewScrollbarPositionLeft(boolean left) - { - if (android.os.Build.VERSION.SDK_INT >= 11) - { - if (left) - { - mExpandableListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_LEFT); - // expandLV.setScrollBarStyle(style); - } - else - { - mExpandableListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); - } - } - } - - - public void setExpandableGroupDescriptor(ExpandableGroupDescriptor groupDescriptor) - { - mGroupDescriptor = groupDescriptor; - } - - - /** - * Mark the given task as completed. - * - * @param taskUri - * The {@link Uri} of the task. - * @param taskTitle - * The name/title of the task. - * @param completedValue - * The value to be set for the completed status. - * @return true if the operation was successful, false otherwise. - */ - private boolean setCompleteTask(Uri taskUri, String taskTitle, boolean completedValue) - { - ContentValues values = new ContentValues(); - values.put(Tasks.STATUS, completedValue ? Tasks.STATUS_COMPLETED : Tasks.STATUS_IN_PROCESS); - if (!completedValue) - { - values.put(Tasks.PERCENT_COMPLETE, 50); - } - - boolean completed = mAppContext.getContentResolver().update(taskUri, values, null, null) != 0; - if (completed) - { - if (completedValue) - { - Snackbar.make(mExpandableListView, getString(R.string.toast_task_completed, taskTitle), Snackbar.LENGTH_SHORT).show(); - } - else - { - Snackbar.make(mExpandableListView, getString(R.string.toast_task_uncompleted, taskTitle), Snackbar.LENGTH_SHORT).show(); - } - } - return completed; - } - - - public void setOpenChildPosition(int openChildPosition) - { - mActivatedPositionChild = openChildPosition; - - } - - - public void setOpenGroupPosition(int openGroupPosition) - { - mActivatedPositionGroup = openGroupPosition; - - } - - - public void notifyDataSetChanged(boolean expandFirst) - { - getLoaderManager().restartLoader(-1, null, this); - } - - Runnable setOpenHandler = new Runnable() - { - @Override - public void run() - { - selectChildView(mExpandableListView, mActivatedPositionGroup, mActivatedPositionChild, false); - mExpandableListView.expandGroups(mSavedExpandedGroups); - setActivatedItem(mActivatedPositionGroup, mActivatedPositionChild); - } - }; - - - public void setActivatedItem(int groupPosition, int childPosition) - { - if (groupPosition != ExpandableListView.INVALID_POSITION && groupPosition < mAdapter.getGroupCount() - && childPosition != ExpandableListView.INVALID_POSITION && childPosition < mAdapter.getChildrenCount(groupPosition)) - { - try - { - mExpandableListView - .setItemChecked(mExpandableListView.getFlatListPosition(ExpandableListView.getPackedPositionForChild(groupPosition, childPosition)), true); - } - catch (NullPointerException e) - { - // for now we just catch the NPE until we've found the reason - // just catching it won't hurt, it's just that the list selection won't be updated properly - - // FIXME: find the actual cause and fix it - } - } - } - - - public void expandCurrentSearchGroup() - { - if (mPageId == R.id.task_group_search && mAdapter.getGroupCount() > 0) - { - Cursor c = mAdapter.getGroup(0); - if (c != null && c.getInt(c.getColumnIndex(SearchHistoryColumns.HISTORIC)) < 1) - { - mExpandableListView.expandGroup(0); - } - } - } - - - public void setPageId(int pageId) - { - mPageId = pageId; - } - - - private void selectChild(final int groupPosition, Cursor childCursor) - { - mSelectedTaskUri = ((TaskListActivity) getActivity()).getSelectedTaskUri(); - if (mSelectedTaskUri != null) - { - new AsyncSelectChildTask().execute(new SelectChildTaskParams(groupPosition, childCursor, mSelectedTaskUri)); - } - } - - - public void openSelectedChild() - { - if (mSelectedChildPosition != null) - { - // post delayed to allow the list view to finish creation - mExpandableListView.postDelayed(new Runnable() - { - @Override - public void run() - { - mExpandableListView.expandGroup(mSelectedChildPosition.groupPosition); - mSelectedChildPosition.flatListPosition = mExpandableListView.getFlatListPosition( - RetainExpandableListView.getPackedPositionForChild(mSelectedChildPosition.groupPosition, mSelectedChildPosition.childPosition)); - - setActivatedItem(mSelectedChildPosition.groupPosition, mSelectedChildPosition.childPosition); - selectChildView(mExpandableListView, mSelectedChildPosition.groupPosition, mSelectedChildPosition.childPosition, true); - mExpandableListView.smoothScrollToPosition(mSelectedChildPosition.flatListPosition); - } - }, 0); - } - } - - - /** Returns the position of the task in the cursor. Returns -1 if the task is not in the cursor **/ - private int getSelectedChildPostion(Uri taskUri, Cursor listCursor) - { - if (taskUri != null && listCursor != null && listCursor.moveToFirst()) - { - Long taskIdToSelect = Long.valueOf(taskUri.getLastPathSegment()); - do - { - Long taskId = listCursor.getLong(listCursor.getColumnIndex(Tasks._ID)); - if (taskId.equals(taskIdToSelect)) - { - return listCursor.getPosition(); - } - } while (listCursor.moveToNext()); - } - return -1; - } - - private static class SelectChildTaskParams - { - int groupPosition; - Uri taskUriToSelect; - Cursor childCursor; - - - SelectChildTaskParams(int groupPosition, Cursor childCursor, Uri taskUriToSelect) - { - this.groupPosition = groupPosition; - this.childCursor = childCursor; - this.taskUriToSelect = taskUriToSelect; - } - } - - private static class ListPosition - { - int groupPosition; - int childPosition; - int flatListPosition; - - - ListPosition(int groupPosition, int childPosition) - { - this.groupPosition = groupPosition; - this.childPosition = childPosition; - } - } - - private class AsyncSelectChildTask extends AsyncTask - { - - @Override - protected Void doInBackground(SelectChildTaskParams... params) - { - int count = params.length; - for (int i = 0; i < count; i++) - { - final SelectChildTaskParams param = params[i]; - - final int childPosition = getSelectedChildPostion(param.taskUriToSelect, param.childCursor); - if (childPosition > -1) - { - mSelectedChildPosition = new ListPosition(param.groupPosition, childPosition); - openSelectedChild(); - } - } - return null; - } - - } + setActivatedItem(groupPosition, childPosition); + return true; + } + + }; + + private final OnGroupCollapseListener mTaskListCollapseListener = new OnGroupCollapseListener() + { + + @Override + public void onGroupCollapse(int groupPosition) + { + if (groupPosition == mActivatedPositionGroup) + { + mActivatedPositionChild = ExpandableListView.INVALID_POSITION; + mActivatedPositionGroup = ExpandableListView.INVALID_POSITION; + } + + } + }; + + + /** + * A callback interface that all activities containing this fragment must implement. This mechanism allows activities to be notified of item selections. + */ + public interface Callbacks + { + /** + * Callback for when an item has been selected. + * + * @param taskUri + * The {@link Uri} of the selected task. + * @param forceReload + * Whether to reload the task or not. + * @param sender + * The sender of the callback. + */ + public void onItemSelected(Uri taskUri, boolean forceReload, int pagePosition); + + public ExpandableGroupDescriptor getGroupDescriptor(int position); + + public void onAddNewTask(); + } + + + /** + * A runnable that periodically updates the list. We need that to update relative dates & times. TODO: we probably should move that to the adapter to update + * only the date & times fields, not the entire list. + */ + private Runnable mListRedrawRunnable = new Runnable() + { + + @Override + public void run() + { + mExpandableListView.invalidateViews(); + mHandler.postDelayed(this, INTERVAL_LISTVIEW_REDRAW); + } + }; + + + public static TaskListFragment newInstance(int instancePosition, boolean twoPaneLayout) + { + TaskListFragment result = new TaskListFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_INSTANCE_ID, instancePosition); + args.putBoolean(ARG_TWO_PANE_LAYOUT, twoPaneLayout); + result.setArguments(args); + return result; + } + + + /** + * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon screen orientation changes). + */ + public TaskListFragment() + { + } + + + @Override + public void onAttach(Activity activity) + { + super.onAttach(activity); + mAuthority = TaskContract.taskAuthority(activity); + + mAppContext = activity.getBaseContext(); + + // Activities containing this fragment must implement its callbacks. + if (!(activity instanceof Callbacks)) + { + throw new IllegalStateException("Activity must implement fragment's callbacks."); + } + + mCallbacks = (Callbacks) activity; + + // load accounts early + Sources.loadModelAsync(activity, TaskContract.LOCAL_ACCOUNT_TYPE, this); + } + + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + mHandler = new Handler(); + setHasOptionsMenu(true); + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + View rootView = inflater.inflate(R.layout.fragment_expandable_task_list, container, false); + mExpandableListView = (RetainExpandableListView) rootView.findViewById(android.R.id.list); + + if (!mTwoPaneLayout) + { + // Add a footer to make sure the floating action button doesn't hide anything. + mExpandableListView.addFooterView(inflater.inflate(R.layout.task_list_group, mExpandableListView, false)); + } + + if (mGroupDescriptor == null) + { + loadGroupDescriptor(); + } + + // setup the views + this.prepareReload(); + + // expand lists + if (mSavedExpandedGroups != null) + { + mExpandableListView.expandGroups(mSavedExpandedGroups); + } + + FlingDetector swiper = new FlingDetector(mExpandableListView, mGroupDescriptor.getElementViewDescriptor().getFlingContentViewId(), + getActivity().getApplicationContext()); + swiper.setOnFlingListener(this); + + return rootView; + } + + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + } + + + @Override + public void onStart() + { + reloadCursor(); + super.onStart(); + } + + + @Override + public void onResume() + { + super.onResume(); + mExpandableListView.invalidateViews(); + startAutomaticRedraw(); + openSelectedChild(); + + if (mTwoPaneLayout) + { + setListViewScrollbarPositionLeft(true); + setActivateOnItemClick(true); + } + } + + + @Override + public void onPause() + { + // we can't rely on save instance state being called before onPause, so we get the expanded groups here again + if (!((TaskListActivity) getActivity()).isInTransientState()) + { + mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); + } + stopAutomaticRedraw(); + super.onPause(); + } + + + @Override + public void onDetach() + { + super.onDetach(); + + } + + + @Override + public void onSaveInstanceState(Bundle outState) + { + if (!((TaskListActivity) getActivity()).isInTransientState()) + { + mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); + } + super.onSaveInstanceState(outState); + } + + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) + { + // create menu + inflater.inflate(R.menu.task_list_fragment_menu, menu); + + // restore menu state + MenuItem item = menu.findItem(R.id.menu_show_completed); + if (item != null) + { + item.setChecked(mSavedCompletedFilter); + + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) + { + if (mSavedCompletedFilter) + { + item.setTitle(R.string.menu_hide_completed); + } + else + { + item.setTitle(R.string.menu_show_completed); + } + } + } + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + int itemId = item.getItemId(); + if (itemId == R.id.menu_show_completed) + { + + mSavedCompletedFilter = !mSavedCompletedFilter; + item.setChecked(mSavedCompletedFilter); + mAdapter.setChildCursorFilter(mSavedCompletedFilter ? null : COMPLETED_FILTER); + + // reload the child cursors only + for (int i = 0; i < mAdapter.getGroupCount(); ++i) + { + mAdapter.reloadGroup(i); + } + return true; + } + else if (itemId == R.id.menu_sync_now) + { + doSyncNow(); + return true; + } + else + { + return super.onOptionsItemSelected(item); + } + } + + + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) + { + + if (mGroupDescriptor != null) + { + mCursorLoader = mGroupDescriptor.getGroupCursorLoader(mAppContext); + } + return mCursorLoader; + + } + + + @Override + public void onLoadFinished(Loader loader, Cursor cursor) + { + + if (mSavedExpandedGroups == null) + { + mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); + } + + mAdapter.setGroupCursor(cursor); + + if (mSavedExpandedGroups != null) + { + mExpandableListView.expandGroups(mSavedExpandedGroups); + if (!((TaskListActivity) getActivity()).isInTransientState()) + { + mSavedExpandedGroups = null; + } + } + + mHandler.post(new Runnable() + { + @Override + public void run() + { + mAdapter.reloadLoadedGroups(); + } + }); + } + + + @Override + public void onLoaderReset(Loader loader) + { + mAdapter.changeCursor(null); + } + + + @Override + public void onChildLoaded(final int pos, Cursor childCursor) + { + if (mActivatedPositionChild != ExpandableListView.INVALID_POSITION) + { + if (pos == mActivatedPositionGroup && mActivatedPositionChild != ExpandableListView.INVALID_POSITION) + { + mHandler.post(setOpenHandler); + } + } + // check for child to select + if (mTwoPaneLayout) + { + selectChild(pos, childCursor); + } + } + + + @Override + public void onModelLoaded(Model model) + { + // nothing to do, we've just loaded the default model to speed up loading the detail view and the editor view. + } + + + private void selectChildView(ExpandableListView expandLV, int groupPosition, int childPosition, boolean force) + { + if (groupPosition < mAdapter.getGroupCount() && childPosition < mAdapter.getChildrenCount(groupPosition)) + { + // a task instance element has been clicked, get it's instance id and notify the activity + ExpandableListAdapter listAdapter = expandLV.getExpandableListAdapter(); + Cursor cursor = (Cursor) listAdapter.getChild(groupPosition, childPosition); + + if (cursor == null) + { + return; + } + // TODO: for now we get the id of the task, not the instance, once we support recurrence we'll have to change that + Long selectTaskId = cursor.getLong(cursor.getColumnIndex(Instances.TASK_ID)); + + if (selectTaskId != null) + { + // Notify the active callbacks interface (the activity, if the fragment is attached to one) that an item has been selected. + + // TODO: use the instance URI one we support recurrence + Uri taskUri = ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), selectTaskId); + + mCallbacks.onItemSelected(taskUri, force, mInstancePosition); + } + } + } + + + /** + * prepares the update of the view after the group descriptor was changed + */ + public void prepareReload() + { + mAdapter = new ExpandableGroupDescriptorAdapter(getActivity(), getLoaderManager(), mGroupDescriptor); + mExpandableListView.setAdapter(mAdapter); + mExpandableListView.setOnChildClickListener((android.widget.ExpandableListView.OnChildClickListener) mTaskItemClickListener); + mExpandableListView.setOnGroupCollapseListener((android.widget.ExpandableListView.OnGroupCollapseListener) mTaskListCollapseListener); + mAdapter.setOnChildLoadedListener(this); + mAdapter.setChildCursorFilter(COMPLETED_FILTER); + restoreFilterState(); + + } + + + private void reloadCursor() + { + getLoaderManager().restartLoader(-1, null, this); + } + + + public void restoreFilterState() + { + if (mSavedCompletedFilter) + { + mAdapter.setChildCursorFilter(mSavedCompletedFilter ? null : COMPLETED_FILTER); + // reload the child cursors only + for (int i = 0; i < mAdapter.getGroupCount(); ++i) + { + mAdapter.reloadGroup(i); + } + } + + } + + + /** + * Trigger a synchronization for all accounts. + */ + private void doSyncNow() + { + AccountManager accountManager = AccountManager.get(mAppContext); + Account[] accounts = accountManager.getAccounts(); + for (Account account : accounts) + { + // TODO: do we need a new bundle for each account or can we reuse it? + Bundle extras = new Bundle(); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + ContentResolver.requestSync(account, mAuthority, extras); + } + } + + + /** + * Remove the task with the given {@link Uri} and title, asking for confirmation first. + * + * @param taskUri + * The {@link Uri} of the atsk to remove. + * @param taskTitle + * the title of the task to remove. + * + * @return + */ + private void removeTask(final Uri taskUri, final String taskTitle) + { + new AlertDialog.Builder(getActivity()).setTitle(R.string.confirm_delete_title).setCancelable(true) + .setNegativeButton(android.R.string.cancel, new OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + // nothing to do here + } + }).setPositiveButton(android.R.string.ok, new OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + // TODO: remove the task in a background task + mAppContext.getContentResolver().delete(taskUri, null, null); + Snackbar.make(mExpandableListView, getString(R.string.toast_task_deleted, taskTitle), Snackbar.LENGTH_SHORT).show(); + } + }).setMessage(getString(R.string.confirm_delete_message_with_title, taskTitle)).create().show(); + } + + + /** + * Opens the task editor for the selected Task. + * + * @param taskUri + * The {@link Uri} of the task. + * @param taskTitle + * The name/title of the task. + */ + private void openTaskEditor(final Uri taskUri, final String accountType) + { + Intent editTaskIntent = new Intent(Intent.ACTION_EDIT); + editTaskIntent.setData(taskUri); + editTaskIntent.putExtra(EditTaskActivity.EXTRA_DATA_ACCOUNT_TYPE, accountType); + startActivity(editTaskIntent); + } + + + @Override + public int canFling(ListView v, int pos) + { + long packedPos = mExpandableListView.getExpandableListPosition(pos); + if (packedPos != ExpandableListView.PACKED_POSITION_VALUE_NULL + && ExpandableListView.getPackedPositionType(packedPos) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) + { + return FlingDetector.RIGHT_FLING | FlingDetector.LEFT_FLING; + } + else + { + return 0; + } + } + + + @Override + public void onFlingStart(ListView listView, View listElement, int position, int direction) + { + + // control the visibility of the views that reveal behind a flinging element regarding the fling direction + int rightFlingViewId = mGroupDescriptor.getElementViewDescriptor().getFlingRevealRightViewId(); + int leftFlingViewId = mGroupDescriptor.getElementViewDescriptor().getFlingRevealLeftViewId(); + TextView rightFlingView = null; + TextView leftFlingView = null; + + if (rightFlingViewId != -1) + { + rightFlingView = (TextView) listElement.findViewById(rightFlingViewId); + } + if (leftFlingViewId != -1) + { + leftFlingView = (TextView) listElement.findViewById(leftFlingViewId); + } + + Resources resources = getActivity().getResources(); + + // change title and icon regarding the task status + long packedPos = mExpandableListView.getExpandableListPosition(position); + if (ExpandableListView.getPackedPositionType(packedPos) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) + { + ExpandableListAdapter listAdapter = mExpandableListView.getExpandableListAdapter(); + Cursor cursor = (Cursor) listAdapter.getChild(ExpandableListView.getPackedPositionGroup(packedPos), + ExpandableListView.getPackedPositionChild(packedPos)); + + if (cursor != null) + { + int taskStatus = cursor.getInt(cursor.getColumnIndex(Instances.STATUS)); + if (leftFlingView != null && rightFlingView != null) + { + if (taskStatus == Instances.STATUS_COMPLETED) + { + leftFlingView.setText(R.string.fling_task_delete); + leftFlingView.setCompoundDrawablesWithIntrinsicBounds(resources.getDrawable(R.drawable.content_discard), null, null, null); + rightFlingView.setText(R.string.fling_task_uncomplete); + rightFlingView.setCompoundDrawablesWithIntrinsicBounds(null, null, resources.getDrawable(R.drawable.content_remove_light), null); + } + else + { + leftFlingView.setText(R.string.fling_task_complete); + leftFlingView.setCompoundDrawablesWithIntrinsicBounds(resources.getDrawable(R.drawable.ic_action_complete), null, null, null); + rightFlingView.setText(R.string.fling_task_edit); + rightFlingView.setCompoundDrawablesWithIntrinsicBounds(null, null, resources.getDrawable(R.drawable.content_edit), null); + } + } + } + } + + if (rightFlingView != null) + { + rightFlingView.setVisibility(direction != FlingDetector.LEFT_FLING ? View.GONE : View.VISIBLE); + } + if (leftFlingView != null) + { + leftFlingView.setVisibility(direction != FlingDetector.RIGHT_FLING ? View.GONE : View.VISIBLE); + } + + } + + + @Override + public boolean onFlingEnd(ListView v, View listElement, int pos, int direction) + { + long packedPos = mExpandableListView.getExpandableListPosition(pos); + if (ExpandableListView.getPackedPositionType(packedPos) == ExpandableListView.PACKED_POSITION_TYPE_CHILD) + { + ExpandableListAdapter listAdapter = mExpandableListView.getExpandableListAdapter(); + Cursor cursor = (Cursor) listAdapter.getChild(ExpandableListView.getPackedPositionGroup(packedPos), + ExpandableListView.getPackedPositionChild(packedPos)); + + if (cursor != null) + { + // TODO: for now we get the id of the task, not the instance, once we support recurrence we'll have to change that + Long taskId = cursor.getLong(cursor.getColumnIndex(Instances.TASK_ID)); + + if (taskId != null) + { + boolean closed = cursor.getLong(cursor.getColumnIndex(Instances.IS_CLOSED)) > 0; + String title = cursor.getString(cursor.getColumnIndex(Instances.TITLE)); + // TODO: use the instance URI once we support recurrence + Uri taskUri = ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), taskId); + + if (direction == FlingDetector.RIGHT_FLING) + { + if (closed) + { + removeTask(taskUri, title); + // we do not know for sure if the task has been removed since the user is asked for confirmation first, so return false + + return false; + + } + else + { + return setCompleteTask(taskUri, title, true); + } + } + else if (direction == FlingDetector.LEFT_FLING) + { + if (closed) + { + return setCompleteTask(taskUri, title, false); + } + else + { + openTaskEditor(taskUri, cursor.getString(cursor.getColumnIndex(Instances.ACCOUNT_TYPE))); + return false; + } + } + } + } + } + + return false; + } + + + @Override + public void onFlingCancel(int direction) + { + // TODO Auto-generated method stub + + } + + + public void loadGroupDescriptor() + { + if (getActivity() != null) + { + TaskListActivity activity = (TaskListActivity) getActivity(); + if (activity != null) + { + mGroupDescriptor = activity.getGroupDescriptor(mPageId); + } + } + } + + + /** + * Starts the automatic list view redraw (e.g. to display changing time values) on the next minute. + */ + public void startAutomaticRedraw() + { + long now = System.currentTimeMillis(); + long millisToInterval = INTERVAL_LISTVIEW_REDRAW - (now % INTERVAL_LISTVIEW_REDRAW); + + mHandler.postDelayed(mListRedrawRunnable, millisToInterval); + } + + + /** + * Stops the automatic list view redraw. + */ + public void stopAutomaticRedraw() + { + mHandler.removeCallbacks(mListRedrawRunnable); + } + + + public int getOpenChildPosition() + { + return mActivatedPositionChild; + } + + + public int getOpenGroupPosition() + { + return mActivatedPositionGroup; + } + + + /** + * Turns on activate-on-click mode. When this mode is on, list items will be given the 'activated' state when touched. + *

    + * Note: this does not work 100% with {@link ExpandableListView}, it doesn't check touched items automatically. + *

    + * + * @param activateOnItemClick + * Whether to enable single choice mode or not. + */ + public void setActivateOnItemClick(boolean activateOnItemClick) + { + mExpandableListView.setChoiceMode(activateOnItemClick ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE); + } + + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void setListViewScrollbarPositionLeft(boolean left) + { + if (android.os.Build.VERSION.SDK_INT >= 11) + { + if (left) + { + mExpandableListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_LEFT); + // expandLV.setScrollBarStyle(style); + } + else + { + mExpandableListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); + } + } + } + + + public void setExpandableGroupDescriptor(ExpandableGroupDescriptor groupDescriptor) + { + mGroupDescriptor = groupDescriptor; + } + + + /** + * Mark the given task as completed. + * + * @param taskUri + * The {@link Uri} of the task. + * @param taskTitle + * The name/title of the task. + * @param completedValue + * The value to be set for the completed status. + * + * @return true if the operation was successful, false otherwise. + */ + private boolean setCompleteTask(Uri taskUri, String taskTitle, boolean completedValue) + { + ContentValues values = new ContentValues(); + values.put(Tasks.STATUS, completedValue ? Tasks.STATUS_COMPLETED : Tasks.STATUS_IN_PROCESS); + if (!completedValue) + { + values.put(Tasks.PERCENT_COMPLETE, 50); + } + + boolean completed = mAppContext.getContentResolver().update(taskUri, values, null, null) != 0; + if (completed) + { + if (completedValue) + { + Snackbar.make(mExpandableListView, getString(R.string.toast_task_completed, taskTitle), Snackbar.LENGTH_SHORT).show(); + } + else + { + Snackbar.make(mExpandableListView, getString(R.string.toast_task_uncompleted, taskTitle), Snackbar.LENGTH_SHORT).show(); + } + } + return completed; + } + + + public void setOpenChildPosition(int openChildPosition) + { + mActivatedPositionChild = openChildPosition; + + } + + + public void setOpenGroupPosition(int openGroupPosition) + { + mActivatedPositionGroup = openGroupPosition; + + } + + + public void notifyDataSetChanged(boolean expandFirst) + { + getLoaderManager().restartLoader(-1, null, this); + } + + + Runnable setOpenHandler = new Runnable() + { + @Override + public void run() + { + selectChildView(mExpandableListView, mActivatedPositionGroup, mActivatedPositionChild, false); + mExpandableListView.expandGroups(mSavedExpandedGroups); + setActivatedItem(mActivatedPositionGroup, mActivatedPositionChild); + } + }; + + + public void setActivatedItem(int groupPosition, int childPosition) + { + if (groupPosition != ExpandableListView.INVALID_POSITION && groupPosition < mAdapter.getGroupCount() + && childPosition != ExpandableListView.INVALID_POSITION && childPosition < mAdapter.getChildrenCount(groupPosition)) + { + try + { + mExpandableListView + .setItemChecked(mExpandableListView.getFlatListPosition(ExpandableListView.getPackedPositionForChild(groupPosition, childPosition)), + true); + } + catch (NullPointerException e) + { + // for now we just catch the NPE until we've found the reason + // just catching it won't hurt, it's just that the list selection won't be updated properly + + // FIXME: find the actual cause and fix it + } + } + } + + + public void expandCurrentSearchGroup() + { + if (mPageId == R.id.task_group_search && mAdapter.getGroupCount() > 0) + { + Cursor c = mAdapter.getGroup(0); + if (c != null && c.getInt(c.getColumnIndex(SearchHistoryColumns.HISTORIC)) < 1) + { + mExpandableListView.expandGroup(0); + } + } + } + + + public void setPageId(int pageId) + { + mPageId = pageId; + } + + + private void selectChild(final int groupPosition, Cursor childCursor) + { + mSelectedTaskUri = ((TaskListActivity) getActivity()).getSelectedTaskUri(); + if (mSelectedTaskUri != null) + { + new AsyncSelectChildTask().execute(new SelectChildTaskParams(groupPosition, childCursor, mSelectedTaskUri)); + } + } + + + public void openSelectedChild() + { + if (mSelectedChildPosition != null) + { + // post delayed to allow the list view to finish creation + mExpandableListView.postDelayed(new Runnable() + { + @Override + public void run() + { + mExpandableListView.expandGroup(mSelectedChildPosition.groupPosition); + mSelectedChildPosition.flatListPosition = mExpandableListView.getFlatListPosition( + RetainExpandableListView.getPackedPositionForChild(mSelectedChildPosition.groupPosition, mSelectedChildPosition.childPosition)); + + setActivatedItem(mSelectedChildPosition.groupPosition, mSelectedChildPosition.childPosition); + selectChildView(mExpandableListView, mSelectedChildPosition.groupPosition, mSelectedChildPosition.childPosition, true); + mExpandableListView.smoothScrollToPosition(mSelectedChildPosition.flatListPosition); + } + }, 0); + } + } + + + /** + * Returns the position of the task in the cursor. Returns -1 if the task is not in the cursor + **/ + private int getSelectedChildPostion(Uri taskUri, Cursor listCursor) + { + if (taskUri != null && listCursor != null && listCursor.moveToFirst()) + { + Long taskIdToSelect = Long.valueOf(taskUri.getLastPathSegment()); + do + { + Long taskId = listCursor.getLong(listCursor.getColumnIndex(Tasks._ID)); + if (taskId.equals(taskIdToSelect)) + { + return listCursor.getPosition(); + } + } while (listCursor.moveToNext()); + } + return -1; + } + + + private static class SelectChildTaskParams + { + int groupPosition; + Uri taskUriToSelect; + Cursor childCursor; + + + SelectChildTaskParams(int groupPosition, Cursor childCursor, Uri taskUriToSelect) + { + this.groupPosition = groupPosition; + this.childCursor = childCursor; + this.taskUriToSelect = taskUriToSelect; + } + } + + + private static class ListPosition + { + int groupPosition; + int childPosition; + int flatListPosition; + + + ListPosition(int groupPosition, int childPosition) + { + this.groupPosition = groupPosition; + this.childPosition = childPosition; + } + } + + + private class AsyncSelectChildTask extends AsyncTask + { + + @Override + protected Void doInBackground(SelectChildTaskParams... params) + { + int count = params.length; + for (int i = 0; i < count; i++) + { + final SelectChildTaskParams param = params[i]; + + final int childPosition = getSelectedChildPostion(param.taskUriToSelect, param.childCursor); + if (childPosition > -1) + { + mSelectedChildPosition = new ListPosition(param.groupPosition, childPosition); + openSelectedChild(); + } + } + return null; + } + + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java index 6bfebd25..2393574d 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java @@ -43,117 +43,117 @@ import org.dmfs.tasks.utils.AppCompatActivity; public class ViewTaskActivity extends AppCompatActivity implements ViewTaskFragment.Callback { - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_task_detail); - - // If should be in two-pane mode, finish to return to main activity - if (getResources().getBoolean(R.bool.has_two_panes)) - { - - Intent taskListIntent = new Intent(this, TaskListActivity.class); - taskListIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, - getIntent().getBooleanExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, false)); - taskListIntent.putExtra(TaskListActivity.EXTRA_DISPLAY_TASK, true); - taskListIntent.setData(getIntent().getData()); - startActivity(taskListIntent); - finish(); - return; - } - - if (savedInstanceState == null) - { - ViewTaskFragment fragment = ViewTaskFragment.newInstance(getIntent().getData()); - getSupportFragmentManager().beginTransaction().add(R.id.task_detail_container, fragment).commit(); - } - } - - - @Override - public void onAttachFragment(Fragment fragment) - { - if (fragment instanceof ViewTaskFragment) - { - final ViewTaskFragment detailFragment = (ViewTaskFragment) fragment; - new Handler().post(new Runnable() - { - @Override - public void run() - { - detailFragment.setupToolbarAsActionbar(ViewTaskActivity.this); - } - }); - } - } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - switch (item.getItemId()) - { - case android.R.id.home: - Intent upIntent = new Intent(this, TaskListActivity.class); - // provision the task uri, so the main activity will be opened with the right task selected - upIntent.setData(getIntent().getData()); - upIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(upIntent); - finish(); - default: - break; - } - return super.onOptionsItemSelected(item); - } - - - @Override - public void onEditTask(Uri taskUri, ContentSet data) - { - Intent editTaskIntent = new Intent(Intent.ACTION_EDIT); - editTaskIntent.setData(taskUri); - if (data != null) - { - Bundle extraBundle = new Bundle(); - extraBundle.putParcelable(EditTaskActivity.EXTRA_DATA_CONTENT_SET, data); - editTaskIntent.putExtra(EditTaskActivity.EXTRA_DATA_BUNDLE, extraBundle); - } - startActivity(editTaskIntent); - } - - - @Override - public void onDelete(Uri taskUri) - { - /* - * The task we're showing has been deleted, just finish. + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_task_detail); + + // If should be in two-pane mode, finish to return to main activity + if (getResources().getBoolean(R.bool.has_two_panes)) + { + + Intent taskListIntent = new Intent(this, TaskListActivity.class); + taskListIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, + getIntent().getBooleanExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, false)); + taskListIntent.putExtra(TaskListActivity.EXTRA_DISPLAY_TASK, true); + taskListIntent.setData(getIntent().getData()); + startActivity(taskListIntent); + finish(); + return; + } + + if (savedInstanceState == null) + { + ViewTaskFragment fragment = ViewTaskFragment.newInstance(getIntent().getData()); + getSupportFragmentManager().beginTransaction().add(R.id.task_detail_container, fragment).commit(); + } + } + + + @Override + public void onAttachFragment(Fragment fragment) + { + if (fragment instanceof ViewTaskFragment) + { + final ViewTaskFragment detailFragment = (ViewTaskFragment) fragment; + new Handler().post(new Runnable() + { + @Override + public void run() + { + detailFragment.setupToolbarAsActionbar(ViewTaskActivity.this); + } + }); + } + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + switch (item.getItemId()) + { + case android.R.id.home: + Intent upIntent = new Intent(this, TaskListActivity.class); + // provision the task uri, so the main activity will be opened with the right task selected + upIntent.setData(getIntent().getData()); + upIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(upIntent); + finish(); + default: + break; + } + return super.onOptionsItemSelected(item); + } + + + @Override + public void onEditTask(Uri taskUri, ContentSet data) + { + Intent editTaskIntent = new Intent(Intent.ACTION_EDIT); + editTaskIntent.setData(taskUri); + if (data != null) + { + Bundle extraBundle = new Bundle(); + extraBundle.putParcelable(EditTaskActivity.EXTRA_DATA_CONTENT_SET, data); + editTaskIntent.putExtra(EditTaskActivity.EXTRA_DATA_BUNDLE, extraBundle); + } + startActivity(editTaskIntent); + } + + + @Override + public void onDelete(Uri taskUri) + { + /* + * The task we're showing has been deleted, just finish. */ - finish(); - } - - - private int darkenColor(int color) - { - float[] hsv = new float[3]; - Color.colorToHSV(color, hsv); - hsv[2] = hsv[2] * 0.75f; - color = Color.HSVToColor(hsv); - return color; - } - - - @SuppressLint("NewApi") - @Override - public void updateColor(int color) - { - - if (VERSION.SDK_INT >= 21) - { - Window window = getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - window.setStatusBarColor(darkenColor(color)); - } - } + finish(); + } + + + private int darkenColor(int color) + { + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] = hsv[2] * 0.75f; + color = Color.HSVToColor(hsv); + return color; + } + + + @SuppressLint("NewApi") + @Override + public void updateColor(int color) + { + + if (VERSION.SDK_INT >= 21) + { + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(darkenColor(color)); + } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java index babf840d..3e205060 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java @@ -37,7 +37,6 @@ import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; -import android.support.v4.widget.NestedScrollView; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar.OnMenuItemClickListener; @@ -73,759 +72,759 @@ import java.util.Set; /** * A fragment representing a single Task detail screen. This fragment is either contained in a {@link TaskListActivity} in two-pane mode (on tablets) or in a * {@link ViewTaskActivity} on handsets. - * + * * @author Arjun Naik * @author Marten Gajda */ public class ViewTaskFragment extends SupportFragment - implements OnModelLoadedListener, OnContentChangeListener, OnMenuItemClickListener, OnOffsetChangedListener + implements OnModelLoadedListener, OnContentChangeListener, OnMenuItemClickListener, OnOffsetChangedListener { - private final static String ARG_URI = "uri"; - - /** - * A set of values that may affect the recurrence set of a task. If one of these values changes we have to submit all of them. - */ - private final static Set RECURRENCE_VALUES = new HashSet( - Arrays.asList(new String[] { Tasks.DUE, Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY, Tasks.RRULE, Tasks.RDATE, Tasks.EXDATE })); - - /** - * The {@link ContentValueMapper} that knows how to map the values in a cursor to {@link ContentValues}. - */ - private static final ContentValueMapper CONTENT_VALUE_MAPPER = EditTaskFragment.CONTENT_VALUE_MAPPER; - - private static final float PERCENTAGE_TO_HIDE_TITLE_DETAILS = 0.3f; - private static final int ALPHA_ANIMATIONS_DURATION = 200; - - /** - * The {@link Uri} of the current task in the view. - */ - @Parameter(key = ARG_URI) - @Retain - private Uri mTaskUri; - - /** - * The values of the current task. - */ - @Retain - private ContentSet mContentSet; - - /** - * The view that contains the details. - */ - private ViewGroup mContent; - - /** - * The {@link Model} of the current task. - */ - private Model mModel; - - /** - * The application context. - */ - private Context mAppContext; - - /** - * The actual detail view. We store this direct reference to be able to clear it when the fragment gets detached. - */ - private TaskView mDetailView; - - private int mListColor; - private int mOldStatus = -1; - private boolean mPinned = false; - private boolean mRestored; - private AppBarLayout mAppBar; - private Toolbar mToolBar; - private View mRootView; - - private int mAppBarOffset = 0; - - private FloatingActionButton mFloatingActionButton; - - /** - * A {@link Callback} to the activity. - */ - private Callback mCallback; - - private boolean mShowFloatingActionButton = false; - - private boolean mIsTheTitleContainerVisible = true; - - /** - * A Runnable that updates the view. - */ - private Runnable mUpdateViewRunnable = new Runnable() - { - @Override - public void run() - { - updateView(); - } - }; - - public interface Callback - { - /** - * This is called to instruct the Activity to call the editor for a specific task. - * - * @param taskUri - * The {@link Uri} of the task to edit. - * @param data - * The task data that belongs to the {@link Uri}. This is purely an optimization and may be null. - */ - public void onEditTask(Uri taskUri, ContentSet data); - - - /** - * This is called to inform the Activity that a task has been deleted. - * - * @param taskUri - * The {@link Uri} of the deleted task. Note that the Uri is likely to be invalid at the time of calling this method. - */ - public void onDelete(Uri taskUri); - - - /** - * Notifies the listener about the list color of the current task. - * - * @param color - * The color. - */ - public void updateColor(int color); - } - - - public static ViewTaskFragment newInstance(Uri uri) - { - ViewTaskFragment result = new ViewTaskFragment(); - if (uri != null) - { - Bundle args = new Bundle(); - args.putParcelable(ARG_URI, uri); - result.setArguments(args); - } - return result; - } - - - /** - * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon screen orientation changes). - */ - public ViewTaskFragment() - { - } - - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - setHasOptionsMenu(true); - } - - - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - - if (!(activity instanceof Callback)) - { - throw new IllegalStateException("Activity must implement TaskViewDetailFragment callback."); - } - - mCallback = (Callback) activity; - mAppContext = activity.getApplicationContext(); - } - - - @Override - public void onDestroyView() - { - super.onDestroyView(); - // remove listener - if (mContentSet != null) - { - mContentSet.removeOnChangeListener(this, null); - } - - if (mTaskUri != null) - { - mAppContext.getContentResolver().unregisterContentObserver(mObserver); - } - - if (mContent != null) - { - mContent.removeAllViews(); - } - - if (mDetailView != null) - { - // remove values, to ensure all listeners get released - mDetailView.setValues(null); - } - - } - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { - mShowFloatingActionButton = getResources().getBoolean(R.bool.opentasks_enabled_detail_view_fab); - - mRootView = inflater.inflate(R.layout.fragment_task_view_detail, container, false); - mContent = (ViewGroup) mRootView.findViewById(R.id.content); - mAppBar = (AppBarLayout) mRootView.findViewById(R.id.appbar); - mToolBar = (Toolbar) mRootView.findViewById(R.id.toolbar); - mToolBar.setOnMenuItemClickListener(this); - mToolBar.setTitle(""); - mAppBar.addOnOffsetChangedListener(this); - - animate(mToolBar.findViewById(R.id.toolbar_title), 0, View.INVISIBLE); - - mFloatingActionButton = (FloatingActionButton) mRootView.findViewById(R.id.floating_action_button); - showFloatingActionButton(false); - mFloatingActionButton.setOnClickListener(new View.OnClickListener() - { - - @Override - public void onClick(View v) - { - completeTask(); - } - }); - - mRestored = savedInstanceState != null; - - if (savedInstanceState != null) - { - if (mContent != null && mContentSet != null) - { - // register for content updates - mContentSet.addOnChangeListener(this, null, true); - - // register observer - if (mTaskUri != null) - { - mAppContext.getContentResolver().registerContentObserver(mTaskUri, false, mObserver); - } - } - } - else if (mTaskUri != null) - { - Uri uri = mTaskUri; - // pretend we didn't load anything yet - mTaskUri = null; - loadUri(uri); - } - - return mRootView; - } - - - @Override - public void onPause() - { - super.onPause(); - persistTask(); - } - - - private void persistTask() - { - Activity activity = getActivity(); - if (mContentSet != null && activity != null) - { - if (mDetailView != null) - { - mDetailView.updateValues(); - } - - if (mContentSet.isUpdate()) - { - mContentSet.persist(activity); - ActivityCompat.invalidateOptionsMenu(activity); - } - } - } - - - /** - * Load the task with the given {@link Uri} in the detail view. - *

    - * At present only Task Uris are supported. - *

    - * TODO: add support for instance Uris. - * - * @param uri - * The {@link Uri} of the task to show. - */ - public void loadUri(Uri uri) - { - showFloatingActionButton(false); - - if (mTaskUri != null) - { - /* - * Unregister the observer for any previously shown task first. + private final static String ARG_URI = "uri"; + + /** + * A set of values that may affect the recurrence set of a task. If one of these values changes we have to submit all of them. + */ + private final static Set RECURRENCE_VALUES = new HashSet( + Arrays.asList(new String[] { Tasks.DUE, Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY, Tasks.RRULE, Tasks.RDATE, Tasks.EXDATE })); + + /** + * The {@link ContentValueMapper} that knows how to map the values in a cursor to {@link ContentValues}. + */ + private static final ContentValueMapper CONTENT_VALUE_MAPPER = EditTaskFragment.CONTENT_VALUE_MAPPER; + + private static final float PERCENTAGE_TO_HIDE_TITLE_DETAILS = 0.3f; + private static final int ALPHA_ANIMATIONS_DURATION = 200; + + /** + * The {@link Uri} of the current task in the view. + */ + @Parameter(key = ARG_URI) + @Retain + private Uri mTaskUri; + + /** + * The values of the current task. + */ + @Retain + private ContentSet mContentSet; + + /** + * The view that contains the details. + */ + private ViewGroup mContent; + + /** + * The {@link Model} of the current task. + */ + private Model mModel; + + /** + * The application context. + */ + private Context mAppContext; + + /** + * The actual detail view. We store this direct reference to be able to clear it when the fragment gets detached. + */ + private TaskView mDetailView; + + private int mListColor; + private int mOldStatus = -1; + private boolean mPinned = false; + private boolean mRestored; + private AppBarLayout mAppBar; + private Toolbar mToolBar; + private View mRootView; + + private int mAppBarOffset = 0; + + private FloatingActionButton mFloatingActionButton; + + /** + * A {@link Callback} to the activity. + */ + private Callback mCallback; + + private boolean mShowFloatingActionButton = false; + + private boolean mIsTheTitleContainerVisible = true; + + /** + * A Runnable that updates the view. + */ + private Runnable mUpdateViewRunnable = new Runnable() + { + @Override + public void run() + { + updateView(); + } + }; + + + public interface Callback + { + /** + * This is called to instruct the Activity to call the editor for a specific task. + * + * @param taskUri + * The {@link Uri} of the task to edit. + * @param data + * The task data that belongs to the {@link Uri}. This is purely an optimization and may be null. + */ + public void onEditTask(Uri taskUri, ContentSet data); + + /** + * This is called to inform the Activity that a task has been deleted. + * + * @param taskUri + * The {@link Uri} of the deleted task. Note that the Uri is likely to be invalid at the time of calling this method. + */ + public void onDelete(Uri taskUri); + + /** + * Notifies the listener about the list color of the current task. + * + * @param color + * The color. + */ + public void updateColor(int color); + } + + + public static ViewTaskFragment newInstance(Uri uri) + { + ViewTaskFragment result = new ViewTaskFragment(); + if (uri != null) + { + Bundle args = new Bundle(); + args.putParcelable(ARG_URI, uri); + result.setArguments(args); + } + return result; + } + + + /** + * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon screen orientation changes). + */ + public ViewTaskFragment() + { + } + + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setHasOptionsMenu(true); + } + + + @Override + public void onAttach(Activity activity) + { + super.onAttach(activity); + + if (!(activity instanceof Callback)) + { + throw new IllegalStateException("Activity must implement TaskViewDetailFragment callback."); + } + + mCallback = (Callback) activity; + mAppContext = activity.getApplicationContext(); + } + + + @Override + public void onDestroyView() + { + super.onDestroyView(); + // remove listener + if (mContentSet != null) + { + mContentSet.removeOnChangeListener(this, null); + } + + if (mTaskUri != null) + { + mAppContext.getContentResolver().unregisterContentObserver(mObserver); + } + + if (mContent != null) + { + mContent.removeAllViews(); + } + + if (mDetailView != null) + { + // remove values, to ensure all listeners get released + mDetailView.setValues(null); + } + + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + mShowFloatingActionButton = getResources().getBoolean(R.bool.opentasks_enabled_detail_view_fab); + + mRootView = inflater.inflate(R.layout.fragment_task_view_detail, container, false); + mContent = (ViewGroup) mRootView.findViewById(R.id.content); + mAppBar = (AppBarLayout) mRootView.findViewById(R.id.appbar); + mToolBar = (Toolbar) mRootView.findViewById(R.id.toolbar); + mToolBar.setOnMenuItemClickListener(this); + mToolBar.setTitle(""); + mAppBar.addOnOffsetChangedListener(this); + + animate(mToolBar.findViewById(R.id.toolbar_title), 0, View.INVISIBLE); + + mFloatingActionButton = (FloatingActionButton) mRootView.findViewById(R.id.floating_action_button); + showFloatingActionButton(false); + mFloatingActionButton.setOnClickListener(new View.OnClickListener() + { + + @Override + public void onClick(View v) + { + completeTask(); + } + }); + + mRestored = savedInstanceState != null; + + if (savedInstanceState != null) + { + if (mContent != null && mContentSet != null) + { + // register for content updates + mContentSet.addOnChangeListener(this, null, true); + + // register observer + if (mTaskUri != null) + { + mAppContext.getContentResolver().registerContentObserver(mTaskUri, false, mObserver); + } + } + } + else if (mTaskUri != null) + { + Uri uri = mTaskUri; + // pretend we didn't load anything yet + mTaskUri = null; + loadUri(uri); + } + + return mRootView; + } + + + @Override + public void onPause() + { + super.onPause(); + persistTask(); + } + + + private void persistTask() + { + Activity activity = getActivity(); + if (mContentSet != null && activity != null) + { + if (mDetailView != null) + { + mDetailView.updateValues(); + } + + if (mContentSet.isUpdate()) + { + mContentSet.persist(activity); + ActivityCompat.invalidateOptionsMenu(activity); + } + } + } + + + /** + * Load the task with the given {@link Uri} in the detail view. + *

    + * At present only Task Uris are supported. + *

    + * TODO: add support for instance Uris. + * + * @param uri + * The {@link Uri} of the task to show. + */ + public void loadUri(Uri uri) + { + showFloatingActionButton(false); + + if (mTaskUri != null) + { + /* + * Unregister the observer for any previously shown task first. */ - mAppContext.getContentResolver().unregisterContentObserver(mObserver); - persistTask(); - } - - Uri oldUri = mTaskUri; - mTaskUri = uri; - if (uri != null) - { - /* + mAppContext.getContentResolver().unregisterContentObserver(mObserver); + persistTask(); + } + + Uri oldUri = mTaskUri; + mTaskUri = uri; + if (uri != null) + { + /* * Create a new ContentSet and load the values for the given Uri. Also register listener and observer for changes in the ContentSet and the Uri. */ - mContentSet = new ContentSet(uri); - mContentSet.addOnChangeListener(this, null, true); - mAppContext.getContentResolver().registerContentObserver(uri, false, mObserver); - mContentSet.update(mAppContext, CONTENT_VALUE_MAPPER); - } - else - { - /* + mContentSet = new ContentSet(uri); + mContentSet.addOnChangeListener(this, null, true); + mAppContext.getContentResolver().registerContentObserver(uri, false, mObserver); + mContentSet.update(mAppContext, CONTENT_VALUE_MAPPER); + } + else + { + /* * Immediately update the view with the empty task uri, i.e. clear the view. */ - mContentSet = null; - if (mContent != null) - { - mContent.removeAllViews(); - } - } - - if ((oldUri == null) != (uri == null)) - { - /* + mContentSet = null; + if (mContent != null) + { + mContent.removeAllViews(); + } + } + + if ((oldUri == null) != (uri == null)) + { + /* * getActivity().invalidateOptionsMenu() doesn't work in Android 2.x so use the compat lib */ - ActivityCompat.invalidateOptionsMenu(getActivity()); - } - - mAppBar.setExpanded(true, false); - } - - - /** - * Update the detail view with the current ContentSet. This removes any previous detail view and creates a new one if {@link #mContentSet} is not - * null. - */ - private void updateView() - { - Activity activity = getActivity(); - if (mContent != null && activity != null) - { - final LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - if (mDetailView != null) - { - // remove values, to ensure all listeners get released - mDetailView.setValues(null); - } - - mContent.removeAllViews(); - if (mContentSet != null) - { - mDetailView = (TaskView) inflater.inflate(R.layout.task_view, mContent, false); - mDetailView.setModel(mModel); - mDetailView.setValues(mContentSet); - mContent.addView(mDetailView); - - TaskView mToolbarInfo = (TaskView) mAppBar.findViewById(R.id.toolbar_content); - if (mToolbarInfo != null) - { - Model minModel = Sources.getInstance(activity).getMinimalModel(TaskFieldAdapters.ACCOUNT_TYPE.get(mContentSet)); - mToolbarInfo.setModel(minModel); - mToolbarInfo.setValues(null); - mToolbarInfo.setValues(mContentSet); - } - ((TextView) mToolBar.findViewById(R.id.toolbar_title)).setText(TaskFieldAdapters.TITLE.get(mContentSet)); - } - } - } - - - /** - * Update the view. This doesn't call {@link #updateView()} right away, instead it posts it. - */ - private void postUpdateView() - { - if (mContent != null) - { - mContent.post(mUpdateViewRunnable); - } - } - - - @Override - public void onModelLoaded(Model model) - { - if (model == null) - { - return; - } - - // the model has been loaded, now update the view - if (mModel == null || !mModel.equals(model)) - { - mModel = model; - if (mRestored) - { - // The fragment has been restored from a saved state - // We need to wait until all views are ready, otherwise the new data might get lost and all widgets show their default state (and no data). - postUpdateView(); - } - else - { - // This is the initial update. Just go ahead and update the view right away to ensure the activity comes up with a filled form. - updateView(); - } - } - } - - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) - { - /* + ActivityCompat.invalidateOptionsMenu(getActivity()); + } + + mAppBar.setExpanded(true, false); + } + + + /** + * Update the detail view with the current ContentSet. This removes any previous detail view and creates a new one if {@link #mContentSet} is not + * null. + */ + private void updateView() + { + Activity activity = getActivity(); + if (mContent != null && activity != null) + { + final LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + if (mDetailView != null) + { + // remove values, to ensure all listeners get released + mDetailView.setValues(null); + } + + mContent.removeAllViews(); + if (mContentSet != null) + { + mDetailView = (TaskView) inflater.inflate(R.layout.task_view, mContent, false); + mDetailView.setModel(mModel); + mDetailView.setValues(mContentSet); + mContent.addView(mDetailView); + + TaskView mToolbarInfo = (TaskView) mAppBar.findViewById(R.id.toolbar_content); + if (mToolbarInfo != null) + { + Model minModel = Sources.getInstance(activity).getMinimalModel(TaskFieldAdapters.ACCOUNT_TYPE.get(mContentSet)); + mToolbarInfo.setModel(minModel); + mToolbarInfo.setValues(null); + mToolbarInfo.setValues(mContentSet); + } + ((TextView) mToolBar.findViewById(R.id.toolbar_title)).setText(TaskFieldAdapters.TITLE.get(mContentSet)); + } + } + } + + + /** + * Update the view. This doesn't call {@link #updateView()} right away, instead it posts it. + */ + private void postUpdateView() + { + if (mContent != null) + { + mContent.post(mUpdateViewRunnable); + } + } + + + @Override + public void onModelLoaded(Model model) + { + if (model == null) + { + return; + } + + // the model has been loaded, now update the view + if (mModel == null || !mModel.equals(model)) + { + mModel = model; + if (mRestored) + { + // The fragment has been restored from a saved state + // We need to wait until all views are ready, otherwise the new data might get lost and all widgets show their default state (and no data). + postUpdateView(); + } + else + { + // This is the initial update. Just go ahead and update the view right away to ensure the activity comes up with a filled form. + updateView(); + } + } + } + + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) + { + /* * Don't show any options if we don't have a task to show. */ - if (mTaskUri != null) - { - menu = mToolBar.getMenu(); - menu.clear(); - - inflater.inflate(R.menu.view_task_fragment_menu, menu); - - if (mContentSet != null) - { - Integer status = TaskFieldAdapters.STATUS.get(mContentSet); - if (status != null) - { - mOldStatus = status; - } - - if (!mShowFloatingActionButton && !(TaskFieldAdapters.IS_CLOSED.get(mContentSet) || status != null && status == Tasks.STATUS_COMPLETED)) - { - MenuItem item = menu.findItem(R.id.complete_task); - item.setEnabled(true); - item.setVisible(true); - } - - // check pinned status - if (TaskFieldAdapters.PINNED.get(mContentSet)) - { - // we disable the edit option, because the task is completed and the action button shows the edit option. - MenuItem item = menu.findItem(R.id.pin_task); - item.setIcon(R.drawable.ic_pin_off_white_24dp); - } - else - { - MenuItem item = menu.findItem(R.id.pin_task); - item.setIcon(R.drawable.ic_pin_white_24dp); - } - } - } - } - - - @Override - public boolean onMenuItemClick(MenuItem item) - { - return onOptionsItemSelected(item); - } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - int itemId = item.getItemId(); - if (itemId == R.id.edit_task) - { - // open editor for this task - mCallback.onEditTask(mTaskUri, mContentSet); - return true; - } - else if (itemId == R.id.delete_task) - { - new AlertDialog.Builder(getActivity()).setTitle(R.string.confirm_delete_title).setCancelable(true) - .setNegativeButton(android.R.string.cancel, new OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - // nothing to do here - } - }).setPositiveButton(android.R.string.ok, new OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - if (mContentSet != null) - { - // TODO: remove the task in a background task - mContentSet.delete(mAppContext); - mCallback.onDelete(mTaskUri); - } - } - }).setMessage(R.string.confirm_delete_message).create().show(); - return true; - } - else if (itemId == R.id.complete_task) - { - completeTask(); - return true; - } - else if (itemId == R.id.pin_task) - { - if (TaskFieldAdapters.PINNED.get(mContentSet)) - { - item.setIcon(R.drawable.ic_pin_white_24dp); - TaskNotificationHandler.unpinTask(mAppContext, mContentSet); - } - else - { - item.setIcon(R.drawable.ic_pin_off_white_24dp); - TaskNotificationHandler.pinTask(mAppContext, mContentSet); - } - persistTask(); - return true; - } - else - { - return super.onOptionsItemSelected(item); - } - } - - - /** - * Completes the current task. - */ - private void completeTask() - { - TaskFieldAdapters.STATUS.set(mContentSet, Tasks.STATUS_COMPLETED); - TaskFieldAdapters.PINNED.set(mContentSet, false); - persistTask(); - Snackbar.make(getActivity().getWindow().getDecorView(), getString(R.string.toast_task_completed, TaskFieldAdapters.TITLE.get(mContentSet)), - Snackbar.LENGTH_SHORT).show(); - // at present we just handle it like deletion, i.e. close the task in phone mode, do nothing in tablet mode - mCallback.onDelete(mTaskUri); - if (mShowFloatingActionButton) - { - // hide fab in two pane mode - mFloatingActionButton.hide(); - } - } - - - @SuppressLint("NewApi") - private void updateColor() - { - mAppBar.setBackgroundColor(mListColor); - - if (mShowFloatingActionButton && mFloatingActionButton.getVisibility() == View.VISIBLE) - { - // the FAB gets a slightly lighter color to stand out a bit more. If it's too light, we darken it instead. - float[] hsv = new float[3]; - Color.colorToHSV(mListColor, hsv); - if (hsv[2] * (1 - hsv[1]) < 0.4) - { - hsv[2] *= 1.2; - } - else - { - hsv[2] /= 1.2; - } - mFloatingActionButton.setBackgroundTintList(new ColorStateList(new int[][] { new int[] { 0 } }, new int[] { Color.HSVToColor(hsv) })); - } - } - - - @SuppressLint("NewApi") - @Override - public void onContentLoaded(ContentSet contentSet) - { - if (contentSet.containsKey(Tasks.ACCOUNT_TYPE)) - { - mListColor = TaskFieldAdapters.LIST_COLOR.get(contentSet); - ((Callback) getActivity()).updateColor(mListColor); - - if (VERSION.SDK_INT >= 11) - { - updateColor(); - } - - Activity activity = getActivity(); - int newStatus = TaskFieldAdapters.STATUS.get(contentSet); - boolean newPinned = TaskFieldAdapters.PINNED.get(contentSet); - if (activity != null && (hasNewStatus(newStatus) || pinChanged(newPinned))) - { - // new need to update the options menu, because the status of the task has changed - ActivityCompat.invalidateOptionsMenu(activity); - } - - mPinned = newPinned; - mOldStatus = newStatus; - - if (mShowFloatingActionButton) - { - if (!TaskFieldAdapters.IS_CLOSED.get(contentSet)) - { - showFloatingActionButton(true); - mFloatingActionButton.show(); - } - else - { - if (mFloatingActionButton.getVisibility() == View.VISIBLE) - { - mFloatingActionButton.hide(); - } - } - } - - if (mModel == null || !TextUtils.equals(mModel.getAccountType(), contentSet.getAsString(Tasks.ACCOUNT_TYPE))) - { - Sources.loadModelAsync(mAppContext, contentSet.getAsString(Tasks.ACCOUNT_TYPE), this); - } - else - { - // the model didn't change, just update the view - postUpdateView(); - } - } - } - - /** - * An observer for the tasks URI. It updates the task view whenever the URI changes. - */ - private final ContentObserver mObserver = new ContentObserver(null) - { - @Override - public void onChange(boolean selfChange) - { - if (mContentSet != null) - { - // reload the task - mContentSet.update(mAppContext, CONTENT_VALUE_MAPPER); - } - } - }; - - - @Override - public void onContentChanged(ContentSet contentSet) - { - } - - - private boolean hasNewStatus(int newStatus) - { - return (mOldStatus != -1 && mOldStatus != newStatus || mOldStatus == -1 && TaskFieldAdapters.IS_CLOSED.get(mContentSet)); - } - - - private boolean pinChanged(boolean newPinned) - { - return !(mPinned == newPinned); - } - - - @SuppressLint("NewApi") - @Override - public void onOffsetChanged(AppBarLayout appBarLayout, int offset) - { - mAppBarOffset = offset; - int maxScroll = appBarLayout.getTotalScrollRange(); - float percentage = (float) Math.abs(offset) / (float) maxScroll; - - handleAlphaOnTitle(percentage); - - if (mIsTheTitleContainerVisible && Build.VERSION.SDK_INT >= 11) - { - mAppBar.findViewById(R.id.toolbar_content).setAlpha(1 - percentage); - } - } - - - private void handleAlphaOnTitle(float percentage) - { - if (percentage >= PERCENTAGE_TO_HIDE_TITLE_DETAILS) - { - if (mIsTheTitleContainerVisible) - { - animate(mAppBar.findViewById(R.id.toolbar_content), ALPHA_ANIMATIONS_DURATION, View.INVISIBLE); - animate(mToolBar.findViewById(R.id.toolbar_title), ALPHA_ANIMATIONS_DURATION, View.VISIBLE); - mIsTheTitleContainerVisible = false; - } - } - else - { - if (!mIsTheTitleContainerVisible) - { - animate(mToolBar.findViewById(R.id.toolbar_title), ALPHA_ANIMATIONS_DURATION, View.INVISIBLE); - animate(mAppBar.findViewById(R.id.toolbar_content), ALPHA_ANIMATIONS_DURATION, View.VISIBLE); - mIsTheTitleContainerVisible = true; - } - } - } - - - private void animate(View v, int duration, int visibility) - { - AlphaAnimation alphaAnimation = (visibility == View.VISIBLE) ? new AlphaAnimation(0f, 1f) : new AlphaAnimation(1f, 0f); - alphaAnimation.setDuration(duration); - alphaAnimation.setFillAfter(true); - v.startAnimation(alphaAnimation); - } - - - /** - * Set the toolbar of this fragment (if any), as the ActionBar if the given Activity. - * - * @param activty - * an {@link AppCompatActivity}. - */ - public void setupToolbarAsActionbar(android.support.v7.app.AppCompatActivity activty) - { - if (mToolBar == null) - { - return; - } - - activty.setSupportActionBar(mToolBar); - if (android.os.Build.VERSION.SDK_INT >= 11) - { - activty.getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - } - - - /** - * Shows or hides the floating action button. - * - * @param show - * true to show the FloatingActionButton, false to hide it. - */ - @SuppressLint("NewApi") - private void showFloatingActionButton(final boolean show) - { - CoordinatorLayout.LayoutParams p = (CoordinatorLayout.LayoutParams) mFloatingActionButton.getLayoutParams(); - if (show) - { - p.setAnchorId(R.id.appbar); - mFloatingActionButton.setLayoutParams(p); - mFloatingActionButton.setVisibility(View.VISIBLE); - // make sure the FAB has the right color - updateColor(); - } - else - { - p.setAnchorId(View.NO_ID); - mFloatingActionButton.setLayoutParams(p); - mFloatingActionButton.setVisibility(View.GONE); - } - } + if (mTaskUri != null) + { + menu = mToolBar.getMenu(); + menu.clear(); + + inflater.inflate(R.menu.view_task_fragment_menu, menu); + + if (mContentSet != null) + { + Integer status = TaskFieldAdapters.STATUS.get(mContentSet); + if (status != null) + { + mOldStatus = status; + } + + if (!mShowFloatingActionButton && !(TaskFieldAdapters.IS_CLOSED.get(mContentSet) || status != null && status == Tasks.STATUS_COMPLETED)) + { + MenuItem item = menu.findItem(R.id.complete_task); + item.setEnabled(true); + item.setVisible(true); + } + + // check pinned status + if (TaskFieldAdapters.PINNED.get(mContentSet)) + { + // we disable the edit option, because the task is completed and the action button shows the edit option. + MenuItem item = menu.findItem(R.id.pin_task); + item.setIcon(R.drawable.ic_pin_off_white_24dp); + } + else + { + MenuItem item = menu.findItem(R.id.pin_task); + item.setIcon(R.drawable.ic_pin_white_24dp); + } + } + } + } + + + @Override + public boolean onMenuItemClick(MenuItem item) + { + return onOptionsItemSelected(item); + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + int itemId = item.getItemId(); + if (itemId == R.id.edit_task) + { + // open editor for this task + mCallback.onEditTask(mTaskUri, mContentSet); + return true; + } + else if (itemId == R.id.delete_task) + { + new AlertDialog.Builder(getActivity()).setTitle(R.string.confirm_delete_title).setCancelable(true) + .setNegativeButton(android.R.string.cancel, new OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + // nothing to do here + } + }).setPositiveButton(android.R.string.ok, new OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + if (mContentSet != null) + { + // TODO: remove the task in a background task + mContentSet.delete(mAppContext); + mCallback.onDelete(mTaskUri); + } + } + }).setMessage(R.string.confirm_delete_message).create().show(); + return true; + } + else if (itemId == R.id.complete_task) + { + completeTask(); + return true; + } + else if (itemId == R.id.pin_task) + { + if (TaskFieldAdapters.PINNED.get(mContentSet)) + { + item.setIcon(R.drawable.ic_pin_white_24dp); + TaskNotificationHandler.unpinTask(mAppContext, mContentSet); + } + else + { + item.setIcon(R.drawable.ic_pin_off_white_24dp); + TaskNotificationHandler.pinTask(mAppContext, mContentSet); + } + persistTask(); + return true; + } + else + { + return super.onOptionsItemSelected(item); + } + } + + + /** + * Completes the current task. + */ + private void completeTask() + { + TaskFieldAdapters.STATUS.set(mContentSet, Tasks.STATUS_COMPLETED); + TaskFieldAdapters.PINNED.set(mContentSet, false); + persistTask(); + Snackbar.make(getActivity().getWindow().getDecorView(), getString(R.string.toast_task_completed, TaskFieldAdapters.TITLE.get(mContentSet)), + Snackbar.LENGTH_SHORT).show(); + // at present we just handle it like deletion, i.e. close the task in phone mode, do nothing in tablet mode + mCallback.onDelete(mTaskUri); + if (mShowFloatingActionButton) + { + // hide fab in two pane mode + mFloatingActionButton.hide(); + } + } + + + @SuppressLint("NewApi") + private void updateColor() + { + mAppBar.setBackgroundColor(mListColor); + + if (mShowFloatingActionButton && mFloatingActionButton.getVisibility() == View.VISIBLE) + { + // the FAB gets a slightly lighter color to stand out a bit more. If it's too light, we darken it instead. + float[] hsv = new float[3]; + Color.colorToHSV(mListColor, hsv); + if (hsv[2] * (1 - hsv[1]) < 0.4) + { + hsv[2] *= 1.2; + } + else + { + hsv[2] /= 1.2; + } + mFloatingActionButton.setBackgroundTintList(new ColorStateList(new int[][] { new int[] { 0 } }, new int[] { Color.HSVToColor(hsv) })); + } + } + + + @SuppressLint("NewApi") + @Override + public void onContentLoaded(ContentSet contentSet) + { + if (contentSet.containsKey(Tasks.ACCOUNT_TYPE)) + { + mListColor = TaskFieldAdapters.LIST_COLOR.get(contentSet); + ((Callback) getActivity()).updateColor(mListColor); + + if (VERSION.SDK_INT >= 11) + { + updateColor(); + } + + Activity activity = getActivity(); + int newStatus = TaskFieldAdapters.STATUS.get(contentSet); + boolean newPinned = TaskFieldAdapters.PINNED.get(contentSet); + if (activity != null && (hasNewStatus(newStatus) || pinChanged(newPinned))) + { + // new need to update the options menu, because the status of the task has changed + ActivityCompat.invalidateOptionsMenu(activity); + } + + mPinned = newPinned; + mOldStatus = newStatus; + + if (mShowFloatingActionButton) + { + if (!TaskFieldAdapters.IS_CLOSED.get(contentSet)) + { + showFloatingActionButton(true); + mFloatingActionButton.show(); + } + else + { + if (mFloatingActionButton.getVisibility() == View.VISIBLE) + { + mFloatingActionButton.hide(); + } + } + } + + if (mModel == null || !TextUtils.equals(mModel.getAccountType(), contentSet.getAsString(Tasks.ACCOUNT_TYPE))) + { + Sources.loadModelAsync(mAppContext, contentSet.getAsString(Tasks.ACCOUNT_TYPE), this); + } + else + { + // the model didn't change, just update the view + postUpdateView(); + } + } + } + + + /** + * An observer for the tasks URI. It updates the task view whenever the URI changes. + */ + private final ContentObserver mObserver = new ContentObserver(null) + { + @Override + public void onChange(boolean selfChange) + { + if (mContentSet != null) + { + // reload the task + mContentSet.update(mAppContext, CONTENT_VALUE_MAPPER); + } + } + }; + + + @Override + public void onContentChanged(ContentSet contentSet) + { + } + + + private boolean hasNewStatus(int newStatus) + { + return (mOldStatus != -1 && mOldStatus != newStatus || mOldStatus == -1 && TaskFieldAdapters.IS_CLOSED.get(mContentSet)); + } + + + private boolean pinChanged(boolean newPinned) + { + return !(mPinned == newPinned); + } + + + @SuppressLint("NewApi") + @Override + public void onOffsetChanged(AppBarLayout appBarLayout, int offset) + { + mAppBarOffset = offset; + int maxScroll = appBarLayout.getTotalScrollRange(); + float percentage = (float) Math.abs(offset) / (float) maxScroll; + + handleAlphaOnTitle(percentage); + + if (mIsTheTitleContainerVisible && Build.VERSION.SDK_INT >= 11) + { + mAppBar.findViewById(R.id.toolbar_content).setAlpha(1 - percentage); + } + } + + + private void handleAlphaOnTitle(float percentage) + { + if (percentage >= PERCENTAGE_TO_HIDE_TITLE_DETAILS) + { + if (mIsTheTitleContainerVisible) + { + animate(mAppBar.findViewById(R.id.toolbar_content), ALPHA_ANIMATIONS_DURATION, View.INVISIBLE); + animate(mToolBar.findViewById(R.id.toolbar_title), ALPHA_ANIMATIONS_DURATION, View.VISIBLE); + mIsTheTitleContainerVisible = false; + } + } + else + { + if (!mIsTheTitleContainerVisible) + { + animate(mToolBar.findViewById(R.id.toolbar_title), ALPHA_ANIMATIONS_DURATION, View.INVISIBLE); + animate(mAppBar.findViewById(R.id.toolbar_content), ALPHA_ANIMATIONS_DURATION, View.VISIBLE); + mIsTheTitleContainerVisible = true; + } + } + } + + + private void animate(View v, int duration, int visibility) + { + AlphaAnimation alphaAnimation = (visibility == View.VISIBLE) ? new AlphaAnimation(0f, 1f) : new AlphaAnimation(1f, 0f); + alphaAnimation.setDuration(duration); + alphaAnimation.setFillAfter(true); + v.startAnimation(alphaAnimation); + } + + + /** + * Set the toolbar of this fragment (if any), as the ActionBar if the given Activity. + * + * @param activty + * an {@link AppCompatActivity}. + */ + public void setupToolbarAsActionbar(android.support.v7.app.AppCompatActivity activty) + { + if (mToolBar == null) + { + return; + } + + activty.setSupportActionBar(mToolBar); + if (android.os.Build.VERSION.SDK_INT >= 11) + { + activty.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + } + + + /** + * Shows or hides the floating action button. + * + * @param show + * true to show the FloatingActionButton, false to hide it. + */ + @SuppressLint("NewApi") + private void showFloatingActionButton(final boolean show) + { + CoordinatorLayout.LayoutParams p = (CoordinatorLayout.LayoutParams) mFloatingActionButton.getLayoutParams(); + if (show) + { + p.setAnchorId(R.id.appbar); + mFloatingActionButton.setLayoutParams(p); + mFloatingActionButton.setVisibility(View.VISIBLE); + // make sure the FAB has the right color + updateColor(); + } + else + { + p.setAnchorId(View.NO_ID); + mFloatingActionButton.setLayoutParams(p); + mFloatingActionButton.setVisibility(View.GONE); + } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/dashclock/DashClockPreferenceActivity.java b/opentasks/src/main/java/org/dmfs/tasks/dashclock/DashClockPreferenceActivity.java index 77835a95..cc58b8fd 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/dashclock/DashClockPreferenceActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/dashclock/DashClockPreferenceActivity.java @@ -17,25 +17,25 @@ package org.dmfs.tasks.dashclock; -import org.dmfs.tasks.R; - import android.os.Bundle; import android.preference.PreferenceActivity; +import org.dmfs.tasks.R; + public class DashClockPreferenceActivity extends PreferenceActivity { - public static final String KEY_PREF_DISPLAY_MODE = "pref_db_displayed_tasks"; - public static final int DISPLAY_MODE_ALL = 1; - public static final int DISPLAY_MODE_DUE = 2; - public static final int DISPLAY_MODE_START = 3; - public static final int DISPLAY_MODE_PINNED = 4; + public static final String KEY_PREF_DISPLAY_MODE = "pref_db_displayed_tasks"; + public static final int DISPLAY_MODE_ALL = 1; + public static final int DISPLAY_MODE_DUE = 2; + public static final int DISPLAY_MODE_START = 3; + public static final int DISPLAY_MODE_PINNED = 4; - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.dashclock_preferences); - } + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.dashclock_preferences); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java b/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java index b672ce53..a8e55b23 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java +++ b/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java @@ -17,8 +17,15 @@ package org.dmfs.tasks.dashclock; -import java.util.Calendar; -import java.util.TimeZone; +import android.content.ContentUris; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.preference.PreferenceManager; +import android.text.format.Time; + +import com.google.android.apps.dashclock.api.DashClockExtension; +import com.google.android.apps.dashclock.api.ExtensionData; import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskContract.Instances; @@ -30,393 +37,388 @@ import org.dmfs.tasks.model.adapters.TimeFieldAdapter; import org.dmfs.tasks.utils.DateFormatter; import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; -import android.content.ContentUris; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.preference.PreferenceManager; -import android.text.format.Time; - -import com.google.android.apps.dashclock.api.DashClockExtension; -import com.google.android.apps.dashclock.api.ExtensionData; +import java.util.Calendar; +import java.util.TimeZone; /** * This class provides an extension for the DashClock widget in order to displays recent tasks. - * + * * @author Tobias Reinsch - * */ public class TasksExtension extends DashClockExtension { - /** Defines the time span for recent tasks in hours **/ - private static final int RECENT_HOURS = 3; - - private static final String[] INSTANCE_PROJECTION = new String[] { Instances._ID, Instances.TASK_ID, Instances.ACCOUNT_NAME, Instances.ACCOUNT_TYPE, - Instances.TITLE, Instances.DESCRIPTION, Instances.STATUS, Instances.DUE, Instances.DTSTART, Instances.TZ, Instances.IS_ALLDAY }; - - private static final String INSTANCE_PINNED_SELECTION = Instances.PINNED + " = 1"; - - private static final String INSTANCE_DUE_SELECTION = Instances.IS_ALLDAY + " = 0 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED + " AND " - + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DUE + " > ? AND " + Instances.DUE + " < ? )"; - private static final String INSTANCE_START_SELECTION = Instances.IS_ALLDAY + " = 0 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED + " AND " - + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DTSTART + " > ? AND " + Instances.DTSTART + " < ? )"; - private static final String INSTANCE_START_DUE_SELECTION = Instances.IS_ALLDAY + " = 0 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED - + " AND " + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND ((" + Instances.DTSTART + " > ? AND " + Instances.DTSTART + " < ? ) OR ( " - + Instances.DUE + " > ? AND " + Instances.DUE + " < ? ))"; - - private static final String INSTANCE_START_SELECTION_ALL_DAY = Instances.IS_ALLDAY + " = 1 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED - + " AND " + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DTSTART + " = ?)"; - - private static final String INSTANCE_DUE_SELECTION_ALL_DAY = Instances.IS_ALLDAY + " = 1 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED - + " AND " + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DUE + " = ?)"; - - private static final String INSTANCE_START_DUE_SELECTION_ALL_DAY = Instances.IS_ALLDAY + " = 1 AND " + Instances.STATUS + " != " - + Instances.STATUS_COMPLETED + " AND " + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DTSTART + " = ? OR " - + Instances.DUE + " = ? )"; - - private String mAuthority; - private int mDisplayMode; - private long mNow; - private DateFormatter mDateFormatter; - - - @Override - protected void onInitialize(boolean isReconnect) - { - // enable automatic dashclock updates on task changes - addWatchContentUris(new String[] { TaskContract.getContentUri(TaskContract.taskAuthority(this)).toString() }); - super.onInitialize(isReconnect); - - mDateFormatter = new DateFormatter(this); - } - - - @Override - protected void onUpdateData(int reason) - { - mNow = System.currentTimeMillis(); - mAuthority = TaskContract.taskAuthority(this); - SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); - mDisplayMode = Integer.valueOf(sharedPref.getString(DashClockPreferenceActivity.KEY_PREF_DISPLAY_MODE, "1")); - publishRecentTaskUpdate(); - } - - - protected void publishRecentTaskUpdate() - { - - // get next task that is due - Cursor recentTaskCursor = null; - Cursor allDayTaskCursor = null; - Cursor pinnedTaskCursor = null; - - try - { - - switch (mDisplayMode) - { - case DashClockPreferenceActivity.DISPLAY_MODE_DUE: - recentTaskCursor = loadRecentDueTaskCursor(); - allDayTaskCursor = loadAllDayTasksDueTodayCursor(); - break; - - case DashClockPreferenceActivity.DISPLAY_MODE_START: - recentTaskCursor = loadRecentStartTaskCursor(); - allDayTaskCursor = loadAllDayTasksStartTodayCursor(); - break; - - case DashClockPreferenceActivity.DISPLAY_MODE_PINNED: - pinnedTaskCursor = loadPinnedTaskCursor(); - break; - - default: - recentTaskCursor = loadRecentStartDueTaskCursor(); - allDayTaskCursor = loadAllDayTasksStartDueTodayCursor(); - pinnedTaskCursor = loadPinnedTaskCursor(); - break; - } - - int recentTaskCount = recentTaskCursor == null ? 0 : recentTaskCursor.getCount(); - int allDayTaskCount = allDayTaskCursor == null ? 0 : allDayTaskCursor.getCount(); - int pinnedTaskCount = pinnedTaskCursor == null ? 0 : pinnedTaskCursor.getCount(); - if ((recentTaskCount + allDayTaskCount + pinnedTaskCount) > 0) - { - // select the right cursor - Cursor c = null; - if (pinnedTaskCount > 0) - { - c = pinnedTaskCursor; - } - else if ((recentTaskCount + allDayTaskCount) > 0) - { - c = recentTaskCount > 0 ? recentTaskCursor : allDayTaskCursor; - } - - c.moveToFirst(); - - boolean isAllDay = allDayTaskCount > 0; - - String description = c.getString(c.getColumnIndex(Tasks.DESCRIPTION)); - if (description != null) - { - description = description.replaceAll("\\[\\s?\\]", " ").replaceAll("\\[[xX]\\]", "✓"); - } - String title = getTaskTitleDisplayString(c, isAllDay); - - // intent - String accountType = c.getString(c.getColumnIndex(Instances.ACCOUNT_TYPE)); - Long taskId = c.getLong(c.getColumnIndex(Instances.TASK_ID)); - Intent clickIntent = buildClickIntent(taskId, accountType); - - // Publish the extension data update. - publishUpdate(new ExtensionData().visible(true).icon(R.drawable.ic_dashboard) - .status(String.valueOf(allDayTaskCount + recentTaskCount + pinnedTaskCount)).expandedTitle(title).expandedBody(description) - .clickIntent(clickIntent)); - } - else - { - // no upcoming task -> empty update - publishUpdate(null); - } - } - finally - { - closeCursor(recentTaskCursor); - closeCursor(allDayTaskCursor); - closeCursor(pinnedTaskCursor); - } - - } - - - private void closeCursor(Cursor cursor) - { - if (cursor == null || cursor.isClosed()) - { - return; - } - cursor.close(); - } - - - private String getTaskTitleDisplayString(Cursor c, boolean isAllDay) - { - if (DashClockPreferenceActivity.DISPLAY_MODE_DUE == mDisplayMode) - { - // DUE event - return getTaskTitleDueString(c, isAllDay); - } - else if (DashClockPreferenceActivity.DISPLAY_MODE_START == mDisplayMode) - { - // START event - return getTaskTitleStartString(c, isAllDay); - } - else if (DashClockPreferenceActivity.DISPLAY_MODE_PINNED == mDisplayMode) - { - // return task title - return TaskFieldAdapters.TITLE.get(c); - } - else - { - // START or DUE event - String timeEventString = isDueEvent(c, isAllDay) ? getTaskTitleDueString(c, isAllDay) : getTaskTitleStartString(c, isAllDay); - if (timeEventString == null) - { - return TaskFieldAdapters.TITLE.get(c); - } - else - { - return timeEventString; - } - } - } - - - private String getTaskTitleDueString(Cursor c, boolean isAllDay) - { - if (isAllDay) - { - return getString(R.string.dashclock_widget_title_due_expanded_allday, c.getString(c.getColumnIndex(Tasks.TITLE))); - } - else - { - TimeFieldAdapter timeFieldAdapter = new TimeFieldAdapter(Instances.DUE, Instances.TZ, Instances.IS_ALLDAY); - Time dueTime = timeFieldAdapter.get(c); - if (dueTime == null) - { - return null; - } - String dueTimeString = mDateFormatter.format(dueTime, DateFormatContext.DASHCLOCK_VIEW); - return getString(R.string.dashclock_widget_title_due_expanded, c.getString(c.getColumnIndex(Tasks.TITLE)), dueTimeString); - } - } - - - private String getTaskTitleStartString(Cursor c, boolean isAllDay) - { - if (isAllDay) - { - return getString(R.string.dashclock_widget_title_start_expanded_allday, c.getString(c.getColumnIndex(Tasks.TITLE))); - } - else - { - TimeFieldAdapter timeFieldAdapter = new TimeFieldAdapter(Instances.DTSTART, Instances.TZ, Instances.IS_ALLDAY); - Time startTime = timeFieldAdapter.get(c); - if (startTime == null) - { - return null; - } - String startTimeString = mDateFormatter.format(startTime, DateFormatContext.DASHCLOCK_VIEW); - return getString(R.string.dashclock_widget_title_start_expanded, c.getString(c.getColumnIndex(Tasks.TITLE)), startTimeString); - } - } - - - private boolean isDueEvent(Cursor c, boolean isAllDay) - { - if (c.isNull(c.getColumnIndex(Instances.DUE))) - { - return false; - } - if (c.isNull(c.getColumnIndex(Instances.DTSTART))) - { - return true; - } - - Long dueTime = c.getLong(c.getColumnIndex(Instances.DUE)); - Long startTime = c.getLong(c.getColumnIndex(Instances.DTSTART)); - - if (isAllDay) - { - // get start of today in UTC - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR_OF_DAY, 0); // clear would not reset the hour of day - calendar.clear(Calendar.MINUTE); - calendar.clear(Calendar.SECOND); - calendar.clear(Calendar.MILLISECOND); - calendar.setTimeZone(TimeZone.getTimeZone("UTC")); - long todayUTC = calendar.getTimeInMillis(); - - if (dueTime == todayUTC) - { - return true; - } - else - { - return false; - } - } - else - { - if (startTime < mNow) - { - return true; - } - else - { - return false; - } - } - - } - - - protected Intent buildClickIntent(long taskId, String accountType) - { - Intent clickIntent = new Intent(Intent.ACTION_VIEW); - clickIntent.setData(ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), taskId)); - clickIntent.putExtra(EditTaskActivity.EXTRA_DATA_ACCOUNT_TYPE, accountType); - - return clickIntent; - } - - - private Cursor loadPinnedTaskCursor() - { - return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_PINNED_SELECTION, null, - Tasks.PRIORITY + " is not null, " + Tasks.PRIORITY + " DESC"); - } - - - private Cursor loadRecentDueTaskCursor() - { - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.HOUR_OF_DAY, RECENT_HOURS); // clear would not reset the hour of day - long later = calendar.getTimeInMillis(); - - return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_DUE_SELECTION, - new String[] { String.valueOf(mNow), String.valueOf(later) }, Instances.DUE); - } - - - private Cursor loadRecentStartTaskCursor() - { - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.HOUR_OF_DAY, RECENT_HOURS); // clear would not reset the hour of day - long later = calendar.getTimeInMillis(); - - return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_START_SELECTION, - new String[] { String.valueOf(mNow), String.valueOf(later) }, Instances.DTSTART); - } - - - private Cursor loadRecentStartDueTaskCursor() - { - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.HOUR_OF_DAY, RECENT_HOURS); // clear would not reset the hour of day - long later = calendar.getTimeInMillis(); - - return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_START_DUE_SELECTION, - new String[] { String.valueOf(mNow), String.valueOf(later), String.valueOf(mNow), String.valueOf(later) }, - Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING); - } - - - private Cursor loadAllDayTasksDueTodayCursor() - { - // get start of today in UTC - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR_OF_DAY, 0); // clear would not reset the hour of day - calendar.clear(Calendar.MINUTE); - calendar.clear(Calendar.SECOND); - calendar.clear(Calendar.MILLISECOND); - calendar.setTimeZone(TimeZone.getTimeZone("UTC")); - long todayUTC = calendar.getTimeInMillis(); - - return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_DUE_SELECTION_ALL_DAY, - new String[] { String.valueOf(todayUTC) }, Instances.DUE); - } - - - private Cursor loadAllDayTasksStartTodayCursor() - { - // get start of today in UTC - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR_OF_DAY, 0); // clear would not reset the hour of day - calendar.clear(Calendar.MINUTE); - calendar.clear(Calendar.SECOND); - calendar.clear(Calendar.MILLISECOND); - calendar.setTimeZone(TimeZone.getTimeZone("UTC")); - long todayUTC = calendar.getTimeInMillis(); - - return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_START_SELECTION_ALL_DAY, - new String[] { String.valueOf(todayUTC) }, Instances.DTSTART); - } - - - private Cursor loadAllDayTasksStartDueTodayCursor() - { - // get start of today in UTC - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR_OF_DAY, 0); // clear would not reset the hour of day - calendar.clear(Calendar.MINUTE); - calendar.clear(Calendar.SECOND); - calendar.clear(Calendar.MILLISECOND); - calendar.setTimeZone(TimeZone.getTimeZone("UTC")); - long todayUTC = calendar.getTimeInMillis(); - - return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_START_DUE_SELECTION_ALL_DAY, - new String[] { String.valueOf(todayUTC), String.valueOf(todayUTC) }, Instances.DUE); - } + /** + * Defines the time span for recent tasks in hours + **/ + private static final int RECENT_HOURS = 3; + + private static final String[] INSTANCE_PROJECTION = new String[] { + Instances._ID, Instances.TASK_ID, Instances.ACCOUNT_NAME, Instances.ACCOUNT_TYPE, + Instances.TITLE, Instances.DESCRIPTION, Instances.STATUS, Instances.DUE, Instances.DTSTART, Instances.TZ, Instances.IS_ALLDAY }; + + private static final String INSTANCE_PINNED_SELECTION = Instances.PINNED + " = 1"; + + private static final String INSTANCE_DUE_SELECTION = Instances.IS_ALLDAY + " = 0 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED + " AND " + + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DUE + " > ? AND " + Instances.DUE + " < ? )"; + private static final String INSTANCE_START_SELECTION = Instances.IS_ALLDAY + " = 0 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED + " AND " + + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DTSTART + " > ? AND " + Instances.DTSTART + " < ? )"; + private static final String INSTANCE_START_DUE_SELECTION = Instances.IS_ALLDAY + " = 0 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED + + " AND " + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND ((" + Instances.DTSTART + " > ? AND " + Instances.DTSTART + " < ? ) OR ( " + + Instances.DUE + " > ? AND " + Instances.DUE + " < ? ))"; + + private static final String INSTANCE_START_SELECTION_ALL_DAY = Instances.IS_ALLDAY + " = 1 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED + + " AND " + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DTSTART + " = ?)"; + + private static final String INSTANCE_DUE_SELECTION_ALL_DAY = Instances.IS_ALLDAY + " = 1 AND " + Instances.STATUS + " != " + Instances.STATUS_COMPLETED + + " AND " + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DUE + " = ?)"; + + private static final String INSTANCE_START_DUE_SELECTION_ALL_DAY = Instances.IS_ALLDAY + " = 1 AND " + Instances.STATUS + " != " + + Instances.STATUS_COMPLETED + " AND " + Instances.STATUS + " != " + Instances.STATUS_CANCELLED + " AND (" + Instances.DTSTART + " = ? OR " + + Instances.DUE + " = ? )"; + + private String mAuthority; + private int mDisplayMode; + private long mNow; + private DateFormatter mDateFormatter; + + + @Override + protected void onInitialize(boolean isReconnect) + { + // enable automatic dashclock updates on task changes + addWatchContentUris(new String[] { TaskContract.getContentUri(TaskContract.taskAuthority(this)).toString() }); + super.onInitialize(isReconnect); + + mDateFormatter = new DateFormatter(this); + } + + + @Override + protected void onUpdateData(int reason) + { + mNow = System.currentTimeMillis(); + mAuthority = TaskContract.taskAuthority(this); + SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); + mDisplayMode = Integer.valueOf(sharedPref.getString(DashClockPreferenceActivity.KEY_PREF_DISPLAY_MODE, "1")); + publishRecentTaskUpdate(); + } + + + protected void publishRecentTaskUpdate() + { + + // get next task that is due + Cursor recentTaskCursor = null; + Cursor allDayTaskCursor = null; + Cursor pinnedTaskCursor = null; + + try + { + + switch (mDisplayMode) + { + case DashClockPreferenceActivity.DISPLAY_MODE_DUE: + recentTaskCursor = loadRecentDueTaskCursor(); + allDayTaskCursor = loadAllDayTasksDueTodayCursor(); + break; + + case DashClockPreferenceActivity.DISPLAY_MODE_START: + recentTaskCursor = loadRecentStartTaskCursor(); + allDayTaskCursor = loadAllDayTasksStartTodayCursor(); + break; + + case DashClockPreferenceActivity.DISPLAY_MODE_PINNED: + pinnedTaskCursor = loadPinnedTaskCursor(); + break; + + default: + recentTaskCursor = loadRecentStartDueTaskCursor(); + allDayTaskCursor = loadAllDayTasksStartDueTodayCursor(); + pinnedTaskCursor = loadPinnedTaskCursor(); + break; + } + + int recentTaskCount = recentTaskCursor == null ? 0 : recentTaskCursor.getCount(); + int allDayTaskCount = allDayTaskCursor == null ? 0 : allDayTaskCursor.getCount(); + int pinnedTaskCount = pinnedTaskCursor == null ? 0 : pinnedTaskCursor.getCount(); + if ((recentTaskCount + allDayTaskCount + pinnedTaskCount) > 0) + { + // select the right cursor + Cursor c = null; + if (pinnedTaskCount > 0) + { + c = pinnedTaskCursor; + } + else if ((recentTaskCount + allDayTaskCount) > 0) + { + c = recentTaskCount > 0 ? recentTaskCursor : allDayTaskCursor; + } + + c.moveToFirst(); + + boolean isAllDay = allDayTaskCount > 0; + + String description = c.getString(c.getColumnIndex(Tasks.DESCRIPTION)); + if (description != null) + { + description = description.replaceAll("\\[\\s?\\]", " ").replaceAll("\\[[xX]\\]", "✓"); + } + String title = getTaskTitleDisplayString(c, isAllDay); + + // intent + String accountType = c.getString(c.getColumnIndex(Instances.ACCOUNT_TYPE)); + Long taskId = c.getLong(c.getColumnIndex(Instances.TASK_ID)); + Intent clickIntent = buildClickIntent(taskId, accountType); + + // Publish the extension data update. + publishUpdate(new ExtensionData().visible(true).icon(R.drawable.ic_dashboard) + .status(String.valueOf(allDayTaskCount + recentTaskCount + pinnedTaskCount)).expandedTitle(title).expandedBody(description) + .clickIntent(clickIntent)); + } + else + { + // no upcoming task -> empty update + publishUpdate(null); + } + } + finally + { + closeCursor(recentTaskCursor); + closeCursor(allDayTaskCursor); + closeCursor(pinnedTaskCursor); + } + + } + + + private void closeCursor(Cursor cursor) + { + if (cursor == null || cursor.isClosed()) + { + return; + } + cursor.close(); + } + + + private String getTaskTitleDisplayString(Cursor c, boolean isAllDay) + { + if (DashClockPreferenceActivity.DISPLAY_MODE_DUE == mDisplayMode) + { + // DUE event + return getTaskTitleDueString(c, isAllDay); + } + else if (DashClockPreferenceActivity.DISPLAY_MODE_START == mDisplayMode) + { + // START event + return getTaskTitleStartString(c, isAllDay); + } + else if (DashClockPreferenceActivity.DISPLAY_MODE_PINNED == mDisplayMode) + { + // return task title + return TaskFieldAdapters.TITLE.get(c); + } + else + { + // START or DUE event + String timeEventString = isDueEvent(c, isAllDay) ? getTaskTitleDueString(c, isAllDay) : getTaskTitleStartString(c, isAllDay); + if (timeEventString == null) + { + return TaskFieldAdapters.TITLE.get(c); + } + else + { + return timeEventString; + } + } + } + + + private String getTaskTitleDueString(Cursor c, boolean isAllDay) + { + if (isAllDay) + { + return getString(R.string.dashclock_widget_title_due_expanded_allday, c.getString(c.getColumnIndex(Tasks.TITLE))); + } + else + { + TimeFieldAdapter timeFieldAdapter = new TimeFieldAdapter(Instances.DUE, Instances.TZ, Instances.IS_ALLDAY); + Time dueTime = timeFieldAdapter.get(c); + if (dueTime == null) + { + return null; + } + String dueTimeString = mDateFormatter.format(dueTime, DateFormatContext.DASHCLOCK_VIEW); + return getString(R.string.dashclock_widget_title_due_expanded, c.getString(c.getColumnIndex(Tasks.TITLE)), dueTimeString); + } + } + + + private String getTaskTitleStartString(Cursor c, boolean isAllDay) + { + if (isAllDay) + { + return getString(R.string.dashclock_widget_title_start_expanded_allday, c.getString(c.getColumnIndex(Tasks.TITLE))); + } + else + { + TimeFieldAdapter timeFieldAdapter = new TimeFieldAdapter(Instances.DTSTART, Instances.TZ, Instances.IS_ALLDAY); + Time startTime = timeFieldAdapter.get(c); + if (startTime == null) + { + return null; + } + String startTimeString = mDateFormatter.format(startTime, DateFormatContext.DASHCLOCK_VIEW); + return getString(R.string.dashclock_widget_title_start_expanded, c.getString(c.getColumnIndex(Tasks.TITLE)), startTimeString); + } + } + + + private boolean isDueEvent(Cursor c, boolean isAllDay) + { + if (c.isNull(c.getColumnIndex(Instances.DUE))) + { + return false; + } + if (c.isNull(c.getColumnIndex(Instances.DTSTART))) + { + return true; + } + + Long dueTime = c.getLong(c.getColumnIndex(Instances.DUE)); + Long startTime = c.getLong(c.getColumnIndex(Instances.DTSTART)); + + if (isAllDay) + { + // get start of today in UTC + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); // clear would not reset the hour of day + calendar.clear(Calendar.MINUTE); + calendar.clear(Calendar.SECOND); + calendar.clear(Calendar.MILLISECOND); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + long todayUTC = calendar.getTimeInMillis(); + + if (dueTime == todayUTC) + { + return true; + } + else + { + return false; + } + } + else + { + if (startTime < mNow) + { + return true; + } + else + { + return false; + } + } + + } + + + protected Intent buildClickIntent(long taskId, String accountType) + { + Intent clickIntent = new Intent(Intent.ACTION_VIEW); + clickIntent.setData(ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), taskId)); + clickIntent.putExtra(EditTaskActivity.EXTRA_DATA_ACCOUNT_TYPE, accountType); + + return clickIntent; + } + + + private Cursor loadPinnedTaskCursor() + { + return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_PINNED_SELECTION, null, + Tasks.PRIORITY + " is not null, " + Tasks.PRIORITY + " DESC"); + } + + + private Cursor loadRecentDueTaskCursor() + { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.HOUR_OF_DAY, RECENT_HOURS); // clear would not reset the hour of day + long later = calendar.getTimeInMillis(); + + return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_DUE_SELECTION, + new String[] { String.valueOf(mNow), String.valueOf(later) }, Instances.DUE); + } + + + private Cursor loadRecentStartTaskCursor() + { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.HOUR_OF_DAY, RECENT_HOURS); // clear would not reset the hour of day + long later = calendar.getTimeInMillis(); + + return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_START_SELECTION, + new String[] { String.valueOf(mNow), String.valueOf(later) }, Instances.DTSTART); + } + + + private Cursor loadRecentStartDueTaskCursor() + { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.HOUR_OF_DAY, RECENT_HOURS); // clear would not reset the hour of day + long later = calendar.getTimeInMillis(); + + return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_START_DUE_SELECTION, + new String[] { String.valueOf(mNow), String.valueOf(later), String.valueOf(mNow), String.valueOf(later) }, + Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING); + } + + + private Cursor loadAllDayTasksDueTodayCursor() + { + // get start of today in UTC + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); // clear would not reset the hour of day + calendar.clear(Calendar.MINUTE); + calendar.clear(Calendar.SECOND); + calendar.clear(Calendar.MILLISECOND); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + long todayUTC = calendar.getTimeInMillis(); + + return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_DUE_SELECTION_ALL_DAY, + new String[] { String.valueOf(todayUTC) }, Instances.DUE); + } + + + private Cursor loadAllDayTasksStartTodayCursor() + { + // get start of today in UTC + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); // clear would not reset the hour of day + calendar.clear(Calendar.MINUTE); + calendar.clear(Calendar.SECOND); + calendar.clear(Calendar.MILLISECOND); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + long todayUTC = calendar.getTimeInMillis(); + + return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_START_SELECTION_ALL_DAY, + new String[] { String.valueOf(todayUTC) }, Instances.DTSTART); + } + + + private Cursor loadAllDayTasksStartDueTodayCursor() + { + // get start of today in UTC + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); // clear would not reset the hour of day + calendar.clear(Calendar.MINUTE); + calendar.clear(Calendar.SECOND); + calendar.clear(Calendar.MILLISECOND); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + long todayUTC = calendar.getTimeInMillis(); + + return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_START_DUE_SELECTION_ALL_DAY, + new String[] { String.valueOf(todayUTC), String.valueOf(todayUTC) }, Instances.DUE); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java index 3ca558ca..cd10a789 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java @@ -25,84 +25,86 @@ import org.dmfs.tasks.utils.ExpandableGroupDescriptor; /** * An abstract factory to create {@link ExpandableGroupDescriptor}s. - * + * * @author Marten Gajda */ public abstract class AbstractGroupingFactory { - /** - * The projection we use when we load instances. We don't need every detail of a task here. This is used by all groupings. - */ - public final static String[] INSTANCE_PROJECTION = new String[] { Instances.INSTANCE_START, Instances.INSTANCE_DURATION, Instances.INSTANCE_DUE, - Instances.IS_ALLDAY, Instances.TZ, Instances.TITLE, Instances.LIST_COLOR, Instances.PRIORITY, Instances.LIST_ID, Instances.TASK_ID, Instances._ID, - Instances.STATUS, Instances.COMPLETED, Instances.IS_CLOSED, Instances.PERCENT_COMPLETE, Instances.ACCOUNT_NAME, Instances.ACCOUNT_TYPE, - Instances.DESCRIPTION }; - - /** - * An adapter to load the due date from the instances projection. This is used by most groupings - */ - public final static TimeFieldAdapter INSTANCE_DUE_ADAPTER = new TimeFieldAdapter(Instances.INSTANCE_DUE, Instances.TZ, Instances.IS_ALLDAY); - - /** - * An adapter to load the start date from the instances projection. This is used by most groupings - */ - public final static TimeFieldAdapter INSTANCE_START_ADAPTER = new TimeFieldAdapter(Instances.INSTANCE_START, Instances.TZ, Instances.IS_ALLDAY); - - /** - * The authority of the content provider. - */ - private final String mAuthority; - - /** - * The instance of the {@link ExpandableGroupDescriptor}. This is created on demand in a lazy manner. - */ - private ExpandableGroupDescriptor mDescriptorInstance; - - - public AbstractGroupingFactory(String authority) - { - mAuthority = authority; - } - - - /** - * Returns an {@link ExpandableChildDescriptor} for this grouping and the given authority. - * - * @param authority - * The authority. - * @return An {@link ExpandableChildDescriptor}. - */ - abstract ExpandableChildDescriptor makeExpandableChildDescriptor(String authority); - - - /** - * Returns an {@link ExpandableGroupDescriptor} for this grouping and the given authority. - * - * @param authority - * The authority. - * @return An {@link ExpandableGroupDescriptor}. - */ - abstract ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority); - - - /** - * Return an {@link ExpandableGroupDescriptor} for this grouping. - *

    - * This method is not synchronized because it's intended to be called from the main thread only. - *

    - * - * @return An {@link ExpandableGroupDescriptor}. - */ - public ExpandableGroupDescriptor getExpandableGroupDescriptor() - { - if (mDescriptorInstance == null) - { - mDescriptorInstance = makeExpandableGroupDescriptor(mAuthority); - } - return mDescriptorInstance; - } - - - public abstract int getId(); + /** + * The projection we use when we load instances. We don't need every detail of a task here. This is used by all groupings. + */ + public final static String[] INSTANCE_PROJECTION = new String[] { + Instances.INSTANCE_START, Instances.INSTANCE_DURATION, Instances.INSTANCE_DUE, + Instances.IS_ALLDAY, Instances.TZ, Instances.TITLE, Instances.LIST_COLOR, Instances.PRIORITY, Instances.LIST_ID, Instances.TASK_ID, Instances._ID, + Instances.STATUS, Instances.COMPLETED, Instances.IS_CLOSED, Instances.PERCENT_COMPLETE, Instances.ACCOUNT_NAME, Instances.ACCOUNT_TYPE, + Instances.DESCRIPTION }; + + /** + * An adapter to load the due date from the instances projection. This is used by most groupings + */ + public final static TimeFieldAdapter INSTANCE_DUE_ADAPTER = new TimeFieldAdapter(Instances.INSTANCE_DUE, Instances.TZ, Instances.IS_ALLDAY); + + /** + * An adapter to load the start date from the instances projection. This is used by most groupings + */ + public final static TimeFieldAdapter INSTANCE_START_ADAPTER = new TimeFieldAdapter(Instances.INSTANCE_START, Instances.TZ, Instances.IS_ALLDAY); + + /** + * The authority of the content provider. + */ + private final String mAuthority; + + /** + * The instance of the {@link ExpandableGroupDescriptor}. This is created on demand in a lazy manner. + */ + private ExpandableGroupDescriptor mDescriptorInstance; + + + public AbstractGroupingFactory(String authority) + { + mAuthority = authority; + } + + + /** + * Returns an {@link ExpandableChildDescriptor} for this grouping and the given authority. + * + * @param authority + * The authority. + * + * @return An {@link ExpandableChildDescriptor}. + */ + abstract ExpandableChildDescriptor makeExpandableChildDescriptor(String authority); + + /** + * Returns an {@link ExpandableGroupDescriptor} for this grouping and the given authority. + * + * @param authority + * The authority. + * + * @return An {@link ExpandableGroupDescriptor}. + */ + abstract ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority); + + + /** + * Return an {@link ExpandableGroupDescriptor} for this grouping. + *

    + * This method is not synchronized because it's intended to be called from the main thread only. + *

    + * + * @return An {@link ExpandableGroupDescriptor}. + */ + public ExpandableGroupDescriptor getExpandableGroupDescriptor() + { + if (mDescriptorInstance == null) + { + mDescriptorInstance = makeExpandableGroupDescriptor(mAuthority); + } + return mDescriptorInstance; + } + + + public abstract int getId(); } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java index eb6b0243..82edec79 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java @@ -17,14 +17,6 @@ package org.dmfs.tasks.groupings; -import java.util.TimeZone; - -import org.dmfs.tasks.R; -import org.dmfs.tasks.model.TaskFieldAdapters; -import org.dmfs.tasks.utils.DateFormatter; -import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; -import org.dmfs.tasks.utils.ViewDescriptor; - import android.annotation.SuppressLint; import android.database.Cursor; import android.support.v4.util.SparseArrayCompat; @@ -35,163 +27,171 @@ import android.widget.FrameLayout.LayoutParams; import android.widget.ImageView; import android.widget.TextView; +import org.dmfs.tasks.R; +import org.dmfs.tasks.model.TaskFieldAdapters; +import org.dmfs.tasks.utils.DateFormatter; +import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; +import org.dmfs.tasks.utils.ViewDescriptor; + +import java.util.TimeZone; + /** * A base implementation of a {@link ViewDescriptor}. It has a number of commonly used methods. - * + * * @author Marten Gajda */ public abstract class BaseTaskViewDescriptor implements ViewDescriptor { - /** - * We use this to get the current time. - */ - protected Time mNow; - - - protected void setDueDate(TextView view, ImageView dueIcon, Time dueDate, boolean isClosed) - { - if (view != null && dueDate != null) - { - Time now = mNow; - if (now == null) - { - now = mNow = new Time(); - } - if (!now.timezone.equals(TimeZone.getDefault().getID())) - { - now.clear(TimeZone.getDefault().getID()); - } - - if (Math.abs(now.toMillis(false) - System.currentTimeMillis()) > 5000) - { - now.setToNow(); - now.normalize(true); - } - - dueDate.normalize(true); - - view.setText(new DateFormatter(view.getContext()).format(dueDate, now, DateFormatContext.LIST_VIEW)); - if (dueIcon != null) - { - dueIcon.setVisibility(View.VISIBLE); - } - - // highlight overdue dates & times, handle allDay tasks separately - if ((!dueDate.allDay && dueDate.before(now) || dueDate.allDay - && (dueDate.year < now.year || dueDate.yearDay <= now.yearDay && dueDate.year == now.year)) - && !isClosed) - { - view.setTextAppearance(view.getContext(), R.style.task_list_overdue_text); - } - else - { - view.setTextAppearance(view.getContext(), R.style.task_list_due_text); - } - } - else if (view != null) - { - view.setText(""); - if (dueIcon != null) - { - dueIcon.setVisibility(View.GONE); - } - } - } - - - protected void setOverlay(View view, int position, int count) - { - View overlayTop = getView(view, R.id.overlay_top); - View overlayBottom = getView(view, R.id.overlay_bottom); - - if (overlayTop != null) - { - overlayTop.setVisibility(position == 0 ? View.VISIBLE : View.GONE); - } - - if (overlayBottom != null) - { - overlayBottom.setVisibility(position == count - 1 ? View.VISIBLE : View.GONE); - } - } - - - protected void setDescription(View view, Cursor cursor) - { - String description = TaskFieldAdapters.DESCRIPTION.get(cursor); - TextView descriptionView = (TextView) getView(view, android.R.id.text1); - if (TextUtils.isEmpty(description)) - { - descriptionView.setVisibility(View.GONE); - } - else - { - descriptionView.setVisibility(View.VISIBLE); - if (description.length() > 150) - { - description = description.substring(0, 150); - } - descriptionView.setText(description); - } - } - - - protected void setColorBar(View view, Cursor cursor) - { - View colorbar = getView(view, R.id.colorbar); - if (colorbar != null) - { - colorbar.setBackgroundColor(TaskFieldAdapters.LIST_COLOR.get(cursor)); - } - } - - - @SuppressLint("NewApi") - protected void resetFlingView(View view) - { - View flingContentView = getView(view, getFlingContentViewId()); - if (flingContentView == null) - { - flingContentView = view; - } - - if (android.os.Build.VERSION.SDK_INT >= 14) - { - if (flingContentView.getTranslationX() != 0) - { - flingContentView.setTranslationX(0); - flingContentView.setAlpha(1); - } - } - else - { - LayoutParams layoutParams = (LayoutParams) flingContentView.getLayoutParams(); - if (layoutParams.leftMargin != 0 || layoutParams.rightMargin != 0) - { - layoutParams.setMargins(0, layoutParams.topMargin, 0, layoutParams.bottomMargin); - flingContentView.setLayoutParams(layoutParams); - } - } - } - - - protected T getView(View view, int viewId) - { - SparseArrayCompat viewHolder = (SparseArrayCompat) view.getTag(); - if (viewHolder == null) - { - viewHolder = new SparseArrayCompat(); - view.setTag(viewHolder); - } - View res = viewHolder.get(viewId); - if (res == null) - { - res = view.findViewById(viewId); - viewHolder.put(viewId, res); - } - return (T) res; - } + /** + * We use this to get the current time. + */ + protected Time mNow; + + + protected void setDueDate(TextView view, ImageView dueIcon, Time dueDate, boolean isClosed) + { + if (view != null && dueDate != null) + { + Time now = mNow; + if (now == null) + { + now = mNow = new Time(); + } + if (!now.timezone.equals(TimeZone.getDefault().getID())) + { + now.clear(TimeZone.getDefault().getID()); + } + + if (Math.abs(now.toMillis(false) - System.currentTimeMillis()) > 5000) + { + now.setToNow(); + now.normalize(true); + } + + dueDate.normalize(true); + + view.setText(new DateFormatter(view.getContext()).format(dueDate, now, DateFormatContext.LIST_VIEW)); + if (dueIcon != null) + { + dueIcon.setVisibility(View.VISIBLE); + } + + // highlight overdue dates & times, handle allDay tasks separately + if ((!dueDate.allDay && dueDate.before(now) || dueDate.allDay + && (dueDate.year < now.year || dueDate.yearDay <= now.yearDay && dueDate.year == now.year)) + && !isClosed) + { + view.setTextAppearance(view.getContext(), R.style.task_list_overdue_text); + } + else + { + view.setTextAppearance(view.getContext(), R.style.task_list_due_text); + } + } + else if (view != null) + { + view.setText(""); + if (dueIcon != null) + { + dueIcon.setVisibility(View.GONE); + } + } + } + + + protected void setOverlay(View view, int position, int count) + { + View overlayTop = getView(view, R.id.overlay_top); + View overlayBottom = getView(view, R.id.overlay_bottom); + + if (overlayTop != null) + { + overlayTop.setVisibility(position == 0 ? View.VISIBLE : View.GONE); + } + + if (overlayBottom != null) + { + overlayBottom.setVisibility(position == count - 1 ? View.VISIBLE : View.GONE); + } + } + + + protected void setDescription(View view, Cursor cursor) + { + String description = TaskFieldAdapters.DESCRIPTION.get(cursor); + TextView descriptionView = (TextView) getView(view, android.R.id.text1); + if (TextUtils.isEmpty(description)) + { + descriptionView.setVisibility(View.GONE); + } + else + { + descriptionView.setVisibility(View.VISIBLE); + if (description.length() > 150) + { + description = description.substring(0, 150); + } + descriptionView.setText(description); + } + } + + + protected void setColorBar(View view, Cursor cursor) + { + View colorbar = getView(view, R.id.colorbar); + if (colorbar != null) + { + colorbar.setBackgroundColor(TaskFieldAdapters.LIST_COLOR.get(cursor)); + } + } + + + @SuppressLint("NewApi") + protected void resetFlingView(View view) + { + View flingContentView = getView(view, getFlingContentViewId()); + if (flingContentView == null) + { + flingContentView = view; + } + + if (android.os.Build.VERSION.SDK_INT >= 14) + { + if (flingContentView.getTranslationX() != 0) + { + flingContentView.setTranslationX(0); + flingContentView.setAlpha(1); + } + } + else + { + LayoutParams layoutParams = (LayoutParams) flingContentView.getLayoutParams(); + if (layoutParams.leftMargin != 0 || layoutParams.rightMargin != 0) + { + layoutParams.setMargins(0, layoutParams.topMargin, 0, layoutParams.bottomMargin); + flingContentView.setLayoutParams(layoutParams); + } + } + } + + + protected T getView(View view, int viewId) + { + SparseArrayCompat viewHolder = (SparseArrayCompat) view.getTag(); + if (viewHolder == null) + { + viewHolder = new SparseArrayCompat(); + view.setTag(viewHolder); + } + View res = viewHolder.get(viewId); + if (res == null) + { + res = view.findViewById(viewId); + viewHolder.put(viewId, res); + } + return (T) res; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java index 4c81cc24..e28c317e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java @@ -17,7 +17,16 @@ package org.dmfs.tasks.groupings; -import java.text.DateFormatSymbols; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Paint; +import android.os.Build; +import android.os.Build.VERSION; +import android.view.View; +import android.widget.BaseExpandableListAdapter; +import android.widget.TextView; import org.dmfs.provider.tasks.TaskContract.Instances; import org.dmfs.tasks.R; @@ -30,21 +39,12 @@ import org.dmfs.tasks.utils.ExpandableGroupDescriptor; import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; import org.dmfs.tasks.utils.ViewDescriptor; -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Paint; -import android.os.Build; -import android.os.Build.VERSION; -import android.view.View; -import android.widget.BaseExpandableListAdapter; -import android.widget.TextView; +import java.text.DateFormatSymbols; /** * Definition of the by-due grouping. - * + *

    *

    * TODO: refactor! *

    @@ -54,282 +54,283 @@ import android.widget.TextView; *

    * TODO: also, don't forget to refactor! *

    - * + *

    * The plan is to provide some kind of GroupingDescriptior that provides the {@link ExpandableGroupDescriptorAdapter}, a name and a set of filters. Also it * should take care of persisting and restoring the open groups, selected filters ... - * + * * @author Marten Gajda * @author Tobias Reinsch */ public class ByDueDate extends AbstractGroupingFactory { - /** - * A {@link ViewDescriptor} that knows how to present the tasks in the task list. - */ - public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() - { - - private int mFlingContentViewId = R.id.flingContentView; - private int mFlingRevealLeftViewId = R.id.fling_reveal_left; - private int mFlingRevealRightViewId = R.id.fling_reveal_right; - - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - TextView title = getView(view, android.R.id.title); - boolean isClosed = cursor.getInt(13) > 0; - - resetFlingView(view); - - if (title != null) - { - String text = cursor.getString(5); - title.setText(text); - if (isClosed) - { - title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } - else - { - title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); - } - } - - setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - - View divider = getView(view, R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); - } - - // display priority - int priority = TaskFieldAdapters.PRIORITY.get(cursor); - View priorityView = getView(view, R.id.task_priority_view_medium); - priorityView.setBackgroundResource(android.R.color.transparent); - priorityView.setVisibility(View.VISIBLE); - - if (priority > 0 && priority < 5) - { - priorityView.setBackgroundResource(R.color.priority_red); - } - if (priority == 5) - { - priorityView.setBackgroundResource(R.color.priority_yellow); - } - if (priority > 5 && priority <= 9) - { - priorityView.setBackgroundResource(R.color.priority_green); - } - if (VERSION.SDK_INT >= 11) - { - // update percentage background - View background = getView(view, R.id.percentage_background_view); - background.setPivotX(0); - Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); - if (percentComplete < 100) - { - background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); - background.setBackgroundResource(R.drawable.task_progress_background_shade); - } - else - { - background.setScaleX(1); - background.setBackgroundResource(R.drawable.complete_task_background_overlay); - } - } - - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); - } - - - @Override - public int getView() - { - return R.layout.task_list_element; - } - - - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; - } - - - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; - } - }; - - /** - * A {@link ViewDescriptor} that knows how to present due date groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { - // DateFormatSymbols.getInstance() not used because it is not available before API level 9 - private final String[] mMonthNames = new DateFormatSymbols().getMonths(); - - - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - int position = cursor.getPosition(); - - // set list title - TextView title = (TextView) view.findViewById(android.R.id.title); - if (title != null) - { - title.setText(getTitle(cursor, view.getContext())); - } - - // set list elements - TextView text2 = (TextView) view.findViewById(android.R.id.text2); - int childrenCount = adapter.getChildrenCount(position); - if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) - { - Resources res = view.getContext().getResources(); - text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); - - } - - // show/hide divider - View divider = view.findViewById(R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); - } - - View colorbar = view.findViewById(R.id.colorbar1); - if (colorbar != null) - { - colorbar.setVisibility(View.GONE); - } - } - - - @Override - public int getView() - { - return R.layout.task_list_group_single_line; - } - - - /** - * Return the title of a due date group. - * - * @param cursor - * A {@link Cursor} pointing to the current group. - * @return A {@link String} with the group name. - */ - private String getTitle(Cursor cursor, Context context) - { - int type = cursor.getInt(cursor.getColumnIndex(TimeRangeCursorFactory.RANGE_TYPE)); - if (type == 0) - { - return context.getString(R.string.task_group_no_due); - } - if ((type & TimeRangeCursorFactory.TYPE_END_OF_TODAY) == TimeRangeCursorFactory.TYPE_END_OF_TODAY) - { - return context.getString(R.string.task_group_due_today); - } - if ((type & TimeRangeCursorFactory.TYPE_END_OF_YESTERDAY) == TimeRangeCursorFactory.TYPE_END_OF_YESTERDAY) - { - return context.getString(R.string.task_group_overdue); - } - if ((type & TimeRangeCursorFactory.TYPE_END_OF_TOMORROW) == TimeRangeCursorFactory.TYPE_END_OF_TOMORROW) - { - return context.getString(R.string.task_group_due_tomorrow); - } - if ((type & TimeRangeCursorFactory.TYPE_END_IN_7_DAYS) == TimeRangeCursorFactory.TYPE_END_IN_7_DAYS) - { - return context.getString(R.string.task_group_due_within_7_days); - } - if ((type & TimeRangeCursorFactory.TYPE_END_OF_A_MONTH) != 0) - { - return context.getString(R.string.task_group_due_in_month, - mMonthNames[cursor.getInt(cursor.getColumnIndex(TimeRangeCursorFactory.RANGE_MONTH))]); - } - if ((type & TimeRangeCursorFactory.TYPE_END_OF_A_YEAR) != 0) - { - return context.getString(R.string.task_group_due_in_year, cursor.getInt(cursor.getColumnIndex(TimeRangeCursorFactory.RANGE_YEAR))); - } - if ((type & TimeRangeCursorFactory.TYPE_NO_END) != 0) - { - return context.getString(R.string.task_group_due_in_future); - } - return ""; - } - - - @Override - public int getFlingContentViewId() - { - return -1; - } - - - @Override - public int getFlingRevealLeftViewId() - { - // TODO Auto-generated method stub - return 0; - } - - - @Override - public int getFlingRevealRightViewId() - { - // TODO Auto-generated method stub - return 0; - } - - }; - - - public ByDueDate(String authority) - { - super(authority); - } - - - @Override - ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) - { - return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and (" + Instances.IS_ALLDAY - + "=0 and (((" + Instances.INSTANCE_DUE + ">=?) and (" + Instances.INSTANCE_DUE + "=? or " - + Instances.INSTANCE_DUE + " is ?) and ? is null))" + "or " + Instances.IS_ALLDAY + "=1 and (((" + Instances.INSTANCE_DUE + ">=?+?) and (" - + Instances.INSTANCE_DUE + "=?+? or " + Instances.INSTANCE_DUE + " is ?) and ? is null)))", - Instances.DEFAULT_SORT_ORDER, 0, 1, 0, 1, 1, 0, 9, 1, 10, 0, 9, 1, 1).setViewDescriptor(TASK_VIEW_DESCRIPTOR); - } - - - @Override - ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) - { - return new ExpandableGroupDescriptor(new TimeRangeCursorLoaderFactory(TimeRangeShortCursorFactory.DEFAULT_PROJECTION), - makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); - } - - - @Override - public int getId() - { - return R.id.task_group_by_due; - } + /** + * A {@link ViewDescriptor} that knows how to present the tasks in the task list. + */ + public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() + { + + private int mFlingContentViewId = R.id.flingContentView; + private int mFlingRevealLeftViewId = R.id.fling_reveal_left; + private int mFlingRevealRightViewId = R.id.fling_reveal_right; + + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + TextView title = getView(view, android.R.id.title); + boolean isClosed = cursor.getInt(13) > 0; + + resetFlingView(view); + + if (title != null) + { + String text = cursor.getString(5); + title.setText(text); + if (isClosed) + { + title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } + else + { + title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); + } + } + + setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + + View divider = getView(view, R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); + } + + // display priority + int priority = TaskFieldAdapters.PRIORITY.get(cursor); + View priorityView = getView(view, R.id.task_priority_view_medium); + priorityView.setBackgroundResource(android.R.color.transparent); + priorityView.setVisibility(View.VISIBLE); + + if (priority > 0 && priority < 5) + { + priorityView.setBackgroundResource(R.color.priority_red); + } + if (priority == 5) + { + priorityView.setBackgroundResource(R.color.priority_yellow); + } + if (priority > 5 && priority <= 9) + { + priorityView.setBackgroundResource(R.color.priority_green); + } + if (VERSION.SDK_INT >= 11) + { + // update percentage background + View background = getView(view, R.id.percentage_background_view); + background.setPivotX(0); + Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); + if (percentComplete < 100) + { + background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); + background.setBackgroundResource(R.drawable.task_progress_background_shade); + } + else + { + background.setScaleX(1); + background.setBackgroundResource(R.drawable.complete_task_background_overlay); + } + } + + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); + } + + + @Override + public int getView() + { + return R.layout.task_list_element; + } + + + @Override + public int getFlingContentViewId() + { + return mFlingContentViewId; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return mFlingRevealLeftViewId; + } + + + @Override + public int getFlingRevealRightViewId() + { + return mFlingRevealRightViewId; + } + }; + + /** + * A {@link ViewDescriptor} that knows how to present due date groups. + */ + public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() + { + // DateFormatSymbols.getInstance() not used because it is not available before API level 9 + private final String[] mMonthNames = new DateFormatSymbols().getMonths(); + + + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + int position = cursor.getPosition(); + + // set list title + TextView title = (TextView) view.findViewById(android.R.id.title); + if (title != null) + { + title.setText(getTitle(cursor, view.getContext())); + } + + // set list elements + TextView text2 = (TextView) view.findViewById(android.R.id.text2); + int childrenCount = adapter.getChildrenCount(position); + if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) + { + Resources res = view.getContext().getResources(); + text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); + + } + + // show/hide divider + View divider = view.findViewById(R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); + } + + View colorbar = view.findViewById(R.id.colorbar1); + if (colorbar != null) + { + colorbar.setVisibility(View.GONE); + } + } + + + @Override + public int getView() + { + return R.layout.task_list_group_single_line; + } + + + /** + * Return the title of a due date group. + * + * @param cursor + * A {@link Cursor} pointing to the current group. + * + * @return A {@link String} with the group name. + */ + private String getTitle(Cursor cursor, Context context) + { + int type = cursor.getInt(cursor.getColumnIndex(TimeRangeCursorFactory.RANGE_TYPE)); + if (type == 0) + { + return context.getString(R.string.task_group_no_due); + } + if ((type & TimeRangeCursorFactory.TYPE_END_OF_TODAY) == TimeRangeCursorFactory.TYPE_END_OF_TODAY) + { + return context.getString(R.string.task_group_due_today); + } + if ((type & TimeRangeCursorFactory.TYPE_END_OF_YESTERDAY) == TimeRangeCursorFactory.TYPE_END_OF_YESTERDAY) + { + return context.getString(R.string.task_group_overdue); + } + if ((type & TimeRangeCursorFactory.TYPE_END_OF_TOMORROW) == TimeRangeCursorFactory.TYPE_END_OF_TOMORROW) + { + return context.getString(R.string.task_group_due_tomorrow); + } + if ((type & TimeRangeCursorFactory.TYPE_END_IN_7_DAYS) == TimeRangeCursorFactory.TYPE_END_IN_7_DAYS) + { + return context.getString(R.string.task_group_due_within_7_days); + } + if ((type & TimeRangeCursorFactory.TYPE_END_OF_A_MONTH) != 0) + { + return context.getString(R.string.task_group_due_in_month, + mMonthNames[cursor.getInt(cursor.getColumnIndex(TimeRangeCursorFactory.RANGE_MONTH))]); + } + if ((type & TimeRangeCursorFactory.TYPE_END_OF_A_YEAR) != 0) + { + return context.getString(R.string.task_group_due_in_year, cursor.getInt(cursor.getColumnIndex(TimeRangeCursorFactory.RANGE_YEAR))); + } + if ((type & TimeRangeCursorFactory.TYPE_NO_END) != 0) + { + return context.getString(R.string.task_group_due_in_future); + } + return ""; + } + + + @Override + public int getFlingContentViewId() + { + return -1; + } + + + @Override + public int getFlingRevealLeftViewId() + { + // TODO Auto-generated method stub + return 0; + } + + + @Override + public int getFlingRevealRightViewId() + { + // TODO Auto-generated method stub + return 0; + } + + }; + + + public ByDueDate(String authority) + { + super(authority); + } + + + @Override + ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) + { + return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and (" + Instances.IS_ALLDAY + + "=0 and (((" + Instances.INSTANCE_DUE + ">=?) and (" + Instances.INSTANCE_DUE + "=? or " + + Instances.INSTANCE_DUE + " is ?) and ? is null))" + "or " + Instances.IS_ALLDAY + "=1 and (((" + Instances.INSTANCE_DUE + ">=?+?) and (" + + Instances.INSTANCE_DUE + "=?+? or " + Instances.INSTANCE_DUE + " is ?) and ? is null)))", + Instances.DEFAULT_SORT_ORDER, 0, 1, 0, 1, 1, 0, 9, 1, 10, 0, 9, 1, 1).setViewDescriptor(TASK_VIEW_DESCRIPTOR); + } + + + @Override + ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) + { + return new ExpandableGroupDescriptor(new TimeRangeCursorLoaderFactory(TimeRangeShortCursorFactory.DEFAULT_PROJECTION), + makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); + } + + + @Override + public int getId() + { + return R.id.task_group_by_due; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java index 9ee13ef1..d6863e05 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java @@ -17,17 +17,6 @@ package org.dmfs.tasks.groupings; -import org.dmfs.provider.tasks.TaskContract.Instances; -import org.dmfs.provider.tasks.TaskContract.TaskLists; -import org.dmfs.tasks.QuickAddDialogFragment; -import org.dmfs.tasks.R; -import org.dmfs.tasks.groupings.cursorloaders.CursorLoaderFactory; -import org.dmfs.tasks.model.TaskFieldAdapters; -import org.dmfs.tasks.utils.ExpandableChildDescriptor; -import org.dmfs.tasks.utils.ExpandableGroupDescriptor; -import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; -import org.dmfs.tasks.utils.ViewDescriptor; - import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; @@ -40,10 +29,21 @@ import android.view.View.OnClickListener; import android.widget.BaseExpandableListAdapter; import android.widget.TextView; +import org.dmfs.provider.tasks.TaskContract.Instances; +import org.dmfs.provider.tasks.TaskContract.TaskLists; +import org.dmfs.tasks.QuickAddDialogFragment; +import org.dmfs.tasks.R; +import org.dmfs.tasks.groupings.cursorloaders.CursorLoaderFactory; +import org.dmfs.tasks.model.TaskFieldAdapters; +import org.dmfs.tasks.utils.ExpandableChildDescriptor; +import org.dmfs.tasks.utils.ExpandableGroupDescriptor; +import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; +import org.dmfs.tasks.utils.ViewDescriptor; + /** * Definition of the by-list grouping. - * + *

    *

    * TODO: refactor! *

    @@ -53,308 +53,312 @@ import android.widget.TextView; *

    * TODO: also, don't forget to refactor! *

    - * + *

    * The plan is to provide some kind of GroupingDescriptior that provides the {@link ExpandableGroupDescriptorAdapter}, a name and a set of filters. Also it * should take care of persisting and restoring the open groups, selected filters ... - * + * * @author Marten Gajda */ @TargetApi(11) public class ByList extends AbstractGroupingFactory { - /** - * A {@link ViewDescriptor} that knows how to present the tasks in the task list. - */ - public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() - { - private int mFlingContentViewId = R.id.flingContentView; - private int mFlingRevealLeftViewId = R.id.fling_reveal_left; - private int mFlingRevealRightViewId = R.id.fling_reveal_right; - - - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - TextView title = (TextView) getView(view, android.R.id.title); - boolean isClosed = cursor.getInt(13) > 0; - - resetFlingView(view); - - if (title != null) - { - String text = cursor.getString(5); - title.setText(text); - if (isClosed) - { - title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } - else - { - title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); - } - } - - setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - - View divider = getView(view, R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); - } - - // display priority - int priority = TaskFieldAdapters.PRIORITY.get(cursor); - View priorityView = getView(view, R.id.task_priority_view_medium); - priorityView.setBackgroundResource(android.R.color.transparent); - priorityView.setVisibility(View.VISIBLE); - - if (priority > 0 && priority < 5) - { - priorityView.setBackgroundResource(R.color.priority_red); - } - if (priority == 5) - { - priorityView.setBackgroundResource(R.color.priority_yellow); - } - if (priority > 5 && priority <= 9) - { - priorityView.setBackgroundResource(R.color.priority_green); - } - - if (VERSION.SDK_INT >= 11) - { - // update percentage background - View background = getView(view, R.id.percentage_background_view); - background.setPivotX(0); - Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); - if (percentComplete < 100) - { - background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); - background.setBackgroundResource(R.drawable.task_progress_background_shade); - } - else - { - background.setScaleX(1); - background.setBackgroundResource(R.drawable.complete_task_background_overlay); - } - } - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); - } - - - @Override - public int getView() - { - return R.layout.task_list_element; - } - - - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; - } - - - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; - } - }; - - /** - * A {@link ViewDescriptor} that knows how to present list groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { - - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - int position = cursor.getPosition(); - - // set list title - TextView title = (TextView) view.findViewById(android.R.id.title); - if (title != null) - { - title.setText(getTitle(cursor, view.getContext())); - } - - // set list account - TextView text1 = (TextView) view.findViewById(android.R.id.text1); - if (text1 != null) - { - text1.setText(cursor.getString(3)); - } - - // set list elements - TextView text2 = (TextView) view.findViewById(android.R.id.text2); - int childrenCount = adapter.getChildrenCount(position); - if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) - { - Resources res = view.getContext().getResources(); - - text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); - } - - // show/hide divider - View divider = view.findViewById(R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); - } - - View colorbar1 = view.findViewById(R.id.colorbar1); - View colorbar2 = view.findViewById(R.id.colorbar2); - View quickAddTask = view.findViewById(R.id.quick_add_task); - if (quickAddTask != null) - { - quickAddTask.setOnClickListener(quickAddClickListener); - quickAddTask.setTag(cursor.getLong(cursor.getColumnIndex(TaskLists._ID))); - } - - if ((flags & FLAG_IS_EXPANDED) != 0) - { - if (colorbar1 != null) - { - colorbar1.setBackgroundColor(TaskFieldAdapters.LIST_COLOR.get(cursor)); - colorbar1.setVisibility(View.VISIBLE); - } - if (colorbar2 != null) - { - colorbar2.setVisibility(View.GONE); - } - - // show quick add and hide task count - if (quickAddTask != null) - { - quickAddTask.setVisibility(View.VISIBLE); - } - if (text2 != null) - { - text2.setVisibility(View.GONE); - } - } - else - { - if (colorbar1 != null) - { - colorbar1.setVisibility(View.INVISIBLE); - } - if (colorbar2 != null) - { - colorbar2.setBackgroundColor(TaskFieldAdapters.LIST_COLOR.get(cursor)); - colorbar2.setVisibility(View.VISIBLE); - } - - // hide quick add and show task count - if (quickAddTask != null) - { - quickAddTask.setVisibility(View.GONE); - } - if (text2 != null) - { - text2.setVisibility(View.VISIBLE); - } - } - } - - private final OnClickListener quickAddClickListener = new OnClickListener() - { - - @Override - public void onClick(View v) - { - Long tag = (Long) v.getTag(); - if (tag != null) - { - QuickAddDialogFragment.newInstance(tag).show(((FragmentActivity) v.getContext()).getSupportFragmentManager(), null); - } - } - }; - - - @Override - public int getView() - { - return R.layout.task_list_group; - } - - - /** - * Return the title of a list group. - * - * @param cursor - * A {@link Cursor} pointing to the current group. - * @return A {@link String} with the group name. - */ - private String getTitle(Cursor cursor, Context context) - { - return cursor.getString(1); - } - - - @Override - public int getFlingContentViewId() - { - return -1; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return -1; - } - - - @Override - public int getFlingRevealRightViewId() - { - return -1; - } - - }; - - - public ByList(String authority) - { - super(authority); - } - - - @Override - public ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) - { - return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and " + Instances.LIST_ID + "=?", - Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING + ", " + Instances.TITLE + " COLLATE NOCASE ASC", 0) - .setViewDescriptor(TASK_VIEW_DESCRIPTOR); - } - - - @Override - public ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) - { - return new ExpandableGroupDescriptor(new CursorLoaderFactory(TaskLists.getContentUri(authority), new String[] { TaskLists._ID, TaskLists.LIST_NAME, - TaskLists.LIST_COLOR, TaskLists.ACCOUNT_NAME }, TaskLists.VISIBLE + ">0 and " + TaskLists.SYNC_ENABLED + ">0", null, TaskLists.ACCOUNT_NAME + ", " - + TaskLists.LIST_NAME), makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); - } - - - @Override - public int getId() - { - return R.id.task_group_by_list; - } + /** + * A {@link ViewDescriptor} that knows how to present the tasks in the task list. + */ + public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() + { + private int mFlingContentViewId = R.id.flingContentView; + private int mFlingRevealLeftViewId = R.id.fling_reveal_left; + private int mFlingRevealRightViewId = R.id.fling_reveal_right; + + + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + TextView title = (TextView) getView(view, android.R.id.title); + boolean isClosed = cursor.getInt(13) > 0; + + resetFlingView(view); + + if (title != null) + { + String text = cursor.getString(5); + title.setText(text); + if (isClosed) + { + title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } + else + { + title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); + } + } + + setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + + View divider = getView(view, R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); + } + + // display priority + int priority = TaskFieldAdapters.PRIORITY.get(cursor); + View priorityView = getView(view, R.id.task_priority_view_medium); + priorityView.setBackgroundResource(android.R.color.transparent); + priorityView.setVisibility(View.VISIBLE); + + if (priority > 0 && priority < 5) + { + priorityView.setBackgroundResource(R.color.priority_red); + } + if (priority == 5) + { + priorityView.setBackgroundResource(R.color.priority_yellow); + } + if (priority > 5 && priority <= 9) + { + priorityView.setBackgroundResource(R.color.priority_green); + } + + if (VERSION.SDK_INT >= 11) + { + // update percentage background + View background = getView(view, R.id.percentage_background_view); + background.setPivotX(0); + Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); + if (percentComplete < 100) + { + background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); + background.setBackgroundResource(R.drawable.task_progress_background_shade); + } + else + { + background.setScaleX(1); + background.setBackgroundResource(R.drawable.complete_task_background_overlay); + } + } + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); + } + + + @Override + public int getView() + { + return R.layout.task_list_element; + } + + + @Override + public int getFlingContentViewId() + { + return mFlingContentViewId; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return mFlingRevealLeftViewId; + } + + + @Override + public int getFlingRevealRightViewId() + { + return mFlingRevealRightViewId; + } + }; + + /** + * A {@link ViewDescriptor} that knows how to present list groups. + */ + public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() + { + + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + int position = cursor.getPosition(); + + // set list title + TextView title = (TextView) view.findViewById(android.R.id.title); + if (title != null) + { + title.setText(getTitle(cursor, view.getContext())); + } + + // set list account + TextView text1 = (TextView) view.findViewById(android.R.id.text1); + if (text1 != null) + { + text1.setText(cursor.getString(3)); + } + + // set list elements + TextView text2 = (TextView) view.findViewById(android.R.id.text2); + int childrenCount = adapter.getChildrenCount(position); + if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) + { + Resources res = view.getContext().getResources(); + + text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); + } + + // show/hide divider + View divider = view.findViewById(R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); + } + + View colorbar1 = view.findViewById(R.id.colorbar1); + View colorbar2 = view.findViewById(R.id.colorbar2); + View quickAddTask = view.findViewById(R.id.quick_add_task); + if (quickAddTask != null) + { + quickAddTask.setOnClickListener(quickAddClickListener); + quickAddTask.setTag(cursor.getLong(cursor.getColumnIndex(TaskLists._ID))); + } + + if ((flags & FLAG_IS_EXPANDED) != 0) + { + if (colorbar1 != null) + { + colorbar1.setBackgroundColor(TaskFieldAdapters.LIST_COLOR.get(cursor)); + colorbar1.setVisibility(View.VISIBLE); + } + if (colorbar2 != null) + { + colorbar2.setVisibility(View.GONE); + } + + // show quick add and hide task count + if (quickAddTask != null) + { + quickAddTask.setVisibility(View.VISIBLE); + } + if (text2 != null) + { + text2.setVisibility(View.GONE); + } + } + else + { + if (colorbar1 != null) + { + colorbar1.setVisibility(View.INVISIBLE); + } + if (colorbar2 != null) + { + colorbar2.setBackgroundColor(TaskFieldAdapters.LIST_COLOR.get(cursor)); + colorbar2.setVisibility(View.VISIBLE); + } + + // hide quick add and show task count + if (quickAddTask != null) + { + quickAddTask.setVisibility(View.GONE); + } + if (text2 != null) + { + text2.setVisibility(View.VISIBLE); + } + } + } + + + private final OnClickListener quickAddClickListener = new OnClickListener() + { + + @Override + public void onClick(View v) + { + Long tag = (Long) v.getTag(); + if (tag != null) + { + QuickAddDialogFragment.newInstance(tag).show(((FragmentActivity) v.getContext()).getSupportFragmentManager(), null); + } + } + }; + + + @Override + public int getView() + { + return R.layout.task_list_group; + } + + + /** + * Return the title of a list group. + * + * @param cursor + * A {@link Cursor} pointing to the current group. + * + * @return A {@link String} with the group name. + */ + private String getTitle(Cursor cursor, Context context) + { + return cursor.getString(1); + } + + + @Override + public int getFlingContentViewId() + { + return -1; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return -1; + } + + + @Override + public int getFlingRevealRightViewId() + { + return -1; + } + + }; + + + public ByList(String authority) + { + super(authority); + } + + + @Override + public ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) + { + return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and " + Instances.LIST_ID + "=?", + Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING + ", " + Instances.TITLE + " COLLATE NOCASE ASC", 0) + .setViewDescriptor(TASK_VIEW_DESCRIPTOR); + } + + + @Override + public ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) + { + return new ExpandableGroupDescriptor(new CursorLoaderFactory(TaskLists.getContentUri(authority), new String[] { + TaskLists._ID, TaskLists.LIST_NAME, + TaskLists.LIST_COLOR, TaskLists.ACCOUNT_NAME }, TaskLists.VISIBLE + ">0 and " + TaskLists.SYNC_ENABLED + ">0", null, + TaskLists.ACCOUNT_NAME + ", " + + TaskLists.LIST_NAME), makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); + } + + + @Override + public int getId() + { + return R.id.task_group_by_list; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java index 48213bc2..ba4cd8f9 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java @@ -17,6 +17,18 @@ package org.dmfs.tasks.groupings; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Paint; +import android.os.Build.VERSION; +import android.support.v4.app.FragmentActivity; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.BaseExpandableListAdapter; +import android.widget.TextView; + import org.dmfs.provider.tasks.TaskContract; import org.dmfs.provider.tasks.TaskContract.Instances; import org.dmfs.provider.tasks.TaskContract.Tasks; @@ -31,317 +43,307 @@ import org.dmfs.tasks.utils.ExpandableGroupDescriptor; import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; import org.dmfs.tasks.utils.ViewDescriptor; -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Paint; -import android.os.Build.VERSION; -import android.support.v4.app.FragmentActivity; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.BaseExpandableListAdapter; -import android.widget.TextView; - /** * Definition of the by-priority grouping. - * + * * @author Tobias Reinsch */ @TargetApi(11) public class ByPriority extends AbstractGroupingFactory { - /** - * A {@link ViewDescriptor} that knows how to present the tasks in the task list grouped by priority. - */ - public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() - { - - private int mFlingContentViewId = R.id.flingContentView; - private int mFlingRevealLeftViewId = R.id.fling_reveal_left; - private int mFlingRevealRightViewId = R.id.fling_reveal_right; - - - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - TextView title = getView(view, android.R.id.title); - boolean isClosed = cursor.getInt(13) > 0; - - resetFlingView(view); - - if (title != null) - { - String text = cursor.getString(5); - title.setText(text); - if (isClosed) - { - title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } - else - { - title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); - } - } - - setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - - View divider = getView(view, R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); - } - - // display priority - int priority = TaskFieldAdapters.PRIORITY.get(cursor); - View priorityView = getView(view, R.id.task_priority_view_medium); - priorityView.setBackgroundResource(android.R.color.transparent); - priorityView.setVisibility(View.VISIBLE); - - if (priority > 0 && priority < 5) - { - priorityView.setBackgroundResource(R.color.priority_red); - } - if (priority == 5) - { - priorityView.setBackgroundResource(R.color.priority_yellow); - } - if (priority > 5 && priority <= 9) - { - priorityView.setBackgroundResource(R.color.priority_green); - } - - if (VERSION.SDK_INT >= 11) - { - // update percentage background - View background = getView(view, R.id.percentage_background_view); - background.setPivotX(0); - Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); - if (percentComplete < 100) - { - background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); - background.setBackgroundResource(R.drawable.task_progress_background_shade); - } - else - { - background.setScaleX(1); - background.setBackgroundResource(R.drawable.complete_task_background_overlay); - } - } - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); - } - - - @Override - public int getView() - { - return R.layout.task_list_element; - } - - - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; - } - - - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; - } - }; - - /** - * A {@link ViewDescriptor} that knows how to present list groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { - - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - int position = cursor.getPosition(); - - // set list title - TextView title = (TextView) view.findViewById(android.R.id.title); - if (title != null) - { - title.setText(getTitle(cursor, view.getContext())); - } - - // set list elements - TextView text2 = (TextView) view.findViewById(android.R.id.text2); - int childrenCount = adapter.getChildrenCount(position); - if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) - { - Resources res = view.getContext().getResources(); - - text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); - } - - // show/hide divider - View divider = view.findViewById(R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); - } - - View colorbar1 = view.findViewById(R.id.colorbar1); - View colorbar2 = view.findViewById(R.id.colorbar2); - View quickAddTask = view.findViewById(R.id.quick_add_task); - if (quickAddTask != null) - { - quickAddTask.setOnClickListener(quickAddClickListener); - quickAddTask.setTag(cursor.getInt(2 /* max priority of this section */)); - } - - if ((flags & FLAG_IS_EXPANDED) != 0) - { - if (colorbar1 != null) - { - colorbar1.setBackgroundColor(cursor.getInt(2)); - colorbar1.setVisibility(View.VISIBLE); - } - if (colorbar2 != null) - { - colorbar2.setVisibility(View.GONE); - } - - // show quick add and hide task count - if (quickAddTask != null) - { - quickAddTask.setVisibility(View.VISIBLE); - } - if (text2 != null) - { - text2.setVisibility(View.GONE); - } - } - else - { - if (colorbar1 != null) - { - colorbar1.setVisibility(View.INVISIBLE); - } - if (colorbar2 != null) - { - colorbar2.setBackgroundColor(cursor.getInt(2)); - colorbar2.setVisibility(View.VISIBLE); - } - - // hide quick add and show task count - if (quickAddTask != null) - { - quickAddTask.setVisibility(View.GONE); - } - if (text2 != null) - { - text2.setVisibility(View.VISIBLE); - } - } - } - - private final OnClickListener quickAddClickListener = new OnClickListener() - { - - @Override - public void onClick(View v) - { - Integer tag = (Integer) v.getTag(); - if (tag != null) - { - ContentSet content = new ContentSet(Tasks.getContentUri(TaskContract.taskAuthority(v.getContext()))); - TaskFieldAdapters.PRIORITY.set(content, tag); - QuickAddDialogFragment.newInstance(content).show(((FragmentActivity) v.getContext()).getSupportFragmentManager(), null); - } - } - }; - - - @Override - public int getView() - { - return R.layout.task_list_group_single_line; - } - - - /** - * Return the title of the priority group. - * - * @param cursor - * A {@link Cursor} pointing to the current group. - * @return A {@link String} with the group name. - */ - private String getTitle(Cursor cursor, Context context) - { - return context.getString(cursor.getInt(cursor.getColumnIndex(PriorityCursorFactory.PRIORITY_TITLE_RES_ID))); - } - - - @Override - public int getFlingContentViewId() - { - return -1; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return -1; - } - - - @Override - public int getFlingRevealRightViewId() - { - return -1; - } - - }; - - - public ByPriority(String authority) - { - super(authority); - } - - - @Override - ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) - { - return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and (" + Instances.PRIORITY - + ">=? and " + Instances.PRIORITY + " <= ? or ? is null and " + Instances.PRIORITY + " <= ? or " + Instances.PRIORITY + " is ?)", - Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING + ", " + Instances.TITLE + " COLLATE NOCASE ASC", 1, 2, 1, 2, 1) - .setViewDescriptor(TASK_VIEW_DESCRIPTOR); - } - - - @Override - ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) - { - return new ExpandableGroupDescriptor(new PriorityCursorLoaderFactory(PriorityCursorFactory.DEFAULT_PROJECTION), - makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); - } - - - @Override - public int getId() - { - return R.id.task_group_by_priority; - } + /** + * A {@link ViewDescriptor} that knows how to present the tasks in the task list grouped by priority. + */ + public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() + { + + private int mFlingContentViewId = R.id.flingContentView; + private int mFlingRevealLeftViewId = R.id.fling_reveal_left; + private int mFlingRevealRightViewId = R.id.fling_reveal_right; + + + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + TextView title = getView(view, android.R.id.title); + boolean isClosed = cursor.getInt(13) > 0; + + resetFlingView(view); + + if (title != null) + { + String text = cursor.getString(5); + title.setText(text); + if (isClosed) + { + title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } + else + { + title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); + } + } + + setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + + View divider = getView(view, R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); + } + + // display priority + int priority = TaskFieldAdapters.PRIORITY.get(cursor); + View priorityView = getView(view, R.id.task_priority_view_medium); + priorityView.setBackgroundResource(android.R.color.transparent); + priorityView.setVisibility(View.VISIBLE); + + if (priority > 0 && priority < 5) + { + priorityView.setBackgroundResource(R.color.priority_red); + } + if (priority == 5) + { + priorityView.setBackgroundResource(R.color.priority_yellow); + } + if (priority > 5 && priority <= 9) + { + priorityView.setBackgroundResource(R.color.priority_green); + } + + if (VERSION.SDK_INT >= 11) + { + // update percentage background + View background = getView(view, R.id.percentage_background_view); + background.setPivotX(0); + Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); + if (percentComplete < 100) + { + background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); + background.setBackgroundResource(R.drawable.task_progress_background_shade); + } + else + { + background.setScaleX(1); + background.setBackgroundResource(R.drawable.complete_task_background_overlay); + } + } + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); + } + + + @Override + public int getView() + { + return R.layout.task_list_element; + } + + + @Override + public int getFlingContentViewId() + { + return mFlingContentViewId; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return mFlingRevealLeftViewId; + } + + + @Override + public int getFlingRevealRightViewId() + { + return mFlingRevealRightViewId; + } + }; + + /** + * A {@link ViewDescriptor} that knows how to present list groups. + */ + public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() + { + + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + int position = cursor.getPosition(); + + // set list title + TextView title = (TextView) view.findViewById(android.R.id.title); + if (title != null) + { + title.setText(getTitle(cursor, view.getContext())); + } + + // set list elements + TextView text2 = (TextView) view.findViewById(android.R.id.text2); + int childrenCount = adapter.getChildrenCount(position); + if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) + { + Resources res = view.getContext().getResources(); + + text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); + } + + // show/hide divider + View divider = view.findViewById(R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); + } + + View colorbar1 = view.findViewById(R.id.colorbar1); + View colorbar2 = view.findViewById(R.id.colorbar2); + View quickAddTask = view.findViewById(R.id.quick_add_task); + if (quickAddTask != null) + { + quickAddTask.setOnClickListener(quickAddClickListener); + quickAddTask.setTag(cursor.getInt(2 /* max priority of this section */)); + } + + if ((flags & FLAG_IS_EXPANDED) != 0) + { + if (colorbar1 != null) + { + colorbar1.setBackgroundColor(cursor.getInt(2)); + colorbar1.setVisibility(View.VISIBLE); + } + if (colorbar2 != null) + { + colorbar2.setVisibility(View.GONE); + } + + // show quick add and hide task count + if (quickAddTask != null) + { + quickAddTask.setVisibility(View.VISIBLE); + } + if (text2 != null) + { + text2.setVisibility(View.GONE); + } + } + else + { + if (colorbar1 != null) + { + colorbar1.setVisibility(View.INVISIBLE); + } + if (colorbar2 != null) + { + colorbar2.setBackgroundColor(cursor.getInt(2)); + colorbar2.setVisibility(View.VISIBLE); + } + + // hide quick add and show task count + if (quickAddTask != null) + { + quickAddTask.setVisibility(View.GONE); + } + if (text2 != null) + { + text2.setVisibility(View.VISIBLE); + } + } + } + + + private final OnClickListener quickAddClickListener = new OnClickListener() + { + + @Override + public void onClick(View v) + { + Integer tag = (Integer) v.getTag(); + if (tag != null) + { + ContentSet content = new ContentSet(Tasks.getContentUri(TaskContract.taskAuthority(v.getContext()))); + TaskFieldAdapters.PRIORITY.set(content, tag); + QuickAddDialogFragment.newInstance(content).show(((FragmentActivity) v.getContext()).getSupportFragmentManager(), null); + } + } + }; + + + @Override + public int getView() + { + return R.layout.task_list_group_single_line; + } + + + /** + * Return the title of the priority group. + * + * @param cursor + * A {@link Cursor} pointing to the current group. + * + * @return A {@link String} with the group name. + */ + private String getTitle(Cursor cursor, Context context) + { + return context.getString(cursor.getInt(cursor.getColumnIndex(PriorityCursorFactory.PRIORITY_TITLE_RES_ID))); + } + + + @Override + public int getFlingContentViewId() + { + return -1; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return -1; + } + + + @Override + public int getFlingRevealRightViewId() + { + return -1; + } + + }; + + + public ByPriority(String authority) + { + super(authority); + } + + + @Override + ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) + { + return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and (" + Instances.PRIORITY + + ">=? and " + Instances.PRIORITY + " <= ? or ? is null and " + Instances.PRIORITY + " <= ? or " + Instances.PRIORITY + " is ?)", + Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING + ", " + Instances.TITLE + " COLLATE NOCASE ASC", 1, 2, 1, 2, 1) + .setViewDescriptor(TASK_VIEW_DESCRIPTOR); + } + + + @Override + ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) + { + return new ExpandableGroupDescriptor(new PriorityCursorLoaderFactory(PriorityCursorFactory.DEFAULT_PROJECTION), + makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); + } + + + @Override + public int getId() + { + return R.id.task_group_by_priority; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java index 81187a3b..63eb015a 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java @@ -17,16 +17,6 @@ package org.dmfs.tasks.groupings; -import org.dmfs.provider.tasks.TaskContract.Instances; -import org.dmfs.tasks.R; -import org.dmfs.tasks.groupings.cursorloaders.ProgressCursorFactory; -import org.dmfs.tasks.groupings.cursorloaders.ProgressCursorLoaderFactory; -import org.dmfs.tasks.model.TaskFieldAdapters; -import org.dmfs.tasks.utils.ExpandableChildDescriptor; -import org.dmfs.tasks.utils.ExpandableGroupDescriptor; -import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; -import org.dmfs.tasks.utils.ViewDescriptor; - import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; @@ -37,262 +27,273 @@ import android.view.View; import android.widget.BaseExpandableListAdapter; import android.widget.TextView; +import org.dmfs.provider.tasks.TaskContract.Instances; +import org.dmfs.tasks.R; +import org.dmfs.tasks.groupings.cursorloaders.ProgressCursorFactory; +import org.dmfs.tasks.groupings.cursorloaders.ProgressCursorLoaderFactory; +import org.dmfs.tasks.model.TaskFieldAdapters; +import org.dmfs.tasks.utils.ExpandableChildDescriptor; +import org.dmfs.tasks.utils.ExpandableGroupDescriptor; +import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; +import org.dmfs.tasks.utils.ViewDescriptor; + /** * Definition of the by-progress grouping. - * + * * @author Tobias Reinsch */ @TargetApi(11) public class ByProgress extends AbstractGroupingFactory { - /** - * A {@link ViewDescriptor} that knows how to present the tasks in the task list grouped by progress. - */ - public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() - { - private int mFlingContentViewId = R.id.flingContentView; - private int mFlingRevealLeftViewId = R.id.fling_reveal_left; - private int mFlingRevealRightViewId = R.id.fling_reveal_right; - - - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - TextView title = getView(view, android.R.id.title); - boolean isClosed = cursor.getInt(13) > 0; - - resetFlingView(view); - - if (title != null) - { - String text = cursor.getString(5); - title.setText(text); - if (isClosed) - { - title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } - else - { - title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); - } - } - - setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - - View divider = getView(view, R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); - } - - // display priority - int priority = TaskFieldAdapters.PRIORITY.get(cursor); - View priorityView = getView(view, R.id.task_priority_view_medium); - priorityView.setBackgroundResource(android.R.color.transparent); - priorityView.setVisibility(View.VISIBLE); - - if (priority > 0 && priority < 5) - { - priorityView.setBackgroundResource(R.color.priority_red); - } - if (priority == 5) - { - priorityView.setBackgroundResource(R.color.priority_yellow); - } - if (priority > 5 && priority <= 9) - { - priorityView.setBackgroundResource(R.color.priority_green); - } - - if (VERSION.SDK_INT >= 11) - { - // update percentage background - View background = getView(view, R.id.percentage_background_view); - background.setPivotX(0); - Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); - if (percentComplete < 100) - { - background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); - background.setBackgroundResource(R.drawable.task_progress_background_shade); - } - else - { - background.setScaleX(1); - background.setBackgroundResource(R.drawable.complete_task_background_overlay); - } - } - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); - } - - - @Override - public int getView() - { - return R.layout.task_list_element; - } - - - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; - } - - - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; - } - }; - - /** - * A {@link ViewDescriptor} that knows how to present list groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { - - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - int position = cursor.getPosition(); - - // set list title - TextView title = (TextView) view.findViewById(android.R.id.title); - if (title != null) - { - title.setText(getTitle(cursor, view.getContext())); - } - - // set list elements - TextView text2 = (TextView) view.findViewById(android.R.id.text2); - int childrenCount = adapter.getChildrenCount(position); - if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) - { - Resources res = view.getContext().getResources(); - - text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); - } - - // show/hide divider - View divider = view.findViewById(R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); - } - - View colorbar1 = view.findViewById(R.id.colorbar1); - View colorbar2 = view.findViewById(R.id.colorbar2); - - if ((flags & FLAG_IS_EXPANDED) != 0) - { - if (colorbar1 != null) - { - colorbar1.setBackgroundColor(cursor.getInt(2)); - colorbar1.setVisibility(View.GONE); - } - if (colorbar2 != null) - { - colorbar2.setVisibility(View.GONE); - } - } - else - { - if (colorbar1 != null) - { - colorbar1.setVisibility(View.GONE); - } - if (colorbar2 != null) - { - colorbar2.setBackgroundColor(cursor.getInt(2)); - colorbar2.setVisibility(View.GONE); - } - } - } - - - @Override - public int getView() - { - return R.layout.task_list_group_single_line; - } - - - /** - * Return the title of the priority group. - * - * @param cursor - * A {@link Cursor} pointing to the current group. - * @return A {@link String} with the group name. - */ - private String getTitle(Cursor cursor, Context context) - { - return context.getString(cursor.getInt(cursor.getColumnIndex(ProgressCursorFactory.PROGRESS_TITLE_RES_ID))); - } - - - @Override - public int getFlingContentViewId() - { - return -1; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return -1; - } - - - @Override - public int getFlingRevealRightViewId() - { - return -1; - } - - }; - - - public ByProgress(String authority) - { - super(authority); - } - - - @Override - ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) - { - return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and (" - + Instances.PERCENT_COMPLETE + ">=? and " + Instances.PERCENT_COMPLETE + " <= ? or ? is null and " + Instances.PERCENT_COMPLETE + " <= ? or " - + Instances.PERCENT_COMPLETE + " is ?)", Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING + ", " + Instances.TITLE - + " COLLATE NOCASE ASC", 1, 2, 1, 2, 1).setViewDescriptor(TASK_VIEW_DESCRIPTOR); - } - - - @Override - ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) - { - return new ExpandableGroupDescriptor(new ProgressCursorLoaderFactory(ProgressCursorFactory.DEFAULT_PROJECTION), - makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); - } - - - @Override - public int getId() - { - return R.id.task_group_by_progress; - } + /** + * A {@link ViewDescriptor} that knows how to present the tasks in the task list grouped by progress. + */ + public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() + { + private int mFlingContentViewId = R.id.flingContentView; + private int mFlingRevealLeftViewId = R.id.fling_reveal_left; + private int mFlingRevealRightViewId = R.id.fling_reveal_right; + + + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + TextView title = getView(view, android.R.id.title); + boolean isClosed = cursor.getInt(13) > 0; + + resetFlingView(view); + + if (title != null) + { + String text = cursor.getString(5); + title.setText(text); + if (isClosed) + { + title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } + else + { + title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); + } + } + + setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + + View divider = getView(view, R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); + } + + // display priority + int priority = TaskFieldAdapters.PRIORITY.get(cursor); + View priorityView = getView(view, R.id.task_priority_view_medium); + priorityView.setBackgroundResource(android.R.color.transparent); + priorityView.setVisibility(View.VISIBLE); + + if (priority > 0 && priority < 5) + { + priorityView.setBackgroundResource(R.color.priority_red); + } + if (priority == 5) + { + priorityView.setBackgroundResource(R.color.priority_yellow); + } + if (priority > 5 && priority <= 9) + { + priorityView.setBackgroundResource(R.color.priority_green); + } + + if (VERSION.SDK_INT >= 11) + { + // update percentage background + View background = getView(view, R.id.percentage_background_view); + background.setPivotX(0); + Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); + if (percentComplete < 100) + { + background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); + background.setBackgroundResource(R.drawable.task_progress_background_shade); + } + else + { + background.setScaleX(1); + background.setBackgroundResource(R.drawable.complete_task_background_overlay); + } + } + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); + } + + + @Override + public int getView() + { + return R.layout.task_list_element; + } + + + @Override + public int getFlingContentViewId() + { + return mFlingContentViewId; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return mFlingRevealLeftViewId; + } + + + @Override + public int getFlingRevealRightViewId() + { + return mFlingRevealRightViewId; + } + }; + + /** + * A {@link ViewDescriptor} that knows how to present list groups. + */ + public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() + { + + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + int position = cursor.getPosition(); + + // set list title + TextView title = (TextView) view.findViewById(android.R.id.title); + if (title != null) + { + title.setText(getTitle(cursor, view.getContext())); + } + + // set list elements + TextView text2 = (TextView) view.findViewById(android.R.id.text2); + int childrenCount = adapter.getChildrenCount(position); + if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) + { + Resources res = view.getContext().getResources(); + + text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); + } + + // show/hide divider + View divider = view.findViewById(R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); + } + + View colorbar1 = view.findViewById(R.id.colorbar1); + View colorbar2 = view.findViewById(R.id.colorbar2); + + if ((flags & FLAG_IS_EXPANDED) != 0) + { + if (colorbar1 != null) + { + colorbar1.setBackgroundColor(cursor.getInt(2)); + colorbar1.setVisibility(View.GONE); + } + if (colorbar2 != null) + { + colorbar2.setVisibility(View.GONE); + } + } + else + { + if (colorbar1 != null) + { + colorbar1.setVisibility(View.GONE); + } + if (colorbar2 != null) + { + colorbar2.setBackgroundColor(cursor.getInt(2)); + colorbar2.setVisibility(View.GONE); + } + } + } + + + @Override + public int getView() + { + return R.layout.task_list_group_single_line; + } + + + /** + * Return the title of the priority group. + * + * @param cursor + * A {@link Cursor} pointing to the current group. + * + * @return A {@link String} with the group name. + */ + private String getTitle(Cursor cursor, Context context) + { + return context.getString(cursor.getInt(cursor.getColumnIndex(ProgressCursorFactory.PROGRESS_TITLE_RES_ID))); + } + + + @Override + public int getFlingContentViewId() + { + return -1; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return -1; + } + + + @Override + public int getFlingRevealRightViewId() + { + return -1; + } + + }; + + + public ByProgress(String authority) + { + super(authority); + } + + + @Override + ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) + { + return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and (" + + Instances.PERCENT_COMPLETE + ">=? and " + Instances.PERCENT_COMPLETE + " <= ? or ? is null and " + Instances.PERCENT_COMPLETE + " <= ? or " + + Instances.PERCENT_COMPLETE + " is ?)", Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING + ", " + Instances.TITLE + + " COLLATE NOCASE ASC", 1, 2, 1, 2, 1).setViewDescriptor(TASK_VIEW_DESCRIPTOR); + } + + + @Override + ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) + { + return new ExpandableGroupDescriptor(new ProgressCursorLoaderFactory(ProgressCursorFactory.DEFAULT_PROJECTION), + makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); + } + + + @Override + public int getId() + { + return R.id.task_group_by_progress; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java index f8bc9f1e..a81f610b 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java @@ -17,20 +17,6 @@ package org.dmfs.tasks.groupings; -import org.dmfs.provider.tasks.TaskContract.Instances; -import org.dmfs.provider.tasks.TaskContract.Tasks; -import org.dmfs.tasks.R; -import org.dmfs.tasks.groupings.cursorloaders.SearchHistoryCursorLoaderFactory; -import org.dmfs.tasks.model.TaskFieldAdapters; -import org.dmfs.tasks.utils.ExpandableChildDescriptor; -import org.dmfs.tasks.utils.ExpandableGroupDescriptor; -import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; -import org.dmfs.tasks.utils.SearchChildDescriptor; -import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper; -import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper.SearchHistoryColumns; -import org.dmfs.tasks.utils.SearchHistoryHelper; -import org.dmfs.tasks.utils.ViewDescriptor; - import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; @@ -46,340 +32,357 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import org.dmfs.provider.tasks.TaskContract.Instances; +import org.dmfs.provider.tasks.TaskContract.Tasks; +import org.dmfs.tasks.R; +import org.dmfs.tasks.groupings.cursorloaders.SearchHistoryCursorLoaderFactory; +import org.dmfs.tasks.model.TaskFieldAdapters; +import org.dmfs.tasks.utils.ExpandableChildDescriptor; +import org.dmfs.tasks.utils.ExpandableGroupDescriptor; +import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; +import org.dmfs.tasks.utils.SearchChildDescriptor; +import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper; +import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper.SearchHistoryColumns; +import org.dmfs.tasks.utils.SearchHistoryHelper; +import org.dmfs.tasks.utils.ViewDescriptor; + /** * Definition of the search history grouping. - * + * * @author Tobias Reinsch * @author Marten Gajda */ public class BySearch extends AbstractGroupingFactory { - /** - * A {@link ViewDescriptor} that knows how to present the tasks in the task list grouped by priority. - */ - public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() - { - - private int mFlingContentViewId = R.id.flingContentView; - private int mFlingRevealLeftViewId = R.id.fling_reveal_left; - private int mFlingRevealRightViewId = R.id.fling_reveal_right; - - - @SuppressLint("NewApi") - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - TextView title = getView(view, android.R.id.title); - boolean isClosed = TaskFieldAdapters.IS_CLOSED.get(cursor); - - resetFlingView(view); - - if (title != null) - { - String text = TaskFieldAdapters.TITLE.get(cursor); - // float score = TaskFieldAdapters.SCORE.get(cursor); - title.setText(text); - if (isClosed) - { - title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } - else - { - title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); - } - } - - setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - - View divider = getView(view, R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); - } - - // display priority - int priority = TaskFieldAdapters.PRIORITY.get(cursor); - View priorityView = getView(view, R.id.task_priority_view_medium); - priorityView.setBackgroundResource(android.R.color.transparent); - priorityView.setVisibility(View.VISIBLE); - - if (priority > 0 && priority < 5) - { - priorityView.setBackgroundResource(R.color.priority_red); - } - if (priority == 5) - { - priorityView.setBackgroundResource(R.color.priority_yellow); - } - if (priority > 5 && priority <= 9) - { - priorityView.setBackgroundResource(R.color.priority_green); - } - - if (VERSION.SDK_INT >= 11) - { - // update percentage background - View background = getView(view, R.id.percentage_background_view); - background.setPivotX(0); - Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); - if (percentComplete < 100) - { - background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); - background.setBackgroundResource(R.drawable.task_progress_background_shade); - } - else - { - background.setScaleX(1); - background.setBackgroundResource(R.drawable.complete_task_background_overlay); - } - } - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); - } - - - @Override - public int getView() - { - return R.layout.task_list_element; - } - - - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; - } - - - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; - } - }; - - /** - * A {@link ViewDescriptor} that knows how to present list groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { - - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - long now = System.currentTimeMillis(); - int position = cursor.getPosition(); - - // set list title - String groupTitle = getTitle(cursor, view.getContext()); - TextView title = (TextView) view.findViewById(android.R.id.title); - if (title != null) - { - title.setText(groupTitle); - - } - // set search time - TextView text1 = (TextView) view.findViewById(android.R.id.text1); - if (text1 != null) - { - text1.setText(DateUtils.getRelativeTimeSpanString( - cursor.getLong(cursor.getColumnIndex(SearchHistoryDatabaseHelper.SearchHistoryColumns.TIMESTAMP)), now, DateUtils.MINUTE_IN_MILLIS)); - } - - // set list elements - TextView text2 = (TextView) view.findViewById(android.R.id.text2); - int childrenCount = adapter.getChildrenCount(position); - if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) - { - Resources res = view.getContext().getResources(); - - text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); - } - - // show/hide divider - View divider = view.findViewById(R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); - } - - View colorbar1 = view.findViewById(R.id.colorbar1); - View colorbar2 = view.findViewById(R.id.colorbar2); - - if (colorbar1 != null) - { - colorbar1.setVisibility(View.GONE); - } - if (colorbar2 != null) - { - colorbar2.setVisibility(View.GONE); - } - - View removeSearch = view.findViewById(R.id.quick_add_task); - if (removeSearch != null) - { - ((ImageView) removeSearch).setImageResource(R.drawable.content_remove); - removeSearch.setOnClickListener(removeListener); - GroupTag tag = (GroupTag) removeSearch.getTag(); - Long groupId = cursor.getLong(cursor.getColumnIndex(SearchHistoryColumns._ID)); - if (tag == null || tag.groupId != groupId) - { - removeSearch.setTag(new GroupTag(groupTitle, groupId)); - } - } - - if ((flags & FLAG_IS_EXPANDED) != 0) - { - if (removeSearch != null) - { - removeSearch.setVisibility(View.VISIBLE); - } - if (text2 != null) - { - text2.setVisibility(View.GONE); - } - } - else - { - if (removeSearch != null) - { - removeSearch.setVisibility(View.GONE); - } - if (text2 != null) - { - text2.setVisibility(View.VISIBLE); - } - } - - // TODO: swap styles instead of modifying the font style - boolean isHistoric = cursor.getInt(cursor.getColumnIndex(SearchHistoryColumns.HISTORIC)) > 0; - Typeface oldtypeface = title.getTypeface(); - title.setTypeface(oldtypeface, isHistoric ? oldtypeface.getStyle() & ~Typeface.ITALIC : oldtypeface.getStyle() | Typeface.ITALIC); - - // set history icon - ImageView icon = (ImageView) view.findViewById(android.R.id.icon); - icon.setImageResource(R.drawable.ic_history); - icon.setVisibility(isHistoric ? View.VISIBLE : View.INVISIBLE); - } - - private final OnClickListener removeListener = new OnClickListener() - { - - @Override - public void onClick(View v) - { - GroupTag tag = (GroupTag) v.getTag(); - if (tag != null) - { - Context context = v.getContext(); - mHelper.removeSearch(tag.groupId); - mSearchCursorFactory.forceUpdate(); - Toast.makeText(context, context.getString(R.string.toast_x_removed, tag.groupName), Toast.LENGTH_SHORT).show(); - } - } - }; - - /** - * A tag that holds information about a search group. - */ - final class GroupTag - { - final String groupName; - final long groupId; - - - GroupTag(String groupName, long groupId) - { - this.groupName = groupName; - this.groupId = groupId; - } - } - - - @Override - public int getView() - { - return R.layout.task_list_group; - } - - - /** - * Return the title of the priority group. - * - * @param cursor - * A {@link Cursor} pointing to the current group. - * @return A {@link String} with the group name. - */ - private String getTitle(Cursor cursor, Context context) - { - return cursor.getString(cursor.getColumnIndex(SearchHistoryDatabaseHelper.SearchHistoryColumns.SEARCH_QUERY)); - } - - - @Override - public int getFlingContentViewId() - { - return -1; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return -1; - } - - - @Override - public int getFlingRevealRightViewId() - { - return -1; - } - - }; - - private final SearchHistoryHelper mHelper; - private final SearchHistoryCursorLoaderFactory mSearchCursorFactory; - - - public BySearch(String authority, SearchHistoryHelper helper) - { - super(authority); - mHelper = helper; - mSearchCursorFactory = new SearchHistoryCursorLoaderFactory(mHelper); - } - - - @Override - public ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) - { - return new SearchChildDescriptor(authority, SearchHistoryDatabaseHelper.SearchHistoryColumns.SEARCH_QUERY, INSTANCE_PROJECTION, null, Tasks.SCORE - + ", " + Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING + ", " + Instances.PRIORITY + ", " + Instances.TITLE - + " COLLATE NOCASE ASC", null).setViewDescriptor(TASK_VIEW_DESCRIPTOR); - - } - - - @Override - public ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) - { - return new ExpandableGroupDescriptor(mSearchCursorFactory, makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); - } - - - @Override - public int getId() - { - return R.id.task_group_search; - } + /** + * A {@link ViewDescriptor} that knows how to present the tasks in the task list grouped by priority. + */ + public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() + { + + private int mFlingContentViewId = R.id.flingContentView; + private int mFlingRevealLeftViewId = R.id.fling_reveal_left; + private int mFlingRevealRightViewId = R.id.fling_reveal_right; + + + @SuppressLint("NewApi") + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + TextView title = getView(view, android.R.id.title); + boolean isClosed = TaskFieldAdapters.IS_CLOSED.get(cursor); + + resetFlingView(view); + + if (title != null) + { + String text = TaskFieldAdapters.TITLE.get(cursor); + // float score = TaskFieldAdapters.SCORE.get(cursor); + title.setText(text); + if (isClosed) + { + title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } + else + { + title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); + } + } + + setDueDate((TextView) getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + + View divider = getView(view, R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); + } + + // display priority + int priority = TaskFieldAdapters.PRIORITY.get(cursor); + View priorityView = getView(view, R.id.task_priority_view_medium); + priorityView.setBackgroundResource(android.R.color.transparent); + priorityView.setVisibility(View.VISIBLE); + + if (priority > 0 && priority < 5) + { + priorityView.setBackgroundResource(R.color.priority_red); + } + if (priority == 5) + { + priorityView.setBackgroundResource(R.color.priority_yellow); + } + if (priority > 5 && priority <= 9) + { + priorityView.setBackgroundResource(R.color.priority_green); + } + + if (VERSION.SDK_INT >= 11) + { + // update percentage background + View background = getView(view, R.id.percentage_background_view); + background.setPivotX(0); + Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); + if (percentComplete < 100) + { + background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); + background.setBackgroundResource(R.drawable.task_progress_background_shade); + } + else + { + background.setScaleX(1); + background.setBackgroundResource(R.drawable.complete_task_background_overlay); + } + } + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); + } + + + @Override + public int getView() + { + return R.layout.task_list_element; + } + + + @Override + public int getFlingContentViewId() + { + return mFlingContentViewId; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return mFlingRevealLeftViewId; + } + + + @Override + public int getFlingRevealRightViewId() + { + return mFlingRevealRightViewId; + } + }; + + /** + * A {@link ViewDescriptor} that knows how to present list groups. + */ + public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() + { + + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + long now = System.currentTimeMillis(); + int position = cursor.getPosition(); + + // set list title + String groupTitle = getTitle(cursor, view.getContext()); + TextView title = (TextView) view.findViewById(android.R.id.title); + if (title != null) + { + title.setText(groupTitle); + + } + // set search time + TextView text1 = (TextView) view.findViewById(android.R.id.text1); + if (text1 != null) + { + text1.setText(DateUtils.getRelativeTimeSpanString( + cursor.getLong(cursor.getColumnIndex(SearchHistoryDatabaseHelper.SearchHistoryColumns.TIMESTAMP)), now, DateUtils.MINUTE_IN_MILLIS)); + } + + // set list elements + TextView text2 = (TextView) view.findViewById(android.R.id.text2); + int childrenCount = adapter.getChildrenCount(position); + if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) + { + Resources res = view.getContext().getResources(); + + text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); + } + + // show/hide divider + View divider = view.findViewById(R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); + } + + View colorbar1 = view.findViewById(R.id.colorbar1); + View colorbar2 = view.findViewById(R.id.colorbar2); + + if (colorbar1 != null) + { + colorbar1.setVisibility(View.GONE); + } + if (colorbar2 != null) + { + colorbar2.setVisibility(View.GONE); + } + + View removeSearch = view.findViewById(R.id.quick_add_task); + if (removeSearch != null) + { + ((ImageView) removeSearch).setImageResource(R.drawable.content_remove); + removeSearch.setOnClickListener(removeListener); + GroupTag tag = (GroupTag) removeSearch.getTag(); + Long groupId = cursor.getLong(cursor.getColumnIndex(SearchHistoryColumns._ID)); + if (tag == null || tag.groupId != groupId) + { + removeSearch.setTag(new GroupTag(groupTitle, groupId)); + } + } + + if ((flags & FLAG_IS_EXPANDED) != 0) + { + if (removeSearch != null) + { + removeSearch.setVisibility(View.VISIBLE); + } + if (text2 != null) + { + text2.setVisibility(View.GONE); + } + } + else + { + if (removeSearch != null) + { + removeSearch.setVisibility(View.GONE); + } + if (text2 != null) + { + text2.setVisibility(View.VISIBLE); + } + } + + // TODO: swap styles instead of modifying the font style + boolean isHistoric = cursor.getInt(cursor.getColumnIndex(SearchHistoryColumns.HISTORIC)) > 0; + Typeface oldtypeface = title.getTypeface(); + title.setTypeface(oldtypeface, isHistoric ? oldtypeface.getStyle() & ~Typeface.ITALIC : oldtypeface.getStyle() | Typeface.ITALIC); + + // set history icon + ImageView icon = (ImageView) view.findViewById(android.R.id.icon); + icon.setImageResource(R.drawable.ic_history); + icon.setVisibility(isHistoric ? View.VISIBLE : View.INVISIBLE); + } + + + private final OnClickListener removeListener = new OnClickListener() + { + + @Override + public void onClick(View v) + { + GroupTag tag = (GroupTag) v.getTag(); + if (tag != null) + { + Context context = v.getContext(); + mHelper.removeSearch(tag.groupId); + mSearchCursorFactory.forceUpdate(); + Toast.makeText(context, context.getString(R.string.toast_x_removed, tag.groupName), Toast.LENGTH_SHORT).show(); + } + } + }; + + + /** + * A tag that holds information about a search group. + */ + final class GroupTag + { + final String groupName; + final long groupId; + + + GroupTag(String groupName, long groupId) + { + this.groupName = groupName; + this.groupId = groupId; + } + } + + + @Override + public int getView() + { + return R.layout.task_list_group; + } + + + /** + * Return the title of the priority group. + * + * @param cursor + * A {@link Cursor} pointing to the current group. + * + * @return A {@link String} with the group name. + */ + private String getTitle(Cursor cursor, Context context) + { + return cursor.getString(cursor.getColumnIndex(SearchHistoryDatabaseHelper.SearchHistoryColumns.SEARCH_QUERY)); + } + + + @Override + public int getFlingContentViewId() + { + return -1; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return -1; + } + + + @Override + public int getFlingRevealRightViewId() + { + return -1; + } + + }; + + private final SearchHistoryHelper mHelper; + private final SearchHistoryCursorLoaderFactory mSearchCursorFactory; + + + public BySearch(String authority, SearchHistoryHelper helper) + { + super(authority); + mHelper = helper; + mSearchCursorFactory = new SearchHistoryCursorLoaderFactory(mHelper); + } + + + @Override + public ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) + { + return new SearchChildDescriptor(authority, SearchHistoryDatabaseHelper.SearchHistoryColumns.SEARCH_QUERY, INSTANCE_PROJECTION, null, Tasks.SCORE + + ", " + Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING + ", " + Instances.PRIORITY + ", " + Instances.TITLE + + " COLLATE NOCASE ASC", null).setViewDescriptor(TASK_VIEW_DESCRIPTOR); + + } + + + @Override + public ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) + { + return new ExpandableGroupDescriptor(mSearchCursorFactory, makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); + } + + + @Override + public int getId() + { + return R.id.task_group_search; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java index 04eb146e..d196eb4b 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java @@ -17,19 +17,6 @@ package org.dmfs.tasks.groupings; -import org.dmfs.provider.tasks.TaskContract.Instances; -import org.dmfs.tasks.R; -import org.dmfs.tasks.groupings.cursorloaders.TimeRangeCursorFactory; -import org.dmfs.tasks.groupings.cursorloaders.TimeRangeStartCursorFactory; -import org.dmfs.tasks.groupings.cursorloaders.TimeRangeStartCursorLoaderFactory; -import org.dmfs.tasks.model.TaskFieldAdapters; -import org.dmfs.tasks.utils.DateFormatter; -import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; -import org.dmfs.tasks.utils.ExpandableChildDescriptor; -import org.dmfs.tasks.utils.ExpandableGroupDescriptor; -import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; -import org.dmfs.tasks.utils.ViewDescriptor; - import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; @@ -43,294 +30,307 @@ import android.widget.BaseExpandableListAdapter; import android.widget.ImageView; import android.widget.TextView; +import org.dmfs.provider.tasks.TaskContract.Instances; +import org.dmfs.tasks.R; +import org.dmfs.tasks.groupings.cursorloaders.TimeRangeCursorFactory; +import org.dmfs.tasks.groupings.cursorloaders.TimeRangeStartCursorFactory; +import org.dmfs.tasks.groupings.cursorloaders.TimeRangeStartCursorLoaderFactory; +import org.dmfs.tasks.model.TaskFieldAdapters; +import org.dmfs.tasks.utils.DateFormatter; +import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; +import org.dmfs.tasks.utils.ExpandableChildDescriptor; +import org.dmfs.tasks.utils.ExpandableGroupDescriptor; +import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; +import org.dmfs.tasks.utils.ViewDescriptor; + /** * Definition of the by-start date grouping. - * - * - * + * * @author Tobias Reinsch */ public class ByStartDate extends AbstractGroupingFactory { - /** - * A {@link ViewDescriptor} that knows how to present the tasks in the task list. - */ - public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() - { - private int mFlingContentViewId = R.id.flingContentView; - private int mFlingRevealLeftViewId = R.id.fling_reveal_left; - private int mFlingRevealRightViewId = R.id.fling_reveal_right; - - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - TextView title = getView(view, android.R.id.title); - boolean isClosed = cursor.getInt(13) > 0; - - resetFlingView(view); - - if (title != null) - { - String text = cursor.getString(5); - title.setText(text); - if (isClosed) - { - title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } - else - { - title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); - } - } - - setDueDate((TextView) getView(view, R.id.task_due_date), (ImageView) getView(view, R.id.task_due_image), INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - - TextView startDateField = getView(view, R.id.task_start_date); - if (startDateField != null) - { - Time startDate = INSTANCE_START_ADAPTER.get(cursor); - - if (startDate != null) - { - - startDateField.setVisibility(View.VISIBLE); - startDateField.setText(new DateFormatter(view.getContext()).format(startDate, DateFormatContext.LIST_VIEW)); - - // format time - startDateField.setTextAppearance(view.getContext(), R.style.task_list_due_text); - - ImageView icon = getView(view, R.id.task_start_image); - if (icon != null) - { - icon.setVisibility(View.VISIBLE); - } - } - else - { - startDateField.setText(""); - } - } - - View divider = getView(view, R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); - } - - // display priority - int priority = TaskFieldAdapters.PRIORITY.get(cursor); - View priorityView = getView(view, R.id.task_priority_view_medium); - priorityView.setBackgroundResource(android.R.color.transparent); - priorityView.setVisibility(View.VISIBLE); - - if (priority > 0 && priority < 5) - { - priorityView.setBackgroundResource(R.color.priority_red); - } - if (priority == 5) - { - priorityView.setBackgroundResource(R.color.priority_yellow); - } - if (priority > 5 && priority <= 9) - { - priorityView.setBackgroundResource(R.color.priority_green); - } - - if (VERSION.SDK_INT >= 11) - { - // update percentage background - View background = getView(view, R.id.percentage_background_view); - background.setPivotX(0); - Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); - if (percentComplete < 100) - { - background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); - background.setBackgroundResource(R.drawable.task_progress_background_shade); - } - else - { - background.setScaleX(1); - background.setBackgroundResource(R.drawable.complete_task_background_overlay); - } - } - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); - } - - - @Override - public int getView() - { - return R.layout.task_list_element; - } - - - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; - } - - - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; - } - }; - - /** - * A {@link ViewDescriptor} that knows how to present start date groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { - @Override - public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) - { - int position = cursor.getPosition(); - - // set list title - TextView title = (TextView) view.findViewById(android.R.id.title); - if (title != null) - { - title.setText(getTitle(cursor, view.getContext())); - } - - // set list elements - TextView text2 = (TextView) view.findViewById(android.R.id.text2); - int childrenCount = adapter.getChildrenCount(position); - if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) - { - Resources res = view.getContext().getResources(); - text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); - - } - - // show/hide divider - View divider = view.findViewById(R.id.divider); - if (divider != null) - { - divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); - } - - View colorbar = view.findViewById(R.id.colorbar1); - if (colorbar != null) - { - colorbar.setVisibility(View.GONE); - } - } - - - @Override - public int getView() - { - return R.layout.task_list_group_single_line; - } - - - /** - * Return the title of a date group. - * - * @param cursor - * A {@link Cursor} pointing to the current group. - * @return A {@link String} with the group name. - */ - private String getTitle(Cursor cursor, Context context) - { - int type = cursor.getInt(cursor.getColumnIndex(TimeRangeCursorFactory.RANGE_TYPE)); - if (type == 0) - { - return context.getString(R.string.task_group_start_started); - } - if ((type & TimeRangeCursorFactory.TYPE_OVERDUE) == TimeRangeCursorFactory.TYPE_OVERDUE) - { - return context.getString(R.string.task_group_start_started); - } - if ((type & TimeRangeCursorFactory.TYPE_END_OF_TODAY) == TimeRangeCursorFactory.TYPE_END_OF_TODAY) - { - return context.getString(R.string.task_group_start_today); - } - if ((type & TimeRangeCursorFactory.TYPE_END_OF_TOMORROW) == TimeRangeCursorFactory.TYPE_END_OF_TOMORROW) - { - return context.getString(R.string.task_group_start_tomorrow); - } - if ((type & TimeRangeCursorFactory.TYPE_END_IN_7_DAYS) == TimeRangeCursorFactory.TYPE_END_IN_7_DAYS) - { - return context.getString(R.string.task_group_start_within_7_days); - } - if ((type & TimeRangeCursorFactory.TYPE_NO_END) != 0) - { - return context.getString(R.string.task_group_start_in_future); - } - return ""; - } - - - @Override - public int getFlingContentViewId() - { - return -1; - } - - - @Override - public int getFlingRevealLeftViewId() - { - return -1; - } - - - @Override - public int getFlingRevealRightViewId() - { - return -1; - } - - }; - - - public ByStartDate(String authority) - { - super(authority); - } - - - @Override - ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) - { - // Note that we're using INSTANCE_START_SORTING to get correct grouping of all-day tasks - return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and (" + Instances.IS_ALLDAY - + "=0 and (((" + Instances.INSTANCE_START + ">=?) and (" + Instances.INSTANCE_START + "=? or " - + Instances.INSTANCE_START + " is ?) and ? is null)) or " + Instances.IS_ALLDAY + "=1 and (((" + Instances.INSTANCE_START + ">=?+?) and (" - + Instances.INSTANCE_START + "=?+? or " + Instances.INSTANCE_START + " is ?) and ? is null)))", - Instances.INSTANCE_START, 0, 1, 0, 1, 1, 0, 9, 1, 10, 0, 9, 1, 1).setViewDescriptor(TASK_VIEW_DESCRIPTOR); - } - - - @Override - ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) - { - return new ExpandableGroupDescriptor(new TimeRangeStartCursorLoaderFactory(TimeRangeStartCursorFactory.DEFAULT_PROJECTION), - makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); - } - - - @Override - public int getId() - { - return R.id.task_group_by_start; - } + /** + * A {@link ViewDescriptor} that knows how to present the tasks in the task list. + */ + public final ViewDescriptor TASK_VIEW_DESCRIPTOR = new BaseTaskViewDescriptor() + { + private int mFlingContentViewId = R.id.flingContentView; + private int mFlingRevealLeftViewId = R.id.fling_reveal_left; + private int mFlingRevealRightViewId = R.id.fling_reveal_right; + + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + TextView title = getView(view, android.R.id.title); + boolean isClosed = cursor.getInt(13) > 0; + + resetFlingView(view); + + if (title != null) + { + String text = cursor.getString(5); + title.setText(text); + if (isClosed) + { + title.setPaintFlags(title.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } + else + { + title.setPaintFlags(title.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); + } + } + + setDueDate((TextView) getView(view, R.id.task_due_date), (ImageView) getView(view, R.id.task_due_image), INSTANCE_DUE_ADAPTER.get(cursor), + isClosed); + + TextView startDateField = getView(view, R.id.task_start_date); + if (startDateField != null) + { + Time startDate = INSTANCE_START_ADAPTER.get(cursor); + + if (startDate != null) + { + + startDateField.setVisibility(View.VISIBLE); + startDateField.setText(new DateFormatter(view.getContext()).format(startDate, DateFormatContext.LIST_VIEW)); + + // format time + startDateField.setTextAppearance(view.getContext(), R.style.task_list_due_text); + + ImageView icon = getView(view, R.id.task_start_image); + if (icon != null) + { + icon.setVisibility(View.VISIBLE); + } + } + else + { + startDateField.setText(""); + } + } + + View divider = getView(view, R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE); + } + + // display priority + int priority = TaskFieldAdapters.PRIORITY.get(cursor); + View priorityView = getView(view, R.id.task_priority_view_medium); + priorityView.setBackgroundResource(android.R.color.transparent); + priorityView.setVisibility(View.VISIBLE); + + if (priority > 0 && priority < 5) + { + priorityView.setBackgroundResource(R.color.priority_red); + } + if (priority == 5) + { + priorityView.setBackgroundResource(R.color.priority_yellow); + } + if (priority > 5 && priority <= 9) + { + priorityView.setBackgroundResource(R.color.priority_green); + } + + if (VERSION.SDK_INT >= 11) + { + // update percentage background + View background = getView(view, R.id.percentage_background_view); + background.setPivotX(0); + Integer percentComplete = TaskFieldAdapters.PERCENT_COMPLETE.get(cursor); + if (percentComplete < 100) + { + background.setScaleX(percentComplete == null ? 0 : percentComplete / 100f); + background.setBackgroundResource(R.drawable.task_progress_background_shade); + } + else + { + background.setScaleX(1); + background.setBackgroundResource(R.drawable.complete_task_background_overlay); + } + } + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); + } + + + @Override + public int getView() + { + return R.layout.task_list_element; + } + + + @Override + public int getFlingContentViewId() + { + return mFlingContentViewId; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return mFlingRevealLeftViewId; + } + + + @Override + public int getFlingRevealRightViewId() + { + return mFlingRevealRightViewId; + } + }; + + /** + * A {@link ViewDescriptor} that knows how to present start date groups. + */ + public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() + { + @Override + public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) + { + int position = cursor.getPosition(); + + // set list title + TextView title = (TextView) view.findViewById(android.R.id.title); + if (title != null) + { + title.setText(getTitle(cursor, view.getContext())); + } + + // set list elements + TextView text2 = (TextView) view.findViewById(android.R.id.text2); + int childrenCount = adapter.getChildrenCount(position); + if (text2 != null && ((ExpandableGroupDescriptorAdapter) adapter).childCursorLoaded(position)) + { + Resources res = view.getContext().getResources(); + text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount)); + + } + + // show/hide divider + View divider = view.findViewById(R.id.divider); + if (divider != null) + { + divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE); + } + + View colorbar = view.findViewById(R.id.colorbar1); + if (colorbar != null) + { + colorbar.setVisibility(View.GONE); + } + } + + + @Override + public int getView() + { + return R.layout.task_list_group_single_line; + } + + + /** + * Return the title of a date group. + * + * @param cursor + * A {@link Cursor} pointing to the current group. + * + * @return A {@link String} with the group name. + */ + private String getTitle(Cursor cursor, Context context) + { + int type = cursor.getInt(cursor.getColumnIndex(TimeRangeCursorFactory.RANGE_TYPE)); + if (type == 0) + { + return context.getString(R.string.task_group_start_started); + } + if ((type & TimeRangeCursorFactory.TYPE_OVERDUE) == TimeRangeCursorFactory.TYPE_OVERDUE) + { + return context.getString(R.string.task_group_start_started); + } + if ((type & TimeRangeCursorFactory.TYPE_END_OF_TODAY) == TimeRangeCursorFactory.TYPE_END_OF_TODAY) + { + return context.getString(R.string.task_group_start_today); + } + if ((type & TimeRangeCursorFactory.TYPE_END_OF_TOMORROW) == TimeRangeCursorFactory.TYPE_END_OF_TOMORROW) + { + return context.getString(R.string.task_group_start_tomorrow); + } + if ((type & TimeRangeCursorFactory.TYPE_END_IN_7_DAYS) == TimeRangeCursorFactory.TYPE_END_IN_7_DAYS) + { + return context.getString(R.string.task_group_start_within_7_days); + } + if ((type & TimeRangeCursorFactory.TYPE_NO_END) != 0) + { + return context.getString(R.string.task_group_start_in_future); + } + return ""; + } + + + @Override + public int getFlingContentViewId() + { + return -1; + } + + + @Override + public int getFlingRevealLeftViewId() + { + return -1; + } + + + @Override + public int getFlingRevealRightViewId() + { + return -1; + } + + }; + + + public ByStartDate(String authority) + { + super(authority); + } + + + @Override + ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) + { + // Note that we're using INSTANCE_START_SORTING to get correct grouping of all-day tasks + return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and (" + Instances.IS_ALLDAY + + "=0 and (((" + Instances.INSTANCE_START + ">=?) and (" + Instances.INSTANCE_START + "=? or " + + Instances.INSTANCE_START + " is ?) and ? is null)) or " + Instances.IS_ALLDAY + "=1 and (((" + Instances.INSTANCE_START + ">=?+?) and (" + + Instances.INSTANCE_START + "=?+? or " + Instances.INSTANCE_START + " is ?) and ? is null)))", + Instances.INSTANCE_START, 0, 1, 0, 1, 1, 0, 9, 1, 10, 0, 9, 1, 1).setViewDescriptor(TASK_VIEW_DESCRIPTOR); + } + + + @Override + ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority) + { + return new ExpandableGroupDescriptor(new TimeRangeStartCursorLoaderFactory(TimeRangeStartCursorFactory.DEFAULT_PROJECTION), + makeExpandableChildDescriptor(authority)).setViewDescriptor(GROUP_VIEW_DESCRIPTOR); + } + + + @Override + public int getId() + { + return R.id.task_group_by_start; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/TabConfig.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/TabConfig.java index e6458087..f87d61d6 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/TabConfig.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/TabConfig.java @@ -17,9 +17,9 @@ package org.dmfs.tasks.groupings; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; import org.dmfs.android.xmlmagic.AndroidParserContext; import org.dmfs.android.xmlmagic.builder.RecyclingReflectionObjectBuilder; @@ -34,239 +34,244 @@ import org.dmfs.xmlobjects.pull.XmlObjectPullParserException; import org.dmfs.xmlobjects.pull.XmlPath; import org.xmlpull.v1.XmlPullParserException; -import android.content.Context; -import android.content.res.Resources; -import android.content.res.XmlResourceParser; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * This class describes the tab configuration. It holds a number of tabs with attributes like title, icon and visibility. - * + * * @author Marten Gajda */ public class TabConfig { - /** - * The XML namespace. - */ - public final static String NAMESPACE = "http://schema.dmfs.org/tasks"; - - /** - * The name of the root elements. - */ - public final static String TAG = "tabconfig"; - - /** - * A Builder that builds a {@link TabConfig} object. - */ - public final static IObjectBuilder BUILDER = new RecyclingReflectionObjectBuilder(TabConfig.class); - - /** - * The {@link XmlElementDescriptor} of the tabconfig element. - */ - public final static ElementDescriptor DESCRIPTOR = ElementDescriptor.register(QualifiedName.get(NAMESPACE, TAG), BUILDER); - - /** - * A Builder for {@link Tab} objects. - */ - public final static IObjectBuilder TAB_BUILDER = new ReflectionObjectBuilder(Tab.class); - - /** - * The {@link XmlElementDescriptor} for tab elements. - */ - public final static ElementDescriptor TAB_DESCRIPTOR = ElementDescriptor.register(QualifiedName.get(NAMESPACE, Tab.TAG), TAB_BUILDER); - - /** - * Represents a single tab with all its attributes. - * - * @author Marten Gajda - */ - public static class Tab - { - public final static String TAG = "tab"; - - @Attribute(name = "title") - private int title; - - @Attribute(name = "icon") - private int icon; - - @Attribute(name = "id") - private int id; - - @Attribute(name = "visible") - private boolean visible = true; - - - /** - * Get the title of this tab. - * - * @return A string resource id for the title. - */ - public int getTitleId() - { - return title; - } - - - /** - * Get the icon of the tab. - * - * @return A drawable resource id. - */ - public int getIcon() - { - return icon; - } - - - /** - * Get the id of the tab. - * - * @return The id. - */ - public int getId() - { - return id; - } - - - /** - * Return the visibility of the tab. - * - * @return true if the tab is visible, false otherwise. - */ - public boolean isVisible() - { - return visible; - } - } - - /** - * All loaded tabs. - */ - @Element(namespace = NAMESPACE, name = Tab.TAG) - private ArrayList mTabs; - - /** - * The visible tabs. - */ - private List mVisible; - - - /** - * Loads a {@link TabConfig} from the given XML resource. - *

    - * A tabconfig XML file must looke like this: - *

    - * - *
    -	 * <tabconfig xmlns="http://schema.dmfs.org/tasks" >
    -	 * 
    -	 *     <tab
    -	 *         id="@+id/tab1_id"
    -	 *         icon="@drawable/tab1_icon"
    -	 *         title="@string/tab1_title" />
    -	 *     <tab
    -	 *         id="@+id/tab2_id"
    -	 *         icon="@drawable/tab2_icon"
    -	 *         title="@string/tab2_title"
    -	 *         visible="false"/>
    -	 * </tabconfig>
    -	 * 
    - * - * @param context - * A {@link Context}. - * @param tabsResource - * The resource id of an XML resource that contains the tabconfig. - * @return A {@link TabConfig} instance. - * - * @throws XmlPullParserException - * @throws IOException - * @throws XmlObjectPullParserException - */ - public static TabConfig load(Context context, int tabsResource) throws XmlPullParserException, IOException, XmlObjectPullParserException - { - Resources res = context.getResources(); - - XmlResourceParser parser = res.getXml(tabsResource); - - XmlObjectPull objectParser = new XmlObjectPull(parser, new AndroidParserContext(context, null)); - - TabConfig groupings = objectParser.pull(DESCRIPTOR, null, new XmlPath()); - groupings.updateVisible(); - - return groupings; - } - - - /** - * Get the {@link Tab} at the specified position. - * - * @param position - * The position among all {@link Tab}s. - * @return The Tab at the given position. - */ - public Tab getItem(int position) - { - return mTabs.get(position); - } - - - /** - * Get one of the visible {@link Tab}s by its position. - * - * @param position - * The position among the visible items. - * @return The Tab at the given position. - */ - public Tab getVisibleItem(int position) - { - return mVisible.get(position); - } - - - /** - * Get the number of all {@link Tab}s. - * - * @return The number of tabs. - */ - public int size() - { - return mTabs.size(); - } - - - /** - * Get the number of visible {@link Tab}s. - * - * @return The number of visible tabs. - */ - public int visibleSize() - { - return mVisible.size(); - } - - - /** - * Update the internal list of visible tabs. - */ - private void updateVisible() - { - List visible = mVisible; - if (visible == null) - { - visible = mVisible = new ArrayList(mTabs.size()); - } - - visible.clear(); - for (Tab tab : mTabs) - { - if (tab.visible) - { - visible.add(tab); - } - } - } + /** + * The XML namespace. + */ + public final static String NAMESPACE = "http://schema.dmfs.org/tasks"; + + /** + * The name of the root elements. + */ + public final static String TAG = "tabconfig"; + + /** + * A Builder that builds a {@link TabConfig} object. + */ + public final static IObjectBuilder BUILDER = new RecyclingReflectionObjectBuilder(TabConfig.class); + + /** + * The {@link XmlElementDescriptor} of the tabconfig element. + */ + public final static ElementDescriptor DESCRIPTOR = ElementDescriptor.register(QualifiedName.get(NAMESPACE, TAG), BUILDER); + + /** + * A Builder for {@link Tab} objects. + */ + public final static IObjectBuilder TAB_BUILDER = new ReflectionObjectBuilder(Tab.class); + + /** + * The {@link XmlElementDescriptor} for tab elements. + */ + public final static ElementDescriptor TAB_DESCRIPTOR = ElementDescriptor.register(QualifiedName.get(NAMESPACE, Tab.TAG), TAB_BUILDER); + + + /** + * Represents a single tab with all its attributes. + * + * @author Marten Gajda + */ + public static class Tab + { + public final static String TAG = "tab"; + + @Attribute(name = "title") + private int title; + + @Attribute(name = "icon") + private int icon; + + @Attribute(name = "id") + private int id; + + @Attribute(name = "visible") + private boolean visible = true; + + + /** + * Get the title of this tab. + * + * @return A string resource id for the title. + */ + public int getTitleId() + { + return title; + } + + + /** + * Get the icon of the tab. + * + * @return A drawable resource id. + */ + public int getIcon() + { + return icon; + } + + + /** + * Get the id of the tab. + * + * @return The id. + */ + public int getId() + { + return id; + } + + + /** + * Return the visibility of the tab. + * + * @return true if the tab is visible, false otherwise. + */ + public boolean isVisible() + { + return visible; + } + } + + + /** + * All loaded tabs. + */ + @Element(namespace = NAMESPACE, name = Tab.TAG) + private ArrayList mTabs; + + /** + * The visible tabs. + */ + private List mVisible; + + + /** + * Loads a {@link TabConfig} from the given XML resource. + *

    + * A tabconfig XML file must looke like this: + *

    + *

    + *

    +     * <tabconfig xmlns="http://schema.dmfs.org/tasks" >
    +     *
    +     *     <tab
    +     *         id="@+id/tab1_id"
    +     *         icon="@drawable/tab1_icon"
    +     *         title="@string/tab1_title" />
    +     *     <tab
    +     *         id="@+id/tab2_id"
    +     *         icon="@drawable/tab2_icon"
    +     *         title="@string/tab2_title"
    +     *         visible="false"/>
    +     * </tabconfig>
    +     * 
    + * + * @param context + * A {@link Context}. + * @param tabsResource + * The resource id of an XML resource that contains the tabconfig. + * + * @return A {@link TabConfig} instance. + * + * @throws XmlPullParserException + * @throws IOException + * @throws XmlObjectPullParserException + */ + public static TabConfig load(Context context, int tabsResource) throws XmlPullParserException, IOException, XmlObjectPullParserException + { + Resources res = context.getResources(); + + XmlResourceParser parser = res.getXml(tabsResource); + + XmlObjectPull objectParser = new XmlObjectPull(parser, new AndroidParserContext(context, null)); + + TabConfig groupings = objectParser.pull(DESCRIPTOR, null, new XmlPath()); + groupings.updateVisible(); + + return groupings; + } + + + /** + * Get the {@link Tab} at the specified position. + * + * @param position + * The position among all {@link Tab}s. + * + * @return The Tab at the given position. + */ + public Tab getItem(int position) + { + return mTabs.get(position); + } + + + /** + * Get one of the visible {@link Tab}s by its position. + * + * @param position + * The position among the visible items. + * + * @return The Tab at the given position. + */ + public Tab getVisibleItem(int position) + { + return mVisible.get(position); + } + + + /** + * Get the number of all {@link Tab}s. + * + * @return The number of tabs. + */ + public int size() + { + return mTabs.size(); + } + + + /** + * Get the number of visible {@link Tab}s. + * + * @return The number of visible tabs. + */ + public int visibleSize() + { + return mVisible.size(); + } + + + /** + * Update the internal list of visible tabs. + */ + private void updateVisible() + { + List visible = mVisible; + if (visible == null) + { + visible = mVisible = new ArrayList(mTabs.size()); + } + + visible.clear(); + for (Tab tab : mTabs) + { + if (tab.visible) + { + visible.add(tab); + } + } + } } \ No newline at end of file diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/AbstractCursorLoaderFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/AbstractCursorLoaderFactory.java index 07d2fd12..586059f4 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/AbstractCursorLoaderFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/AbstractCursorLoaderFactory.java @@ -24,17 +24,18 @@ import android.support.v4.content.Loader; /** * An abstract factory that can create Loader instances. - * + * * @author Marten Gajda */ public abstract class AbstractCursorLoaderFactory { - /** - * Get a new {@link Loader} instance. Override this method to return a custom Loader for Cursors. - * - * @param context - * A {@link Context}. - * @return A brand new {@link Loader} for {@link Cursor}s. - */ - public abstract Loader getLoader(Context context); + /** + * Get a new {@link Loader} instance. Override this method to return a custom Loader for Cursors. + * + * @param context + * A {@link Context}. + * + * @return A brand new {@link Loader} for {@link Cursor}s. + */ + public abstract Loader getLoader(Context context); } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/AbstractCustomCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/AbstractCustomCursorFactory.java index c76f22a3..7026bdaf 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/AbstractCustomCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/AbstractCustomCursorFactory.java @@ -22,30 +22,30 @@ import android.database.Cursor; /** * A factory that builds shiny new {@link Cursor}s with time ranges. - * + * * @author Marten Gajda */ public abstract class AbstractCustomCursorFactory { - protected String[] mProjection; - - - /** - * Initialize the factory with the given projection. - * - * @param projection - * An array of column names. - */ - public AbstractCustomCursorFactory(String[] projection) - { - mProjection = projection; - } - - - /** - * Get a new {@link Cursor} from this factory. - * - * @return A {@link Cursor}. - */ - public abstract Cursor getCursor(); + protected String[] mProjection; + + + /** + * Initialize the factory with the given projection. + * + * @param projection + * An array of column names. + */ + public AbstractCustomCursorFactory(String[] projection) + { + mProjection = projection; + } + + + /** + * Get a new {@link Cursor} from this factory. + * + * @return A {@link Cursor}. + */ + public abstract Cursor getCursor(); } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/CursorLoaderFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/CursorLoaderFactory.java index 870e28d3..250758ec 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/CursorLoaderFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/CursorLoaderFactory.java @@ -26,42 +26,42 @@ import android.support.v4.content.Loader; /** * A concrete Factory for a {@link CursorLoader}. - * + * * @author Marten Gajda */ public class CursorLoaderFactory extends AbstractCursorLoaderFactory { - private final Uri mUri; - private final String[] mProjection; - private final String mSelection; - private final String[] mSelectionArgs; - private final String mSortOrder; + private final Uri mUri; + private final String[] mProjection; + private final String mSelection; + private final String[] mSelectionArgs; + private final String mSortOrder; - /** - * Initialize the Factory with the arguments to initialize the CursorLoader. The parameters are just passed to - * {@link CursorLoader#CursorLoader(Context, Uri, String[], String, String[], String)}. - * - * @param uri - * @param projection - * @param selection - * @param selectionArgs - * @param sortOrder - */ - public CursorLoaderFactory(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) - { - mUri = uri; - mProjection = projection; - mSelection = selection; - mSelectionArgs = selectionArgs; - mSortOrder = sortOrder; - } + /** + * Initialize the Factory with the arguments to initialize the CursorLoader. The parameters are just passed to + * {@link CursorLoader#CursorLoader(Context, Uri, String[], String, String[], String)}. + * + * @param uri + * @param projection + * @param selection + * @param selectionArgs + * @param sortOrder + */ + public CursorLoaderFactory(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) + { + mUri = uri; + mProjection = projection; + mSelection = selection; + mSelectionArgs = selectionArgs; + mSortOrder = sortOrder; + } - @Override - public Loader getLoader(Context context) - { - return new CursorLoader(context, mUri, mProjection, mSelection, mSelectionArgs, mSortOrder); - } + @Override + public Loader getLoader(Context context) + { + return new CursorLoader(context, mUri, mProjection, mSelection, mSelectionArgs, mSortOrder); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/CustomCursorLoader.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/CustomCursorLoader.java index fa98e295..719bda85 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/CustomCursorLoader.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/CustomCursorLoader.java @@ -24,94 +24,94 @@ import android.support.v4.content.Loader; /** * A very simple {@link Loader} that returns the {@link Cursor} from a {@link AbstractCustomCursorFactory}. - * + * * @author Marten Gajda */ public class CustomCursorLoader extends Loader { - /** - * The current Cursor. - */ - private Cursor mCursor; - - /** - * The factory that creates our Cursor. - */ - private final AbstractCustomCursorFactory mCursorFactory; - - - public CustomCursorLoader(Context context, AbstractCustomCursorFactory factory) - { - super(context); - - mCursorFactory = factory; - } - - - @Override - public void deliverResult(Cursor cursor) - { - if (isReset()) - { - // An async query came in while the loader is stopped - if (cursor != null && !cursor.isClosed()) - { - cursor.close(); - } - return; - } - Cursor oldCursor = mCursor; - mCursor = cursor; - - if (isStarted()) - { - super.deliverResult(cursor); - } - - if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) - { - oldCursor.close(); - } - } - - - @Override - protected void onStartLoading() - { - if (mCursor == null || takeContentChanged()) - { - // deliver a new cursor, deliverResult will take care of the old one if any - deliverResult(mCursorFactory.getCursor()); - } - else - { - // just deliver the same cursor - deliverResult(mCursor); - } - } - - - @Override - protected void onForceLoad() - { - // just create a new cursor, deliverResult will take care of storing the new cursor and closing the old one - deliverResult(mCursorFactory.getCursor()); - } - - - @Override - protected void onReset() - { - super.onReset(); - - onStopLoading(); - - // ensure the cursor is closed before we release it - if (mCursor != null && !mCursor.isClosed()) - { - mCursor.close(); - } - - mCursor = null; - } + /** + * The current Cursor. + */ + private Cursor mCursor; + + /** + * The factory that creates our Cursor. + */ + private final AbstractCustomCursorFactory mCursorFactory; + + + public CustomCursorLoader(Context context, AbstractCustomCursorFactory factory) + { + super(context); + + mCursorFactory = factory; + } + + + @Override + public void deliverResult(Cursor cursor) + { + if (isReset()) + { + // An async query came in while the loader is stopped + if (cursor != null && !cursor.isClosed()) + { + cursor.close(); + } + return; + } + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) + { + super.deliverResult(cursor); + } + + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) + { + oldCursor.close(); + } + } + + + @Override + protected void onStartLoading() + { + if (mCursor == null || takeContentChanged()) + { + // deliver a new cursor, deliverResult will take care of the old one if any + deliverResult(mCursorFactory.getCursor()); + } + else + { + // just deliver the same cursor + deliverResult(mCursor); + } + } + + + @Override + protected void onForceLoad() + { + // just create a new cursor, deliverResult will take care of storing the new cursor and closing the old one + deliverResult(mCursorFactory.getCursor()); + } + + + @Override + protected void onReset() + { + super.onReset(); + + onStopLoading(); + + // ensure the cursor is closed before we release it + if (mCursor != null && !mCursor.isClosed()) + { + mCursor.close(); + } + + mCursor = null; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/EmptyCursorLoaderFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/EmptyCursorLoaderFactory.java index 049d5efe..6f1d5651 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/EmptyCursorLoaderFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/EmptyCursorLoaderFactory.java @@ -26,22 +26,22 @@ import android.support.v4.content.CursorLoader; /** * A simple cursor loader factory that returns {@link CursorLoader}s that return empty cursors. This is meant as a hack to be able to return null * values from onCreateLoader. - * + * * @author Marten Gajda */ public class EmptyCursorLoaderFactory extends CustomCursorLoader { - public EmptyCursorLoaderFactory(Context context, String[] projection) - { - super(context, new AbstractCustomCursorFactory(projection) - { + public EmptyCursorLoaderFactory(Context context, String[] projection) + { + super(context, new AbstractCustomCursorFactory(projection) + { - @Override - public Cursor getCursor() - { - return new MatrixCursor(mProjection); - } - }); - } + @Override + public Cursor getCursor() + { + return new MatrixCursor(mProjection); + } + }); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/PriorityCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/PriorityCursorFactory.java index fc020a98..07672799 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/PriorityCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/PriorityCursorFactory.java @@ -17,59 +17,60 @@ package org.dmfs.tasks.groupings.cursorloaders; -import org.dmfs.tasks.R; - import android.database.Cursor; import android.database.MatrixCursor; +import org.dmfs.tasks.R; + /** * A factory that builds shiny new {@link Cursor}s with priority information. - * + * * @author Tobias Reinsch */ public final class PriorityCursorFactory extends AbstractCustomCursorFactory { - public final static String PRIORITY_ID = "_id"; - public final static String PRIORITY_MIN_STATUS = "min_status"; - public final static String PRIORITY_MAX_STATUS = "max_status"; - public final static String PRIORITY_TYPE = "type"; - public final static String PRIORITY_TITLE_RES_ID = "res_id"; + public final static String PRIORITY_ID = "_id"; + public final static String PRIORITY_MIN_STATUS = "min_status"; + public final static String PRIORITY_MAX_STATUS = "max_status"; + public final static String PRIORITY_TYPE = "type"; + public final static String PRIORITY_TITLE_RES_ID = "res_id"; - public final static int PRIORITY_TYPE_NONE = 0; - public final static int PRIORITY_TYPE_LOW = 3; - public final static int PRIORITY_TYPE_MEDIUM = 2; - public final static int PRIORITY_TYPE_HIGH = 1; + public final static int PRIORITY_TYPE_NONE = 0; + public final static int PRIORITY_TYPE_LOW = 3; + public final static int PRIORITY_TYPE_MEDIUM = 2; + public final static int PRIORITY_TYPE_HIGH = 1; - public static final String[] DEFAULT_PROJECTION = new String[] { PRIORITY_ID, PRIORITY_MIN_STATUS, PRIORITY_MAX_STATUS, PRIORITY_TYPE, - PRIORITY_TITLE_RES_ID }; - private static final Integer[] ROW_PRIORITY_NONE = new Integer[] { 1, null, 0, PRIORITY_TYPE_NONE, R.string.task_group_priority_none }; - private static final Integer[] ROW_PRIORITY_HIGH = new Integer[] { 2, 1, 4, PRIORITY_TYPE_HIGH, R.string.task_group_priority_high }; - private static final Integer[] ROW_PRIORITY_MEDIUM = new Integer[] { 3, 5, 5, PRIORITY_TYPE_MEDIUM, R.string.task_group_priority_medium }; - private static final Integer[] ROW_PRIORITY_LOW = new Integer[] { 4, 6, 9, PRIORITY_TYPE_LOW, R.string.task_group_priority_low }; + public static final String[] DEFAULT_PROJECTION = new String[] { + PRIORITY_ID, PRIORITY_MIN_STATUS, PRIORITY_MAX_STATUS, PRIORITY_TYPE, + PRIORITY_TITLE_RES_ID }; + private static final Integer[] ROW_PRIORITY_NONE = new Integer[] { 1, null, 0, PRIORITY_TYPE_NONE, R.string.task_group_priority_none }; + private static final Integer[] ROW_PRIORITY_HIGH = new Integer[] { 2, 1, 4, PRIORITY_TYPE_HIGH, R.string.task_group_priority_high }; + private static final Integer[] ROW_PRIORITY_MEDIUM = new Integer[] { 3, 5, 5, PRIORITY_TYPE_MEDIUM, R.string.task_group_priority_medium }; + private static final Integer[] ROW_PRIORITY_LOW = new Integer[] { 4, 6, 9, PRIORITY_TYPE_LOW, R.string.task_group_priority_low }; - /** - * Initialize the factory with the given projection. - * - * @param projection - * An array of column names. - */ - public PriorityCursorFactory(String[] projection) - { - super(projection); - } + /** + * Initialize the factory with the given projection. + * + * @param projection + * An array of column names. + */ + public PriorityCursorFactory(String[] projection) + { + super(projection); + } - @Override - public Cursor getCursor() - { - MatrixCursor result = new MatrixCursor(DEFAULT_PROJECTION); - result.addRow(ROW_PRIORITY_HIGH); - result.addRow(ROW_PRIORITY_MEDIUM); - result.addRow(ROW_PRIORITY_LOW); - result.addRow(ROW_PRIORITY_NONE); - return result; - } + @Override + public Cursor getCursor() + { + MatrixCursor result = new MatrixCursor(DEFAULT_PROJECTION); + result.addRow(ROW_PRIORITY_HIGH); + result.addRow(ROW_PRIORITY_MEDIUM); + result.addRow(ROW_PRIORITY_LOW); + result.addRow(ROW_PRIORITY_NONE); + return result; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/PriorityCursorLoaderFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/PriorityCursorLoaderFactory.java index 9722f9f9..5ed92742 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/PriorityCursorLoaderFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/PriorityCursorLoaderFactory.java @@ -24,24 +24,24 @@ import android.support.v4.content.Loader; /** * An {@link AbstractCursorLoaderFactory} that returns {@link CursorLoaderFactory} that know how to load cursors with priority information as values. - * + * * @author Tobias Reinsch */ public class PriorityCursorLoaderFactory extends AbstractCursorLoaderFactory { - private final String[] mProjection; + private final String[] mProjection; - public PriorityCursorLoaderFactory(String[] projection) - { - mProjection = projection; - } + public PriorityCursorLoaderFactory(String[] projection) + { + mProjection = projection; + } - @Override - public Loader getLoader(Context context) - { - return new CustomCursorLoader(context, new PriorityCursorFactory(mProjection)); - } + @Override + public Loader getLoader(Context context) + { + return new CustomCursorLoader(context, new PriorityCursorFactory(mProjection)); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/ProgressCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/ProgressCursorFactory.java index 0473817d..d13f55d5 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/ProgressCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/ProgressCursorFactory.java @@ -17,62 +17,63 @@ package org.dmfs.tasks.groupings.cursorloaders; -import org.dmfs.tasks.R; - import android.database.Cursor; import android.database.MatrixCursor; +import org.dmfs.tasks.R; + /** * A factory that builds shiny new {@link Cursor}s with progress information. - * + * * @author Tobias Reinsch */ public final class ProgressCursorFactory extends AbstractCustomCursorFactory { - public final static String PROGRESS_ID = "_id"; - public final static String PROGRESS_MIN_STATUS = "min_status"; - public final static String PROGRESS_MAX_STATUS = "max_status"; - public final static String PROGRESS_TYPE = "type"; - public final static String PROGRESS_TITLE_RES_ID = "res_id"; + public final static String PROGRESS_ID = "_id"; + public final static String PROGRESS_MIN_STATUS = "min_status"; + public final static String PROGRESS_MAX_STATUS = "max_status"; + public final static String PROGRESS_TYPE = "type"; + public final static String PROGRESS_TITLE_RES_ID = "res_id"; - public final static int PROGRESS_TYPE_0 = 0; - public final static int PROGRESS_TYPE_40 = 1; - public final static int PROGRESS_TYPE_60 = 2; - public final static int PROGRESS_TYPE_80 = 3; - public final static int PROGRESS_TYPE_100 = 4; + public final static int PROGRESS_TYPE_0 = 0; + public final static int PROGRESS_TYPE_40 = 1; + public final static int PROGRESS_TYPE_60 = 2; + public final static int PROGRESS_TYPE_80 = 3; + public final static int PROGRESS_TYPE_100 = 4; - public static final String[] DEFAULT_PROJECTION = new String[] { PROGRESS_ID, PROGRESS_MIN_STATUS, PROGRESS_MAX_STATUS, PROGRESS_TYPE, - PROGRESS_TITLE_RES_ID }; - private static final Integer[] ROW_PROGRESS_TYPE_0 = new Integer[] { 1, null, 0, PROGRESS_TYPE_0, R.string.task_group_progress_0 }; - private static final Integer[] ROW_PROGRESS_TYPE_40 = new Integer[] { 2, 1, 40, PROGRESS_TYPE_40, R.string.task_group_progress_40 }; - private static final Integer[] ROW_PROGRESS_TYPE_60 = new Integer[] { 3, 41, 60, PROGRESS_TYPE_60, R.string.task_group_progress_60 }; - private static final Integer[] ROW_PROGRESS_TYPE_80 = new Integer[] { 4, 61, 99, PROGRESS_TYPE_80, R.string.task_group_progress_80 }; - private static final Integer[] ROW_PROGRESS_TYPE_100 = new Integer[] { 5, 100, 100, PROGRESS_TYPE_100, R.string.task_group_progress_100 }; + public static final String[] DEFAULT_PROJECTION = new String[] { + PROGRESS_ID, PROGRESS_MIN_STATUS, PROGRESS_MAX_STATUS, PROGRESS_TYPE, + PROGRESS_TITLE_RES_ID }; + private static final Integer[] ROW_PROGRESS_TYPE_0 = new Integer[] { 1, null, 0, PROGRESS_TYPE_0, R.string.task_group_progress_0 }; + private static final Integer[] ROW_PROGRESS_TYPE_40 = new Integer[] { 2, 1, 40, PROGRESS_TYPE_40, R.string.task_group_progress_40 }; + private static final Integer[] ROW_PROGRESS_TYPE_60 = new Integer[] { 3, 41, 60, PROGRESS_TYPE_60, R.string.task_group_progress_60 }; + private static final Integer[] ROW_PROGRESS_TYPE_80 = new Integer[] { 4, 61, 99, PROGRESS_TYPE_80, R.string.task_group_progress_80 }; + private static final Integer[] ROW_PROGRESS_TYPE_100 = new Integer[] { 5, 100, 100, PROGRESS_TYPE_100, R.string.task_group_progress_100 }; - /** - * Initialize the factory with the given projection. - * - * @param projection - * An array of column names. - */ - public ProgressCursorFactory(String[] projection) - { - super(projection); - } + /** + * Initialize the factory with the given projection. + * + * @param projection + * An array of column names. + */ + public ProgressCursorFactory(String[] projection) + { + super(projection); + } - @Override - public Cursor getCursor() - { - MatrixCursor result = new MatrixCursor(DEFAULT_PROJECTION); - result.addRow(ROW_PROGRESS_TYPE_80); - result.addRow(ROW_PROGRESS_TYPE_60); - result.addRow(ROW_PROGRESS_TYPE_40); - result.addRow(ROW_PROGRESS_TYPE_0); - result.addRow(ROW_PROGRESS_TYPE_100); - return result; - } + @Override + public Cursor getCursor() + { + MatrixCursor result = new MatrixCursor(DEFAULT_PROJECTION); + result.addRow(ROW_PROGRESS_TYPE_80); + result.addRow(ROW_PROGRESS_TYPE_60); + result.addRow(ROW_PROGRESS_TYPE_40); + result.addRow(ROW_PROGRESS_TYPE_0); + result.addRow(ROW_PROGRESS_TYPE_100); + return result; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/ProgressCursorLoaderFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/ProgressCursorLoaderFactory.java index f989971c..ac71c734 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/ProgressCursorLoaderFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/ProgressCursorLoaderFactory.java @@ -24,24 +24,24 @@ import android.support.v4.content.Loader; /** * An {@link AbstractCursorLoaderFactory} that returns {@link CursorLoaderFactory} that know how to load cursors with progress information as values. - * + * * @author Tobias Reinsch */ public class ProgressCursorLoaderFactory extends AbstractCursorLoaderFactory { - private final String[] mProjection; + private final String[] mProjection; - public ProgressCursorLoaderFactory(String[] projection) - { - mProjection = projection; - } + public ProgressCursorLoaderFactory(String[] projection) + { + mProjection = projection; + } - @Override - public Loader getLoader(Context context) - { - return new CustomCursorLoader(context, new ProgressCursorFactory(mProjection)); - } + @Override + public Loader getLoader(Context context) + { + return new CustomCursorLoader(context, new ProgressCursorFactory(mProjection)); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/SearchHistoryCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/SearchHistoryCursorFactory.java index 814442aa..31731de0 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/SearchHistoryCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/SearchHistoryCursorFactory.java @@ -17,39 +17,39 @@ package org.dmfs.tasks.groupings.cursorloaders; -import org.dmfs.tasks.utils.SearchHistoryHelper; - import android.content.Context; import android.database.Cursor; +import org.dmfs.tasks.utils.SearchHistoryHelper; + /** * A factory that builds shiny new {@link Cursor}s with the search history - * + * * @author Tobias Reinsch */ public final class SearchHistoryCursorFactory extends AbstractCustomCursorFactory { - private final SearchHistoryHelper mHelper; + private final SearchHistoryHelper mHelper; - /** - * Initialize the factory with the given projection. - * - * @param projection - * An array of column names. - */ - public SearchHistoryCursorFactory(Context context, String[] projection, SearchHistoryHelper helper) - { - super(projection); - mHelper = helper; - } + /** + * Initialize the factory with the given projection. + * + * @param projection + * An array of column names. + */ + public SearchHistoryCursorFactory(Context context, String[] projection, SearchHistoryHelper helper) + { + super(projection); + mHelper = helper; + } - @Override - public Cursor getCursor() - { - return mHelper.getSearchHistory(); - } + @Override + public Cursor getCursor() + { + return mHelper.getSearchHistory(); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/SearchHistoryCursorLoaderFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/SearchHistoryCursorLoaderFactory.java index ec61c52a..3a8f768e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/SearchHistoryCursorLoaderFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/SearchHistoryCursorLoaderFactory.java @@ -17,47 +17,47 @@ package org.dmfs.tasks.groupings.cursorloaders; -import org.dmfs.tasks.utils.SearchHistoryHelper; - import android.content.Context; import android.database.Cursor; import android.support.v4.content.Loader; +import org.dmfs.tasks.utils.SearchHistoryHelper; + /** * An {@link AbstractCursorLoaderFactory} that returns {@link CursorLoaderFactory} that know how to load cursors with the search history. - * + * * @author Tobias Reinsch */ public class SearchHistoryCursorLoaderFactory extends AbstractCursorLoaderFactory { - private final SearchHistoryHelper mSeachHistory; - private CustomCursorLoader mLastLoader; + private final SearchHistoryHelper mSeachHistory; + private CustomCursorLoader mLastLoader; - public SearchHistoryCursorLoaderFactory(SearchHistoryHelper history) - { - mSeachHistory = history; - } + public SearchHistoryCursorLoaderFactory(SearchHistoryHelper history) + { + mSeachHistory = history; + } - @Override - public Loader getLoader(Context context) - { - return mLastLoader = new CustomCursorLoader(context, new SearchHistoryCursorFactory(context, null, mSeachHistory)); + @Override + public Loader getLoader(Context context) + { + return mLastLoader = new CustomCursorLoader(context, new SearchHistoryCursorFactory(context, null, mSeachHistory)); - } + } - /** - * Trigger an update for the last loader that has been created. - */ - public void forceUpdate() - { - if (mLastLoader != null && !mLastLoader.isAbandoned()) - { - mLastLoader.forceLoad(); - } - } + /** + * Trigger an update for the last loader that has been created. + */ + public void forceUpdate() + { + if (mLastLoader != null && !mLastLoader.isAbandoned()) + { + mLastLoader.forceLoad(); + } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorFactory.java index f1861c1a..f1cf9972 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorFactory.java @@ -17,228 +17,229 @@ package org.dmfs.tasks.groupings.cursorloaders; -import java.util.Arrays; -import java.util.List; -import java.util.TimeZone; - import android.database.Cursor; import android.database.MatrixCursor; import android.text.format.Time; +import java.util.Arrays; +import java.util.List; +import java.util.TimeZone; + /** * A factory that builds shiny new {@link Cursor}s with time ranges. - * + *

    * Note that all times are all-day and normalized to UTC. That means 2014-09-08 will be returned as 2014-09-08 00:00 UTC, no matter which time zone you're in. - * + *

    * TODO: fix javadoc - * + * * @author Marten Gajda */ public class TimeRangeCursorFactory extends AbstractCustomCursorFactory { - public final static String RANGE_ID = "_id"; - public final static String RANGE_TYPE = "type"; - - public final static int TYPE_END_OF_DAY = 0x01; - public final static int TYPE_END_OF_YESTERDAY = 0x02 | TYPE_END_OF_DAY; - public final static int TYPE_END_OF_TODAY = 0x04 | TYPE_END_OF_DAY; - public final static int TYPE_END_OF_TOMORROW = 0x08 | TYPE_END_OF_DAY; + public final static String RANGE_ID = "_id"; + public final static String RANGE_TYPE = "type"; - public final static int TYPE_END_IN_7_DAYS = 0x10 | TYPE_END_OF_DAY; + public final static int TYPE_END_OF_DAY = 0x01; + public final static int TYPE_END_OF_YESTERDAY = 0x02 | TYPE_END_OF_DAY; + public final static int TYPE_END_OF_TODAY = 0x04 | TYPE_END_OF_DAY; + public final static int TYPE_END_OF_TOMORROW = 0x08 | TYPE_END_OF_DAY; - /** - * Not supported yet - */ - public final static int TYPE_END_OF_A_WEEK = 0x0100; - /** - * Not supported yet - */ - public final static int TYPE_END_OF_LAST_WEEK = 0x0200 | TYPE_END_OF_A_WEEK; - /** - * Not supported yet - */ - public final static int TYPE_END_OF_THIS_WEEK = 0x0400 | TYPE_END_OF_A_WEEK; - /** - * Not supported yet - */ - public final static int TYPE_END_OF_NEXT_WEEK = 0x0800 | TYPE_END_OF_A_WEEK; + public final static int TYPE_END_IN_7_DAYS = 0x10 | TYPE_END_OF_DAY; - public final static int TYPE_END_OF_A_MONTH = 0x010000; - public final static int TYPE_END_OF_LAST_MONTH = 0x020000 | TYPE_END_OF_A_MONTH; - public final static int TYPE_END_OF_THIS_MONTH = 0x040000 | TYPE_END_OF_A_MONTH; - public final static int TYPE_END_OF_NEXT_MONTH = 0x080000 | TYPE_END_OF_A_MONTH; + /** + * Not supported yet + */ + public final static int TYPE_END_OF_A_WEEK = 0x0100; + /** + * Not supported yet + */ + public final static int TYPE_END_OF_LAST_WEEK = 0x0200 | TYPE_END_OF_A_WEEK; + /** + * Not supported yet + */ + public final static int TYPE_END_OF_THIS_WEEK = 0x0400 | TYPE_END_OF_A_WEEK; + /** + * Not supported yet + */ + public final static int TYPE_END_OF_NEXT_WEEK = 0x0800 | TYPE_END_OF_A_WEEK; - public final static int TYPE_END_OF_A_YEAR = 0x01000000; - public final static int TYPE_END_OF_LAST_YEAR = 0x02000000 | TYPE_END_OF_A_YEAR; - public final static int TYPE_END_OF_THIS_YEAR = 0x04000000 | TYPE_END_OF_A_YEAR; - public final static int TYPE_END_OF_NEXT_YEAR = 0x08000000 | TYPE_END_OF_A_YEAR; + public final static int TYPE_END_OF_A_MONTH = 0x010000; + public final static int TYPE_END_OF_LAST_MONTH = 0x020000 | TYPE_END_OF_A_MONTH; + public final static int TYPE_END_OF_THIS_MONTH = 0x040000 | TYPE_END_OF_A_MONTH; + public final static int TYPE_END_OF_NEXT_MONTH = 0x080000 | TYPE_END_OF_A_MONTH; - public final static int TYPE_OVERDUE = 0x20000000; + public final static int TYPE_END_OF_A_YEAR = 0x01000000; + public final static int TYPE_END_OF_LAST_YEAR = 0x02000000 | TYPE_END_OF_A_YEAR; + public final static int TYPE_END_OF_THIS_YEAR = 0x04000000 | TYPE_END_OF_A_YEAR; + public final static int TYPE_END_OF_NEXT_YEAR = 0x08000000 | TYPE_END_OF_A_YEAR; - public final static int TYPE_NO_END = 0x80000000; + public final static int TYPE_OVERDUE = 0x20000000; - public final static String RANGE_START = "start"; + public final static int TYPE_NO_END = 0x80000000; - public final static String RANGE_END = "end"; + public final static String RANGE_START = "start"; - public final static String RANGE_YEAR = "year"; + public final static String RANGE_END = "end"; - public final static String RANGE_MONTH = "month"; + public final static String RANGE_YEAR = "year"; - public final static String RANGE_OPEN_FUTURE = "open_future"; + public final static String RANGE_MONTH = "month"; - public final static String RANGE_OPEN_PAST = "open_past"; + public final static String RANGE_OPEN_FUTURE = "open_future"; - public final static String RANGE_NULL_ROW = "null_row"; + public final static String RANGE_OPEN_PAST = "open_past"; - public final static String RANGE_START_TZ_OFFSET = "start_tz_offset"; + public final static String RANGE_NULL_ROW = "null_row"; - public final static String RANGE_END_TZ_OFFSET = "end_tz_offset"; + public final static String RANGE_START_TZ_OFFSET = "start_tz_offset"; - public static final String[] DEFAULT_PROJECTION = new String[] { RANGE_START, RANGE_END, RANGE_ID, RANGE_YEAR, RANGE_MONTH, RANGE_OPEN_PAST, - RANGE_OPEN_FUTURE, RANGE_NULL_ROW, RANGE_TYPE, RANGE_START_TZ_OFFSET, RANGE_END_TZ_OFFSET }; + public final static String RANGE_END_TZ_OFFSET = "end_tz_offset"; - protected final static long MAX_TIME = Long.MAX_VALUE / 2; - protected final static long MIN_TIME = Long.MIN_VALUE / 2; + public static final String[] DEFAULT_PROJECTION = new String[] { + RANGE_START, RANGE_END, RANGE_ID, RANGE_YEAR, RANGE_MONTH, RANGE_OPEN_PAST, + RANGE_OPEN_FUTURE, RANGE_NULL_ROW, RANGE_TYPE, RANGE_START_TZ_OFFSET, RANGE_END_TZ_OFFSET }; - protected final List mProjectionList; + protected final static long MAX_TIME = Long.MAX_VALUE / 2; + protected final static long MIN_TIME = Long.MIN_VALUE / 2; - protected final Time mTime; - protected final TimeZone mTimezone; + protected final List mProjectionList; + protected final Time mTime; + protected final TimeZone mTimezone; - public TimeRangeCursorFactory(String[] projection) - { - super(projection); - mProjectionList = Arrays.asList(projection); - mTimezone = TimeZone.getDefault(); - mTime = new Time(mTimezone.getID()); - } + public TimeRangeCursorFactory(String[] projection) + { + super(projection); + mProjectionList = Arrays.asList(projection); + mTimezone = TimeZone.getDefault(); + mTime = new Time(mTimezone.getID()); + } - public Cursor getCursor() - { - mTime.setToNow(); - MatrixCursor result = new MatrixCursor(mProjection); + public Cursor getCursor() + { + mTime.setToNow(); - // get time of today 00:00:00 - Time time = new Time(mTimezone.getID()); - time.set(mTime.monthDay, mTime.month, mTime.year); + MatrixCursor result = new MatrixCursor(mProjection); - // null row, for tasks without due date - if (mProjectionList.contains(RANGE_NULL_ROW)) - { - result.addRow(makeRow(1, 0, null, null)); - } + // get time of today 00:00:00 + Time time = new Time(mTimezone.getID()); + time.set(mTime.monthDay, mTime.month, mTime.year); - long t1 = time.toMillis(false); + // null row, for tasks without due date + if (mProjectionList.contains(RANGE_NULL_ROW)) + { + result.addRow(makeRow(1, 0, null, null)); + } - // open past row for overdue tasks - if (mProjectionList.contains(RANGE_OPEN_PAST)) - { - result.addRow(makeRow(2, TYPE_END_OF_YESTERDAY, MIN_TIME, t1)); - } - - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + long t1 = time.toMillis(false); - // today row - long t2 = time.toMillis(false); - result.addRow(makeRow(3, TYPE_END_OF_TODAY, t1, t2)); - - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + // open past row for overdue tasks + if (mProjectionList.contains(RANGE_OPEN_PAST)) + { + result.addRow(makeRow(2, TYPE_END_OF_YESTERDAY, MIN_TIME, t1)); + } + + time.monthDay += 1; + time.yearDay += 1; + time.normalize(true); + + // today row + long t2 = time.toMillis(false); + result.addRow(makeRow(3, TYPE_END_OF_TODAY, t1, t2)); + + time.monthDay += 1; + time.yearDay += 1; + time.normalize(true); - // tomorrow row - long t3 = time.toMillis(false); - result.addRow(makeRow(4, TYPE_END_OF_TOMORROW, t2, t3)); + // tomorrow row + long t3 = time.toMillis(false); + result.addRow(makeRow(4, TYPE_END_OF_TOMORROW, t2, t3)); - time.monthDay += 5; - time.yearDay += 5; - time.normalize(true); + time.monthDay += 5; + time.yearDay += 5; + time.normalize(true); - // next week row - long t4 = time.toMillis(false); - result.addRow(makeRow(5, TYPE_END_IN_7_DAYS, t3, t4)); + // next week row + long t4 = time.toMillis(false); + result.addRow(makeRow(5, TYPE_END_IN_7_DAYS, t3, t4)); - time.set(1, time.month + 1, time.year); - time.normalize(true); + time.set(1, time.month + 1, time.year); + time.normalize(true); - // month row - long t5 = time.toMillis(false); - result.addRow(makeRow(6, TYPE_END_OF_A_MONTH, t4, t5)); + // month row + long t5 = time.toMillis(false); + result.addRow(makeRow(6, TYPE_END_OF_A_MONTH, t4, t5)); - time.set(1, 0, time.year + 1); - // rest of year row - long t6 = time.toMillis(false); - result.addRow(makeRow(7, TYPE_END_OF_A_YEAR, t5, t6)); + time.set(1, 0, time.year + 1); + // rest of year row + long t6 = time.toMillis(false); + result.addRow(makeRow(7, TYPE_END_OF_A_YEAR, t5, t6)); - // open future for future tasks - if (mProjectionList.contains(RANGE_OPEN_FUTURE)) - { - result.addRow(makeRow(8, TYPE_NO_END, t6, MAX_TIME)); - } + // open future for future tasks + if (mProjectionList.contains(RANGE_OPEN_FUTURE)) + { + result.addRow(makeRow(8, TYPE_NO_END, t6, MAX_TIME)); + } - return result; - } + return result; + } - protected Object[] makeRow(int id, int type, Long start, Long end) - { - Object[] result = new Object[mProjection.length]; - - insertValue(result, RANGE_ID, id); - insertValue(result, RANGE_TYPE, type); - insertValue(result, RANGE_START, start); - insertValue(result, RANGE_END, end); - - if (start != null && start > MIN_TIME && end != null && end < MAX_TIME) - { - mTime.set((start + end) >> 1); - insertValue(result, RANGE_YEAR, mTime.year); - insertValue(result, RANGE_MONTH, mTime.month); - } - - if (start == null || start <= MIN_TIME) - { - insertValue(result, RANGE_OPEN_PAST, 1); - insertValue(result, RANGE_START_TZ_OFFSET, 0); - } - else - { - insertValue(result, RANGE_START_TZ_OFFSET, mTimezone.getOffset(start)); - } - - if (end == null || end >= MAX_TIME) - { - insertValue(result, RANGE_OPEN_FUTURE, 1); - insertValue(result, RANGE_END_TZ_OFFSET, 0); - } - else - { - insertValue(result, RANGE_END_TZ_OFFSET, mTimezone.getOffset(end)); - } - - if (start == null && end == null) - { - insertValue(result, RANGE_NULL_ROW, 1); - } - - return result; - } - - - private void insertValue(Object[] row, String column, Object value) - { - int index = mProjectionList.indexOf(column); - if (index >= 0) - { - row[index] = value; - } - } + protected Object[] makeRow(int id, int type, Long start, Long end) + { + Object[] result = new Object[mProjection.length]; + + insertValue(result, RANGE_ID, id); + insertValue(result, RANGE_TYPE, type); + insertValue(result, RANGE_START, start); + insertValue(result, RANGE_END, end); + + if (start != null && start > MIN_TIME && end != null && end < MAX_TIME) + { + mTime.set((start + end) >> 1); + insertValue(result, RANGE_YEAR, mTime.year); + insertValue(result, RANGE_MONTH, mTime.month); + } + + if (start == null || start <= MIN_TIME) + { + insertValue(result, RANGE_OPEN_PAST, 1); + insertValue(result, RANGE_START_TZ_OFFSET, 0); + } + else + { + insertValue(result, RANGE_START_TZ_OFFSET, mTimezone.getOffset(start)); + } + + if (end == null || end >= MAX_TIME) + { + insertValue(result, RANGE_OPEN_FUTURE, 1); + insertValue(result, RANGE_END_TZ_OFFSET, 0); + } + else + { + insertValue(result, RANGE_END_TZ_OFFSET, mTimezone.getOffset(end)); + } + + if (start == null && end == null) + { + insertValue(result, RANGE_NULL_ROW, 1); + } + + return result; + } + + + private void insertValue(Object[] row, String column, Object value) + { + int index = mProjectionList.indexOf(column); + if (index >= 0) + { + row[index] = value; + } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoader.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoader.java index 20743357..386dc9ef 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoader.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoader.java @@ -17,80 +17,80 @@ package org.dmfs.tasks.groupings.cursorloaders; -import java.util.TimeZone; - -import org.dmfs.tasks.utils.TimeChangeListener; -import org.dmfs.tasks.utils.TimeChangeObserver; - import android.content.Context; import android.database.Cursor; import android.support.v4.content.Loader; import android.text.format.Time; +import org.dmfs.tasks.utils.TimeChangeListener; +import org.dmfs.tasks.utils.TimeChangeObserver; + +import java.util.TimeZone; + /** * A very simple {@link Loader} that returns the {@link Cursor} from a {@link TimeRangeCursorFactory}. It also delivers a new Cursor each time the time or the * time zone changes and each day at midnight. - * + * * @author Marten Gajda */ public class TimeRangeCursorLoader extends CustomCursorLoader implements TimeChangeListener { - /** - * A helper to retrieve the timestamp for midnight. - */ - private final Time mMidnight = new Time(); - private final TimeChangeObserver mTimeChangeObserver; - - - public TimeRangeCursorLoader(Context context, String[] projection) - { - super(context, new TimeRangeShortCursorFactory(projection)); - - // set trigger at midnight - mTimeChangeObserver = new TimeChangeObserver(context, this); - mTimeChangeObserver.setNextAlarm(getMidnightTimestamp()); - } - - - @Override - public void onTimeUpdate(TimeChangeObserver observer) - { - // reset next alarm - observer.setNextAlarm(getMidnightTimestamp()); - - // notify LoaderManager - onContentChanged(); - } - - - @Override - public void onAlarm(TimeChangeObserver observer) - { - // set next alarm - observer.setNextAlarm(getMidnightTimestamp()); - - // notify LoaderManager - onContentChanged(); - } - - - @Override - protected void onReset() - { - mTimeChangeObserver.releaseReceiver(); - super.onReset(); - } - - - private long getMidnightTimestamp() - { - mMidnight.clear(TimeZone.getDefault().getID()); - mMidnight.setToNow(); - mMidnight.set(mMidnight.monthDay, mMidnight.month, mMidnight.year); - ++mMidnight.monthDay; - mMidnight.normalize(true); - return mMidnight.toMillis(false); - } + /** + * A helper to retrieve the timestamp for midnight. + */ + private final Time mMidnight = new Time(); + private final TimeChangeObserver mTimeChangeObserver; + + + public TimeRangeCursorLoader(Context context, String[] projection) + { + super(context, new TimeRangeShortCursorFactory(projection)); + + // set trigger at midnight + mTimeChangeObserver = new TimeChangeObserver(context, this); + mTimeChangeObserver.setNextAlarm(getMidnightTimestamp()); + } + + + @Override + public void onTimeUpdate(TimeChangeObserver observer) + { + // reset next alarm + observer.setNextAlarm(getMidnightTimestamp()); + + // notify LoaderManager + onContentChanged(); + } + + + @Override + public void onAlarm(TimeChangeObserver observer) + { + // set next alarm + observer.setNextAlarm(getMidnightTimestamp()); + + // notify LoaderManager + onContentChanged(); + } + + + @Override + protected void onReset() + { + mTimeChangeObserver.releaseReceiver(); + super.onReset(); + } + + + private long getMidnightTimestamp() + { + mMidnight.clear(TimeZone.getDefault().getID()); + mMidnight.setToNow(); + mMidnight.set(mMidnight.monthDay, mMidnight.month, mMidnight.year); + ++mMidnight.monthDay; + mMidnight.normalize(true); + return mMidnight.toMillis(false); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoaderFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoaderFactory.java index 599ea63b..177dd4b2 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoaderFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoaderFactory.java @@ -24,23 +24,23 @@ import android.support.v4.content.Loader; /** * A factory that builds {@link TimeRangeCursorLoader}s. - * + * * @author Marten Gajda */ public class TimeRangeCursorLoaderFactory extends AbstractCursorLoaderFactory { - private final String[] mProjection; + private final String[] mProjection; - public TimeRangeCursorLoaderFactory(String[] projection) - { - mProjection = projection; - } + public TimeRangeCursorLoaderFactory(String[] projection) + { + mProjection = projection; + } - @Override - public Loader getLoader(Context context) - { - return new TimeRangeCursorLoader(context, mProjection); - } + @Override + public Loader getLoader(Context context) + { + return new TimeRangeCursorLoader(context, mProjection); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeShortCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeShortCursorFactory.java index 18b00de4..d33466a6 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeShortCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeShortCursorFactory.java @@ -24,61 +24,61 @@ import android.text.format.Time; /** * A factory that builds shorter {@link Cursor}s with time ranges. - * + *

    * Note that all times are all-day and normalized to UTC. That means 2014-09-08 will be returned as 2014-09-08 00:00 UTC, no matter which time zone you're in. - * + * * @author Tobias Reinsch * @author Marten Gajda */ public class TimeRangeShortCursorFactory extends TimeRangeCursorFactory { - public TimeRangeShortCursorFactory(String[] projection) - { - super(projection); - } + public TimeRangeShortCursorFactory(String[] projection) + { + super(projection); + } - @Override - public Cursor getCursor() - { - mTime.setToNow(); + @Override + public Cursor getCursor() + { + mTime.setToNow(); - MatrixCursor result = new MatrixCursor(mProjection); + MatrixCursor result = new MatrixCursor(mProjection); - Time time = new Time(mTimezone.getID()); - time.set(mTime.monthDay + 1, mTime.month, mTime.year); + Time time = new Time(mTimezone.getID()); + time.set(mTime.monthDay + 1, mTime.month, mTime.year); - // today row (including overdue) - long t2 = time.toMillis(false); - result.addRow(makeRow(1, TYPE_END_OF_TODAY, MIN_TIME, t2)); + // today row (including overdue) + long t2 = time.toMillis(false); + result.addRow(makeRow(1, TYPE_END_OF_TODAY, MIN_TIME, t2)); - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + time.monthDay += 1; + time.yearDay += 1; + time.normalize(true); - // tomorrow row - long t3 = time.toMillis(false); - result.addRow(makeRow(2, TYPE_END_OF_TOMORROW, t2, t3)); + // tomorrow row + long t3 = time.toMillis(false); + result.addRow(makeRow(2, TYPE_END_OF_TOMORROW, t2, t3)); - time.monthDay += 5; - time.yearDay += 5; - time.normalize(true); + time.monthDay += 5; + time.yearDay += 5; + time.normalize(true); - // next week row - long t4 = time.toMillis(false); - result.addRow(makeRow(3, TYPE_END_IN_7_DAYS, t3, t4)); + // next week row + long t4 = time.toMillis(false); + result.addRow(makeRow(3, TYPE_END_IN_7_DAYS, t3, t4)); - time.monthDay += 1; - time.normalize(true); + time.monthDay += 1; + time.normalize(true); - // open future for future tasks (including tasks without dates) - if (mProjectionList.contains(RANGE_OPEN_FUTURE)) - { - result.addRow(makeRow(4, TYPE_NO_END, t4, null)); - } + // open future for future tasks (including tasks without dates) + if (mProjectionList.contains(RANGE_OPEN_FUTURE)) + { + result.addRow(makeRow(4, TYPE_NO_END, t4, null)); + } - return result; - } + return result; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorFactory.java index 193c2fa4..e4135240 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorFactory.java @@ -24,75 +24,74 @@ import android.text.format.Time; /** * A factory that builds shorter {@link Cursor}s with time ranges. - * - * + * * @author Tobias Reinsch */ public class TimeRangeStartCursorFactory extends TimeRangeCursorFactory { - public TimeRangeStartCursorFactory(String[] projection) - { - super(projection); + public TimeRangeStartCursorFactory(String[] projection) + { + super(projection); - } + } - @Override - public Cursor getCursor() - { + @Override + public Cursor getCursor() + { - mTime.setToNow(); - ; + mTime.setToNow(); + ; - MatrixCursor result = new MatrixCursor(mProjection); + MatrixCursor result = new MatrixCursor(mProjection); - // get time of today 00:00:00 - Time time = new Time(mTime.timezone); - time.set(mTime.monthDay, mTime.month, mTime.year); + // get time of today 00:00:00 + Time time = new Time(mTime.timezone); + time.set(mTime.monthDay, mTime.month, mTime.year); - // already started row - long t1 = time.toMillis(false); - result.addRow(makeRow(1, TYPE_OVERDUE, MIN_TIME, t1)); + // already started row + long t1 = time.toMillis(false); + result.addRow(makeRow(1, TYPE_OVERDUE, MIN_TIME, t1)); - time.hour = 0; - time.minute = 0; - time.second = 0; + time.hour = 0; + time.minute = 0; + time.second = 0; - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + time.monthDay += 1; + time.yearDay += 1; + time.normalize(true); - // today row - long t2 = time.toMillis(false); - result.addRow(makeRow(2, TYPE_END_OF_TODAY, t1, t2)); + // today row + long t2 = time.toMillis(false); + result.addRow(makeRow(2, TYPE_END_OF_TODAY, t1, t2)); - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + time.monthDay += 1; + time.yearDay += 1; + time.normalize(true); - // tomorrow row - long t3 = time.toMillis(false); - result.addRow(makeRow(3, TYPE_END_OF_TOMORROW, t2, t3)); + // tomorrow row + long t3 = time.toMillis(false); + result.addRow(makeRow(3, TYPE_END_OF_TOMORROW, t2, t3)); - time.monthDay += 5; - time.yearDay += 5; - time.normalize(true); + time.monthDay += 5; + time.yearDay += 5; + time.normalize(true); - // next week row - long t4 = time.toMillis(false); - result.addRow(makeRow(4, TYPE_END_IN_7_DAYS, t3, t4)); + // next week row + long t4 = time.toMillis(false); + result.addRow(makeRow(4, TYPE_END_IN_7_DAYS, t3, t4)); - time.monthDay += 1; - time.normalize(true); + time.monthDay += 1; + time.normalize(true); - // open past future for future tasks (including tasks without dates) - if (mProjectionList.contains(RANGE_OPEN_FUTURE)) - { - result.addRow(makeRow(5, TYPE_NO_END, t4, MAX_TIME)); - } + // open past future for future tasks (including tasks without dates) + if (mProjectionList.contains(RANGE_OPEN_FUTURE)) + { + result.addRow(makeRow(5, TYPE_NO_END, t4, MAX_TIME)); + } - return result; - } + return result; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoader.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoader.java index 7f2c3037..2b4bedae 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoader.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoader.java @@ -17,80 +17,80 @@ package org.dmfs.tasks.groupings.cursorloaders; -import java.util.TimeZone; - -import org.dmfs.tasks.utils.TimeChangeListener; -import org.dmfs.tasks.utils.TimeChangeObserver; - import android.content.Context; import android.database.Cursor; import android.support.v4.content.Loader; import android.text.format.Time; +import org.dmfs.tasks.utils.TimeChangeListener; +import org.dmfs.tasks.utils.TimeChangeObserver; + +import java.util.TimeZone; + /** * A very simple {@link Loader} that returns the {@link Cursor} from a {@link TimeRangeStartCursorFactory}. It also delivers a new Cursor each time the time or * the time zone changes and each day at midnight. - * + * * @author Tobias Reinsch */ public class TimeRangeStartCursorLoader extends CustomCursorLoader implements TimeChangeListener { - /** - * A helper to retrieve the timestamp for midnight. - */ - private final Time mMidnight = new Time(); - private final TimeChangeObserver mTimeChangeObserver; - - - public TimeRangeStartCursorLoader(Context context, String[] projection) - { - super(context, new TimeRangeStartCursorFactory(projection)); - - // set trigger at midnight - mTimeChangeObserver = new TimeChangeObserver(context, this); - mTimeChangeObserver.setNextAlarm(getMidnightTimestamp()); - } - - - @Override - public void onTimeUpdate(TimeChangeObserver observer) - { - // reset next alarm - observer.setNextAlarm(getMidnightTimestamp()); - - // notify LoaderManager - onContentChanged(); - } - - - @Override - public void onAlarm(TimeChangeObserver observer) - { - // set next alarm - observer.setNextAlarm(getMidnightTimestamp()); - - // notify LoaderManager - onContentChanged(); - } - - - @Override - protected void onReset() - { - mTimeChangeObserver.releaseReceiver(); - super.onReset(); - } - - - private long getMidnightTimestamp() - { - mMidnight.clear(TimeZone.getDefault().getID()); - mMidnight.setToNow(); - mMidnight.set(mMidnight.monthDay, mMidnight.month, mMidnight.year); - ++mMidnight.monthDay; - mMidnight.normalize(true); - return mMidnight.toMillis(false); - } + /** + * A helper to retrieve the timestamp for midnight. + */ + private final Time mMidnight = new Time(); + private final TimeChangeObserver mTimeChangeObserver; + + + public TimeRangeStartCursorLoader(Context context, String[] projection) + { + super(context, new TimeRangeStartCursorFactory(projection)); + + // set trigger at midnight + mTimeChangeObserver = new TimeChangeObserver(context, this); + mTimeChangeObserver.setNextAlarm(getMidnightTimestamp()); + } + + + @Override + public void onTimeUpdate(TimeChangeObserver observer) + { + // reset next alarm + observer.setNextAlarm(getMidnightTimestamp()); + + // notify LoaderManager + onContentChanged(); + } + + + @Override + public void onAlarm(TimeChangeObserver observer) + { + // set next alarm + observer.setNextAlarm(getMidnightTimestamp()); + + // notify LoaderManager + onContentChanged(); + } + + + @Override + protected void onReset() + { + mTimeChangeObserver.releaseReceiver(); + super.onReset(); + } + + + private long getMidnightTimestamp() + { + mMidnight.clear(TimeZone.getDefault().getID()); + mMidnight.setToNow(); + mMidnight.set(mMidnight.monthDay, mMidnight.month, mMidnight.year); + ++mMidnight.monthDay; + mMidnight.normalize(true); + return mMidnight.toMillis(false); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoaderFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoaderFactory.java index eff24048..af911bb3 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoaderFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoaderFactory.java @@ -24,23 +24,23 @@ import android.support.v4.content.Loader; /** * A factory that builds {@link TimeRangeStartCursorLoader}s. - * + * * @author Tobias Reinsch */ public class TimeRangeStartCursorLoaderFactory extends AbstractCursorLoaderFactory { - private final String[] mProjection; + private final String[] mProjection; - public TimeRangeStartCursorLoaderFactory(String[] projection) - { - mProjection = projection; - } + public TimeRangeStartCursorLoaderFactory(String[] projection) + { + mProjection = projection; + } - @Override - public Loader getLoader(Context context) - { - return new TimeRangeStartCursorLoader(context, mProjection); - } + @Override + public Loader getLoader(Context context) + { + return new TimeRangeStartCursorLoader(context, mProjection); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/AbstractFilter.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/AbstractFilter.java index 0f96ace6..55fbc04f 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/AbstractFilter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/AbstractFilter.java @@ -22,26 +22,25 @@ import java.util.List; /** * An abstract filter for child cursors in a grouped list. - * + * * @author Marten Gajda */ public interface AbstractFilter { - /** - * Append the selection part of this filter to a {@link StringBuilder}. This is much more efficiently when you're using a StringBuilder anyway. - * - * @param stringBuilder - * The {@link StringBuilder} where to append the selection string. - */ - public abstract void getSelection(StringBuilder stringBuilder); - + /** + * Append the selection part of this filter to a {@link StringBuilder}. This is much more efficiently when you're using a StringBuilder anyway. + * + * @param stringBuilder + * The {@link StringBuilder} where to append the selection string. + */ + public abstract void getSelection(StringBuilder stringBuilder); - /** - * Append the selection arguments of this filter to a {@link List} of {@link String}s. - * - * @param selectionArgs - * The List where to append the arguments. - */ - public abstract void getSelectionArgs(List selectionArgs); + /** + * Append the selection arguments of this filter to a {@link List} of {@link String}s. + * + * @param selectionArgs + * The List where to append the arguments. + */ + public abstract void getSelectionArgs(List selectionArgs); } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/AndFilter.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/AndFilter.java index 559f6f45..12d99dfe 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/AndFilter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/AndFilter.java @@ -19,13 +19,13 @@ package org.dmfs.tasks.groupings.filters; /** * A filter that joins a list of {@link AbstractFilter}s using the "AND" operator. - * + * * @author Marten Gajda */ public final class AndFilter extends BinaryOperationFilter { - public AndFilter(AbstractFilter... filters) - { - super("AND", filters); - } + public AndFilter(AbstractFilter... filters) + { + super("AND", filters); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/BinaryOperationFilter.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/BinaryOperationFilter.java index 83b9c9c0..b82318f0 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/BinaryOperationFilter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/BinaryOperationFilter.java @@ -22,67 +22,67 @@ import java.util.List; /** * A filter that joins a list of {@link AbstractFilter}s using the specified operator. - * + * * @author Marten Gajda */ public class BinaryOperationFilter implements AbstractFilter { - private final AbstractFilter[] mFilters; - private final String mOperator; + private final AbstractFilter[] mFilters; + private final String mOperator; - /** - * Create a new filter that joins a list of {@link AbstractFilter}s using the specified operator. - * - * @param operator - * The operator to use (must be a valid binary boolean operator like "OR" or "AND"). - * @param filters - * A number of {@link AbstractFilter}s. - */ - public BinaryOperationFilter(String operator, AbstractFilter... filters) - { - mFilters = filters; - mOperator = operator; - } + /** + * Create a new filter that joins a list of {@link AbstractFilter}s using the specified operator. + * + * @param operator + * The operator to use (must be a valid binary boolean operator like "OR" or "AND"). + * @param filters + * A number of {@link AbstractFilter}s. + */ + public BinaryOperationFilter(String operator, AbstractFilter... filters) + { + mFilters = filters; + mOperator = operator; + } - @Override - public final void getSelection(StringBuilder stringBuilder) - { - AbstractFilter[] filters = mFilters; - if (filters.length == 0) - { - // return a valid filter that always matches - stringBuilder.append("1=1"); - return; - } + @Override + public final void getSelection(StringBuilder stringBuilder) + { + AbstractFilter[] filters = mFilters; + if (filters.length == 0) + { + // return a valid filter that always matches + stringBuilder.append("1=1"); + return; + } - boolean first = true; - for (AbstractFilter filter : filters) - { - if (first) - { - first = false; - stringBuilder.append("("); - } - else - { - stringBuilder.append(" ("); - stringBuilder.append(mOperator); - stringBuilder.append(" ("); - } - filter.getSelection(stringBuilder); - } - stringBuilder.append(")"); - } + boolean first = true; + for (AbstractFilter filter : filters) + { + if (first) + { + first = false; + stringBuilder.append("("); + } + else + { + stringBuilder.append(" ("); + stringBuilder.append(mOperator); + stringBuilder.append(" ("); + } + filter.getSelection(stringBuilder); + } + stringBuilder.append(")"); + } - @Override - public final void getSelectionArgs(List selectionArgs) - { - for (AbstractFilter filter : mFilters) - { - filter.getSelectionArgs(selectionArgs); - } - } + @Override + public final void getSelectionArgs(List selectionArgs) + { + for (AbstractFilter filter : mFilters) + { + filter.getSelectionArgs(selectionArgs); + } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/ConstantFilter.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/ConstantFilter.java index 140fd15a..f9d77c85 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/ConstantFilter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/ConstantFilter.java @@ -22,50 +22,50 @@ import java.util.List; /** * A filter that filters by a constant selection. - * + * * @author Marten Gajda */ public final class ConstantFilter implements AbstractFilter { - private final String mSelection; - private final String[] mSelectionArgs; + private final String mSelection; + private final String[] mSelectionArgs; - /** - * Creates a ConstantFilter. - * - * @param selection - * The selection string. - * @param selectionArgs - * The positional selection arguments. - */ - public ConstantFilter(String selection, String... selectionArgs) - { - mSelection = selection; - mSelectionArgs = selectionArgs; - } + /** + * Creates a ConstantFilter. + * + * @param selection + * The selection string. + * @param selectionArgs + * The positional selection arguments. + */ + public ConstantFilter(String selection, String... selectionArgs) + { + mSelection = selection; + mSelectionArgs = selectionArgs; + } - @Override - public void getSelection(StringBuilder stringBuilder) - { - if (mSelection != null) - { - stringBuilder.append(mSelection); - } - } + @Override + public void getSelection(StringBuilder stringBuilder) + { + if (mSelection != null) + { + stringBuilder.append(mSelection); + } + } - @Override - public void getSelectionArgs(List selectionArgs) - { - if (mSelectionArgs != null) - { - // append all arguments, if any - for (String arg : mSelectionArgs) - { - selectionArgs.add(arg); - } - } - } + @Override + public void getSelectionArgs(List selectionArgs) + { + if (mSelectionArgs != null) + { + // append all arguments, if any + for (String arg : mSelectionArgs) + { + selectionArgs.add(arg); + } + } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/OrFilter.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/OrFilter.java index 7f616126..73f65df9 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/OrFilter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/filters/OrFilter.java @@ -19,13 +19,13 @@ package org.dmfs.tasks.groupings.filters; /** * A filter that joins a list of {@link AbstractFilter}s using the "OR" operator. - * + * * @author Marten Gajda */ public final class OrFilter extends BinaryOperationFilter { - public OrFilter(AbstractFilter... filters) - { - super("OR", filters); - } + public OrFilter(AbstractFilter... filters) + { + super("OR", filters); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListSelectionFragment.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListSelectionFragment.java index 2db7df48..4313e9a5 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListSelectionFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListSelectionFragment.java @@ -17,14 +17,6 @@ package org.dmfs.tasks.homescreen; -import java.util.ArrayList; - -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.TaskContract.TaskLists; -import org.dmfs.tasks.R; -import org.dmfs.tasks.utils.TasksListCursorAdapter; -import org.dmfs.tasks.utils.TasksListCursorAdapter.SelectionEnabledListener; - import android.app.Activity; import android.database.Cursor; import android.net.Uri; @@ -41,145 +33,153 @@ import android.view.ViewGroup; import android.widget.ListView; import android.widget.TextView; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.TaskContract.TaskLists; +import org.dmfs.tasks.R; +import org.dmfs.tasks.utils.TasksListCursorAdapter; +import org.dmfs.tasks.utils.TasksListCursorAdapter.SelectionEnabledListener; + +import java.util.ArrayList; + /** * Provides the selection of task list. - * + * * @author Tobias Reinsch - * */ public class TaskListSelectionFragment extends ListFragment implements LoaderManager.LoaderCallbacks { - public static final String LIST_LOADER_URI = "uri"; - public static final String LIST_LOADER_FILTER = "filter"; - - public static final String LIST_LOADER_VISIBLE_LISTS_FILTER = TaskLists.SYNC_ENABLED + "=1"; - - /** - * Projection into the task list. - */ - private final static String[] TASK_LIST_PROJECTION = new String[] { TaskContract.TaskListColumns._ID, TaskContract.TaskListColumns.LIST_NAME, - TaskContract.TaskListSyncColumns.ACCOUNT_TYPE, TaskContract.TaskListSyncColumns.ACCOUNT_NAME, TaskContract.TaskListColumns.LIST_COLOR }; - - private TasksListCursorAdapter mTaskListAdapter; - private Activity mActivity; - private ListView mTaskList; - private String mAuthority; - private onSelectionListener mListener; - private TextView mButtonOk; - private TextView mButtonCancel; - - - public TaskListSelectionFragment(onSelectionListener listener) - { - mListener = listener; - } - - - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - mActivity = activity; - mAuthority = TaskContract.taskAuthority(activity); - } - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { - View rootView = inflater.inflate(R.layout.fragment_task_list_selection, container, false); - mButtonOk = (TextView) rootView.findViewById(android.R.id.button1); - mButtonCancel = (TextView) rootView.findViewById(android.R.id.button2); - - mButtonOk.setOnClickListener(new OnClickListener() - { - @Override - public void onClick(View v) - { - if (mListener != null) - { - mListener.onSelection(mTaskListAdapter.getSelectedLists()); - } - - } - }); - mButtonCancel.setOnClickListener(new OnClickListener() - { - @Override - public void onClick(View v) - { - if (mListener != null) - { - mListener.onSelectionCancel(); - } - - } - }); - - return rootView; - } - - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) - { - super.onActivityCreated(savedInstanceState); - mTaskList = getListView(); - mTaskListAdapter = new TasksListCursorAdapter(mActivity); - mTaskList.setAdapter(mTaskListAdapter); - - mTaskListAdapter.setSelectionEnabledListener(new SelectionEnabledListener() - { - @Override - public void onSelectionEnabled() - { - mButtonOk.setEnabled(true); - } - - - @Override - public void onSelectionDisabled() - { - mButtonOk.setEnabled(false); - - } - }); - - Bundle bundle = new Bundle(); - bundle.putParcelable(LIST_LOADER_URI, TaskLists.getContentUri(mAuthority)); - bundle.putString(LIST_LOADER_FILTER, LIST_LOADER_VISIBLE_LISTS_FILTER); - getLoaderManager().restartLoader(-2, bundle, this); - } - - - @Override - public Loader onCreateLoader(int id, Bundle bundle) - { - return new CursorLoader(mActivity, (Uri) bundle.getParcelable(LIST_LOADER_URI), TASK_LIST_PROJECTION, bundle.getString(LIST_LOADER_FILTER), null, null); - } - - - @Override - public void onLoadFinished(Loader loader, Cursor cursor) - { - mTaskListAdapter.changeCursor(cursor); - } - - - @Override - public void onLoaderReset(Loader loader) - { - mTaskListAdapter.changeCursor(null); - } - - public interface onSelectionListener - { - public void onSelection(ArrayList selectedLists); - - - public void onSelectionCancel(); - } + public static final String LIST_LOADER_URI = "uri"; + public static final String LIST_LOADER_FILTER = "filter"; + + public static final String LIST_LOADER_VISIBLE_LISTS_FILTER = TaskLists.SYNC_ENABLED + "=1"; + + /** + * Projection into the task list. + */ + private final static String[] TASK_LIST_PROJECTION = new String[] { + TaskContract.TaskListColumns._ID, TaskContract.TaskListColumns.LIST_NAME, + TaskContract.TaskListSyncColumns.ACCOUNT_TYPE, TaskContract.TaskListSyncColumns.ACCOUNT_NAME, TaskContract.TaskListColumns.LIST_COLOR }; + + private TasksListCursorAdapter mTaskListAdapter; + private Activity mActivity; + private ListView mTaskList; + private String mAuthority; + private onSelectionListener mListener; + private TextView mButtonOk; + private TextView mButtonCancel; + + + public TaskListSelectionFragment(onSelectionListener listener) + { + mListener = listener; + } + + + @Override + public void onAttach(Activity activity) + { + super.onAttach(activity); + mActivity = activity; + mAuthority = TaskContract.taskAuthority(activity); + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + View rootView = inflater.inflate(R.layout.fragment_task_list_selection, container, false); + mButtonOk = (TextView) rootView.findViewById(android.R.id.button1); + mButtonCancel = (TextView) rootView.findViewById(android.R.id.button2); + + mButtonOk.setOnClickListener(new OnClickListener() + { + @Override + public void onClick(View v) + { + if (mListener != null) + { + mListener.onSelection(mTaskListAdapter.getSelectedLists()); + } + + } + }); + mButtonCancel.setOnClickListener(new OnClickListener() + { + @Override + public void onClick(View v) + { + if (mListener != null) + { + mListener.onSelectionCancel(); + } + + } + }); + + return rootView; + } + + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + mTaskList = getListView(); + mTaskListAdapter = new TasksListCursorAdapter(mActivity); + mTaskList.setAdapter(mTaskListAdapter); + + mTaskListAdapter.setSelectionEnabledListener(new SelectionEnabledListener() + { + @Override + public void onSelectionEnabled() + { + mButtonOk.setEnabled(true); + } + + + @Override + public void onSelectionDisabled() + { + mButtonOk.setEnabled(false); + + } + }); + + Bundle bundle = new Bundle(); + bundle.putParcelable(LIST_LOADER_URI, TaskLists.getContentUri(mAuthority)); + bundle.putString(LIST_LOADER_FILTER, LIST_LOADER_VISIBLE_LISTS_FILTER); + getLoaderManager().restartLoader(-2, bundle, this); + } + + + @Override + public Loader onCreateLoader(int id, Bundle bundle) + { + return new CursorLoader(mActivity, (Uri) bundle.getParcelable(LIST_LOADER_URI), TASK_LIST_PROJECTION, bundle.getString(LIST_LOADER_FILTER), null, null); + } + + + @Override + public void onLoadFinished(Loader loader, Cursor cursor) + { + mTaskListAdapter.changeCursor(cursor); + } + + + @Override + public void onLoaderReset(Loader loader) + { + mTaskListAdapter.changeCursor(null); + } + + + public interface onSelectionListener + { + public void onSelection(ArrayList selectedLists); + + public void onSelectionCancel(); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java index 316b1724..7b4f76d6 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java @@ -24,104 +24,114 @@ import android.text.format.Time; /** * This Class is used to for storing data of a single task in the task list widget. - * + * * @author Arjun Naik * @author Marten Gajda */ public class TaskListWidgetItem { - /** The task title. */ - private final String mTaskTitle; - - /** The due date. */ - private final Time mDueDate; - - /** The task color. */ - private final int mTaskColor; - - /** The task ID. */ - private final int mTaskId; - - /** The flag to indicate if task is closed. */ - private final boolean mIsClosed; - - - /** - * Instantiates a new task list widget item. - * - * @param id - * the id of the task - * @param title - * the title of the task - * @param due - * the due date of the task - * @param color - * the color of the list of the task - * @param isClosed - * the flag to indicate if closed - */ - public TaskListWidgetItem(int id, String title, Time due, int color, boolean isClosed) - { - mTaskId = id; - mTaskTitle = title; - mDueDate = due; - mTaskColor = color; - mIsClosed = isClosed; - } - - - /** - * Gets the task color. - * - * @return the task color - */ - public int getTaskColor() - { - return mTaskColor; - } - - - /** - * Gets the due date. - * - * @return the due date - */ - public Time getDueDate() - { - return mDueDate; - } - - - /** - * Gets the task title. - * - * @return the task title - */ - public String getTaskTitle() - { - return mTaskTitle; - } - - - /** - * Gets the task id. - * - * @return the task id - */ - public long getTaskId() - { - return mTaskId; - } - - - /** - * Gets the checks if is closed. - * - * @return the checks if is closed - */ - public boolean getIsClosed() - { - return mIsClosed; - } + /** + * The task title. + */ + private final String mTaskTitle; + + /** + * The due date. + */ + private final Time mDueDate; + + /** + * The task color. + */ + private final int mTaskColor; + + /** + * The task ID. + */ + private final int mTaskId; + + /** + * The flag to indicate if task is closed. + */ + private final boolean mIsClosed; + + + /** + * Instantiates a new task list widget item. + * + * @param id + * the id of the task + * @param title + * the title of the task + * @param due + * the due date of the task + * @param color + * the color of the list of the task + * @param isClosed + * the flag to indicate if closed + */ + public TaskListWidgetItem(int id, String title, Time due, int color, boolean isClosed) + { + mTaskId = id; + mTaskTitle = title; + mDueDate = due; + mTaskColor = color; + mIsClosed = isClosed; + } + + + /** + * Gets the task color. + * + * @return the task color + */ + public int getTaskColor() + { + return mTaskColor; + } + + + /** + * Gets the due date. + * + * @return the due date + */ + public Time getDueDate() + { + return mDueDate; + } + + + /** + * Gets the task title. + * + * @return the task title + */ + public String getTaskTitle() + { + return mTaskTitle; + } + + + /** + * Gets the task id. + * + * @return the task id + */ + public long getTaskId() + { + return mTaskId; + } + + + /** + * Gets the checks if is closed. + * + * @return the checks if is closed + */ + public boolean getIsClosed() + { + return mIsClosed; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProvider.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProvider.java index 01179e8d..6bcea15e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProvider.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProvider.java @@ -50,151 +50,158 @@ import java.util.ArrayList; /** * The provider for the widget on Android Honeycomb and up. - * + * * @author Arjun Naik * @author Tobias Reinsch */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class TaskListWidgetProvider extends AppWidgetProvider { - private final static String TAG = "TaskListWidgetProvider"; - public static String ACTION_CREATE_TASK = "CreateTask"; - - /* - * Override the onReceive method from the {@link BroadcastReceiver } class so that we can intercept broadcast for manual refresh of widget. - * - * @see android.appwidget.AppWidgetProvider#onReceive(android.content.Context, android.content.Intent) - */ - @Override - public void onReceive(Context context, Intent intent) - { - super.onReceive(context, intent); - - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); - int[] appWidgetIds = appWidgetManager.getAppWidgetIds(getComponentName(context)); - String action = intent.getAction(); - if (action.equals(Intent.ACTION_PROVIDER_CHANGED)) - { - appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.task_list_widget_lv); - } - else if(action.equals(ACTION_CREATE_TASK)) - { - int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0); - WidgetConfigurationDatabaseHelper configHelper = new WidgetConfigurationDatabaseHelper(context); - SQLiteDatabase db = configHelper.getReadableDatabase(); - ArrayList widgetLists = WidgetConfigurationDatabaseHelper.loadTaskLists(db, widgetId); - db.close(); - ArrayList writableLists = new ArrayList<>(); - String authority = TaskContract.taskAuthority(context); - if(!widgetLists.isEmpty()) { - Cursor cursor = context.getContentResolver().query( - TaskLists.getContentUri(authority), - new String[]{TaskLists._ID}, - TaskLists.SYNC_ENABLED + "=1 AND " + TaskLists._ID + " IN (" + TextUtils.join(",", widgetLists) + ")", - null, - null); - if (cursor != null) { - while (cursor.moveToNext()) { - writableLists.add(cursor.getLong(0)); - } - cursor.close(); - } - } - Intent editTaskIntent = new Intent(Intent.ACTION_INSERT); - editTaskIntent.setData(Tasks.getContentUri(authority)); - editTaskIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if(!writableLists.isEmpty()) - { - Long preselectList; - if(writableLists.size()==1) { - // if there is only one list, then select this one - preselectList = writableLists.get(0); - } else { - // if there are multiple lists, then select the most recently used - preselectList = RecentlyUsedLists.getRecentFromList(context, writableLists); - } - Log.d(getClass().getSimpleName(), "create task with preselected list "+preselectList); - ContentSet contentSet = new ContentSet(Tasks.getContentUri(authority)); - contentSet.put(Tasks.LIST_ID, preselectList); - Bundle extraBundle = new Bundle(); - extraBundle.putParcelable(EditTaskActivity.EXTRA_DATA_CONTENT_SET, contentSet); - editTaskIntent.putExtra(EditTaskActivity.EXTRA_DATA_BUNDLE, extraBundle); - } - context.startActivity(editTaskIntent); - } - } - - - protected ComponentName getComponentName(Context context) - { - return new ComponentName(context, TaskListWidgetProvider.class); - } - - - /* - * This method is called periodically to update the widget. - * - * @see android.appwidget.AppWidgetProvider#onUpdate(android.content.Context, android.appwidget.AppWidgetManager, int[]) - */ - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) - { - /* - * Iterate over all the widgets of this type and update them individually. + private final static String TAG = "TaskListWidgetProvider"; + public static String ACTION_CREATE_TASK = "CreateTask"; + + + /* + * Override the onReceive method from the {@link BroadcastReceiver } class so that we can intercept broadcast for manual refresh of widget. + * + * @see android.appwidget.AppWidgetProvider#onReceive(android.content.Context, android.content.Intent) + */ + @Override + public void onReceive(Context context, Intent intent) + { + super.onReceive(context, intent); + + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(getComponentName(context)); + String action = intent.getAction(); + if (action.equals(Intent.ACTION_PROVIDER_CHANGED)) + { + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.task_list_widget_lv); + } + else if (action.equals(ACTION_CREATE_TASK)) + { + int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0); + WidgetConfigurationDatabaseHelper configHelper = new WidgetConfigurationDatabaseHelper(context); + SQLiteDatabase db = configHelper.getReadableDatabase(); + ArrayList widgetLists = WidgetConfigurationDatabaseHelper.loadTaskLists(db, widgetId); + db.close(); + ArrayList writableLists = new ArrayList<>(); + String authority = TaskContract.taskAuthority(context); + if (!widgetLists.isEmpty()) + { + Cursor cursor = context.getContentResolver().query( + TaskLists.getContentUri(authority), + new String[] { TaskLists._ID }, + TaskLists.SYNC_ENABLED + "=1 AND " + TaskLists._ID + " IN (" + TextUtils.join(",", widgetLists) + ")", + null, + null); + if (cursor != null) + { + while (cursor.moveToNext()) + { + writableLists.add(cursor.getLong(0)); + } + cursor.close(); + } + } + Intent editTaskIntent = new Intent(Intent.ACTION_INSERT); + editTaskIntent.setData(Tasks.getContentUri(authority)); + editTaskIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (!writableLists.isEmpty()) + { + Long preselectList; + if (writableLists.size() == 1) + { + // if there is only one list, then select this one + preselectList = writableLists.get(0); + } + else + { + // if there are multiple lists, then select the most recently used + preselectList = RecentlyUsedLists.getRecentFromList(context, writableLists); + } + Log.d(getClass().getSimpleName(), "create task with preselected list " + preselectList); + ContentSet contentSet = new ContentSet(Tasks.getContentUri(authority)); + contentSet.put(Tasks.LIST_ID, preselectList); + Bundle extraBundle = new Bundle(); + extraBundle.putParcelable(EditTaskActivity.EXTRA_DATA_CONTENT_SET, contentSet); + editTaskIntent.putExtra(EditTaskActivity.EXTRA_DATA_BUNDLE, extraBundle); + } + context.startActivity(editTaskIntent); + } + } + + + protected ComponentName getComponentName(Context context) + { + return new ComponentName(context, TaskListWidgetProvider.class); + } + + + /* + * This method is called periodically to update the widget. + * + * @see android.appwidget.AppWidgetProvider#onUpdate(android.content.Context, android.appwidget.AppWidgetManager, int[]) + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) + { + /* + * Iterate over all the widgets of this type and update them individually. */ - for (int i = 0; i < appWidgetIds.length; i++) - { - Log.d(TAG, "updating widget " + i); - - /** Create an Intent with the {@link RemoteViewsService } and pass it the Widget Id */ - Intent remoteServiceIntent = new Intent(context, TaskListWidgetUpdaterService.class); - remoteServiceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]); - remoteServiceIntent.setData(Uri.parse(remoteServiceIntent.toUri(Intent.URI_INTENT_SCHEME))); - - RemoteViews widget = new RemoteViews(context.getPackageName(), R.layout.task_list_widget); - - /** Add pending Intent to start the Tasks app when the title is clicked */ - Intent tasksAppIntent = new Intent(context, TaskListActivity.class); - tasksAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PendingIntent taskAppPI = PendingIntent.getActivity(context, 0, tasksAppIntent, PendingIntent.FLAG_UPDATE_CURRENT); - widget.setOnClickPendingIntent(android.R.id.button1, taskAppPI); - - /** Add a pending Intent to start new Task Activity on the new Task Button */ - Intent editTaskIntent = new Intent(context, TaskListWidgetProvider.class); - editTaskIntent.setAction(ACTION_CREATE_TASK); - editTaskIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]); - PendingIntent newTaskPI = PendingIntent.getBroadcast(context, appWidgetIds[i], editTaskIntent, PendingIntent.FLAG_UPDATE_CURRENT); - widget.setOnClickPendingIntent(android.R.id.button2, newTaskPI); - - /** Set the {@link RemoteViewsService } subclass as the adapter for the {@link ListView} in the widget. */ - if (android.os.Build.VERSION.SDK_INT < 14) - { - widget.setRemoteAdapter(appWidgetIds[i], R.id.task_list_widget_lv, remoteServiceIntent); - } - else - { - widget.setRemoteAdapter(R.id.task_list_widget_lv, remoteServiceIntent); - } - - Intent detailIntent = new Intent(Intent.ACTION_VIEW); - detailIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, true); - PendingIntent clickPI = PendingIntent.getActivity(context, 0, detailIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - widget.setPendingIntentTemplate(R.id.task_list_widget_lv, clickPI); + for (int i = 0; i < appWidgetIds.length; i++) + { + Log.d(TAG, "updating widget " + i); + + /** Create an Intent with the {@link RemoteViewsService } and pass it the Widget Id */ + Intent remoteServiceIntent = new Intent(context, TaskListWidgetUpdaterService.class); + remoteServiceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]); + remoteServiceIntent.setData(Uri.parse(remoteServiceIntent.toUri(Intent.URI_INTENT_SCHEME))); + + RemoteViews widget = new RemoteViews(context.getPackageName(), R.layout.task_list_widget); + + /** Add pending Intent to start the Tasks app when the title is clicked */ + Intent tasksAppIntent = new Intent(context, TaskListActivity.class); + tasksAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent taskAppPI = PendingIntent.getActivity(context, 0, tasksAppIntent, PendingIntent.FLAG_UPDATE_CURRENT); + widget.setOnClickPendingIntent(android.R.id.button1, taskAppPI); + + /** Add a pending Intent to start new Task Activity on the new Task Button */ + Intent editTaskIntent = new Intent(context, TaskListWidgetProvider.class); + editTaskIntent.setAction(ACTION_CREATE_TASK); + editTaskIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]); + PendingIntent newTaskPI = PendingIntent.getBroadcast(context, appWidgetIds[i], editTaskIntent, PendingIntent.FLAG_UPDATE_CURRENT); + widget.setOnClickPendingIntent(android.R.id.button2, newTaskPI); + + /** Set the {@link RemoteViewsService } subclass as the adapter for the {@link ListView} in the widget. */ + if (android.os.Build.VERSION.SDK_INT < 14) + { + widget.setRemoteAdapter(appWidgetIds[i], R.id.task_list_widget_lv, remoteServiceIntent); + } + else + { + widget.setRemoteAdapter(R.id.task_list_widget_lv, remoteServiceIntent); + } + + Intent detailIntent = new Intent(Intent.ACTION_VIEW); + detailIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, true); + PendingIntent clickPI = PendingIntent.getActivity(context, 0, detailIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + widget.setPendingIntentTemplate(R.id.task_list_widget_lv, clickPI); /* Finally update the widget */ - appWidgetManager.updateAppWidget(appWidgetIds[i], widget); - } - } + appWidgetManager.updateAppWidget(appWidgetIds[i], widget); + } + } - @Override - public void onDeleted(Context context, int[] appWidgetIds) - { - // Delete configuration - WidgetConfigurationDatabaseHelper dbHelper = new WidgetConfigurationDatabaseHelper(context); - dbHelper.deleteWidgetConfiguration(appWidgetIds); + @Override + public void onDeleted(Context context, int[] appWidgetIds) + { + // Delete configuration + WidgetConfigurationDatabaseHelper dbHelper = new WidgetConfigurationDatabaseHelper(context); + dbHelper.deleteWidgetConfiguration(appWidgetIds); - super.onDeleted(context, appWidgetIds); - } + super.onDeleted(context, appWidgetIds); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLarge.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLarge.java index f9f95eb6..97c0087d 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLarge.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLarge.java @@ -28,34 +28,35 @@ import android.os.Build; /** * The provider for the large homescreen widget on Android Honeycomb and up. + * * @author Tobias Reinsch */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class TaskListWidgetProviderLarge extends TaskListWidgetProvider { - /* - * Override the onReceive method from the {@link BroadcastReceiver } class so that we can intercept broadcast for manual refresh of widget. - * - * @see android.appwidget.AppWidgetProvider#onReceive(android.content.Context, android.content.Intent) - */ - @Override - public void onReceive(Context context, Intent intent) - { - super.onReceive(context, intent); - } - - - @Override - public void onDeleted(Context context, int[] appWidgetIds) - { - super.onDeleted(context, appWidgetIds); - } - - - @Override - protected ComponentName getComponentName(Context context) - { - return new ComponentName(context, TaskListWidgetProviderLarge.class); - } + /* + * Override the onReceive method from the {@link BroadcastReceiver } class so that we can intercept broadcast for manual refresh of widget. + * + * @see android.appwidget.AppWidgetProvider#onReceive(android.content.Context, android.content.Intent) + */ + @Override + public void onReceive(Context context, Intent intent) + { + super.onReceive(context, intent); + } + + + @Override + public void onDeleted(Context context, int[] appWidgetIds) + { + super.onDeleted(context, appWidgetIds); + } + + + @Override + protected ComponentName getComponentName(Context context) + { + return new ComponentName(context, TaskListWidgetProviderLarge.class); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLargeLegacy.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLargeLegacy.java index 21573837..bd0ab944 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLargeLegacy.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLargeLegacy.java @@ -19,15 +19,9 @@ package org.dmfs.tasks.homescreen; -import org.dmfs.tasks.R; -import org.dmfs.tasks.utils.WidgetConfigurationDatabaseHelper; - -import android.annotation.TargetApi; -import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.os.Build; /** @@ -39,27 +33,28 @@ import android.os.Build; public class TaskListWidgetProviderLargeLegacy extends TaskListWidgetProviderLegacy { - /* - * Override the onReceive method from the {@link BroadcastReceiver } class so that we can intercept broadcast for manual refresh of widget. - * - * @see android.appwidget.AppWidgetProvider#onReceive(android.content.Context, android.content.Intent) - */ - @Override - public void onReceive(Context context, Intent intent) - { - super.onReceive(context, intent); - } - - @Override - public void onDeleted(Context context, int[] appWidgetIds) - { - super.onDeleted(context, appWidgetIds); - } - - - @Override - protected ComponentName getComponentName(Context context) - { - return new ComponentName(context, TaskListWidgetProviderLargeLegacy.class); - } + /* + * Override the onReceive method from the {@link BroadcastReceiver } class so that we can intercept broadcast for manual refresh of widget. + * + * @see android.appwidget.AppWidgetProvider#onReceive(android.content.Context, android.content.Intent) + */ + @Override + public void onReceive(Context context, Intent intent) + { + super.onReceive(context, intent); + } + + + @Override + public void onDeleted(Context context, int[] appWidgetIds) + { + super.onDeleted(context, appWidgetIds); + } + + + @Override + protected ComponentName getComponentName(Context context) + { + return new ComponentName(context, TaskListWidgetProviderLargeLegacy.class); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLegacy.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLegacy.java index dc70e5c7..32d4f50a 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLegacy.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLegacy.java @@ -48,138 +48,138 @@ import java.util.TimeZone; /** * The provider for the small widget for legacy Android 2.x devices - * + * * @author Tobias Reinsch */ public class TaskListWidgetProviderLegacy extends AppWidgetProvider { - private final static String TAG = "TaskListWidgetProvider"; - - - /* - * Override the onReceive method from the {@link BroadcastReceiver } class so that we can intercept broadcast for manual refresh of widget. - * - * @see android.appwidget.AppWidgetProvider#onReceive(android.content.Context, android.content.Intent) - */ - @Override - public void onReceive(Context context, Intent intent) - { - super.onReceive(context, intent); - - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); - int[] appWidgetIds = appWidgetManager.getAppWidgetIds(getComponentName(context)); - String action = intent.getAction(); - if (action.equals(Intent.ACTION_PROVIDER_CHANGED)) - { - // on Android 2.x we update the widget directly - onUpdate(context, appWidgetManager, appWidgetIds); - } - } - - - protected ComponentName getComponentName(Context context) - { - return new ComponentName(context, TaskListWidgetProviderLegacy.class); - } - - - /* - * This method is called periodically to update the widget. - * - * @see android.appwidget.AppWidgetProvider#onUpdate(android.content.Context, android.appwidget.AppWidgetManager, int[]) - */ - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - @SuppressWarnings("deprecation") - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) - { - String authority = TaskContract.taskAuthority(context); - - RemoteViews widget = new RemoteViews(context.getPackageName(), R.layout.task_list_widget); - widget.removeAllViews(android.R.id.list); - DateFormatter dateFormatter = new DateFormatter(context); - ContentResolver resolver = context.getContentResolver(); - Cursor cursor = resolver.query(TaskContract.Instances.getContentUri(authority), null, - TaskContract.Instances.VISIBLE + ">0 and " + TaskContract.Instances.IS_CLOSED + "=0 AND (" + TaskContract.Instances.INSTANCE_START + "<=" - + System.currentTimeMillis() + " OR " + TaskContract.Instances.INSTANCE_START + " is null OR " + TaskContract.Instances.INSTANCE_START + " = " - + TaskContract.Instances.INSTANCE_DUE + " )", - null, TaskContract.Instances.INSTANCE_DUE + " is null, " + TaskContract.Instances.DEFAULT_SORT_ORDER + ", " + TaskContract.Instances.PRIORITY - + " is null, " + TaskContract.Instances.PRIORITY + ", " + TaskContract.Instances.CREATED + " DESC"); - - cursor.moveToFirst(); - int count = 0; - Time now = new Time(); - now.clear(TimeZone.getDefault().getID()); - now.setToNow(); - now.normalize(true); - Resources resources = context.getResources(); - while (!cursor.isAfterLast() && count < 7) - { - RemoteViews taskItem = new RemoteViews(context.getPackageName(), R.layout.task_list_widget_item); - int taskColor = TaskFieldAdapters.LIST_COLOR.get(cursor); - taskItem.setInt(R.id.task_list_color, "setBackgroundColor", taskColor); - String title = TaskFieldAdapters.TITLE.get(cursor); - taskItem.setTextViewText(android.R.id.title, title); - Time dueDate = TaskFieldAdapters.DUE.get(cursor); - if (dueDate != null) - { - dueDate.normalize(true); - - taskItem.setTextViewText(android.R.id.text1, dateFormatter.format(dueDate, DateFormatContext.WIDGET_VIEW)); - - // highlight overdue dates & times - if ((!dueDate.allDay && dueDate.before(now) - || dueDate.allDay && (dueDate.year < now.year || dueDate.yearDay <= now.yearDay && dueDate.year == now.year)) - && !TaskFieldAdapters.IS_CLOSED.get(cursor)) - { - taskItem.setTextColor(android.R.id.text1, resources.getColor(R.color.holo_red_light)); - } - else - { - taskItem.setTextColor(android.R.id.text1, resources.getColor(R.color.lighter_gray)); - } - } + private final static String TAG = "TaskListWidgetProvider"; + + + /* + * Override the onReceive method from the {@link BroadcastReceiver } class so that we can intercept broadcast for manual refresh of widget. + * + * @see android.appwidget.AppWidgetProvider#onReceive(android.content.Context, android.content.Intent) + */ + @Override + public void onReceive(Context context, Intent intent) + { + super.onReceive(context, intent); + + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(getComponentName(context)); + String action = intent.getAction(); + if (action.equals(Intent.ACTION_PROVIDER_CHANGED)) + { + // on Android 2.x we update the widget directly + onUpdate(context, appWidgetManager, appWidgetIds); + } + } + + + protected ComponentName getComponentName(Context context) + { + return new ComponentName(context, TaskListWidgetProviderLegacy.class); + } + + + /* + * This method is called periodically to update the widget. + * + * @see android.appwidget.AppWidgetProvider#onUpdate(android.content.Context, android.appwidget.AppWidgetManager, int[]) + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + @SuppressWarnings("deprecation") + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) + { + String authority = TaskContract.taskAuthority(context); + + RemoteViews widget = new RemoteViews(context.getPackageName(), R.layout.task_list_widget); + widget.removeAllViews(android.R.id.list); + DateFormatter dateFormatter = new DateFormatter(context); + ContentResolver resolver = context.getContentResolver(); + Cursor cursor = resolver.query(TaskContract.Instances.getContentUri(authority), null, + TaskContract.Instances.VISIBLE + ">0 and " + TaskContract.Instances.IS_CLOSED + "=0 AND (" + TaskContract.Instances.INSTANCE_START + "<=" + + System.currentTimeMillis() + " OR " + TaskContract.Instances.INSTANCE_START + " is null OR " + TaskContract.Instances.INSTANCE_START + " = " + + TaskContract.Instances.INSTANCE_DUE + " )", + null, TaskContract.Instances.INSTANCE_DUE + " is null, " + TaskContract.Instances.DEFAULT_SORT_ORDER + ", " + TaskContract.Instances.PRIORITY + + " is null, " + TaskContract.Instances.PRIORITY + ", " + TaskContract.Instances.CREATED + " DESC"); + + cursor.moveToFirst(); + int count = 0; + Time now = new Time(); + now.clear(TimeZone.getDefault().getID()); + now.setToNow(); + now.normalize(true); + Resources resources = context.getResources(); + while (!cursor.isAfterLast() && count < 7) + { + RemoteViews taskItem = new RemoteViews(context.getPackageName(), R.layout.task_list_widget_item); + int taskColor = TaskFieldAdapters.LIST_COLOR.get(cursor); + taskItem.setInt(R.id.task_list_color, "setBackgroundColor", taskColor); + String title = TaskFieldAdapters.TITLE.get(cursor); + taskItem.setTextViewText(android.R.id.title, title); + Time dueDate = TaskFieldAdapters.DUE.get(cursor); + if (dueDate != null) + { + dueDate.normalize(true); + + taskItem.setTextViewText(android.R.id.text1, dateFormatter.format(dueDate, DateFormatContext.WIDGET_VIEW)); + + // highlight overdue dates & times + if ((!dueDate.allDay && dueDate.before(now) + || dueDate.allDay && (dueDate.year < now.year || dueDate.yearDay <= now.yearDay && dueDate.year == now.year)) + && !TaskFieldAdapters.IS_CLOSED.get(cursor)) + { + taskItem.setTextColor(android.R.id.text1, resources.getColor(R.color.holo_red_light)); + } + else + { + taskItem.setTextColor(android.R.id.text1, resources.getColor(R.color.lighter_gray)); + } + } /* - * Create and set a {@link PendingIntent} to view the task when the list is clicked. + * Create and set a {@link PendingIntent} to view the task when the list is clicked. */ - Intent itemClickIntent = new Intent(); - itemClickIntent.setAction(Intent.ACTION_VIEW); - itemClickIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, true); - itemClickIntent.setData(ContentUris.withAppendedId(Tasks.getContentUri(authority), TaskFieldAdapters.TASK_ID.get(cursor))); - PendingIntent launchPI = PendingIntent.getActivity(context, 0, itemClickIntent, 0); - taskItem.setOnClickPendingIntent(R.id.widget_list_item, launchPI); - - widget.addView(android.R.id.list, taskItem); - cursor.moveToNext(); - count++; - } - cursor.close(); - - /** Add pending Intent to start the Tasks app when the title is clicked */ - Intent tasksAppIntent = new Intent(context, TaskListActivity.class); - tasksAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PendingIntent taskAppPI = PendingIntent.getActivity(context, 0, tasksAppIntent, PendingIntent.FLAG_UPDATE_CURRENT); - widget.setOnClickPendingIntent(android.R.id.button1, taskAppPI); - - /** Add a pending Intent to start new Task Activity on the new Task Button */ - Intent editTaskIntent = new Intent(Intent.ACTION_INSERT); - editTaskIntent.setData(Tasks.getContentUri(authority)); - editTaskIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PendingIntent newTaskPI = PendingIntent.getActivity(context, 0, editTaskIntent, PendingIntent.FLAG_UPDATE_CURRENT); - widget.setOnClickPendingIntent(android.R.id.button2, newTaskPI); - - appWidgetManager.updateAppWidget(appWidgetIds, widget); - return; - } - - - @Override - public void onDeleted(Context context, int[] appWidgetIds) - { - // Delete configuration - WidgetConfigurationDatabaseHelper dbHelper = new WidgetConfigurationDatabaseHelper(context); - dbHelper.deleteWidgetConfiguration(appWidgetIds); - - super.onDeleted(context, appWidgetIds); - } + Intent itemClickIntent = new Intent(); + itemClickIntent.setAction(Intent.ACTION_VIEW); + itemClickIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, true); + itemClickIntent.setData(ContentUris.withAppendedId(Tasks.getContentUri(authority), TaskFieldAdapters.TASK_ID.get(cursor))); + PendingIntent launchPI = PendingIntent.getActivity(context, 0, itemClickIntent, 0); + taskItem.setOnClickPendingIntent(R.id.widget_list_item, launchPI); + + widget.addView(android.R.id.list, taskItem); + cursor.moveToNext(); + count++; + } + cursor.close(); + + /** Add pending Intent to start the Tasks app when the title is clicked */ + Intent tasksAppIntent = new Intent(context, TaskListActivity.class); + tasksAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent taskAppPI = PendingIntent.getActivity(context, 0, tasksAppIntent, PendingIntent.FLAG_UPDATE_CURRENT); + widget.setOnClickPendingIntent(android.R.id.button1, taskAppPI); + + /** Add a pending Intent to start new Task Activity on the new Task Button */ + Intent editTaskIntent = new Intent(Intent.ACTION_INSERT); + editTaskIntent.setData(Tasks.getContentUri(authority)); + editTaskIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent newTaskPI = PendingIntent.getActivity(context, 0, editTaskIntent, PendingIntent.FLAG_UPDATE_CURRENT); + widget.setOnClickPendingIntent(android.R.id.button2, newTaskPI); + + appWidgetManager.updateAppWidget(appWidgetIds, widget); + return; + } + + + @Override + public void onDeleted(Context context, int[] appWidgetIds) + { + // Delete configuration + WidgetConfigurationDatabaseHelper dbHelper = new WidgetConfigurationDatabaseHelper(context); + dbHelper.deleteWidgetConfiguration(appWidgetIds); + + super.onDeleted(context, appWidgetIds); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetSettingsActivity.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetSettingsActivity.java index bc6e6234..dd94b840 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetSettingsActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetSettingsActivity.java @@ -17,103 +17,102 @@ package org.dmfs.tasks.homescreen; -import java.util.ArrayList; - -import org.dmfs.tasks.R; -import org.dmfs.tasks.homescreen.TaskListSelectionFragment.onSelectionListener; -import org.dmfs.tasks.utils.WidgetConfigurationDatabaseHelper; - import android.appwidget.AppWidgetManager; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.support.v4.app.FragmentActivity; +import org.dmfs.tasks.R; +import org.dmfs.tasks.homescreen.TaskListSelectionFragment.onSelectionListener; +import org.dmfs.tasks.utils.WidgetConfigurationDatabaseHelper; + +import java.util.ArrayList; + /** * Allows to configure the task list widget prior to adding to the home screen - * + * * @author Tobias Reinsch - * */ public class TaskListWidgetSettingsActivity extends FragmentActivity implements onSelectionListener { - private int mAppWidgetId; - private Intent mResultIntent; + private int mAppWidgetId; + private Intent mResultIntent; - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_widget_configuration); + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_widget_configuration); - Intent intent = getIntent(); - if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) - { - mAppWidgetId = intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID); + Intent intent = getIntent(); + if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) + { + mAppWidgetId = intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID); - // make the result intent and set the result to canceled - mResultIntent = new Intent(); - mResultIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); - setResult(RESULT_CANCELED, mResultIntent); - } + // make the result intent and set the result to canceled + mResultIntent = new Intent(); + mResultIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); + setResult(RESULT_CANCELED, mResultIntent); + } - TaskListSelectionFragment fragment = new TaskListSelectionFragment(this); - getSupportFragmentManager().beginTransaction().add(R.id.task_list_selection_container, fragment).commit(); - } + TaskListSelectionFragment fragment = new TaskListSelectionFragment(this); + getSupportFragmentManager().beginTransaction().add(R.id.task_list_selection_container, fragment).commit(); + } - @Override - public void onSelection(ArrayList selectedLists) - { - persistSelectedTaskLists(selectedLists); - finishWithResult(true); + @Override + public void onSelection(ArrayList selectedLists) + { + persistSelectedTaskLists(selectedLists); + finishWithResult(true); - } + } - @Override - public void onSelectionCancel() - { - finishWithResult(false); + @Override + public void onSelectionCancel() + { + finishWithResult(false); - } + } - private void persistSelectedTaskLists(ArrayList lists) - { - WidgetConfigurationDatabaseHelper dbHelper = new WidgetConfigurationDatabaseHelper(this); - SQLiteDatabase db = dbHelper.getWritableDatabase(); + private void persistSelectedTaskLists(ArrayList lists) + { + WidgetConfigurationDatabaseHelper dbHelper = new WidgetConfigurationDatabaseHelper(this); + SQLiteDatabase db = dbHelper.getWritableDatabase(); - // delete old configuration - WidgetConfigurationDatabaseHelper.deleteConfiguration(db, mAppWidgetId); + // delete old configuration + WidgetConfigurationDatabaseHelper.deleteConfiguration(db, mAppWidgetId); - // add new configuration - for (Long taskId : lists) - { - WidgetConfigurationDatabaseHelper.insertTaskList(db, mAppWidgetId, taskId); - } - db.close(); - } + // add new configuration + for (Long taskId : lists) + { + WidgetConfigurationDatabaseHelper.insertTaskList(db, mAppWidgetId, taskId); + } + db.close(); + } - private void finishWithResult(boolean ok) - { - Bundle bundle = new Bundle(); - bundle.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); - Intent intent = new Intent(); - intent.putExtras(bundle); + private void finishWithResult(boolean ok) + { + Bundle bundle = new Bundle(); + bundle.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); + Intent intent = new Intent(); + intent.putExtras(bundle); - if (ok) - { - setResult(RESULT_OK, intent); - } - else - { - setResult(RESULT_CANCELED, intent); - } + if (ok) + { + setResult(RESULT_OK, intent); + } + else + { + setResult(RESULT_CANCELED, intent); + } - finish(); - } + finish(); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java index d468ac9b..a0950aee 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java @@ -56,388 +56,404 @@ import java.util.concurrent.Executors; *

    * TODO: add support for multiple widgets with different configuration *

    - * + * * @author Arjun Naik * @author Marten Gajda */ @SuppressLint("NewApi") public class TaskListWidgetUpdaterService extends RemoteViewsService { - private final static String TAG = "TaskListWidgetUpdaterService"; - - - /* - * Return an instance of {@link TaskListViewsFactory} - * - * @see android.widget.RemoteViewsService#onGetViewFactory(android.content.Intent) - */ - @Override - public RemoteViewsFactory onGetViewFactory(Intent intent) - { - - return new TaskListViewsFactory(this, intent); - } - - /** - * This class implements the {@link RemoteViewsFactory} interface. It provides the data for the {@link TaskListWidgetProvider}. It loads the due tasks - * asynchronously using a {@link CursorLoader}. It also provides methods to the remote views to retrieve the data. - */ - public static class TaskListViewsFactory implements RemoteViewsService.RemoteViewsFactory, TimeChangeListener - { - /** The {@link TaskListWidgetItem} array which stores the tasks to be displayed. When the cursor loads it is updated. */ - private TaskListWidgetItem[] mItems = null; - - /** The {@link Context} of the {@link Application} to which this widget belongs. */ - private Context mContext; - - /** The app widget id. */ - private int mAppWidgetId = -1; - - /** This variable is used to store the current time for reference. */ - private Time mNow; - - /** The resource from the {@link Application}. */ - private Resources mResources; - - /** The due date formatter. */ - private DateFormatter mDueDateFormatter; - - /** - * The executor to reload the tasks. - */ - private final Executor mExecutor = Executors.newSingleThreadExecutor(); - - private String mAuthority; - - /** A status variable that is used in onDataSetChanged to switch between updating the view and reloading the the whole dataset **/ - private boolean mDoNotReload; - - - /** - * Instantiates a new task list views factory. - * - * @param context - * the context - * @param intent - * the intent - */ - public TaskListViewsFactory(Context context, Intent intent) - { - mContext = context; - mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - mResources = context.getResources(); - mDueDateFormatter = new DateFormatter(context); - new TimeChangeObserver(context, this); - mAuthority = TaskContract.taskAuthority(context); - } - - - public void reload() - { - mExecutor.execute(mReloadTasks); - } - - - /** - * Required for the broadcast receiver. - */ - public TaskListViewsFactory() - { - } - - - /* - * (non-Javadoc) - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#onCreate() - */ - @Override - public void onCreate() - { - } - - - /* - * (non-Javadoc) - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#onDestroy() - */ - @Override - public void onDestroy() - { - // no-op - } - - - /* - * (non-Javadoc) - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#getCount() - */ - @Override - public int getCount() - { - if (mItems == null) - { - return 0; - } - return (mItems.length); - } - - - /* - * (non-Javadoc) - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#getViewAt(int) - */ - @Override - public RemoteViews getViewAt(int position) - { - TaskListWidgetItem[] items = mItems; - - /** We use this check because there is a small gap between when the database is updated and the widget is notified */ - if (items == null || position < 0 || position >= items.length) - { - return null; - } - RemoteViews row = new RemoteViews(mContext.getPackageName(), R.layout.task_list_widget_item); - - String taskTitle = items[position].getTaskTitle(); - row.setTextViewText(android.R.id.title, taskTitle); - row.setInt(R.id.task_list_color, "setBackgroundColor", items[position].getTaskColor()); - - Time dueDate = items[position].getDueDate(); - if (dueDate != null) - { - if (mNow == null) - { - mNow = new Time(); - } - mNow.clear(TimeZone.getDefault().getID()); - mNow.setToNow(); - dueDate.normalize(true); - mNow.normalize(true); - - row.setTextViewText(android.R.id.text1, mDueDateFormatter.format(dueDate, DateFormatContext.WIDGET_VIEW)); - - // highlight overdue dates & times - if ((!dueDate.allDay && dueDate.before(mNow) || dueDate.allDay - && (dueDate.year < mNow.year || dueDate.yearDay <= mNow.yearDay && dueDate.year == mNow.year)) - && !items[position].getIsClosed()) - { - row.setTextColor(android.R.id.text1, mResources.getColor(R.color.holo_red_light)); - } - else - { - row.setTextColor(android.R.id.text1, mResources.getColor(R.color.lighter_gray)); - } - } - else - { - row.setTextViewText(android.R.id.text1, null); - } - - - Uri taskUri = ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), items[position].getTaskId()); - Intent i = new Intent(); - i.setData(taskUri); - row.setOnClickFillInIntent(R.id.widget_list_item, i); - - return (row); - } - - - /* - * Don't show any loading views - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#getLoadingView() - */ - @Override - public RemoteViews getLoadingView() - { - return null; - } - - - /* - * Only single type of list item. - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#getViewTypeCount() - */ - @Override - public int getViewTypeCount() - { - return 1; - } - - - /* - * The position corresponds to the ID. - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#getItemId(int) - */ - @Override - public long getItemId(int position) - { - return position; - } - - - /* - * - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#hasStableIds() - */ - @Override - public boolean hasStableIds() - { - return true; - } - - - /* - * Nothing to do when data set is changed. - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#onDataSetChanged() - */ - @Override - public void onDataSetChanged() - { - if (mDoNotReload) - { - mDoNotReload = false; - } - else - { - reload(); - } - } - - - /* - * @see org.dmfs.tasks.utils.TimeChangeListener#onTimeUpdate(org.dmfs.tasks.utils.TimeChangeObserver) - */ - @Override - public void onTimeUpdate(TimeChangeObserver timeChangeObserver) - { - // reload the tasks - reload(); - } - - - /* - * This function is not used. - * - * @see org.dmfs.tasks.utils.TimeChangeListener#onAlarm(org.dmfs.tasks.utils.TimeChangeObserver) - */ - @Override - public void onAlarm(TimeChangeObserver timeChangeObserver) - { - // Not listening for Alarms in this service. - } - - - /** - * Gets the array of {@link TaskListWidgetItem}s. - * - * @return the widget items - */ - public static TaskListWidgetItem[] getWidgetItems(Cursor mTasksCursor) - { - if (mTasksCursor.getCount() > 0) - { - TaskListWidgetItem[] items = new TaskListWidgetItem[mTasksCursor.getCount()]; - int itemIndex = 0; - - while (mTasksCursor.moveToNext()) - { - items[itemIndex] = new TaskListWidgetItem(TaskFieldAdapters.INSTANCE_TASK_ID.get(mTasksCursor), TaskFieldAdapters.TITLE.get(mTasksCursor), - TaskFieldAdapters.DUE.get(mTasksCursor), TaskFieldAdapters.LIST_COLOR.get(mTasksCursor), TaskFieldAdapters.IS_CLOSED.get(mTasksCursor)); - itemIndex++; - } - return items; - } - return null; - } - - /** - * A {@link Runnable} that loads the tasks to show in the widget. - */ - private Runnable mReloadTasks = new Runnable() - { - - @Override - public void run() - { - // load TaskLists for this widget - WidgetConfigurationDatabaseHelper configHelper = new WidgetConfigurationDatabaseHelper(mContext); - SQLiteDatabase db = configHelper.getWritableDatabase(); - - ArrayList lists = WidgetConfigurationDatabaseHelper.loadTaskLists(db, mAppWidgetId); - db.close(); - - // build selection string - StringBuilder selection = new StringBuilder(TaskContract.Instances.VISIBLE + ">0 and " + TaskContract.Instances.IS_CLOSED + "=0 AND (" - + TaskContract.Instances.INSTANCE_START + "<=" + System.currentTimeMillis() + " OR " + TaskContract.Instances.INSTANCE_START - + " is null OR " + TaskContract.Instances.INSTANCE_START + " = " + TaskContract.Instances.INSTANCE_DUE + " )"); - - if (lists != null && !lists.isEmpty()) - { - selection.append(" AND ( "); - - for (int i = 0; i < lists.size(); i++) - { - Long listId = lists.get(i); - - if (i < lists.size() - 1) - { - selection.append(Instances.LIST_ID).append(" = ").append(listId).append(" OR "); - } - else - { - selection.append(Instances.LIST_ID).append(" = ").append(listId).append(" ) "); - } - } - } - - // load all upcoming non-completed tasks - Cursor c = mContext.getContentResolver().query( - TaskContract.Instances.getContentUri(mAuthority), - null, - selection.toString(), - null, - Instances.INSTANCE_DUE + " is null, " + Instances.DEFAULT_SORT_ORDER + ", " - + Instances.PRIORITY + " is null, " + Instances.PRIORITY + ", " - + Instances.INSTANCE_START + " is null, " + Instances.INSTANCE_START_SORTING + ", " - + Instances.CREATED + " DESC"); - - if (c != null) - { - try - { - mItems = getWidgetItems(c); - } - finally - { - c.close(); - } - } - else - { - mItems = new TaskListWidgetItem[0]; - } - - // tell to only update the view in the next onDataSetChanged(); - mDoNotReload = true; - - // notify the widget manager about the update - AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext); - if (mAppWidgetId != -1) - { - widgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.task_list_widget_lv); - - } - } - }; - } + private final static String TAG = "TaskListWidgetUpdaterService"; + + + /* + * Return an instance of {@link TaskListViewsFactory} + * + * @see android.widget.RemoteViewsService#onGetViewFactory(android.content.Intent) + */ + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) + { + + return new TaskListViewsFactory(this, intent); + } + + + /** + * This class implements the {@link RemoteViewsFactory} interface. It provides the data for the {@link TaskListWidgetProvider}. It loads the due tasks + * asynchronously using a {@link CursorLoader}. It also provides methods to the remote views to retrieve the data. + */ + public static class TaskListViewsFactory implements RemoteViewsService.RemoteViewsFactory, TimeChangeListener + { + /** + * The {@link TaskListWidgetItem} array which stores the tasks to be displayed. When the cursor loads it is updated. + */ + private TaskListWidgetItem[] mItems = null; + + /** + * The {@link Context} of the {@link Application} to which this widget belongs. + */ + private Context mContext; + + /** + * The app widget id. + */ + private int mAppWidgetId = -1; + + /** + * This variable is used to store the current time for reference. + */ + private Time mNow; + + /** + * The resource from the {@link Application}. + */ + private Resources mResources; + + /** + * The due date formatter. + */ + private DateFormatter mDueDateFormatter; + + /** + * The executor to reload the tasks. + */ + private final Executor mExecutor = Executors.newSingleThreadExecutor(); + + private String mAuthority; + + /** + * A status variable that is used in onDataSetChanged to switch between updating the view and reloading the the whole dataset + **/ + private boolean mDoNotReload; + + + /** + * Instantiates a new task list views factory. + * + * @param context + * the context + * @param intent + * the intent + */ + public TaskListViewsFactory(Context context, Intent intent) + { + mContext = context; + mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + mResources = context.getResources(); + mDueDateFormatter = new DateFormatter(context); + new TimeChangeObserver(context, this); + mAuthority = TaskContract.taskAuthority(context); + } + + + public void reload() + { + mExecutor.execute(mReloadTasks); + } + + + /** + * Required for the broadcast receiver. + */ + public TaskListViewsFactory() + { + } + + + /* + * (non-Javadoc) + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#onCreate() + */ + @Override + public void onCreate() + { + } + + + /* + * (non-Javadoc) + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#onDestroy() + */ + @Override + public void onDestroy() + { + // no-op + } + + + /* + * (non-Javadoc) + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#getCount() + */ + @Override + public int getCount() + { + if (mItems == null) + { + return 0; + } + return (mItems.length); + } + + + /* + * (non-Javadoc) + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#getViewAt(int) + */ + @Override + public RemoteViews getViewAt(int position) + { + TaskListWidgetItem[] items = mItems; + + /** We use this check because there is a small gap between when the database is updated and the widget is notified */ + if (items == null || position < 0 || position >= items.length) + { + return null; + } + RemoteViews row = new RemoteViews(mContext.getPackageName(), R.layout.task_list_widget_item); + + String taskTitle = items[position].getTaskTitle(); + row.setTextViewText(android.R.id.title, taskTitle); + row.setInt(R.id.task_list_color, "setBackgroundColor", items[position].getTaskColor()); + + Time dueDate = items[position].getDueDate(); + if (dueDate != null) + { + if (mNow == null) + { + mNow = new Time(); + } + mNow.clear(TimeZone.getDefault().getID()); + mNow.setToNow(); + dueDate.normalize(true); + mNow.normalize(true); + + row.setTextViewText(android.R.id.text1, mDueDateFormatter.format(dueDate, DateFormatContext.WIDGET_VIEW)); + + // highlight overdue dates & times + if ((!dueDate.allDay && dueDate.before(mNow) || dueDate.allDay + && (dueDate.year < mNow.year || dueDate.yearDay <= mNow.yearDay && dueDate.year == mNow.year)) + && !items[position].getIsClosed()) + { + row.setTextColor(android.R.id.text1, mResources.getColor(R.color.holo_red_light)); + } + else + { + row.setTextColor(android.R.id.text1, mResources.getColor(R.color.lighter_gray)); + } + } + else + { + row.setTextViewText(android.R.id.text1, null); + } + + Uri taskUri = ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), items[position].getTaskId()); + Intent i = new Intent(); + i.setData(taskUri); + row.setOnClickFillInIntent(R.id.widget_list_item, i); + + return (row); + } + + + /* + * Don't show any loading views + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#getLoadingView() + */ + @Override + public RemoteViews getLoadingView() + { + return null; + } + + + /* + * Only single type of list item. + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#getViewTypeCount() + */ + @Override + public int getViewTypeCount() + { + return 1; + } + + + /* + * The position corresponds to the ID. + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#getItemId(int) + */ + @Override + public long getItemId(int position) + { + return position; + } + + + /* + * + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#hasStableIds() + */ + @Override + public boolean hasStableIds() + { + return true; + } + + + /* + * Nothing to do when data set is changed. + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#onDataSetChanged() + */ + @Override + public void onDataSetChanged() + { + if (mDoNotReload) + { + mDoNotReload = false; + } + else + { + reload(); + } + } + + + /* + * @see org.dmfs.tasks.utils.TimeChangeListener#onTimeUpdate(org.dmfs.tasks.utils.TimeChangeObserver) + */ + @Override + public void onTimeUpdate(TimeChangeObserver timeChangeObserver) + { + // reload the tasks + reload(); + } + + + /* + * This function is not used. + * + * @see org.dmfs.tasks.utils.TimeChangeListener#onAlarm(org.dmfs.tasks.utils.TimeChangeObserver) + */ + @Override + public void onAlarm(TimeChangeObserver timeChangeObserver) + { + // Not listening for Alarms in this service. + } + + + /** + * Gets the array of {@link TaskListWidgetItem}s. + * + * @return the widget items + */ + public static TaskListWidgetItem[] getWidgetItems(Cursor mTasksCursor) + { + if (mTasksCursor.getCount() > 0) + { + TaskListWidgetItem[] items = new TaskListWidgetItem[mTasksCursor.getCount()]; + int itemIndex = 0; + + while (mTasksCursor.moveToNext()) + { + items[itemIndex] = new TaskListWidgetItem(TaskFieldAdapters.INSTANCE_TASK_ID.get(mTasksCursor), TaskFieldAdapters.TITLE.get(mTasksCursor), + TaskFieldAdapters.DUE.get(mTasksCursor), TaskFieldAdapters.LIST_COLOR.get(mTasksCursor), + TaskFieldAdapters.IS_CLOSED.get(mTasksCursor)); + itemIndex++; + } + return items; + } + return null; + } + + + /** + * A {@link Runnable} that loads the tasks to show in the widget. + */ + private Runnable mReloadTasks = new Runnable() + { + + @Override + public void run() + { + // load TaskLists for this widget + WidgetConfigurationDatabaseHelper configHelper = new WidgetConfigurationDatabaseHelper(mContext); + SQLiteDatabase db = configHelper.getWritableDatabase(); + + ArrayList lists = WidgetConfigurationDatabaseHelper.loadTaskLists(db, mAppWidgetId); + db.close(); + + // build selection string + StringBuilder selection = new StringBuilder(TaskContract.Instances.VISIBLE + ">0 and " + TaskContract.Instances.IS_CLOSED + "=0 AND (" + + TaskContract.Instances.INSTANCE_START + "<=" + System.currentTimeMillis() + " OR " + TaskContract.Instances.INSTANCE_START + + " is null OR " + TaskContract.Instances.INSTANCE_START + " = " + TaskContract.Instances.INSTANCE_DUE + " )"); + + if (lists != null && !lists.isEmpty()) + { + selection.append(" AND ( "); + + for (int i = 0; i < lists.size(); i++) + { + Long listId = lists.get(i); + + if (i < lists.size() - 1) + { + selection.append(Instances.LIST_ID).append(" = ").append(listId).append(" OR "); + } + else + { + selection.append(Instances.LIST_ID).append(" = ").append(listId).append(" ) "); + } + } + } + + // load all upcoming non-completed tasks + Cursor c = mContext.getContentResolver().query( + TaskContract.Instances.getContentUri(mAuthority), + null, + selection.toString(), + null, + Instances.INSTANCE_DUE + " is null, " + Instances.DEFAULT_SORT_ORDER + ", " + + Instances.PRIORITY + " is null, " + Instances.PRIORITY + ", " + + Instances.INSTANCE_START + " is null, " + Instances.INSTANCE_START_SORTING + ", " + + Instances.CREATED + " DESC"); + + if (c != null) + { + try + { + mItems = getWidgetItems(c); + } + finally + { + c.close(); + } + } + else + { + mItems = new TaskListWidgetItem[0]; + } + + // tell to only update the view in the next onDataSetChanged(); + mDoNotReload = true; + + // notify the widget manager about the update + AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext); + if (mAppWidgetId != -1) + { + widgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.task_list_widget_lv); + + } + } + }; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/AbstractArrayChoicesAdapter.java b/opentasks/src/main/java/org/dmfs/tasks/model/AbstractArrayChoicesAdapter.java index 59da6a86..1fa1bdd5 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/AbstractArrayChoicesAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/AbstractArrayChoicesAdapter.java @@ -17,96 +17,95 @@ package org.dmfs.tasks.model; -import java.util.List; - import android.graphics.drawable.Drawable; +import java.util.List; + /** * Abstract class used for array type adapter. - * + * * @author Arjun Naik * @author Marten Gajda - * */ public abstract class AbstractArrayChoicesAdapter implements IChoicesAdapter { - protected List mChoices; - protected List mVisibleChoices; - protected List mTitles; - protected List mDrawables; - - - @Override - public String getTitle(Object object) - { - if (mChoices != null) - { - int index = mChoices.indexOf(object); - if (index >= 0) - { - return mTitles.get(index); - } - } - return null; - } - - - @Override - public Drawable getDrawable(Object object) - { - if (mDrawables != null && mChoices != null) - { - int index = mChoices.indexOf(object); - if (index >= 0) - { - return mDrawables.get(index); - } - } - return null; - } - - - @Override - public int getIndex(Object object) - { - int index = mVisibleChoices.indexOf(object); - if (index == -1) - { - // not within visible choices, we should return an alternate value if we have any - int hiddenIndex = mChoices.indexOf(object); - if (hiddenIndex >= 0) - { - // there is a hidden element of that value, return the visible element with the same display value - // TODO: we should introduce some kind of tag that uniquely identifies elements that are the same - - String title = mTitles.get(hiddenIndex); - - for (int i = 0, count = mVisibleChoices.size(); i < count; ++i) - { - Object o = mVisibleChoices.get(i); - if (title.equals(getTitle(o))) - { - return i; - } - } - } - } - return index; - } - - - @Override - public int getCount() - { - return mVisibleChoices.size(); - } - - - @Override - public Object getItem(int position) - { - return mVisibleChoices.get(position); - } + protected List mChoices; + protected List mVisibleChoices; + protected List mTitles; + protected List mDrawables; + + + @Override + public String getTitle(Object object) + { + if (mChoices != null) + { + int index = mChoices.indexOf(object); + if (index >= 0) + { + return mTitles.get(index); + } + } + return null; + } + + + @Override + public Drawable getDrawable(Object object) + { + if (mDrawables != null && mChoices != null) + { + int index = mChoices.indexOf(object); + if (index >= 0) + { + return mDrawables.get(index); + } + } + return null; + } + + + @Override + public int getIndex(Object object) + { + int index = mVisibleChoices.indexOf(object); + if (index == -1) + { + // not within visible choices, we should return an alternate value if we have any + int hiddenIndex = mChoices.indexOf(object); + if (hiddenIndex >= 0) + { + // there is a hidden element of that value, return the visible element with the same display value + // TODO: we should introduce some kind of tag that uniquely identifies elements that are the same + + String title = mTitles.get(hiddenIndex); + + for (int i = 0, count = mVisibleChoices.size(); i < count; ++i) + { + Object o = mVisibleChoices.get(i); + if (title.equals(getTitle(o))) + { + return i; + } + } + } + } + return index; + } + + + @Override + public int getCount() + { + return mVisibleChoices.size(); + } + + + @Override + public Object getItem(int position) + { + return mVisibleChoices.get(position); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/ArrayChoicesAdapter.java b/opentasks/src/main/java/org/dmfs/tasks/model/ArrayChoicesAdapter.java index 0e78c0a8..3d72802c 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/ArrayChoicesAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/ArrayChoicesAdapter.java @@ -17,68 +17,69 @@ package org.dmfs.tasks.model; -import java.util.ArrayList; - import android.graphics.drawable.Drawable; +import java.util.ArrayList; + /** * Used for creating a generic array type adapter. Supports adding choices of types regular and hidden into the adapter. - * + * * @author Arjun Naik * @author Marten Gajda - * */ public class ArrayChoicesAdapter extends AbstractArrayChoicesAdapter { - public ArrayChoicesAdapter() - { - mChoices = new ArrayList(); - mDrawables = new ArrayList(); - mTitles = new ArrayList(); - mVisibleChoices = new ArrayList(); - } + public ArrayChoicesAdapter() + { + mChoices = new ArrayList(); + mDrawables = new ArrayList(); + mTitles = new ArrayList(); + mVisibleChoices = new ArrayList(); + } - /** - * Adds a choice which is visible. - * - * @param choice - * Choice to be adde3d - * @param title - * Title of the choice - * @param drawable - * {@link Drawable} used to display choice - * @return itself as a reference so that it can used for chaining. - */ - public ArrayChoicesAdapter addChoice(Object choice, String title, Drawable drawable) - { - mVisibleChoices.add(choice); - mChoices.add(choice); - mTitles.add(title); - mDrawables.add(drawable); - return this; - } + /** + * Adds a choice which is visible. + * + * @param choice + * Choice to be adde3d + * @param title + * Title of the choice + * @param drawable + * {@link Drawable} used to display choice + * + * @return itself as a reference so that it can used for chaining. + */ + public ArrayChoicesAdapter addChoice(Object choice, String title, Drawable drawable) + { + mVisibleChoices.add(choice); + mChoices.add(choice); + mTitles.add(title); + mDrawables.add(drawable); + return this; + } - /** - * Add a choice which is hidden. - * - * @param choice - * Choice to be adde3d - * @param title - * Title of the choice - * @param drawable - * {@link Drawable} used to display choice - * @return itself as a reference so that it can used for chaining. - */ - public ArrayChoicesAdapter addHiddenChoice(Object choice, String title, Drawable drawable) - { - mChoices.add(choice); - mTitles.add(title); - mDrawables.add(drawable); - return this; - } + /** + * Add a choice which is hidden. + * + * @param choice + * Choice to be adde3d + * @param title + * Title of the choice + * @param drawable + * {@link Drawable} used to display choice + * + * @return itself as a reference so that it can used for chaining. + */ + public ArrayChoicesAdapter addHiddenChoice(Object choice, String title, Drawable drawable) + { + mChoices.add(choice); + mTitles.add(title); + mDrawables.add(drawable); + return this; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/CheckListItem.java b/opentasks/src/main/java/org/dmfs/tasks/model/CheckListItem.java index 6602fd90..d1a09b5e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/CheckListItem.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/CheckListItem.java @@ -5,31 +5,34 @@ import android.text.TextUtils; public final class CheckListItem { - public boolean checked; - public String text; + public boolean checked; + public String text; - public CheckListItem(boolean checked, String text) - { - this.checked = checked; - this.text = text; - } + public CheckListItem(boolean checked, String text) + { + this.checked = checked; + this.text = text; + } - @Override - public int hashCode() - { - return text != null ? (text.hashCode() << 1) + (checked ? 1 : 0) : (checked ? 1 : 0); - } + @Override + public int hashCode() + { + return text != null ? (text.hashCode() << 1) + (checked ? 1 : 0) : (checked ? 1 : 0); + } - public boolean equals(Object o) - { - if (!(o instanceof CheckListItem)) - { - return false; - } - CheckListItem other = (CheckListItem) o; - return TextUtils.equals(text, other.text) && checked == other.checked; - }; + public boolean equals(Object o) + { + if (!(o instanceof CheckListItem)) + { + return false; + } + CheckListItem other = (CheckListItem) o; + return TextUtils.equals(text, other.text) && checked == other.checked; + } + + + ; } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java b/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java index eec5436b..1ef23305 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java @@ -17,19 +17,6 @@ package org.dmfs.tasks.model; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.WeakHashMap; - -import org.dmfs.tasks.utils.AsyncContentLoader; -import org.dmfs.tasks.utils.ContentValueMapper; -import org.dmfs.tasks.utils.OnContentLoadedListener; -import org.dmfs.tasks.utils.SetFromMap; - import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.ContentValues; @@ -39,643 +26,657 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.Log; +import org.dmfs.tasks.utils.AsyncContentLoader; +import org.dmfs.tasks.utils.ContentValueMapper; +import org.dmfs.tasks.utils.OnContentLoadedListener; +import org.dmfs.tasks.utils.SetFromMap; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.WeakHashMap; + /** * A ContentSet takes care of loading and storing the values for a specific {@link Uri}. *

    * This class is {@link Parcelable} to allow storing it in a Bundle. *

    - * + * * @author Marten Gajda */ public final class ContentSet implements OnContentLoadedListener, Parcelable { - private static final String TAG = "ContentSet"; - - /** - * The {@link ContentValues} that have been read from the database (or null for insert operations). - */ - private ContentValues mBeforeContentValues; - - /** - * The {@link ContentValues} that have been modified. - */ - private ContentValues mAfterContentValues; - - /** - * The {@link Uri} we operate on. For insert operations this is a directory URI, otherwise it has to be an item URI. - */ - private Uri mUri; - - /** - * A {@link Map} for the {@link OnContentChangeListener}s. A listener registers for a specific key in a content set or for null to e notified - * of full reloads. - */ - private final Map> mOnChangeListeners = new HashMap>(); - - /** - * A counter for the number of bulk updates currently running. It is incremented on {@link #startBulkUpdate()} and decremented on - * {@link #finishBulkUpdate()}. If this values becomes null in {@link #finishBulkUpdate()} all listeners get notified. - */ - private int mBulkUpdates = 0; - - /** - * Holds all {@link OnContentChangeListener}s that need to be notified, because something has changed during a bulk update. - */ - private final Set mPendingNotifications = new HashSet(); - - /** - * Holds the name of the keys we've updated in {@link #mAfterContentValues}. - * - * Before Android SDK level 11 there is no {@link ContentValues#keySet()} method. To be able to determine the keys in there we have to maintain the set - * ourselves. - * - * Don't use this before calling {@link #ensureAfter()} at least once. - */ - private Set mAfterKeys; - - /** - * Indicates that loading is in process. - */ - private boolean mLoading = false; - - - /** - * Private constructor that is used when creating a ContentSet form a parcel. - */ - private ContentSet() - { - } - - - /** - * Create a new ContentSet for a specific {@link Uri}. uri is either a directory URI or an item URI. To load the content of an item URI call - * {@link #update(Context, ContentValueMapper)}. - * - * @param uri - * A content URI, either a directory URI or an item URI. - */ - public ContentSet(Uri uri) - { - if (uri == null) - { - throw new IllegalArgumentException("uri must not be null"); - } - - mUri = uri; - } - - - /** - * Clone constructor. - * - * @param other - * The {@link ContentSet} to clone. - */ - public ContentSet(ContentSet other) - { - if (other == null) - { - throw new IllegalArgumentException("other must not be null"); - } - - if (other.mBeforeContentValues != null) - { - mBeforeContentValues = new ContentValues(other.mBeforeContentValues); - } - - if (other.mAfterContentValues != null) - { - mAfterContentValues = new ContentValues(other.mAfterContentValues); - mAfterKeys = new HashSet(other.mAfterKeys); - } - - mUri = other.mUri; - } - - - /** - * Load the content. This method must not be called if the URI of this ContentSet is a directory URI and it has not been persited yet. - * - * @param context - * A context. - * @param mapper - * The {@link ContentValueMapper} to use when loading the values. - */ - public void update(Context context, ContentValueMapper mapper) - { - String itemType = context.getContentResolver().getType(mUri); - if (itemType != null && !itemType.startsWith(ContentResolver.CURSOR_DIR_BASE_TYPE)) - { - mLoading = true; - new AsyncContentLoader(context, this, mapper).execute(mUri); - } - else - { - throw new UnsupportedOperationException("Can not load content from a directoy URI: " + mUri); - } - } - - - @Override - public void onContentLoaded(ContentValues values) - { - mBeforeContentValues = values; - mLoading = false; - notifyLoadedListeners(); - } - - - /** - * Returns whether this {@link ContentSet} is currently loading values. - * - * @return true is an asynchronous loading operation is in progress, false otherwise. - */ - public boolean isLoading() - { - return mLoading; - } - - - /** - * Delete this content. This ContentSet can no longer be used after this method has been called! - * - * @param context - * A context. - */ - public void delete(Context context) - { - if (mUri != null) - { - String itemType = context.getContentResolver().getType(mUri); - if (itemType != null && !itemType.startsWith(ContentResolver.CURSOR_DIR_BASE_TYPE)) - { - context.getContentResolver().delete(mUri, null, null); - mBeforeContentValues = null; - mAfterContentValues = null; - mAfterKeys = null; - mUri = null; - } - else - { - throw new UnsupportedOperationException("Can not load delete a directoy URI: " + mUri); - } - } - else - { - Log.w(TAG, "Trying to delete empty ContentSet"); - } - - } - - - public Uri persist(Context context) - { - if (mAfterContentValues == null || mAfterContentValues.size() == 0) - { - // nothing to do here - return mUri; - } - - if (isInsert()) - { - // update uri with new uri - mUri = context.getContentResolver().insert(mUri, mAfterContentValues); - } - else if (isUpdate()) - { - context.getContentResolver().update(mUri, mAfterContentValues, null, null); - } - // else nothing to do - - mAfterContentValues = null; - - return mUri; - } - - - public boolean isInsert() - { - return mBeforeContentValues == null && mAfterContentValues != null && mAfterContentValues.size() > 0; - } - - - public boolean isUpdate() - { - return mBeforeContentValues != null && mAfterContentValues != null && mAfterContentValues.size() > 0; - } - - - public boolean containsKey(String key) - { - return mAfterContentValues != null && mAfterContentValues.containsKey(key) || mBeforeContentValues != null && mBeforeContentValues.containsKey(key); - } - - - public boolean persistsKey(String key) - { - return mAfterContentValues != null && mAfterContentValues.containsKey(key); - } - - - public boolean updatesAnyKey(Set keys) - { - if (mAfterContentValues == null) - { - return false; - } - - Set keySet = new HashSet(mAfterKeys); - - keySet.retainAll(keys); - - // if there is any element left in keySet both sets had at least one element in common - return !keySet.isEmpty(); - } - - - public void ensureUpdates(Set keys) - { - if (mBeforeContentValues == null || keys == null || keys.isEmpty()) - { - // nothing to do - return; - } - - ContentValues tempValues = new ContentValues(mBeforeContentValues); - /* - * Remove all values from tempValues that already are in mAfterContentValues or that are not required to be updated. + private static final String TAG = "ContentSet"; + + /** + * The {@link ContentValues} that have been read from the database (or null for insert operations). + */ + private ContentValues mBeforeContentValues; + + /** + * The {@link ContentValues} that have been modified. + */ + private ContentValues mAfterContentValues; + + /** + * The {@link Uri} we operate on. For insert operations this is a directory URI, otherwise it has to be an item URI. + */ + private Uri mUri; + + /** + * A {@link Map} for the {@link OnContentChangeListener}s. A listener registers for a specific key in a content set or for null to e notified + * of full reloads. + */ + private final Map> mOnChangeListeners = new HashMap>(); + + /** + * A counter for the number of bulk updates currently running. It is incremented on {@link #startBulkUpdate()} and decremented on + * {@link #finishBulkUpdate()}. If this values becomes null in {@link #finishBulkUpdate()} all listeners get notified. + */ + private int mBulkUpdates = 0; + + /** + * Holds all {@link OnContentChangeListener}s that need to be notified, because something has changed during a bulk update. + */ + private final Set mPendingNotifications = new HashSet(); + + /** + * Holds the name of the keys we've updated in {@link #mAfterContentValues}. + *

    + * Before Android SDK level 11 there is no {@link ContentValues#keySet()} method. To be able to determine the keys in there we have to maintain the set + * ourselves. + *

    + * Don't use this before calling {@link #ensureAfter()} at least once. + */ + private Set mAfterKeys; + + /** + * Indicates that loading is in process. + */ + private boolean mLoading = false; + + + /** + * Private constructor that is used when creating a ContentSet form a parcel. + */ + private ContentSet() + { + } + + + /** + * Create a new ContentSet for a specific {@link Uri}. uri is either a directory URI or an item URI. To load the content of an item URI call + * {@link #update(Context, ContentValueMapper)}. + * + * @param uri + * A content URI, either a directory URI or an item URI. + */ + public ContentSet(Uri uri) + { + if (uri == null) + { + throw new IllegalArgumentException("uri must not be null"); + } + + mUri = uri; + } + + + /** + * Clone constructor. + * + * @param other + * The {@link ContentSet} to clone. + */ + public ContentSet(ContentSet other) + { + if (other == null) + { + throw new IllegalArgumentException("other must not be null"); + } + + if (other.mBeforeContentValues != null) + { + mBeforeContentValues = new ContentValues(other.mBeforeContentValues); + } + + if (other.mAfterContentValues != null) + { + mAfterContentValues = new ContentValues(other.mAfterContentValues); + mAfterKeys = new HashSet(other.mAfterKeys); + } + + mUri = other.mUri; + } + + + /** + * Load the content. This method must not be called if the URI of this ContentSet is a directory URI and it has not been persited yet. + * + * @param context + * A context. + * @param mapper + * The {@link ContentValueMapper} to use when loading the values. + */ + public void update(Context context, ContentValueMapper mapper) + { + String itemType = context.getContentResolver().getType(mUri); + if (itemType != null && !itemType.startsWith(ContentResolver.CURSOR_DIR_BASE_TYPE)) + { + mLoading = true; + new AsyncContentLoader(context, this, mapper).execute(mUri); + } + else + { + throw new UnsupportedOperationException("Can not load content from a directoy URI: " + mUri); + } + } + + + @Override + public void onContentLoaded(ContentValues values) + { + mBeforeContentValues = values; + mLoading = false; + notifyLoadedListeners(); + } + + + /** + * Returns whether this {@link ContentSet} is currently loading values. + * + * @return true is an asynchronous loading operation is in progress, false otherwise. + */ + public boolean isLoading() + { + return mLoading; + } + + + /** + * Delete this content. This ContentSet can no longer be used after this method has been called! + * + * @param context + * A context. + */ + public void delete(Context context) + { + if (mUri != null) + { + String itemType = context.getContentResolver().getType(mUri); + if (itemType != null && !itemType.startsWith(ContentResolver.CURSOR_DIR_BASE_TYPE)) + { + context.getContentResolver().delete(mUri, null, null); + mBeforeContentValues = null; + mAfterContentValues = null; + mAfterKeys = null; + mUri = null; + } + else + { + throw new UnsupportedOperationException("Can not load delete a directoy URI: " + mUri); + } + } + else + { + Log.w(TAG, "Trying to delete empty ContentSet"); + } + + } + + + public Uri persist(Context context) + { + if (mAfterContentValues == null || mAfterContentValues.size() == 0) + { + // nothing to do here + return mUri; + } + + if (isInsert()) + { + // update uri with new uri + mUri = context.getContentResolver().insert(mUri, mAfterContentValues); + } + else if (isUpdate()) + { + context.getContentResolver().update(mUri, mAfterContentValues, null, null); + } + // else nothing to do + + mAfterContentValues = null; + + return mUri; + } + + + public boolean isInsert() + { + return mBeforeContentValues == null && mAfterContentValues != null && mAfterContentValues.size() > 0; + } + + + public boolean isUpdate() + { + return mBeforeContentValues != null && mAfterContentValues != null && mAfterContentValues.size() > 0; + } + + + public boolean containsKey(String key) + { + return mAfterContentValues != null && mAfterContentValues.containsKey(key) || mBeforeContentValues != null && mBeforeContentValues.containsKey(key); + } + + + public boolean persistsKey(String key) + { + return mAfterContentValues != null && mAfterContentValues.containsKey(key); + } + + + public boolean updatesAnyKey(Set keys) + { + if (mAfterContentValues == null) + { + return false; + } + + Set keySet = new HashSet(mAfterKeys); + + keySet.retainAll(keys); + + // if there is any element left in keySet both sets had at least one element in common + return !keySet.isEmpty(); + } + + + public void ensureUpdates(Set keys) + { + if (mBeforeContentValues == null || keys == null || keys.isEmpty()) + { + // nothing to do + return; + } + + ContentValues tempValues = new ContentValues(mBeforeContentValues); + /* + * Remove all values from tempValues that already are in mAfterContentValues or that are not required to be updated. * * This is tricky because we can't rely on ContentValues.keySet(). That's why we have to iterate all keys and remove the ones we don't have to update. * Since we can't modify tempValues while iterating over its keys we have to create a temporary Set. * * TODO: find a better way to solve this */ - for (Entry entry : new HashSet>(tempValues.valueSet())) - { - String key = entry.getKey(); - if (!keys.contains(key) || persistsKey(key)) - { - tempValues.remove(key); - } - } - - if (mAfterContentValues != null) - { - mAfterContentValues.putAll(tempValues); - } - else - { - mAfterContentValues = tempValues; - } - } - - - private ContentValues ensureAfter() - { - ContentValues values = mAfterContentValues; - if (values == null) - { - values = new ContentValues(); - mAfterContentValues = values; - // also create mAfterKeys - mAfterKeys = new HashSet(); - } - return values; - } - - - public void put(String key, Integer value) - { - Integer oldValue = getAsInteger(key); - if (value != null && !value.equals(oldValue) || value == null && oldValue != null) - { - if (mBeforeContentValues != null && mBeforeContentValues.containsKey(key)) - { - Integer beforeValue = mBeforeContentValues.getAsInteger(key); - if (beforeValue != null && beforeValue.equals(value) || beforeValue == null && value == null) - { - // value equals before value, so remove it from after values - mAfterContentValues.remove(key); - mAfterKeys.remove(key); - notifyUpdateListeners(key); - return; - } - } - // value has changed, update - ensureAfter().put(key, value); - mAfterKeys.add(key); - notifyUpdateListeners(key); - } - } - - - public Integer getAsInteger(String key) - { - final ContentValues after = mAfterContentValues; - if (after != null && after.containsKey(key)) - { - return mAfterContentValues.getAsInteger(key); - } - return mBeforeContentValues == null ? null : mBeforeContentValues.getAsInteger(key); - } - - - public void put(String key, Long value) - { - Long oldValue = getAsLong(key); - if (value != null && !value.equals(oldValue) || value == null && oldValue != null) - { - if (mBeforeContentValues != null && mBeforeContentValues.containsKey(key)) - { - Long beforeValue = mBeforeContentValues.getAsLong(key); - if (beforeValue != null && beforeValue.equals(value) || beforeValue == null && value == null) - { - // value equals before value, so remove it from after values - mAfterContentValues.remove(key); - mAfterKeys.remove(key); - notifyUpdateListeners(key); - return; - } - } - ensureAfter().put(key, value); - mAfterKeys.add(key); - notifyUpdateListeners(key); - } - } - - - public Long getAsLong(String key) - { - final ContentValues after = mAfterContentValues; - if (after != null && after.containsKey(key)) - { - return mAfterContentValues.getAsLong(key); - } - return mBeforeContentValues == null ? null : mBeforeContentValues.getAsLong(key); - } - - - public void put(String key, String value) - { - String oldValue = getAsString(key); - if (value != null && !value.equals(oldValue) || value == null && oldValue != null) - { - if (mBeforeContentValues != null && mBeforeContentValues.containsKey(key)) - { - String beforeValue = mBeforeContentValues.getAsString(key); - if (beforeValue != null && beforeValue.equals(value) || beforeValue == null && value == null) - { - // value equals before value, so remove it from after values - mAfterContentValues.remove(key); - mAfterKeys.remove(key); - notifyUpdateListeners(key); - return; - } - } - ensureAfter().put(key, value); - mAfterKeys.add(key); - notifyUpdateListeners(key); - } - } - - - public String getAsString(String key) - { - final ContentValues after = mAfterContentValues; - if (after != null && after.containsKey(key)) - { - return mAfterContentValues.getAsString(key); - } - return mBeforeContentValues == null ? null : mBeforeContentValues.getAsString(key); - } - - - public void put(String key, Float value) - { - Float oldValue = getAsFloat(key); - if (value != null && !value.equals(oldValue) || value == null && oldValue != null) - { - if (mBeforeContentValues != null && mBeforeContentValues.containsKey(key)) - { - Float beforeValue = mBeforeContentValues.getAsFloat(key); - if (beforeValue != null && beforeValue.equals(value) || beforeValue == null && value == null) - { - // value equals before value, so remove it from after values - mAfterContentValues.remove(key); - mAfterKeys.remove(key); - notifyUpdateListeners(key); - return; - } - } - ensureAfter().put(key, value); - mAfterKeys.add(key); - notifyUpdateListeners(key); - } - } - - - public Float getAsFloat(String key) - { - final ContentValues after = mAfterContentValues; - if (after != null && after.containsKey(key)) - { - return mAfterContentValues.getAsFloat(key); - } - return mBeforeContentValues == null ? null : mBeforeContentValues.getAsFloat(key); - } - - - /** - * Returns the Uri this {@link ContentSet} is read from (or has been written to). This may be a directory {@link Uri} if the ContentSet has not been stored - * yet. - * - * @return The {@link Uri}. - */ - public Uri getUri() - { - return mUri; - } - - - /** - * Start a new bulk update. You should use this when you update multiple values at once and you don't want to send an update notification every time. When - * you're done call {@link #finishBulkUpdate()} which sned the notifications (unless there is another bulk update in progress). - */ - public void startBulkUpdate() - { - ++mBulkUpdates; - } - - - /** - * Finish a bulk update and notify all listeners of values that have been changed (unless there is still another bilk update in progress). - */ - public void finishBulkUpdate() - { - if (mBulkUpdates == 1) - { - Set listeners = new HashSet(mPendingNotifications); - mPendingNotifications.clear(); - for (OnContentChangeListener listener : listeners) - { - listener.onContentChanged(this); - } - } - --mBulkUpdates; - } - - - /** - * Remove the value with the given key from the ContentSet. This is actually replacing the value by null. - * - * @param key - * The key of the value to remove. - */ - public void remove(String key) - { - if (mAfterContentValues != null) - { - mAfterContentValues.putNull(key); - mAfterKeys.add(key); - } - else if (mBeforeContentValues != null && mBeforeContentValues.get(key) != null) - { - ensureAfter().putNull(key); - mAfterKeys.add(key); - } - } - - - @TargetApi(9) - public void addOnChangeListener(OnContentChangeListener listener, String key, boolean notify) - { - Set listenerSet = mOnChangeListeners.get(key); - if (listenerSet == null) - { - // using a "WeakHashSet" ensures that we don't prevent listeners from getting garbage-collected. - - if (android.os.Build.VERSION.SDK_INT > 8) - { - listenerSet = Collections.newSetFromMap(new WeakHashMap()); - } - else - { - listenerSet = new SetFromMap(new WeakHashMap()); - } - mOnChangeListeners.put(key, listenerSet); - } - - listenerSet.add(listener); - - if (notify && (mBeforeContentValues != null || mAfterContentValues != null)) - { - listener.onContentLoaded(this); - } - } - - - public void removeOnChangeListener(OnContentChangeListener listener, String key) - { - Set listenerSet = mOnChangeListeners.get(key); - if (listenerSet != null) - { - listenerSet.remove(listener); - } - } - - - private void notifyUpdateListeners(String key) - { - Set listenerSet = mOnChangeListeners.get(key); - if (listenerSet != null) - { - for (OnContentChangeListener listener : listenerSet) - { - if (mBulkUpdates > 0) - { - mPendingNotifications.add(listener); - } - else - { - listener.onContentChanged(this); - } - } - } - } - - - private void notifyLoadedListeners() - { - Set listenerSet = mOnChangeListeners.get(null); - if (listenerSet != null) - { - for (OnContentChangeListener listener : listenerSet) - { - listener.onContentLoaded(this); - } - } - } - - - @Override - public int describeContents() - { - return 0; - } - - - @Override - public void writeToParcel(Parcel dest, int flags) - { - dest.writeParcelable(mUri, flags); - dest.writeParcelable(mBeforeContentValues, flags); - dest.writeParcelable(mAfterContentValues, flags); - - if (mAfterContentValues != null) - { - // It's not possible to write a Set to a parcel, so write the number of members and each member individually. - dest.writeInt(mAfterKeys.size()); - for (String key : mAfterKeys) - { - dest.writeString(key); - } - } - } - - - public void readFromParcel(Parcel source) - { - ClassLoader loader = getClass().getClassLoader(); - mUri = source.readParcelable(loader); - mBeforeContentValues = source.readParcelable(loader); - mAfterContentValues = source.readParcelable(loader); - - if (mAfterContentValues != null) - { - int count = source.readInt(); - Set keys = new HashSet(); - while (--count >= 0) - { - keys.add(source.readString()); - } - mAfterKeys = keys; - } - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() - { - public ContentSet createFromParcel(Parcel in) - { - final ContentSet state = new ContentSet(); - state.readFromParcel(in); - return state; - } - - - public ContentSet[] newArray(int size) - { - return new ContentSet[size]; - } - }; + for (Entry entry : new HashSet>(tempValues.valueSet())) + { + String key = entry.getKey(); + if (!keys.contains(key) || persistsKey(key)) + { + tempValues.remove(key); + } + } + + if (mAfterContentValues != null) + { + mAfterContentValues.putAll(tempValues); + } + else + { + mAfterContentValues = tempValues; + } + } + + + private ContentValues ensureAfter() + { + ContentValues values = mAfterContentValues; + if (values == null) + { + values = new ContentValues(); + mAfterContentValues = values; + // also create mAfterKeys + mAfterKeys = new HashSet(); + } + return values; + } + + + public void put(String key, Integer value) + { + Integer oldValue = getAsInteger(key); + if (value != null && !value.equals(oldValue) || value == null && oldValue != null) + { + if (mBeforeContentValues != null && mBeforeContentValues.containsKey(key)) + { + Integer beforeValue = mBeforeContentValues.getAsInteger(key); + if (beforeValue != null && beforeValue.equals(value) || beforeValue == null && value == null) + { + // value equals before value, so remove it from after values + mAfterContentValues.remove(key); + mAfterKeys.remove(key); + notifyUpdateListeners(key); + return; + } + } + // value has changed, update + ensureAfter().put(key, value); + mAfterKeys.add(key); + notifyUpdateListeners(key); + } + } + + + public Integer getAsInteger(String key) + { + final ContentValues after = mAfterContentValues; + if (after != null && after.containsKey(key)) + { + return mAfterContentValues.getAsInteger(key); + } + return mBeforeContentValues == null ? null : mBeforeContentValues.getAsInteger(key); + } + + + public void put(String key, Long value) + { + Long oldValue = getAsLong(key); + if (value != null && !value.equals(oldValue) || value == null && oldValue != null) + { + if (mBeforeContentValues != null && mBeforeContentValues.containsKey(key)) + { + Long beforeValue = mBeforeContentValues.getAsLong(key); + if (beforeValue != null && beforeValue.equals(value) || beforeValue == null && value == null) + { + // value equals before value, so remove it from after values + mAfterContentValues.remove(key); + mAfterKeys.remove(key); + notifyUpdateListeners(key); + return; + } + } + ensureAfter().put(key, value); + mAfterKeys.add(key); + notifyUpdateListeners(key); + } + } + + + public Long getAsLong(String key) + { + final ContentValues after = mAfterContentValues; + if (after != null && after.containsKey(key)) + { + return mAfterContentValues.getAsLong(key); + } + return mBeforeContentValues == null ? null : mBeforeContentValues.getAsLong(key); + } + + + public void put(String key, String value) + { + String oldValue = getAsString(key); + if (value != null && !value.equals(oldValue) || value == null && oldValue != null) + { + if (mBeforeContentValues != null && mBeforeContentValues.containsKey(key)) + { + String beforeValue = mBeforeContentValues.getAsString(key); + if (beforeValue != null && beforeValue.equals(value) || beforeValue == null && value == null) + { + // value equals before value, so remove it from after values + mAfterContentValues.remove(key); + mAfterKeys.remove(key); + notifyUpdateListeners(key); + return; + } + } + ensureAfter().put(key, value); + mAfterKeys.add(key); + notifyUpdateListeners(key); + } + } + + + public String getAsString(String key) + { + final ContentValues after = mAfterContentValues; + if (after != null && after.containsKey(key)) + { + return mAfterContentValues.getAsString(key); + } + return mBeforeContentValues == null ? null : mBeforeContentValues.getAsString(key); + } + + + public void put(String key, Float value) + { + Float oldValue = getAsFloat(key); + if (value != null && !value.equals(oldValue) || value == null && oldValue != null) + { + if (mBeforeContentValues != null && mBeforeContentValues.containsKey(key)) + { + Float beforeValue = mBeforeContentValues.getAsFloat(key); + if (beforeValue != null && beforeValue.equals(value) || beforeValue == null && value == null) + { + // value equals before value, so remove it from after values + mAfterContentValues.remove(key); + mAfterKeys.remove(key); + notifyUpdateListeners(key); + return; + } + } + ensureAfter().put(key, value); + mAfterKeys.add(key); + notifyUpdateListeners(key); + } + } + + + public Float getAsFloat(String key) + { + final ContentValues after = mAfterContentValues; + if (after != null && after.containsKey(key)) + { + return mAfterContentValues.getAsFloat(key); + } + return mBeforeContentValues == null ? null : mBeforeContentValues.getAsFloat(key); + } + + + /** + * Returns the Uri this {@link ContentSet} is read from (or has been written to). This may be a directory {@link Uri} if the ContentSet has not been stored + * yet. + * + * @return The {@link Uri}. + */ + public Uri getUri() + { + return mUri; + } + + + /** + * Start a new bulk update. You should use this when you update multiple values at once and you don't want to send an update notification every time. When + * you're done call {@link #finishBulkUpdate()} which sned the notifications (unless there is another bulk update in progress). + */ + public void startBulkUpdate() + { + ++mBulkUpdates; + } + + + /** + * Finish a bulk update and notify all listeners of values that have been changed (unless there is still another bilk update in progress). + */ + public void finishBulkUpdate() + { + if (mBulkUpdates == 1) + { + Set listeners = new HashSet(mPendingNotifications); + mPendingNotifications.clear(); + for (OnContentChangeListener listener : listeners) + { + listener.onContentChanged(this); + } + } + --mBulkUpdates; + } + + + /** + * Remove the value with the given key from the ContentSet. This is actually replacing the value by null. + * + * @param key + * The key of the value to remove. + */ + public void remove(String key) + { + if (mAfterContentValues != null) + { + mAfterContentValues.putNull(key); + mAfterKeys.add(key); + } + else if (mBeforeContentValues != null && mBeforeContentValues.get(key) != null) + { + ensureAfter().putNull(key); + mAfterKeys.add(key); + } + } + + + @TargetApi(9) + public void addOnChangeListener(OnContentChangeListener listener, String key, boolean notify) + { + Set listenerSet = mOnChangeListeners.get(key); + if (listenerSet == null) + { + // using a "WeakHashSet" ensures that we don't prevent listeners from getting garbage-collected. + + if (android.os.Build.VERSION.SDK_INT > 8) + { + listenerSet = Collections.newSetFromMap(new WeakHashMap()); + } + else + { + listenerSet = new SetFromMap(new WeakHashMap()); + } + mOnChangeListeners.put(key, listenerSet); + } + + listenerSet.add(listener); + + if (notify && (mBeforeContentValues != null || mAfterContentValues != null)) + { + listener.onContentLoaded(this); + } + } + + + public void removeOnChangeListener(OnContentChangeListener listener, String key) + { + Set listenerSet = mOnChangeListeners.get(key); + if (listenerSet != null) + { + listenerSet.remove(listener); + } + } + + + private void notifyUpdateListeners(String key) + { + Set listenerSet = mOnChangeListeners.get(key); + if (listenerSet != null) + { + for (OnContentChangeListener listener : listenerSet) + { + if (mBulkUpdates > 0) + { + mPendingNotifications.add(listener); + } + else + { + listener.onContentChanged(this); + } + } + } + } + + + private void notifyLoadedListeners() + { + Set listenerSet = mOnChangeListeners.get(null); + if (listenerSet != null) + { + for (OnContentChangeListener listener : listenerSet) + { + listener.onContentLoaded(this); + } + } + } + + + @Override + public int describeContents() + { + return 0; + } + + + @Override + public void writeToParcel(Parcel dest, int flags) + { + dest.writeParcelable(mUri, flags); + dest.writeParcelable(mBeforeContentValues, flags); + dest.writeParcelable(mAfterContentValues, flags); + + if (mAfterContentValues != null) + { + // It's not possible to write a Set to a parcel, so write the number of members and each member individually. + dest.writeInt(mAfterKeys.size()); + for (String key : mAfterKeys) + { + dest.writeString(key); + } + } + } + + + public void readFromParcel(Parcel source) + { + ClassLoader loader = getClass().getClassLoader(); + mUri = source.readParcelable(loader); + mBeforeContentValues = source.readParcelable(loader); + mAfterContentValues = source.readParcelable(loader); + + if (mAfterContentValues != null) + { + int count = source.readInt(); + Set keys = new HashSet(); + while (--count >= 0) + { + keys.add(source.readString()); + } + mAfterKeys = keys; + } + } + + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() + { + public ContentSet createFromParcel(Parcel in) + { + final ContentSet state = new ContentSet(); + state.readFromParcel(in); + return state; + } + + + public ContentSet[] newArray(int size) + { + return new ContentSet[size]; + } + }; } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/CursorChoicesAdapter.java b/opentasks/src/main/java/org/dmfs/tasks/model/CursorChoicesAdapter.java index c03dff2e..094388dd 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/CursorChoicesAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/CursorChoicesAdapter.java @@ -26,86 +26,86 @@ import android.graphics.drawable.Drawable; *

    * TODO: This is merely a stub. It doesn't do anything useful yet. *

    - * + * * @author Marten Gajda */ public class CursorChoicesAdapter implements IChoicesAdapter { - @SuppressWarnings("unused") - private static final String TAG = "CursorChoicesAdapter"; - private final Cursor mCursor; - @SuppressWarnings("unused") - private String mTitleColumn; - private String mKeyColumn; + @SuppressWarnings("unused") + private static final String TAG = "CursorChoicesAdapter"; + private final Cursor mCursor; + @SuppressWarnings("unused") + private String mTitleColumn; + private String mKeyColumn; - public CursorChoicesAdapter(Cursor cursor) - { - mCursor = cursor; - } + public CursorChoicesAdapter(Cursor cursor) + { + mCursor = cursor; + } - @Override - public String getTitle(Object object) - { - // return mCursor.getString(mCursor.getColumnIndex(mTitleColumn)); - return null; - } + @Override + public String getTitle(Object object) + { + // return mCursor.getString(mCursor.getColumnIndex(mTitleColumn)); + return null; + } - @Override - public Drawable getDrawable(Object object) - { - return null; + @Override + public Drawable getDrawable(Object object) + { + return null; - } + } - public String getKeyColumn() - { - return mKeyColumn; - } + public String getKeyColumn() + { + return mKeyColumn; + } - public CursorChoicesAdapter setKeyColumn(String keyColumn) - { - mKeyColumn = keyColumn; - return this; - } + public CursorChoicesAdapter setKeyColumn(String keyColumn) + { + mKeyColumn = keyColumn; + return this; + } - public CursorChoicesAdapter setTitleColumn(String column) - { - mTitleColumn = column; - return this; - } + public CursorChoicesAdapter setTitleColumn(String column) + { + mTitleColumn = column; + return this; + } - public Cursor getChoices() - { - return mCursor; - } + public Cursor getChoices() + { + return mCursor; + } - @Override - public int getIndex(Object id) - { - return 0; - } + @Override + public int getIndex(Object id) + { + return 0; + } - @Override - public int getCount() - { - return mCursor.getCount(); - } + @Override + public int getCount() + { + return mCursor.getCount(); + } - @Override - public Object getItem(int position) - { - return null; - } + @Override + public Object getItem(int position) + { + return null; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java b/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java index 06a20551..8248f250 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java @@ -17,165 +17,166 @@ package org.dmfs.tasks.model; +import android.content.Context; +import android.text.util.Linkify; + import org.dmfs.provider.tasks.TaskContract.Tasks; import org.dmfs.tasks.R; import org.dmfs.tasks.model.layout.LayoutDescriptor; -import android.content.Context; -import android.text.util.Linkify; - /** * The default model for sync adapters that don't provide a model definition. - * + * * @author Marten Gajda */ public class DefaultModel extends Model { - final static LayoutDescriptor TEXT_VIEW = new LayoutDescriptor(R.layout.text_field_view).setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL); - final static LayoutDescriptor TEXT_VIEW_NO_LINKS = new LayoutDescriptor(R.layout.text_field_view).setOption(LayoutDescriptor.OPTION_LINKIFY, 0); - private final static LayoutDescriptor LOCATION_VIEW = new LayoutDescriptor(R.layout.opentasks_location_field_view).setOption(LayoutDescriptor.OPTION_LINKIFY, 0); - private final static LayoutDescriptor TEXT_EDIT = new LayoutDescriptor(R.layout.text_field_editor); - private final static LayoutDescriptor TEXT_EDIT_SINGLE_LINE = new LayoutDescriptor(R.layout.text_field_editor).setOption(LayoutDescriptor.OPTION_MULTILINE, - false); - private final static LayoutDescriptor CHECKLIST_VIEW = new LayoutDescriptor(R.layout.checklist_field_view); - private final static LayoutDescriptor CHECKLIST_EDIT = new LayoutDescriptor(R.layout.checklist_field_editor); - private final static LayoutDescriptor CHOICES_VIEW = new LayoutDescriptor(R.layout.choices_field_view); - private final static LayoutDescriptor CHOICES_EDIT = new LayoutDescriptor(R.layout.choices_field_editor); - private final static LayoutDescriptor PROGRESS_VIEW = new LayoutDescriptor(R.layout.percentage_field_view); - private final static LayoutDescriptor PROGRESS_EDIT = new LayoutDescriptor(R.layout.percentage_field_editor); - private final static LayoutDescriptor TIME_VIEW = new LayoutDescriptor(R.layout.time_field_view); - private final static LayoutDescriptor TIME_VIEW_ADD_BUTTON = new LayoutDescriptor(R.layout.time_field_view).setOption( - LayoutDescriptor.OPTION_TIME_FIELD_SHOW_ADD_BUTTONS, true); - private final static LayoutDescriptor TIME_EDIT = new LayoutDescriptor(R.layout.time_field_editor); - @SuppressWarnings("unused") - private final static LayoutDescriptor BOOLEAN_VIEW = new LayoutDescriptor(R.layout.boolean_field_view); - private final static LayoutDescriptor BOOLEAN_EDIT = new LayoutDescriptor(R.layout.boolean_field_editor); - private final static LayoutDescriptor URL_VIEW = new LayoutDescriptor(R.layout.url_field_view); - private final static LayoutDescriptor URL_EDIT = new LayoutDescriptor(R.layout.url_field_editor); - - final static LayoutDescriptor LIST_COLOR_VIEW = new LayoutDescriptor(R.layout.list_color_view); - - - public DefaultModel(Context context, String accountType) - { - super(context, accountType); - } - - - @Override - public void inflate() - { - if (mInflated) - { - return; - } - - Context context = getContext(); + final static LayoutDescriptor TEXT_VIEW = new LayoutDescriptor(R.layout.text_field_view).setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL); + final static LayoutDescriptor TEXT_VIEW_NO_LINKS = new LayoutDescriptor(R.layout.text_field_view).setOption(LayoutDescriptor.OPTION_LINKIFY, 0); + private final static LayoutDescriptor LOCATION_VIEW = new LayoutDescriptor(R.layout.opentasks_location_field_view).setOption( + LayoutDescriptor.OPTION_LINKIFY, 0); + private final static LayoutDescriptor TEXT_EDIT = new LayoutDescriptor(R.layout.text_field_editor); + private final static LayoutDescriptor TEXT_EDIT_SINGLE_LINE = new LayoutDescriptor(R.layout.text_field_editor).setOption(LayoutDescriptor.OPTION_MULTILINE, + false); + private final static LayoutDescriptor CHECKLIST_VIEW = new LayoutDescriptor(R.layout.checklist_field_view); + private final static LayoutDescriptor CHECKLIST_EDIT = new LayoutDescriptor(R.layout.checklist_field_editor); + private final static LayoutDescriptor CHOICES_VIEW = new LayoutDescriptor(R.layout.choices_field_view); + private final static LayoutDescriptor CHOICES_EDIT = new LayoutDescriptor(R.layout.choices_field_editor); + private final static LayoutDescriptor PROGRESS_VIEW = new LayoutDescriptor(R.layout.percentage_field_view); + private final static LayoutDescriptor PROGRESS_EDIT = new LayoutDescriptor(R.layout.percentage_field_editor); + private final static LayoutDescriptor TIME_VIEW = new LayoutDescriptor(R.layout.time_field_view); + private final static LayoutDescriptor TIME_VIEW_ADD_BUTTON = new LayoutDescriptor(R.layout.time_field_view).setOption( + LayoutDescriptor.OPTION_TIME_FIELD_SHOW_ADD_BUTTONS, true); + private final static LayoutDescriptor TIME_EDIT = new LayoutDescriptor(R.layout.time_field_editor); + @SuppressWarnings("unused") + private final static LayoutDescriptor BOOLEAN_VIEW = new LayoutDescriptor(R.layout.boolean_field_view); + private final static LayoutDescriptor BOOLEAN_EDIT = new LayoutDescriptor(R.layout.boolean_field_editor); + private final static LayoutDescriptor URL_VIEW = new LayoutDescriptor(R.layout.url_field_view); + private final static LayoutDescriptor URL_EDIT = new LayoutDescriptor(R.layout.url_field_editor); + + final static LayoutDescriptor LIST_COLOR_VIEW = new LayoutDescriptor(R.layout.list_color_view); + + + public DefaultModel(Context context, String accountType) + { + super(context, accountType); + } + + + @Override + public void inflate() + { + if (mInflated) + { + return; + } + + Context context = getContext(); /* - * Add a couple of fields to the model. + * Add a couple of fields to the model. */ - // task list color - addField(new FieldDescriptor(context, R.id.task_field_list_color, R.string.task_list, null, TaskFieldAdapters.LIST_COLOR) - .setViewLayout(LIST_COLOR_VIEW).setEditorLayout(LIST_COLOR_VIEW).setNoAutoAdd(true)); - - // task list name - addField(new FieldDescriptor(context, R.id.task_field_list_name, R.string.task_list, null, TaskFieldAdapters.LIST_NAME).setViewLayout( - new LayoutDescriptor(R.layout.text_field_view_nodivider_large).setOption(LayoutDescriptor.OPTION_NO_TITLE, true)).setNoAutoAdd(true)); - // account name - addField(new FieldDescriptor(context, R.id.task_field_account_name, R.string.task_list, null, TaskFieldAdapters.ACCOUNT_NAME).setViewLayout( - new LayoutDescriptor(R.layout.text_field_view_nodivider_small).setOption(LayoutDescriptor.OPTION_NO_TITLE, true)).setNoAutoAdd(true)); - - // task title - addField(new FieldDescriptor(context, R.id.task_field_title, R.string.task_title, TaskFieldAdapters.TITLE).setEditorLayout(TEXT_EDIT_SINGLE_LINE)); - - ArrayChoicesAdapter aca = new ArrayChoicesAdapter(); - aca.addHiddenChoice(null, context.getString(R.string.status_needs_action), null); - aca.addChoice(Tasks.STATUS_NEEDS_ACTION, context.getString(R.string.status_needs_action), null); - aca.addChoice(Tasks.STATUS_IN_PROCESS, context.getString(R.string.status_in_process), null); - aca.addChoice(Tasks.STATUS_COMPLETED, context.getString(R.string.status_completed), null); - aca.addChoice(Tasks.STATUS_CANCELLED, context.getString(R.string.status_cancelled), null); - - // status - addField(new FieldDescriptor(context, R.id.task_field_status, R.string.task_status, TaskFieldAdapters.STATUS).setViewLayout(CHOICES_VIEW) - .setEditorLayout(CHOICES_EDIT).setChoices(aca).setIcon(R.drawable.ic_detail_status)); - - // location - addField(new FieldDescriptor(context, R.id.task_field_location, R.string.task_location, TaskFieldAdapters.LOCATION).setViewLayout(LOCATION_VIEW) - .setEditorLayout(TEXT_EDIT).setIcon(R.drawable.ic_detail_location)); - - // description - addField(new FieldDescriptor(context, R.id.task_field_description, R.string.task_description, TaskFieldAdapters.DESCRIPTION) - .setViewLayout(TEXT_VIEW.setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL)).setEditorLayout(TEXT_EDIT) - .setIcon(R.drawable.ic_detail_description)); - - // description - addField(new FieldDescriptor(context, R.id.task_field_checklist, R.string.task_checklist, TaskFieldAdapters.CHECKLIST).setViewLayout(CHECKLIST_VIEW) - .setEditorLayout(CHECKLIST_EDIT).setIcon(R.drawable.ic_detail_checklist)); - - // start - addField(new FieldDescriptor(context, R.id.task_field_dtstart, R.string.task_start, TaskFieldAdapters.DTSTART).setViewLayout(TIME_VIEW) - .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_start)); - - // due - addField(new FieldDescriptor(context, R.id.task_field_due, R.string.task_due, TaskFieldAdapters.DUE).setEditorLayout(TIME_EDIT).setIcon( - R.drawable.ic_detail_due)); - - // all day flag - addField(new FieldDescriptor(context, R.id.task_field_all_day, R.string.task_all_day, TaskFieldAdapters.ALLDAY).setEditorLayout(BOOLEAN_EDIT)); - - TimeZoneChoicesAdapter tzaca = new TimeZoneChoicesAdapter(context); - // time zone - addField(new FieldDescriptor(context, R.id.task_field_timezone, R.string.task_timezone, TaskFieldAdapters.TIMEZONE).setEditorLayout(CHOICES_EDIT) - .setChoices(tzaca)); - - // completed - addField(new FieldDescriptor(context, R.id.task_field_completed, R.string.task_completed, TaskFieldAdapters.COMPLETED).setViewLayout(TIME_VIEW) - .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_completed)); - - // percent complete - addField(new FieldDescriptor(context, R.id.task_field_percent_complete, R.string.task_percent_complete, TaskFieldAdapters.PERCENT_COMPLETE) - .setViewLayout(PROGRESS_VIEW).setEditorLayout(PROGRESS_EDIT).setIcon(R.drawable.ic_detail_progress)); - - ArrayChoicesAdapter aca2 = new ArrayChoicesAdapter(); - aca2.addChoice(null, context.getString(R.string.priority_undefined), null); - aca2.addHiddenChoice(0, context.getString(R.string.priority_undefined), null); - aca2.addChoice(9, context.getString(R.string.priority_low), null); - aca2.addHiddenChoice(8, context.getString(R.string.priority_low), null); - aca2.addHiddenChoice(7, context.getString(R.string.priority_low), null); - aca2.addHiddenChoice(6, context.getString(R.string.priority_low), null); - aca2.addChoice(5, context.getString(R.string.priority_medium), null); - aca2.addHiddenChoice(4, context.getString(R.string.priority_high), null); - aca2.addHiddenChoice(3, context.getString(R.string.priority_high), null); - aca2.addHiddenChoice(2, context.getString(R.string.priority_high), null); - aca2.addChoice(1, context.getString(R.string.priority_high), null); - - // priority - addField(new FieldDescriptor(context, R.id.task_field_priority, R.string.task_priority, TaskFieldAdapters.PRIORITY).setViewLayout(CHOICES_VIEW) - .setEditorLayout(CHOICES_EDIT).setChoices(aca2).setIcon(R.drawable.ic_detail_priority)); - - ArrayChoicesAdapter aca3 = new ArrayChoicesAdapter(); - aca3.addChoice(null, context.getString(R.string.classification_not_specified), null); - aca3.addChoice(Tasks.CLASSIFICATION_PUBLIC, context.getString(R.string.classification_public), null); - aca3.addChoice(Tasks.CLASSIFICATION_PRIVATE, context.getString(R.string.classification_private), null); - aca3.addChoice(Tasks.CLASSIFICATION_CONFIDENTIAL, context.getString(R.string.classification_confidential), null); - - // privacy - addField(new FieldDescriptor(context, R.id.task_field_classification, R.string.task_classification, TaskFieldAdapters.CLASSIFICATION) - .setViewLayout(CHOICES_VIEW).setEditorLayout(CHOICES_EDIT).setChoices(aca3).setIcon(R.drawable.ic_detail_visibility)); - - // url - addField(new FieldDescriptor(context, R.id.task_field_url, R.string.task_url, TaskFieldAdapters.URL).setViewLayout(URL_VIEW).setEditorLayout(URL_EDIT) - .setIcon(R.drawable.ic_detail_url)); - - // task list name - addField(new FieldDescriptor(context, R.id.task_field_list_and_account_name, R.string.task_list, null, TaskFieldAdapters.LIST_AND_ACCOUNT_NAME) - .setViewLayout(TEXT_VIEW_NO_LINKS).setIcon(R.drawable.ic_detail_list)); - - setAllowRecurrence(false); - setAllowExceptions(false); - - mInflated = true; - } + // task list color + addField(new FieldDescriptor(context, R.id.task_field_list_color, R.string.task_list, null, TaskFieldAdapters.LIST_COLOR) + .setViewLayout(LIST_COLOR_VIEW).setEditorLayout(LIST_COLOR_VIEW).setNoAutoAdd(true)); + + // task list name + addField(new FieldDescriptor(context, R.id.task_field_list_name, R.string.task_list, null, TaskFieldAdapters.LIST_NAME).setViewLayout( + new LayoutDescriptor(R.layout.text_field_view_nodivider_large).setOption(LayoutDescriptor.OPTION_NO_TITLE, true)).setNoAutoAdd(true)); + // account name + addField(new FieldDescriptor(context, R.id.task_field_account_name, R.string.task_list, null, TaskFieldAdapters.ACCOUNT_NAME).setViewLayout( + new LayoutDescriptor(R.layout.text_field_view_nodivider_small).setOption(LayoutDescriptor.OPTION_NO_TITLE, true)).setNoAutoAdd(true)); + + // task title + addField(new FieldDescriptor(context, R.id.task_field_title, R.string.task_title, TaskFieldAdapters.TITLE).setEditorLayout(TEXT_EDIT_SINGLE_LINE)); + + ArrayChoicesAdapter aca = new ArrayChoicesAdapter(); + aca.addHiddenChoice(null, context.getString(R.string.status_needs_action), null); + aca.addChoice(Tasks.STATUS_NEEDS_ACTION, context.getString(R.string.status_needs_action), null); + aca.addChoice(Tasks.STATUS_IN_PROCESS, context.getString(R.string.status_in_process), null); + aca.addChoice(Tasks.STATUS_COMPLETED, context.getString(R.string.status_completed), null); + aca.addChoice(Tasks.STATUS_CANCELLED, context.getString(R.string.status_cancelled), null); + + // status + addField(new FieldDescriptor(context, R.id.task_field_status, R.string.task_status, TaskFieldAdapters.STATUS).setViewLayout(CHOICES_VIEW) + .setEditorLayout(CHOICES_EDIT).setChoices(aca).setIcon(R.drawable.ic_detail_status)); + + // location + addField(new FieldDescriptor(context, R.id.task_field_location, R.string.task_location, TaskFieldAdapters.LOCATION).setViewLayout(LOCATION_VIEW) + .setEditorLayout(TEXT_EDIT).setIcon(R.drawable.ic_detail_location)); + + // description + addField(new FieldDescriptor(context, R.id.task_field_description, R.string.task_description, TaskFieldAdapters.DESCRIPTION) + .setViewLayout(TEXT_VIEW.setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL)).setEditorLayout(TEXT_EDIT) + .setIcon(R.drawable.ic_detail_description)); + + // description + addField(new FieldDescriptor(context, R.id.task_field_checklist, R.string.task_checklist, TaskFieldAdapters.CHECKLIST).setViewLayout(CHECKLIST_VIEW) + .setEditorLayout(CHECKLIST_EDIT).setIcon(R.drawable.ic_detail_checklist)); + + // start + addField(new FieldDescriptor(context, R.id.task_field_dtstart, R.string.task_start, TaskFieldAdapters.DTSTART).setViewLayout(TIME_VIEW) + .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_start)); + + // due + addField(new FieldDescriptor(context, R.id.task_field_due, R.string.task_due, TaskFieldAdapters.DUE).setEditorLayout(TIME_EDIT).setIcon( + R.drawable.ic_detail_due)); + + // all day flag + addField(new FieldDescriptor(context, R.id.task_field_all_day, R.string.task_all_day, TaskFieldAdapters.ALLDAY).setEditorLayout(BOOLEAN_EDIT)); + + TimeZoneChoicesAdapter tzaca = new TimeZoneChoicesAdapter(context); + // time zone + addField(new FieldDescriptor(context, R.id.task_field_timezone, R.string.task_timezone, TaskFieldAdapters.TIMEZONE).setEditorLayout(CHOICES_EDIT) + .setChoices(tzaca)); + + // completed + addField(new FieldDescriptor(context, R.id.task_field_completed, R.string.task_completed, TaskFieldAdapters.COMPLETED).setViewLayout(TIME_VIEW) + .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_completed)); + + // percent complete + addField(new FieldDescriptor(context, R.id.task_field_percent_complete, R.string.task_percent_complete, TaskFieldAdapters.PERCENT_COMPLETE) + .setViewLayout(PROGRESS_VIEW).setEditorLayout(PROGRESS_EDIT).setIcon(R.drawable.ic_detail_progress)); + + ArrayChoicesAdapter aca2 = new ArrayChoicesAdapter(); + aca2.addChoice(null, context.getString(R.string.priority_undefined), null); + aca2.addHiddenChoice(0, context.getString(R.string.priority_undefined), null); + aca2.addChoice(9, context.getString(R.string.priority_low), null); + aca2.addHiddenChoice(8, context.getString(R.string.priority_low), null); + aca2.addHiddenChoice(7, context.getString(R.string.priority_low), null); + aca2.addHiddenChoice(6, context.getString(R.string.priority_low), null); + aca2.addChoice(5, context.getString(R.string.priority_medium), null); + aca2.addHiddenChoice(4, context.getString(R.string.priority_high), null); + aca2.addHiddenChoice(3, context.getString(R.string.priority_high), null); + aca2.addHiddenChoice(2, context.getString(R.string.priority_high), null); + aca2.addChoice(1, context.getString(R.string.priority_high), null); + + // priority + addField(new FieldDescriptor(context, R.id.task_field_priority, R.string.task_priority, TaskFieldAdapters.PRIORITY).setViewLayout(CHOICES_VIEW) + .setEditorLayout(CHOICES_EDIT).setChoices(aca2).setIcon(R.drawable.ic_detail_priority)); + + ArrayChoicesAdapter aca3 = new ArrayChoicesAdapter(); + aca3.addChoice(null, context.getString(R.string.classification_not_specified), null); + aca3.addChoice(Tasks.CLASSIFICATION_PUBLIC, context.getString(R.string.classification_public), null); + aca3.addChoice(Tasks.CLASSIFICATION_PRIVATE, context.getString(R.string.classification_private), null); + aca3.addChoice(Tasks.CLASSIFICATION_CONFIDENTIAL, context.getString(R.string.classification_confidential), null); + + // privacy + addField(new FieldDescriptor(context, R.id.task_field_classification, R.string.task_classification, TaskFieldAdapters.CLASSIFICATION) + .setViewLayout(CHOICES_VIEW).setEditorLayout(CHOICES_EDIT).setChoices(aca3).setIcon(R.drawable.ic_detail_visibility)); + + // url + addField(new FieldDescriptor(context, R.id.task_field_url, R.string.task_url, TaskFieldAdapters.URL).setViewLayout(URL_VIEW).setEditorLayout(URL_EDIT) + .setIcon(R.drawable.ic_detail_url)); + + // task list name + addField(new FieldDescriptor(context, R.id.task_field_list_and_account_name, R.string.task_list, null, TaskFieldAdapters.LIST_AND_ACCOUNT_NAME) + .setViewLayout(TEXT_VIEW_NO_LINKS).setIcon(R.drawable.ic_detail_list)); + + setAllowRecurrence(false); + setAllowExceptions(false); + + mInflated = true; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/FieldDescriptor.java b/opentasks/src/main/java/org/dmfs/tasks/model/FieldDescriptor.java index 4c3a53d4..3547aa05 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/FieldDescriptor.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/FieldDescriptor.java @@ -17,390 +17,396 @@ package org.dmfs.tasks.model; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + import org.dmfs.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.model.layout.LayoutDescriptor; import org.dmfs.tasks.model.layout.LayoutOptions; import org.dmfs.tasks.widget.AbstractFieldView; -import android.content.Context; -import android.view.LayoutInflater; -import android.view.ViewGroup; - /** * A FieldDescriptor holds all information about a certain task property or attribute. - * + * * @author Marten Gajda */ public final class FieldDescriptor { - /** - * An id that identifies the field. - */ - private final int mFieldId; - - /** - * The title of the field. - */ - private final String mTitle; - - /** - * A hint. This is not used by all editors. - */ - private String mHint; - - /** - * The content type of an extended property. - * - * This is currently unused and is subject to change in upcoming version. - */ - private final String mContentType; - - /** - * The {@link FieldAdapter} that knows how to load the values of this field form a {@link ContentSet}. - */ - private final FieldAdapter mFieldAdapter; - - /** - * A class implementing an {@link IChoicesAdapter} that provides the choices for this field. Can be null if this field doesn't support choices. - */ - private IChoicesAdapter mChoices = null; - - /** - * A {@link LayoutDescriptor} that provides the layout of an editor for this field. - */ - private LayoutDescriptor mEditLayout = null; - - /** - * A {@link LayoutDescriptor} that provides the layout of a detail view for this field. - */ - private LayoutDescriptor mViewLayout = null; - - /** - * Icon resource id of the field, if any. - */ - private int mIconId = 0; - - /** - * Specifies whether this field should be added automatically. - */ - private boolean mNoAutoAdd = false; - - - /** - * Constructor for a new field description. - * - * @param context - * The context holding the title resource. - * @param titleId - * The id of the title resource. - * @param fieldAdapter - * A {@link FieldAdapter} for this field. - */ - public FieldDescriptor(Context context, int fieldId, int titleId, FieldAdapter fieldAdapter) - { - this(fieldId, context.getString(titleId), null, fieldAdapter); - } - - - /** - * Constructor for a new field description. - * - * @param context - * The context holding the title resource. - * @param titleId - * The id of the title resource. - * @param contentType - * The contentType of this property. - * @param fieldAdapter - * A {@link FieldAdapter} for this field. - */ - public FieldDescriptor(Context context, int fieldId, int titleId, String contentType, FieldAdapter fieldAdapter) - { - this(fieldId, context.getString(titleId), contentType, fieldAdapter); - } - - - /** - * Constructor for a new field description. - * - * @param title - * A string for the title of this field. - * @param fieldAdapter - * A {@link FieldAdapter} for this field. - */ - public FieldDescriptor(int fieldId, String title, FieldAdapter fieldAdapter) - { - this(fieldId, title, null, fieldAdapter); - } - - - /** - * Constructor for a new data field description (a field with a content type). - * - * @param title - * A string for the title of this field. - * @param contentType - * The content type of the field. - * @param fieldAdapter - * A {@link FieldAdapter} for this field. - */ - public FieldDescriptor(int fieldId, String title, String contentType, FieldAdapter fieldAdapter) - { - if (fieldAdapter == null) - { - throw new NullPointerException("fieldAdapter must not be null!"); - } - mFieldId = fieldId; - mTitle = title; - mContentType = contentType; - mHint = title; // use title as hint by default - mFieldAdapter = fieldAdapter; - } - - - /** - * Returns the field id of the field this {@link FieldDescriptor} describes. - * - * @return The field id. - */ - public int getFieldId() - { - return mFieldId; - } - - - public FieldDescriptor setNoAutoAdd(boolean noAutoAdd) - { - mNoAutoAdd = noAutoAdd; - return this; - } - - - public boolean autoAdd() - { - return !mNoAutoAdd; - } - - - /** - * Returns the title of this field. - * - * @return The title. - */ - public String getTitle() - { - return mTitle; - } - - - /** - * Sets an icon id for this {@link FieldDescriptor}. - * - * @param iconId - * The id of the icon resource. - * @return This instance. - */ - public FieldDescriptor setIcon(int iconId) - { - mIconId = iconId; - return this; - } - - - /** - * Get the icon id of this {@link FieldDescriptor}. - * - * @return The icon resource id or 0 if there is no icon for this field. - */ - public int getIcon() - { - return mIconId; - } - - - /** - * Return the content type for this field. - * - * @return The content type or {@code null} if this field has no content type. - */ - public String getContentType() - { - return mContentType; - } - - - /** - * Returns the hint for this field. - * - * @return The hint. - */ - public String getHint() - { - return mHint; - } - - - /** - * Sets the hint for this field. - * - * @param hint - * The hint for this field. - * @return This instance. - */ - public FieldDescriptor setHint(String hint) - { - mHint = hint; - return this; - } - - - /** - * Returns a {@link FieldAdapter} for this field. - * - * @return The {@link FieldAdapter} instance. Will never be {@code null}. - */ - public FieldAdapter getFieldAdapter() - { - return mFieldAdapter; - } - - - /** - * Return a choices adapter for this field. - * - * @return An {@link IChoicesAdapter} or null if this field doesn't support choice. - */ - public IChoicesAdapter getChoices() - { - return mChoices; - } - - - /** - * Set an {@link IChoicesAdapter} for this field. - * - * @param choices - * An {@link IChoicesAdapter} or null to disable choices for this field. - * @return This instance. - */ - public FieldDescriptor setChoices(IChoicesAdapter choices) - { - mChoices = choices; - return this; - } - - - /** - * Returns an inflated view to edit this field. This method takes a parent (that can be null) but it doesn't attach the editor to the parent. - * - * @param inflater - * A {@link LayoutInflater}. - * @param parent - * The parent {@link ViewGroup} of the editor. - * @return An {@link AbstractFieldView} that can edit this field or null if this field is not editable. - */ - public AbstractFieldView getEditorView(LayoutInflater inflater, ViewGroup parent) - { - if (mEditLayout == null) - { - return null; - } - - AbstractFieldView view = (AbstractFieldView) mEditLayout.inflate(inflater, parent, false); - view.setFieldDescription(this, mEditLayout.getOptions()); - return view; - } - - - /** - * Returns an inflated view to edit this field. - * - * @param inflater - * A {@link LayoutInflater}. - * @return An {@link AbstractFieldView} that can edit this field or null if this field is not editable. - */ - public AbstractFieldView getEditorView(LayoutInflater inflater) - { - return getEditorView(inflater, null); - } - - - /** - * Returns an inflated view to show this field. This method takes a parent (that can be null) but it doesn't attach the detail view to the - * parent. - * - * @param inflater - * A {@link LayoutInflater}. - * @param parent - * The parent {@link ViewGroup} of the detail view. - * @return An {@link AbstractFieldView} that can edit this field or null if this field can be viewed. - */ - public AbstractFieldView getDetailView(LayoutInflater inflater, ViewGroup parent) - { - if (mViewLayout == null) - { - return null; - } - - AbstractFieldView view = (AbstractFieldView) mViewLayout.inflate(inflater, parent, false); - view.setFieldDescription(this, mViewLayout.getOptions()); - return view; - } - - - /** - * Returns an inflated view to show this field. - * - * @param inflater - * A {@link LayoutInflater}. - */ - public AbstractFieldView getDetailView(LayoutInflater inflater) - { - if (mViewLayout == null) - { - return null; - } - - AbstractFieldView view = (AbstractFieldView) mViewLayout.inflate(inflater); - view.setFieldDescription(this, mViewLayout.getOptions()); - return view; - } - - - public LayoutOptions getViewLayoutOptions() - { - if (mViewLayout == null) - { - return null; - } - - return mViewLayout.getOptions(); - } - - - public LayoutOptions getEditLayoutOptions() - { - if (mEditLayout == null) - { - return null; - } - - return mEditLayout.getOptions(); - } - - - FieldDescriptor setEditorLayout(LayoutDescriptor layoutDescriptor) - { - mEditLayout = layoutDescriptor; - return this; - } - - - FieldDescriptor setViewLayout(LayoutDescriptor layoutDescriptor) - { - mViewLayout = layoutDescriptor; - return this; - } + /** + * An id that identifies the field. + */ + private final int mFieldId; + + /** + * The title of the field. + */ + private final String mTitle; + + /** + * A hint. This is not used by all editors. + */ + private String mHint; + + /** + * The content type of an extended property. + *

    + * This is currently unused and is subject to change in upcoming version. + */ + private final String mContentType; + + /** + * The {@link FieldAdapter} that knows how to load the values of this field form a {@link ContentSet}. + */ + private final FieldAdapter mFieldAdapter; + + /** + * A class implementing an {@link IChoicesAdapter} that provides the choices for this field. Can be null if this field doesn't support choices. + */ + private IChoicesAdapter mChoices = null; + + /** + * A {@link LayoutDescriptor} that provides the layout of an editor for this field. + */ + private LayoutDescriptor mEditLayout = null; + + /** + * A {@link LayoutDescriptor} that provides the layout of a detail view for this field. + */ + private LayoutDescriptor mViewLayout = null; + + /** + * Icon resource id of the field, if any. + */ + private int mIconId = 0; + + /** + * Specifies whether this field should be added automatically. + */ + private boolean mNoAutoAdd = false; + + + /** + * Constructor for a new field description. + * + * @param context + * The context holding the title resource. + * @param titleId + * The id of the title resource. + * @param fieldAdapter + * A {@link FieldAdapter} for this field. + */ + public FieldDescriptor(Context context, int fieldId, int titleId, FieldAdapter fieldAdapter) + { + this(fieldId, context.getString(titleId), null, fieldAdapter); + } + + + /** + * Constructor for a new field description. + * + * @param context + * The context holding the title resource. + * @param titleId + * The id of the title resource. + * @param contentType + * The contentType of this property. + * @param fieldAdapter + * A {@link FieldAdapter} for this field. + */ + public FieldDescriptor(Context context, int fieldId, int titleId, String contentType, FieldAdapter fieldAdapter) + { + this(fieldId, context.getString(titleId), contentType, fieldAdapter); + } + + + /** + * Constructor for a new field description. + * + * @param title + * A string for the title of this field. + * @param fieldAdapter + * A {@link FieldAdapter} for this field. + */ + public FieldDescriptor(int fieldId, String title, FieldAdapter fieldAdapter) + { + this(fieldId, title, null, fieldAdapter); + } + + + /** + * Constructor for a new data field description (a field with a content type). + * + * @param title + * A string for the title of this field. + * @param contentType + * The content type of the field. + * @param fieldAdapter + * A {@link FieldAdapter} for this field. + */ + public FieldDescriptor(int fieldId, String title, String contentType, FieldAdapter fieldAdapter) + { + if (fieldAdapter == null) + { + throw new NullPointerException("fieldAdapter must not be null!"); + } + mFieldId = fieldId; + mTitle = title; + mContentType = contentType; + mHint = title; // use title as hint by default + mFieldAdapter = fieldAdapter; + } + + + /** + * Returns the field id of the field this {@link FieldDescriptor} describes. + * + * @return The field id. + */ + public int getFieldId() + { + return mFieldId; + } + + + public FieldDescriptor setNoAutoAdd(boolean noAutoAdd) + { + mNoAutoAdd = noAutoAdd; + return this; + } + + + public boolean autoAdd() + { + return !mNoAutoAdd; + } + + + /** + * Returns the title of this field. + * + * @return The title. + */ + public String getTitle() + { + return mTitle; + } + + + /** + * Sets an icon id for this {@link FieldDescriptor}. + * + * @param iconId + * The id of the icon resource. + * + * @return This instance. + */ + public FieldDescriptor setIcon(int iconId) + { + mIconId = iconId; + return this; + } + + + /** + * Get the icon id of this {@link FieldDescriptor}. + * + * @return The icon resource id or 0 if there is no icon for this field. + */ + public int getIcon() + { + return mIconId; + } + + + /** + * Return the content type for this field. + * + * @return The content type or {@code null} if this field has no content type. + */ + public String getContentType() + { + return mContentType; + } + + + /** + * Returns the hint for this field. + * + * @return The hint. + */ + public String getHint() + { + return mHint; + } + + + /** + * Sets the hint for this field. + * + * @param hint + * The hint for this field. + * + * @return This instance. + */ + public FieldDescriptor setHint(String hint) + { + mHint = hint; + return this; + } + + + /** + * Returns a {@link FieldAdapter} for this field. + * + * @return The {@link FieldAdapter} instance. Will never be {@code null}. + */ + public FieldAdapter getFieldAdapter() + { + return mFieldAdapter; + } + + + /** + * Return a choices adapter for this field. + * + * @return An {@link IChoicesAdapter} or null if this field doesn't support choice. + */ + public IChoicesAdapter getChoices() + { + return mChoices; + } + + + /** + * Set an {@link IChoicesAdapter} for this field. + * + * @param choices + * An {@link IChoicesAdapter} or null to disable choices for this field. + * + * @return This instance. + */ + public FieldDescriptor setChoices(IChoicesAdapter choices) + { + mChoices = choices; + return this; + } + + + /** + * Returns an inflated view to edit this field. This method takes a parent (that can be null) but it doesn't attach the editor to the parent. + * + * @param inflater + * A {@link LayoutInflater}. + * @param parent + * The parent {@link ViewGroup} of the editor. + * + * @return An {@link AbstractFieldView} that can edit this field or null if this field is not editable. + */ + public AbstractFieldView getEditorView(LayoutInflater inflater, ViewGroup parent) + { + if (mEditLayout == null) + { + return null; + } + + AbstractFieldView view = (AbstractFieldView) mEditLayout.inflate(inflater, parent, false); + view.setFieldDescription(this, mEditLayout.getOptions()); + return view; + } + + + /** + * Returns an inflated view to edit this field. + * + * @param inflater + * A {@link LayoutInflater}. + * + * @return An {@link AbstractFieldView} that can edit this field or null if this field is not editable. + */ + public AbstractFieldView getEditorView(LayoutInflater inflater) + { + return getEditorView(inflater, null); + } + + + /** + * Returns an inflated view to show this field. This method takes a parent (that can be null) but it doesn't attach the detail view to the + * parent. + * + * @param inflater + * A {@link LayoutInflater}. + * @param parent + * The parent {@link ViewGroup} of the detail view. + * + * @return An {@link AbstractFieldView} that can edit this field or null if this field can be viewed. + */ + public AbstractFieldView getDetailView(LayoutInflater inflater, ViewGroup parent) + { + if (mViewLayout == null) + { + return null; + } + + AbstractFieldView view = (AbstractFieldView) mViewLayout.inflate(inflater, parent, false); + view.setFieldDescription(this, mViewLayout.getOptions()); + return view; + } + + + /** + * Returns an inflated view to show this field. + * + * @param inflater + * A {@link LayoutInflater}. + */ + public AbstractFieldView getDetailView(LayoutInflater inflater) + { + if (mViewLayout == null) + { + return null; + } + + AbstractFieldView view = (AbstractFieldView) mViewLayout.inflate(inflater); + view.setFieldDescription(this, mViewLayout.getOptions()); + return view; + } + + + public LayoutOptions getViewLayoutOptions() + { + if (mViewLayout == null) + { + return null; + } + + return mViewLayout.getOptions(); + } + + + public LayoutOptions getEditLayoutOptions() + { + if (mEditLayout == null) + { + return null; + } + + return mEditLayout.getOptions(); + } + + + FieldDescriptor setEditorLayout(LayoutDescriptor layoutDescriptor) + { + mEditLayout = layoutDescriptor; + return this; + } + + + FieldDescriptor setViewLayout(LayoutDescriptor layoutDescriptor) + { + mViewLayout = layoutDescriptor; + return this; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/IChoicesAdapter.java b/opentasks/src/main/java/org/dmfs/tasks/model/IChoicesAdapter.java index 4f7e0577..d200df2c 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/IChoicesAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/IChoicesAdapter.java @@ -22,59 +22,59 @@ import android.graphics.drawable.Drawable; /** * An interface to a class that provides a number of choices of any type to the user. Choices must be unique and they are matched by {@link #equals(Object)}. - * + * * @author Marten Gajda */ public interface IChoicesAdapter { - /** - * Get the title of the given object. - * - * @param object - * An object that is among the choices. - * @return The title or null if no such object is found among the choices. - */ - public String getTitle(Object object); - - - /** - * Get a {@link Drawable} for the given object. - * - * @param object - * An object that is among the choices. - * @return A {@link Drawable} or null if no such object is found among the choices. - */ - public Drawable getDrawable(Object object); - - - /** - * Get the position of the object among the choices. - * - * @param object - * An object that is among the choices. - * @return - * The position of the choice or -1 if no such object is found among the choices. - */ - public int getIndex(Object object); + /** + * Get the title of the given object. + * + * @param object + * An object that is among the choices. + * + * @return The title or null if no such object is found among the choices. + */ + public String getTitle(Object object); + /** + * Get a {@link Drawable} for the given object. + * + * @param object + * An object that is among the choices. + * + * @return A {@link Drawable} or null if no such object is found among the choices. + */ + public Drawable getDrawable(Object object); - /** - * Get the number of choices. - * - * @return The number of choices. - */ - public int getCount(); + /** + * Get the position of the object among the choices. + * + * @param object + * An object that is among the choices. + * + * @return The position of the choice or -1 if no such object is found among the choices. + */ + public int getIndex(Object object); + /** + * Get the number of choices. + * + * @return The number of choices. + */ + public int getCount(); - /** - * Get the choice at the specified position. - * - * @param position - * The position. - * @return The choice object. - * @throws IndexOutOfBoundsException - * if the position is invalid. - */ - public Object getItem(int position); + /** + * Get the choice at the specified position. + * + * @param position + * The position. + * + * @return The choice object. + * + * @throws IndexOutOfBoundsException + * if the position is invalid. + */ + public Object getItem(int position); } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/IllegalDataKindException.java b/opentasks/src/main/java/org/dmfs/tasks/model/IllegalDataKindException.java index da3573ba..63ab063e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/IllegalDataKindException.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/IllegalDataKindException.java @@ -20,14 +20,14 @@ package org.dmfs.tasks.model; public class IllegalDataKindException extends Exception { - /** - * Generated serial. - */ - private static final long serialVersionUID = 8108680361456157978L; + /** + * Generated serial. + */ + private static final long serialVersionUID = 8108680361456157978L; - public IllegalDataKindException(String msg) - { - super(msg); - } + public IllegalDataKindException(String msg) + { + super(msg); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java b/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java index fcc8bf3f..9a03ad6f 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java @@ -17,55 +17,55 @@ package org.dmfs.tasks.model; -import org.dmfs.tasks.R; -import org.dmfs.tasks.model.layout.LayoutDescriptor; - import android.content.Context; import android.text.util.Linkify; +import org.dmfs.tasks.R; +import org.dmfs.tasks.model.layout.LayoutDescriptor; + /** * A minimal model every sync adapter should support. - * + * * @author Marten Gajda */ public class MinimalModel extends Model { - private final static LayoutDescriptor TEXT_VIEW = new LayoutDescriptor(R.layout.text_field_view).setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL); - private final static LayoutDescriptor TEXT_EDIT_SINGLE_LINE = new LayoutDescriptor(R.layout.text_field_editor).setOption(LayoutDescriptor.OPTION_MULTILINE, - false); - private final static LayoutDescriptor TIME_VIEW_ADD_BUTTON = new LayoutDescriptor(R.layout.time_field_view).setOption( - LayoutDescriptor.OPTION_TIME_FIELD_SHOW_ADD_BUTTONS, true); - private final static LayoutDescriptor TIME_EDIT = new LayoutDescriptor(R.layout.time_field_editor); + private final static LayoutDescriptor TEXT_VIEW = new LayoutDescriptor(R.layout.text_field_view).setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL); + private final static LayoutDescriptor TEXT_EDIT_SINGLE_LINE = new LayoutDescriptor(R.layout.text_field_editor).setOption(LayoutDescriptor.OPTION_MULTILINE, + false); + private final static LayoutDescriptor TIME_VIEW_ADD_BUTTON = new LayoutDescriptor(R.layout.time_field_view).setOption( + LayoutDescriptor.OPTION_TIME_FIELD_SHOW_ADD_BUTTONS, true); + private final static LayoutDescriptor TIME_EDIT = new LayoutDescriptor(R.layout.time_field_editor); - MinimalModel(Context context, String accountType) - { - super(context, accountType); - } + MinimalModel(Context context, String accountType) + { + super(context, accountType); + } - @Override - public void inflate() - { - if (mInflated) - { - return; - } + @Override + public void inflate() + { + if (mInflated) + { + return; + } - Context context = getContext(); + Context context = getContext(); - // task title - addField(new FieldDescriptor(context, R.id.task_field_title, R.string.task_title, TaskFieldAdapters.TITLE).setViewLayout(TEXT_VIEW).setEditorLayout( - TEXT_EDIT_SINGLE_LINE)); + // task title + addField(new FieldDescriptor(context, R.id.task_field_title, R.string.task_title, TaskFieldAdapters.TITLE).setViewLayout(TEXT_VIEW).setEditorLayout( + TEXT_EDIT_SINGLE_LINE)); - // due - addField(new FieldDescriptor(context, R.id.task_field_due, R.string.task_due, TaskFieldAdapters.DUE).setViewLayout(TIME_VIEW_ADD_BUTTON) - .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_due)); + // due + addField(new FieldDescriptor(context, R.id.task_field_due, R.string.task_due, TaskFieldAdapters.DUE).setViewLayout(TIME_VIEW_ADD_BUTTON) + .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_due)); - setAllowRecurrence(false); - setAllowExceptions(false); + setAllowRecurrence(false); + setAllowExceptions(false); - mInflated = true; - } + mInflated = true; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/Model.java b/opentasks/src/main/java/org/dmfs/tasks/model/Model.java index 0888722a..6f4c7534 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/Model.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/Model.java @@ -17,13 +17,6 @@ package org.dmfs.tasks.model; -import java.util.ArrayList; -import java.util.List; - -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.provider.tasks.TaskContract.TaskLists; -import org.dmfs.tasks.ManageListActivity; - import android.accounts.Account; import android.app.Activity; import android.content.ComponentName; @@ -33,234 +26,241 @@ import android.content.Intent; import android.support.v4.util.SparseArrayCompat; import android.text.TextUtils; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.provider.tasks.TaskContract.TaskLists; +import org.dmfs.tasks.ManageListActivity; + +import java.util.ArrayList; +import java.util.List; + /** * An abstract model class. - * + * * @author Marten Gajda */ public abstract class Model { - private final static String INTENT_CATEGORY_PREFIX = "org.dmfs.intent.category."; - private final static String EXTRA_COLOR_HINT = "org.dmfs.COLOR_HINT"; - private final static String EXTRA_TITLE_HINT = "org.dmfs.TITLE_HINT"; + private final static String INTENT_CATEGORY_PREFIX = "org.dmfs.intent.category."; + private final static String EXTRA_COLOR_HINT = "org.dmfs.COLOR_HINT"; + private final static String EXTRA_TITLE_HINT = "org.dmfs.TITLE_HINT"; - /** - * A {@link List} of {@link FieldDescriptor}s of all fields that a model supports. - */ - private final List mFields = new ArrayList(); - private final SparseArrayCompat mFieldIndex = new SparseArrayCompat(16); + /** + * A {@link List} of {@link FieldDescriptor}s of all fields that a model supports. + */ + private final List mFields = new ArrayList(); + private final SparseArrayCompat mFieldIndex = new SparseArrayCompat(16); - private final Context mContext; - private final String mAuthority; + private final Context mContext; + private final String mAuthority; - boolean mInflated = false; + boolean mInflated = false; - private boolean mAllowRecurrence = false; - private boolean mAllowExceptions = false; - private int mIconId = -1; - private int mLabelId = -1; - private String mAccountType; + private boolean mAllowRecurrence = false; + private boolean mAllowExceptions = false; + private int mIconId = -1; + private int mLabelId = -1; + private String mAccountType; - private Boolean mSupportsInsertListIntent; - private Boolean mSupportsEditListIntent; + private Boolean mSupportsInsertListIntent; + private Boolean mSupportsEditListIntent; - protected Model(Context context, String accountType) - { - mContext = context; - mAccountType = accountType; - mAuthority = TaskContract.taskAuthority(context); - } + protected Model(Context context, String accountType) + { + mContext = context; + mAccountType = accountType; + mAuthority = TaskContract.taskAuthority(context); + } - public final Context getContext() - { - return mContext; - } + public final Context getContext() + { + return mContext; + } - public abstract void inflate() throws ModelInflaterException; + public abstract void inflate() throws ModelInflaterException; - /** - * Adds another field (identified by its field descriptor) to this model. - * - * @param descriptor - * The {@link FieldDescriptor} of the field to add. - */ - protected void addField(FieldDescriptor descriptor) - { - mFields.add(descriptor); - mFieldIndex.put(descriptor.getFieldId(), descriptor); - } + /** + * Adds another field (identified by its field descriptor) to this model. + * + * @param descriptor + * The {@link FieldDescriptor} of the field to add. + */ + protected void addField(FieldDescriptor descriptor) + { + mFields.add(descriptor); + mFieldIndex.put(descriptor.getFieldId(), descriptor); + } - public FieldDescriptor getField(int fieldId) - { - return mFieldIndex.get(fieldId, null); - } + public FieldDescriptor getField(int fieldId) + { + return mFieldIndex.get(fieldId, null); + } - public List getFields() - { - try - { - inflate(); - } - catch (ModelInflaterException e) - { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return new ArrayList(mFields); - } + public List getFields() + { + try + { + inflate(); + } + catch (ModelInflaterException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return new ArrayList(mFields); + } - public boolean getAllowRecurrence() - { - return mAllowRecurrence; - } + public boolean getAllowRecurrence() + { + return mAllowRecurrence; + } - void setAllowRecurrence(boolean allowRecurrence) - { - mAllowRecurrence = allowRecurrence; - } + void setAllowRecurrence(boolean allowRecurrence) + { + mAllowRecurrence = allowRecurrence; + } - public boolean getAllowExceptions() - { - return mAllowExceptions; - } + public boolean getAllowExceptions() + { + return mAllowExceptions; + } - void setAllowExceptions(boolean allowExceptions) - { - mAllowExceptions = allowExceptions; - } + void setAllowExceptions(boolean allowExceptions) + { + mAllowExceptions = allowExceptions; + } - public int getIconId() - { - return mIconId; - } + public int getIconId() + { + return mIconId; + } - void setIconId(int iconId) - { - mIconId = iconId; - } + void setIconId(int iconId) + { + mIconId = iconId; + } - public int getLabelId() - { - return mLabelId; - } - - - void setLabelId(int titleId) - { - mLabelId = titleId; - } + public int getLabelId() + { + return mLabelId; + } + + + void setLabelId(int titleId) + { + mLabelId = titleId; + } - public String getAccountType() - { - return mAccountType; - } - - - public String getAccountLabel() - { - return ""; - } - - - public void startInsertIntent(Activity activity, Account account) - { - if (!hasInsertActivity()) - { - throw new IllegalStateException("Syncadapter for " + mAccountType + " does not support inserting lists."); - } - - activity.startActivity(getListIntent(mContext, Intent.ACTION_INSERT, account)); - } - - - public void startEditIntent(Activity activity, Account account, long listId, String nameHint, Integer colorHint) - { - if (!hasEditActivity()) - { - throw new IllegalStateException("Syncadapter for " + mAccountType + " does not support editing lists."); - } - - Intent intent = getListIntent(mContext, Intent.ACTION_EDIT, account); - intent.setData(ContentUris.withAppendedId(TaskLists.getContentUri(mAuthority), listId)); - if (nameHint != null) - { - intent.putExtra(EXTRA_TITLE_HINT, nameHint); - } - if (colorHint != null) - { - intent.putExtra(EXTRA_COLOR_HINT, colorHint); - } - activity.startActivity(intent); - } - - - public boolean hasEditActivity() - { - if (mSupportsEditListIntent == null) - { - ComponentName editComponent = getListIntent(mContext, Intent.ACTION_EDIT, null).setData( - ContentUris.withAppendedId(TaskLists.getContentUri(mAuthority), 0 /* for pure intent resolution it doesn't matter which id we append */)) - .resolveActivity(mContext.getPackageManager()); - mSupportsEditListIntent = editComponent != null; - } - - return mSupportsEditListIntent; - } + public String getAccountType() + { + return mAccountType; + } + + + public String getAccountLabel() + { + return ""; + } + + + public void startInsertIntent(Activity activity, Account account) + { + if (!hasInsertActivity()) + { + throw new IllegalStateException("Syncadapter for " + mAccountType + " does not support inserting lists."); + } + + activity.startActivity(getListIntent(mContext, Intent.ACTION_INSERT, account)); + } + + + public void startEditIntent(Activity activity, Account account, long listId, String nameHint, Integer colorHint) + { + if (!hasEditActivity()) + { + throw new IllegalStateException("Syncadapter for " + mAccountType + " does not support editing lists."); + } + + Intent intent = getListIntent(mContext, Intent.ACTION_EDIT, account); + intent.setData(ContentUris.withAppendedId(TaskLists.getContentUri(mAuthority), listId)); + if (nameHint != null) + { + intent.putExtra(EXTRA_TITLE_HINT, nameHint); + } + if (colorHint != null) + { + intent.putExtra(EXTRA_COLOR_HINT, colorHint); + } + activity.startActivity(intent); + } + + + public boolean hasEditActivity() + { + if (mSupportsEditListIntent == null) + { + ComponentName editComponent = getListIntent(mContext, Intent.ACTION_EDIT, null).setData( + ContentUris.withAppendedId(TaskLists.getContentUri(mAuthority), 0 /* for pure intent resolution it doesn't matter which id we append */)) + .resolveActivity(mContext.getPackageManager()); + mSupportsEditListIntent = editComponent != null; + } + + return mSupportsEditListIntent; + } - - public boolean hasInsertActivity() - { - if (mSupportsInsertListIntent == null) - { - ComponentName insertComponent = getListIntent(mContext, Intent.ACTION_INSERT, null).resolveActivity(mContext.getPackageManager()); - mSupportsInsertListIntent = insertComponent != null; - } - - return mSupportsInsertListIntent; - } - - - private Intent getListIntent(Context context, String action, Account account) - { - // insert action - Intent insertIntent = new Intent(); - insertIntent.setAction(action); - insertIntent.setData(TaskLists.getContentUri(mAuthority)); - insertIntent.addCategory(INTENT_CATEGORY_PREFIX + mAccountType); - if (account != null) - { - insertIntent.putExtra(ManageListActivity.EXTRA_ACCOUNT, account); - } - return insertIntent; - } - - - @Override - public boolean equals(Object o) - { - if (!(o instanceof Model)) - { - return false; - } - Class otherClass = o.getClass(); - Class myClass = getClass(); - - return myClass.equals(otherClass) && TextUtils.equals(mAccountType, ((Model) o).mAccountType); - } + + public boolean hasInsertActivity() + { + if (mSupportsInsertListIntent == null) + { + ComponentName insertComponent = getListIntent(mContext, Intent.ACTION_INSERT, null).resolveActivity(mContext.getPackageManager()); + mSupportsInsertListIntent = insertComponent != null; + } + + return mSupportsInsertListIntent; + } + + + private Intent getListIntent(Context context, String action, Account account) + { + // insert action + Intent insertIntent = new Intent(); + insertIntent.setAction(action); + insertIntent.setData(TaskLists.getContentUri(mAuthority)); + insertIntent.addCategory(INTENT_CATEGORY_PREFIX + mAccountType); + if (account != null) + { + insertIntent.putExtra(ManageListActivity.EXTRA_ACCOUNT, account); + } + return insertIntent; + } + + + @Override + public boolean equals(Object o) + { + if (!(o instanceof Model)) + { + return false; + } + Class otherClass = o.getClass(); + Class myClass = getClass(); + + return myClass.equals(otherClass) && TextUtils.equals(mAccountType, ((Model) o).mAccountType); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/ModelInflaterException.java b/opentasks/src/main/java/org/dmfs/tasks/model/ModelInflaterException.java index 78e150a1..2138c119 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/ModelInflaterException.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/ModelInflaterException.java @@ -20,20 +20,20 @@ package org.dmfs.tasks.model; public class ModelInflaterException extends Exception { - /** - * Generated serial. - */ - private static final long serialVersionUID = 2498148128490322210L; + /** + * Generated serial. + */ + private static final long serialVersionUID = 2498148128490322210L; - public ModelInflaterException(String msg) - { - super(msg); - } + public ModelInflaterException(String msg) + { + super(msg); + } - public ModelInflaterException(String msg, Throwable e) - { - super(msg, e); - } + public ModelInflaterException(String msg, Throwable e) + { + super(msg, e); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/OnContentChangeListener.java b/opentasks/src/main/java/org/dmfs/tasks/model/OnContentChangeListener.java index bcfa7a14..a52fbf7d 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/OnContentChangeListener.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/OnContentChangeListener.java @@ -19,25 +19,24 @@ package org.dmfs.tasks.model; /** * Interface for listeners that listen to changes in a {@link ContentSet}. - * + * * @author Marten Gajda */ public interface OnContentChangeListener { - /** - * Called whenever a specific key in a {@link ContentSet} has changed. - * - * @param contentSet - * The {@link ContentSet} that contains the changed key. - */ - public void onContentChanged(ContentSet contentSet); - + /** + * Called whenever a specific key in a {@link ContentSet} has changed. + * + * @param contentSet + * The {@link ContentSet} that contains the changed key. + */ + public void onContentChanged(ContentSet contentSet); - /** - * Called whenever the {@link ContentSet} has been (re-)loaded. - * - * @param contentSet - * The {@link ContentSet} that has been reloaded. - */ - public void onContentLoaded(ContentSet contentSet); + /** + * Called whenever the {@link ContentSet} has been (re-)loaded. + * + * @param contentSet + * The {@link ContentSet} that has been reloaded. + */ + public void onContentLoaded(ContentSet contentSet); } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/ResourceArrayChoicesAdapter.java b/opentasks/src/main/java/org/dmfs/tasks/model/ResourceArrayChoicesAdapter.java index 8acad873..c0e29154 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/ResourceArrayChoicesAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/ResourceArrayChoicesAdapter.java @@ -17,79 +17,79 @@ package org.dmfs.tasks.model; -import java.util.ArrayList; -import java.util.Arrays; - import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import java.util.ArrayList; +import java.util.Arrays; + /** * ArrayAdapter which loads the Array elements from a resource. - * + * * @author Arjun Naik */ public class ResourceArrayChoicesAdapter extends AbstractArrayChoicesAdapter { - /** - * Constructor uses the resource id and the context to load the array - * - * @param valuesResource - * The resource id of the array - * @param context - * The context - */ - public ResourceArrayChoicesAdapter(int valuesResource, Context context) - { - mChoices = mVisibleChoices = new ArrayList(Arrays.asList(context.getResources().getStringArray(valuesResource))); - } + /** + * Constructor uses the resource id and the context to load the array + * + * @param valuesResource + * The resource id of the array + * @param context + * The context + */ + public ResourceArrayChoicesAdapter(int valuesResource, Context context) + { + mChoices = mVisibleChoices = new ArrayList(Arrays.asList(context.getResources().getStringArray(valuesResource))); + } - /** - * Constructor uses the resource id of the values array and the titles array and the context to load the array - * - * @param valuesResource - * The resource id of the values array - * @param titlesResource - * The resource id of the titles array - * @param context - * The context - */ - public ResourceArrayChoicesAdapter(int valuesResource, int titlesResource, Context context) - { - Resources resources = context.getResources(); - mTitles = Arrays.asList(resources.getStringArray(titlesResource)); - mChoices = mVisibleChoices = new ArrayList(Arrays.asList(resources.getStringArray(valuesResource))); - } + /** + * Constructor uses the resource id of the values array and the titles array and the context to load the array + * + * @param valuesResource + * The resource id of the values array + * @param titlesResource + * The resource id of the titles array + * @param context + * The context + */ + public ResourceArrayChoicesAdapter(int valuesResource, int titlesResource, Context context) + { + Resources resources = context.getResources(); + mTitles = Arrays.asList(resources.getStringArray(titlesResource)); + mChoices = mVisibleChoices = new ArrayList(Arrays.asList(resources.getStringArray(valuesResource))); + } - /** - * Constructor uses the resource id of the values array and the titles array and the context to load the array - * - * @param valuesResource - * The resource id of the values array - * @param titlesResource - * The resource id of the titles array - * @param drawablesResource - * The resource id of the drawables array - * @param context - * The context - */ - public ResourceArrayChoicesAdapter(int valuesResource, int titlesResource, int drawablesResource, Context context) - { - Resources resources = context.getResources(); - mTitles = Arrays.asList(resources.getStringArray(titlesResource)); - mChoices = mVisibleChoices = new ArrayList(Arrays.asList(resources.getStringArray(valuesResource))); - TypedArray drawables = resources.obtainTypedArray(drawablesResource); - mDrawables = new ArrayList(); - for (int i = 0; i < drawables.length(); i++) - { - mDrawables.add(drawables.getDrawable(i)); - } - drawables.recycle(); - } + /** + * Constructor uses the resource id of the values array and the titles array and the context to load the array + * + * @param valuesResource + * The resource id of the values array + * @param titlesResource + * The resource id of the titles array + * @param drawablesResource + * The resource id of the drawables array + * @param context + * The context + */ + public ResourceArrayChoicesAdapter(int valuesResource, int titlesResource, int drawablesResource, Context context) + { + Resources resources = context.getResources(); + mTitles = Arrays.asList(resources.getStringArray(titlesResource)); + mChoices = mVisibleChoices = new ArrayList(Arrays.asList(resources.getStringArray(valuesResource))); + TypedArray drawables = resources.obtainTypedArray(drawablesResource); + mDrawables = new ArrayList(); + for (int i = 0; i < drawables.length(); i++) + { + mDrawables.add(drawables.getDrawable(i)); + } + drawables.recycle(); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/Sources.java b/opentasks/src/main/java/org/dmfs/tasks/model/Sources.java index 7b943d27..abac31dd 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/Sources.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/Sources.java @@ -17,15 +17,6 @@ package org.dmfs.tasks.model; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.dmfs.provider.tasks.TaskContract; -import org.dmfs.tasks.utils.AsyncModelLoader; -import org.dmfs.tasks.utils.OnModelLoadedListener; - import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; @@ -39,286 +30,298 @@ import android.content.SyncAdapterType; import android.text.TextUtils; import android.util.Log; +import org.dmfs.provider.tasks.TaskContract; +import org.dmfs.tasks.utils.AsyncModelLoader; +import org.dmfs.tasks.utils.OnModelLoadedListener; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * Holds model definitions for all available task sources. - * + * * @author Marten Gajda */ public final class Sources extends BroadcastReceiver implements OnAccountsUpdateListener { - public final static String TAG = "org.dmfs.tasks.model.Sources"; - - /** - * A Singleton instance in order to allow freeing it under memory pressure. - */ - private static Sources sInstance = null; - - /** - * Maps account types to their respective task model. - */ - private Map mAccountModelMap = new HashMap(); - - /** - * Our application context. - */ - private final Context mContext; - - /** - * The cached account manager. - */ - private final AccountManager mAccountManager; - - private final String mAuthority; - - - /** - * Get the Sources singleton instance. Don't call this from the UI thread since it may take a long time to gather all the information from the account - * manager. - */ - public static synchronized Sources getInstance(Context context) - { - if (sInstance == null) - { - sInstance = new Sources(context); - } - return sInstance; - } - - - /** - * Load a model asynchronously. This might be executed as a synchronous operation if the models have been loaded already. - * - * @param context - * A {@link Context}. - * @param accountType - * The account type of the model to load. - * @param listener - * The listener to call when the model has been loaded. - * @return true if the models were loaded already and the operation was executed synchronously, false otherwise. - */ - public static boolean loadModelAsync(Context context, String accountType, OnModelLoadedListener listener) - { - if (sInstance == null) - { - new AsyncModelLoader(context, listener).execute(accountType); - return false; - } - else - { - Sources sources = getInstance(context); - listener.onModelLoaded(sources.getModel(accountType)); - return true; - } - } - - - /** - * Initialize all model sources. - * - * @param context - */ - private Sources(Context context) - { - mContext = context.getApplicationContext(); - - mAuthority = TaskContract.taskAuthority(context); - - // register to receive package changes - IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addAction(Intent.ACTION_PACKAGE_CHANGED); - filter.addDataScheme("package"); - mContext.registerReceiver(this, filter); - - // register to receive locale changes, we do that to reload labels and titles in that case - filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); - mContext.registerReceiver(this, filter); - - // get accounts and build model map - mAccountManager = AccountManager.get(mContext); - mAccountManager.addOnAccountsUpdatedListener(this, null, false); - getAccounts(); - } - - - /** - * Builds the model map. This method determines all available task sources and loads their respective models from XML (falling back to {@link DefaultModel - * if no XML was found or it is broken). - */ - protected void getAccounts() - { - // remove old models if any - mAccountModelMap.clear(); - - final AuthenticatorDescription[] authenticators = mAccountManager.getAuthenticatorTypes(); - - final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes(); - - for (SyncAdapterType syncAdapter : syncAdapters) - { - if (!mAuthority.equals(syncAdapter.authority)) - { - // this sync-adapter is not for Tasks, skip it - continue; - } - - AuthenticatorDescription authenticator = getAuthenticator(authenticators, syncAdapter.accountType); - - if (authenticator == null) - { - // no authenticator for this account available - continue; - } - - Model model; - try - { - // try to load the XML model - model = new XmlModel(mContext, authenticator); - model.inflate(); - Log.i(TAG, "inflated model for " + authenticator.type); - } - catch (ModelInflaterException e) - { - Log.e(TAG, "error inflating model for " + authenticator.packageName, e); - model = new DefaultModel(mContext, authenticator.type); - try - { - model.inflate(); - } - catch (ModelInflaterException e1) - { - continue; - } - } - - if (model.getIconId() == -1) - { - model.setIconId(authenticator.iconId); - } - if (model.getLabelId() == -1) - { - model.setLabelId(authenticator.labelId); - } - - mAccountModelMap.put(authenticator.type, model); - } - - try - { - // add default model for LOCAL account type (i.e. the unsynced account). - Model defaultModel = new DefaultModel(mContext, TaskContract.LOCAL_ACCOUNT_TYPE); - defaultModel.inflate(); - mAccountModelMap.put(TaskContract.LOCAL_ACCOUNT_TYPE, defaultModel); - } - catch (ModelInflaterException e) - { - Log.e(TAG, "could not inflate default model", e); - } - - } - - - /** - * Return the {@link AuthenticatorDescription} for the given account type. - * - * @param accountType - * The account type to find. - * @return The {@link AuthenticatorDescription} for the given account type or {@code null} if no such account exists. - */ - private AuthenticatorDescription getAuthenticator(AuthenticatorDescription[] authenticators, String accountType) - { - for (AuthenticatorDescription auth : authenticators) - { - if (TextUtils.equals(accountType, auth.type)) - { - return auth; - } - } - // no authenticator for that account type found - return null; - } - - - /** - * Return the task model for the given account type. - * - * @param accountType - * The account type. - * @return A {@link Model} instance for the given account type or {@code null} if no model was found. - */ - public Model getModel(String accountType) - { - return mAccountModelMap.get(accountType); - } - - - public Model getMinimalModel(String accountType) - { - Model result = new MinimalModel(mContext, accountType); - try - { - result.inflate(); - } - catch (ModelInflaterException e) - { - throw new RuntimeException("can't inflate mimimal model", e); - } - return result; - } - - - /** - * Return all accounts that support the task authority. - * - * @return A {@link List} of {@link Account}s, will never be null. - */ - public List getExistingAccounts() - { - List result = new ArrayList(); - Account[] accounts = mAccountManager.getAccounts(); - for (Account account : accounts) - { - if (getModel(account.type) != null && ContentResolver.getIsSyncable(account, mAuthority) > 0) - { - result.add(account); - } - } - return result; - } - - - /** - * Return a default model. This model can be used if {@link #getModel(String)} returned {@code null}. Which should not happen btw. - * - * @return A {@link Model} instance. - */ - public Model getDefaultModel() - { - return mAccountModelMap.get(TaskContract.LOCAL_ACCOUNT_TYPE); - } - - - @Override - public void onAccountsUpdated(Account[] accounts) - { - // the account list has changed, rebuild model map + public final static String TAG = "org.dmfs.tasks.model.Sources"; + + /** + * A Singleton instance in order to allow freeing it under memory pressure. + */ + private static Sources sInstance = null; + + /** + * Maps account types to their respective task model. + */ + private Map mAccountModelMap = new HashMap(); + + /** + * Our application context. + */ + private final Context mContext; + + /** + * The cached account manager. + */ + private final AccountManager mAccountManager; + + private final String mAuthority; + + + /** + * Get the Sources singleton instance. Don't call this from the UI thread since it may take a long time to gather all the information from the account + * manager. + */ + public static synchronized Sources getInstance(Context context) + { + if (sInstance == null) + { + sInstance = new Sources(context); + } + return sInstance; + } + + + /** + * Load a model asynchronously. This might be executed as a synchronous operation if the models have been loaded already. + * + * @param context + * A {@link Context}. + * @param accountType + * The account type of the model to load. + * @param listener + * The listener to call when the model has been loaded. + * + * @return true if the models were loaded already and the operation was executed synchronously, false otherwise. + */ + public static boolean loadModelAsync(Context context, String accountType, OnModelLoadedListener listener) + { + if (sInstance == null) + { + new AsyncModelLoader(context, listener).execute(accountType); + return false; + } + else + { + Sources sources = getInstance(context); + listener.onModelLoaded(sources.getModel(accountType)); + return true; + } + } + + + /** + * Initialize all model sources. + * + * @param context + */ + private Sources(Context context) + { + mContext = context.getApplicationContext(); + + mAuthority = TaskContract.taskAuthority(context); + + // register to receive package changes + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + mContext.registerReceiver(this, filter); + + // register to receive locale changes, we do that to reload labels and titles in that case + filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); + mContext.registerReceiver(this, filter); + + // get accounts and build model map + mAccountManager = AccountManager.get(mContext); + mAccountManager.addOnAccountsUpdatedListener(this, null, false); + getAccounts(); + } + + + /** + * Builds the model map. This method determines all available task sources and loads their respective models from XML (falling back to {@link DefaultModel + * if no XML was found or it is broken). + */ + protected void getAccounts() + { + // remove old models if any + mAccountModelMap.clear(); + + final AuthenticatorDescription[] authenticators = mAccountManager.getAuthenticatorTypes(); + + final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes(); + + for (SyncAdapterType syncAdapter : syncAdapters) + { + if (!mAuthority.equals(syncAdapter.authority)) + { + // this sync-adapter is not for Tasks, skip it + continue; + } + + AuthenticatorDescription authenticator = getAuthenticator(authenticators, syncAdapter.accountType); + + if (authenticator == null) + { + // no authenticator for this account available + continue; + } + + Model model; + try + { + // try to load the XML model + model = new XmlModel(mContext, authenticator); + model.inflate(); + Log.i(TAG, "inflated model for " + authenticator.type); + } + catch (ModelInflaterException e) + { + Log.e(TAG, "error inflating model for " + authenticator.packageName, e); + model = new DefaultModel(mContext, authenticator.type); + try + { + model.inflate(); + } + catch (ModelInflaterException e1) + { + continue; + } + } + + if (model.getIconId() == -1) + { + model.setIconId(authenticator.iconId); + } + if (model.getLabelId() == -1) + { + model.setLabelId(authenticator.labelId); + } + + mAccountModelMap.put(authenticator.type, model); + } + + try + { + // add default model for LOCAL account type (i.e. the unsynced account). + Model defaultModel = new DefaultModel(mContext, TaskContract.LOCAL_ACCOUNT_TYPE); + defaultModel.inflate(); + mAccountModelMap.put(TaskContract.LOCAL_ACCOUNT_TYPE, defaultModel); + } + catch (ModelInflaterException e) + { + Log.e(TAG, "could not inflate default model", e); + } + + } + + + /** + * Return the {@link AuthenticatorDescription} for the given account type. + * + * @param accountType + * The account type to find. + * + * @return The {@link AuthenticatorDescription} for the given account type or {@code null} if no such account exists. + */ + private AuthenticatorDescription getAuthenticator(AuthenticatorDescription[] authenticators, String accountType) + { + for (AuthenticatorDescription auth : authenticators) + { + if (TextUtils.equals(accountType, auth.type)) + { + return auth; + } + } + // no authenticator for that account type found + return null; + } + + + /** + * Return the task model for the given account type. + * + * @param accountType + * The account type. + * + * @return A {@link Model} instance for the given account type or {@code null} if no model was found. + */ + public Model getModel(String accountType) + { + return mAccountModelMap.get(accountType); + } + + + public Model getMinimalModel(String accountType) + { + Model result = new MinimalModel(mContext, accountType); + try + { + result.inflate(); + } + catch (ModelInflaterException e) + { + throw new RuntimeException("can't inflate mimimal model", e); + } + return result; + } + + + /** + * Return all accounts that support the task authority. + * + * @return A {@link List} of {@link Account}s, will never be null. + */ + public List getExistingAccounts() + { + List result = new ArrayList(); + Account[] accounts = mAccountManager.getAccounts(); + for (Account account : accounts) + { + if (getModel(account.type) != null && ContentResolver.getIsSyncable(account, mAuthority) > 0) + { + result.add(account); + } + } + return result; + } + + + /** + * Return a default model. This model can be used if {@link #getModel(String)} returned {@code null}. Which should not happen btw. + * + * @return A {@link Model} instance. + */ + public Model getDefaultModel() + { + return mAccountModelMap.get(TaskContract.LOCAL_ACCOUNT_TYPE); + } + + + @Override + public void onAccountsUpdated(Account[] accounts) + { + // the account list has changed, rebuild model map /* - * FIXME: Do we have to rebuild the model map? An account was added not a new model. Instead we could cache the existing accounts and update it here. + * FIXME: Do we have to rebuild the model map? An account was added not a new model. Instead we could cache the existing accounts and update it here. */ - getAccounts(); - } - - - @Override - public void onReceive(Context context, Intent intent) - { - // something has changed, rebuild model map - // TODO: determine what exactly has changed and apply only necessary - // modifications - getAccounts(); - } + getAccounts(); + } + + + @Override + public void onReceive(Context context, Intent intent) + { + // something has changed, rebuild model map + // TODO: determine what exactly has changed and apply only necessary + // modifications + getAccounts(); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java b/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java index 372ef086..5c4d7ca1 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java @@ -44,144 +44,145 @@ import org.dmfs.tasks.model.defaults.DefaultBefore; /** * This class holds a static reference for all field adapters. That allows us to use them across different models. - * + * * @author Marten Gajda */ public final class TaskFieldAdapters { - /** - * Adapter for the all day flag of a task. - */ - public final static BooleanFieldAdapter ALLDAY = new BooleanFieldAdapter(Tasks.IS_ALLDAY); - - /** - * Adapter for the percent complete value of a task. - */ - public final static IntegerFieldAdapter PERCENT_COMPLETE = new IntegerFieldAdapter(Tasks.PERCENT_COMPLETE); - - /** - * Adapter for the status of a task. - */ - public final static IntegerFieldAdapter STATUS = (IntegerFieldAdapter) new IntegerFieldAdapter(Tasks.STATUS, Tasks.STATUS_NEEDS_ACTION) - .addContraint(new AdjustPercentComplete(PERCENT_COMPLETE)); - - /** - * Adapter for the priority value of a task. - */ - public final static IntegerFieldAdapter PRIORITY = new IntegerFieldAdapter(Tasks.PRIORITY); - - /** - * Adapter for the classification value of a task. - */ - public final static IntegerFieldAdapter CLASSIFICATION = new IntegerFieldAdapter(Tasks.CLASSIFICATION); - - /** - * Adapter for the list name of a task. - */ - public final static StringFieldAdapter LIST_NAME = new StringFieldAdapter(Tasks.LIST_NAME); - - /** - * Adapter for the account name of a task. - */ - public final static StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(Tasks.ACCOUNT_NAME); - - /** - * Adapter for the account type of a task. - */ - public final static StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(Tasks.ACCOUNT_TYPE); - - /** - * Adapter for the title of a task. - */ - public final static StringFieldAdapter TITLE = new StringFieldAdapter(Tasks.TITLE); - - /** - * Adapter for the location of a task. - */ - public final static StringFieldAdapter LOCATION = new StringFieldAdapter(Tasks.LOCATION); - - /** - * Adapter for the description of a task. - */ - public final static DescriptionStringFieldAdapter DESCRIPTION = new DescriptionStringFieldAdapter(Tasks.DESCRIPTION); - - /** - * Adapter for the checklist of a task. - */ - public final static ChecklistFieldAdapter CHECKLIST = (ChecklistFieldAdapter) new ChecklistFieldAdapter(Tasks.DESCRIPTION) - .addContraint(new ChecklistConstraint(STATUS, PERCENT_COMPLETE)); - - /** - * Private adapter for the start date of a task. We need this to reference DTSTART from DUE. - */ - private final static TimeFieldAdapter _DTSTART = new TimeFieldAdapter(Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY); - private final static TimeFieldAdapter _DUE = new TimeFieldAdapter(Tasks.DUE, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the due date of a task. - */ - public final static FieldAdapter