From 4f46226819962c501b2efc8c0d6231bb414f7431 Mon Sep 17 00:00:00 2001 From: althafvly Date: Fri, 21 Nov 2025 11:47:17 +0530 Subject: [PATCH] Apply Spotless formatting across the project --- opentasks-contract/build.gradle | 5 +- .../org/dmfs/tasks/contract/TaskContract.java | 2794 ++++++++--------- .../org/dmfs/tasks/contract/UriFactory.java | 47 +- opentasks-provider/build.gradle | 5 +- .../TaskProviderDetachInstancesTest.java | 1460 +++++---- .../tasks/TaskProviderInstancesTest.java | 594 ++-- .../tasks/TaskProviderObserverTest.java | 315 +- .../tasks/TaskProviderRecurrenceTest.java | 2265 +++++++------ .../tasks/TaskProviderRelatingTest.java | 247 +- .../tasks/TaskProviderReparentingTest.java | 683 ++-- .../dmfs/provider/tasks/TaskProviderTest.java | 1693 +++++----- .../tasks/matchers/NotifiesMatcher.java | 155 +- .../provider/tasks/matchers/UriMatcher.java | 37 +- .../res/values/opentasks_defaults.xml | 2 +- .../java/org/dmfs/ngrams/NGramGenerator.java | 215 +- .../dmfs/provider/tasks/AuthorityUtil.java | 23 +- .../dmfs/provider/tasks/ContentOperation.java | 687 ++-- .../provider/tasks/FTSDatabaseHelper.java | 1107 ++++--- .../provider/tasks/ProviderOperation.java | 21 +- .../provider/tasks/SQLiteContentProvider.java | 562 ++-- .../provider/tasks/TaskDatabaseHelper.java | 2326 +++++++++----- .../org/dmfs/provider/tasks/TaskProvider.java | 2287 +++++++------- .../tasks/TaskProviderBroadcastReceiver.java | 117 +- .../java/org/dmfs/provider/tasks/Utils.java | 185 +- .../provider/tasks/handler/AlarmHandler.java | 176 +- .../tasks/handler/CategoryHandler.java | 452 ++- .../tasks/handler/DefaultPropertyHandler.java | 47 +- .../tasks/handler/PropertyHandler.java | 216 +- .../tasks/handler/PropertyHandlerFactory.java | 53 +- .../tasks/handler/RelationHandler.java | 420 ++- .../tasks/model/AbstractInstanceAdapter.java | 17 +- .../tasks/model/AbstractListAdapter.java | 46 +- .../tasks/model/AbstractTaskAdapter.java | 68 +- .../model/ContentValuesInstanceAdapter.java | 213 +- .../tasks/model/ContentValuesListAdapter.java | 170 +- .../tasks/model/ContentValuesTaskAdapter.java | 167 +- .../CursorContentValuesInstanceAdapter.java | 284 +- .../model/CursorContentValuesListAdapter.java | 186 +- .../model/CursorContentValuesTaskAdapter.java | 225 +- .../provider/tasks/model/EntityAdapter.java | 229 +- .../provider/tasks/model/InstanceAdapter.java | 140 +- .../provider/tasks/model/ListAdapter.java | 75 +- .../provider/tasks/model/TaskAdapter.java | 572 ++-- .../model/adapters/BinaryFieldAdapter.java | 107 +- .../model/adapters/BooleanFieldAdapter.java | 110 +- .../model/adapters/DateTimeFieldAdapter.java | 340 +- .../DateTimeIterableFieldAdapter.java | 269 +- .../model/adapters/DurationFieldAdapter.java | 116 +- .../tasks/model/adapters/FieldAdapter.java | 218 +- .../model/adapters/FloatFieldAdapter.java | 107 +- .../model/adapters/IntegerFieldAdapter.java | 109 +- .../model/adapters/LongFieldAdapter.java | 94 +- .../model/adapters/RRuleFieldAdapter.java | 145 +- .../model/adapters/SimpleFieldAdapter.java | 128 +- .../model/adapters/StringFieldAdapter.java | 99 +- .../tasks/model/adapters/UrlFieldAdapter.java | 101 +- .../tasks/processors/EntityProcessor.java | 12 +- .../provider/tasks/processors/Logging.java | 71 +- .../tasks/processors/NoOpProcessor.java | 38 +- .../tasks/processors/instances/Detaching.java | 550 ++-- .../instances/TaskValueDelegate.java | 454 +-- .../processors/instances/Validating.java | 274 +- .../processors/lists/ListCommitProcessor.java | 44 +- .../tasks/processors/lists/Validating.java | 156 +- .../processors/tasks/AutoCompleting.java | 307 +- .../tasks/processors/tasks/Instantiating.java | 672 ++-- .../tasks/processors/tasks/Moving.java | 329 +- .../tasks/processors/tasks/Originating.java | 75 +- .../tasks/processors/tasks/Relating.java | 227 +- .../tasks/processors/tasks/Reparenting.java | 157 +- .../tasks/processors/tasks/Searchable.java | 64 +- .../processors/tasks/TaskCommitProcessor.java | 63 +- .../tasks/processors/tasks/Validating.java | 396 ++- .../processors/tasks/instancedata/Dated.java | 41 +- .../tasks/instancedata/Distant.java | 20 +- .../tasks/instancedata/DueDated.java | 17 +- .../tasks/instancedata/Enduring.java | 54 +- .../tasks/instancedata/Overridden.java | 53 +- .../tasks/instancedata/StartDated.java | 17 +- .../tasks/instancedata/TaskRelated.java | 50 +- .../instancedata/VanillaInstanceData.java | 35 +- .../provider/tasks/utils/ContainsValues.java | 73 +- .../tasks/utils/InstanceValuesIterable.java | 153 +- .../dmfs/provider/tasks/utils/Limited.java | 36 +- .../provider/tasks/utils/LimitedIterator.java | 57 +- .../tasks/utils/OverrideValuesFunction.java | 46 +- .../dmfs/provider/tasks/utils/Profiled.java | 99 +- .../org/dmfs/provider/tasks/utils/Range.java | 49 +- .../provider/tasks/utils/ResourceArray.java | 36 +- .../provider/tasks/utils/RowIterator.java | 51 +- .../provider/tasks/utils/TableColumns.java | 53 +- .../tasks/utils/TaskInstanceIterable.java | 79 +- .../tasks/utils/TaskInstanceIterator.java | 75 +- .../dmfs/provider/tasks/utils/Timestamps.java | 47 +- .../org/dmfs/provider/tasks/utils/With.java | 47 +- .../org/dmfs/provider/tasks/utils/Zipped.java | 24 +- .../src/main/res/values-pt-rBR/strings.xml | 2 +- .../main/res/values/opentasks_defaults.xml | 2 +- .../opentasks_provider_changed_receivers.xml | 2 +- .../DateTimeIterableFieldAdapterTest.java | 385 +-- .../tasks/instancedata/DatedTest.java | 58 +- .../tasks/instancedata/DistantTest.java | 26 +- .../tasks/instancedata/DueDatedTest.java | 108 +- .../tasks/instancedata/EnduringTest.java | 94 +- .../tasks/instancedata/OverriddenTest.java | 156 +- .../tasks/instancedata/StartDatedTest.java | 108 +- .../tasks/instancedata/TaskRelatedTest.java | 24 +- .../instancedata/VanillaInstanceDataTest.java | 41 +- .../tasks/utils/ContainsValuesTest.java | 110 +- .../tasks/utils/ContentValuesWithLong.java | 54 +- .../tasks/utils/TaskInstanceIterableTest.java | 298 +- .../tasks/utils/TaskInstanceIteratorTest.java | 169 +- .../dmfs/provider/tasks/utils/ZippedTest.java | 53 +- opentasks-theme/build.gradle | 2 +- opentasks-theme/src/main/AndroidManifest.xml | 2 +- opentasks-theme/src/main/res/values/attrs.xml | 2 +- .../src/main/res/values/colors.xml | 2 +- .../src/main/res/values/dimens.xml | 2 +- opentasks/build.gradle | 11 +- .../utils/DateTimeToTimeConversionTest.java | 186 +- .../draglinearlayout/DragLinearLayout.java | 1535 +++++---- .../android/widgets/ColoredShapeCheckBox.java | 258 +- .../java/org/dmfs/tasks/AboutActivity.java | 57 +- .../java/org/dmfs/tasks/AboutFragment.java | 24 +- .../tasks/AppAppearanceSettingsFragment.java | 57 +- .../AppNotificationSettingsFragment.java | 18 +- .../org/dmfs/tasks/AppSettingsActivity.java | 126 +- .../org/dmfs/tasks/AppSettingsFragment.java | 18 +- .../java/org/dmfs/tasks/EditTaskActivity.java | 316 +- .../java/org/dmfs/tasks/EditTaskFragment.java | 1163 ++++--- .../org/dmfs/tasks/EmptyTaskFragment.java | 69 +- .../dmfs/tasks/InputTextDialogFragment.java | 497 ++- .../src/main/java/org/dmfs/tasks/JobIds.java | 19 +- .../org/dmfs/tasks/ManageListActivity.java | 663 ++-- .../dmfs/tasks/QuickAddDialogFragment.java | 793 +++-- .../org/dmfs/tasks/SettingsListFragment.java | 912 +++--- .../tasks/StaleListBroadcastReceiver.java | 161 +- .../org/dmfs/tasks/SyncSettingsActivity.java | 248 +- .../org/dmfs/tasks/TaskGroupPagerAdapter.java | 226 +- .../java/org/dmfs/tasks/TaskListActivity.java | 1269 ++++---- .../java/org/dmfs/tasks/TaskListFragment.java | 1624 +++++----- .../java/org/dmfs/tasks/TasksApplication.java | 47 +- .../java/org/dmfs/tasks/ViewTaskActivity.java | 221 +- .../java/org/dmfs/tasks/ViewTaskFragment.java | 1421 ++++----- .../tasks/actions/CancelDelayedAction.java | 45 +- .../actions/CancelNotificationAction.java | 46 +- .../dmfs/tasks/actions/CompleteAction.java | 11 +- .../org/dmfs/tasks/actions/Composite.java | 47 +- .../org/dmfs/tasks/actions/Conditional.java | 45 +- .../dmfs/tasks/actions/DeferDueAction.java | 29 +- .../org/dmfs/tasks/actions/DelayedAction.java | 50 +- .../tasks/actions/DelegatingTaskAction.java | 35 +- .../org/dmfs/tasks/actions/NotifyAction.java | 355 ++- .../tasks/actions/NotifyStickyAction.java | 22 +- .../org/dmfs/tasks/actions/OpenAction.java | 41 +- .../actions/PersistNotificationAction.java | 52 +- .../org/dmfs/tasks/actions/PinAction.java | 11 +- .../dmfs/tasks/actions/PostUndoAction.java | 98 +- .../actions/RemoveNotificationAction.java | 19 +- .../org/dmfs/tasks/actions/TaskAction.java | 18 +- .../org/dmfs/tasks/actions/UpdateAction.java | 41 +- .../tasks/actions/UpdateWidgetsAction.java | 46 +- .../tasks/actions/WipeNotificationAction.java | 11 +- .../conditions/NotificationEnabled.java | 28 +- .../actions/utils/NotificationPrefs.java | 25 +- .../DashClockPreferenceActivity.java | 31 +- .../dmfs/tasks/dashclock/TasksExtension.java | 787 ++--- .../groupings/AbstractGroupingFactory.java | 161 +- .../groupings/BaseTaskViewDescriptor.java | 522 ++- .../org/dmfs/tasks/groupings/ByDueDate.java | 375 ++- .../java/org/dmfs/tasks/groupings/ByList.java | 370 +-- .../org/dmfs/tasks/groupings/ByPriority.java | 347 +- .../org/dmfs/tasks/groupings/ByProgress.java | 266 +- .../org/dmfs/tasks/groupings/BySearch.java | 423 ++- .../org/dmfs/tasks/groupings/ByStartDate.java | 377 ++- .../org/dmfs/tasks/groupings/TabConfig.java | 358 +-- .../AbstractCursorLoaderFactory.java | 20 +- .../AbstractCustomCursorFactory.java | 42 +- .../cursorloaders/CursorLoaderFactory.java | 67 +- .../cursorloaders/CustomCursorLoader.java | 134 +- .../EmptyCursorLoaderFactory.java | 28 +- .../cursorloaders/PriorityCursorFactory.java | 81 +- .../PriorityCursorLoaderFactory.java | 27 +- .../cursorloaders/ProgressCursorFactory.java | 88 +- .../ProgressCursorLoaderFactory.java | 27 +- .../SearchHistoryCursorFactory.java | 45 +- .../SearchHistoryCursorLoaderFactory.java | 56 +- .../cursorloaders/TimeRangeCursorFactory.java | 314 +- .../cursorloaders/TimeRangeCursorLoader.java | 117 +- .../TimeRangeCursorLoaderFactory.java | 24 +- .../TimeRangeShortCursorFactory.java | 76 +- .../TimeRangeStartCursorFactory.java | 92 +- .../TimeRangeStartCursorLoader.java | 117 +- .../TimeRangeStartCursorLoaderFactory.java | 24 +- .../groupings/filters/AbstractFilter.java | 31 +- .../tasks/groupings/filters/AndFilter.java | 10 +- .../filters/BinaryOperationFilter.java | 93 +- .../groupings/filters/ConstantFilter.java | 64 +- .../tasks/groupings/filters/OrFilter.java | 10 +- .../homescreen/TaskListSelectionFragment.java | 236 +- .../tasks/homescreen/TaskListWidgetItem.java | 185 +- .../homescreen/TaskListWidgetProvider.java | 267 +- .../TaskListWidgetProviderLarge.java | 49 +- .../TaskListWidgetSettingsActivity.java | 140 +- .../TaskListWidgetUpdaterService.java | 692 ++-- .../dmfs/tasks/linkify/ActionModeLinkify.java | 248 +- .../dmfs/tasks/linkify/ViewObservables.java | 83 +- .../model/AbstractArrayChoicesAdapter.java | 136 +- .../dmfs/tasks/model/ArrayChoicesAdapter.java | 92 +- .../org/dmfs/tasks/model/CheckListItem.java | 50 +- .../java/org/dmfs/tasks/model/ContentSet.java | 852 +++-- .../tasks/model/CursorChoicesAdapter.java | 116 +- .../org/dmfs/tasks/model/DefaultModel.java | 422 ++- .../org/dmfs/tasks/model/DescriptionItem.java | 58 +- .../org/dmfs/tasks/model/FieldDescriptor.java | 639 ++-- .../org/dmfs/tasks/model/IChoicesAdapter.java | 88 +- .../tasks/model/IllegalDataKindException.java | 17 +- .../org/dmfs/tasks/model/MinimalModel.java | 69 +- .../main/java/org/dmfs/tasks/model/Model.java | 433 ++- .../tasks/model/ModelInflaterException.java | 25 +- .../tasks/model/OnContentChangeListener.java | 29 +- .../model/ResourceArrayChoicesAdapter.java | 109 +- .../java/org/dmfs/tasks/model/Sources.java | 475 ++- .../dmfs/tasks/model/TaskFieldAdapters.java | 311 +- .../tasks/model/TimeZoneChoicesAdapter.java | 304 +- .../java/org/dmfs/tasks/model/XmlModel.java | 1090 ++++--- .../model/adapters/BooleanFieldAdapter.java | 186 +- .../model/adapters/ChecklistFieldAdapter.java | 354 +-- .../model/adapters/ColorFieldAdapter.java | 140 +- .../CustomizedDefaultFieldAdapter.java | 152 +- .../model/adapters/DateTimeFieldAdapter.java | 341 +- .../adapters/DescriptionFieldAdapter.java | 350 +-- .../DescriptionStringFieldAdapter.java | 214 +- .../tasks/model/adapters/FieldAdapter.java | 248 +- .../model/adapters/FloatFieldAdapter.java | 174 +- .../adapters/FormattedStringFieldAdapter.java | 167 +- .../model/adapters/IntegerFieldAdapter.java | 174 +- .../adapters/OptionalLongFieldAdapter.java | 181 +- .../model/adapters/RRuleFieldAdapter.java | 189 +- .../model/adapters/StringFieldAdapter.java | 174 +- .../model/adapters/TimeFieldAdapter.java | 347 +- .../tasks/model/adapters/TimeZoneWrapper.java | 322 +- .../model/adapters/TimezoneFieldAdapter.java | 383 +-- .../tasks/model/adapters/UriFieldAdapter.java | 195 +- .../model/constraints/AbstractConstraint.java | 32 +- .../constraints/AdjustPercentComplete.java | 47 +- .../dmfs/tasks/model/constraints/After.java | 46 +- .../model/constraints/BeforeOrShiftTime.java | 81 +- .../constraints/ChecklistConstraint.java | 100 +- .../constraints/DescriptionConstraint.java | 115 +- .../tasks/model/constraints/NotAfter.java | 45 +- .../tasks/model/constraints/NotBefore.java | 45 +- .../tasks/model/constraints/ShiftIfAfter.java | 45 +- .../tasks/model/constraints/ShiftTime.java | 69 +- .../tasks/model/constraints/UpdateAllDay.java | 72 +- .../dmfs/tasks/model/defaults/Default.java | 29 +- .../tasks/model/defaults/DefaultAfter.java | 58 +- .../tasks/model/defaults/DefaultBefore.java | 59 +- .../tasks/model/layout/LayoutDescriptor.java | 211 +- .../tasks/model/layout/LayoutOptions.java | 178 +- .../tasks/notification/ActionReceiver.java | 18 +- .../tasks/notification/ActionService.java | 311 +- .../notification/TaskNotificationHandler.java | 20 +- .../notification/TaskNotificationService.java | 225 +- .../tasks/notification/signals/AllSignal.java | 13 +- .../notification/signals/Conditional.java | 46 +- .../signals/DelegatingNotificationSignal.java | 26 +- .../tasks/notification/signals/Lights.java | 36 +- .../tasks/notification/signals/NoSignal.java | 12 +- .../signals/NotificationSignal.java | 20 +- .../notification/signals/SettingsSignal.java | 45 +- .../tasks/notification/signals/Sound.java | 36 +- .../tasks/notification/signals/Toggled.java | 76 +- .../tasks/notification/signals/Vibration.java | 36 +- .../notification/state/JsonStateInfo.java | 57 +- .../tasks/notification/state/PrefState.java | 74 +- .../tasks/notification/state/RowState.java | 66 +- .../notification/state/RowStateInfo.java | 83 +- .../tasks/notification/state/StateInfo.java | 27 +- .../state/TaskNotificationState.java | 17 +- .../quicksettings/TaskQuickSettingsTile.java | 29 +- .../dmfs/tasks/share/ShareIntentFactory.java | 52 +- .../org/dmfs/tasks/share/TaskBindings.java | 135 +- .../dmfs/tasks/share/TaskShareDetails.java | 115 +- .../org/dmfs/tasks/share/TaskShareTitle.java | 24 +- .../org/dmfs/tasks/share/TimeFormatter.java | 70 +- .../dmfs/tasks/utils/AsyncContentLoader.java | 139 +- .../dmfs/tasks/utils/AsyncModelLoader.java | 104 +- .../org/dmfs/tasks/utils/BaseActivity.java | 149 +- .../dmfs/tasks/utils/ContentValueMapper.java | 229 +- .../utils/DatabaseInitializedReceiver.java | 44 +- .../org/dmfs/tasks/utils/DateFormatter.java | 758 +++-- .../utils/DescriptionMovementMethod.java | 117 +- .../utils/ExpandableChildDescriptor.java | 345 +- .../utils/ExpandableGroupDescriptor.java | 191 +- .../ExpandableGroupDescriptorAdapter.java | 360 +-- .../org/dmfs/tasks/utils/FlingDetector.java | 887 +++--- .../org/dmfs/tasks/utils/ManifestAppName.java | 41 +- .../utils/ObservableSparseArrayCompat.java | 164 +- .../tasks/utils/OnChildLoadedListener.java | 23 +- .../tasks/utils/OnContentLoadedListener.java | 20 +- .../tasks/utils/OnModelLoadedListener.java | 18 +- .../dmfs/tasks/utils/RecentlyUsedLists.java | 217 +- .../tasks/utils/RetainExpandableListView.java | 169 +- .../tasks/utils/SafeFragmentUiRunnable.java | 47 +- .../tasks/utils/SearchChildDescriptor.java | 89 +- .../utils/SearchHistoryDatabaseHelper.java | 192 +- .../dmfs/tasks/utils/SearchHistoryHelper.java | 167 +- .../java/org/dmfs/tasks/utils/SetFromMap.java | 146 +- .../java/org/dmfs/tasks/utils/Sorted.java | 49 +- .../dmfs/tasks/utils/TaskIntentFactory.java | 18 +- .../tasks/utils/TasksListCursorAdapter.java | 251 +- .../utils/TasksListCursorSpinnerAdapter.java | 180 +- .../dmfs/tasks/utils/TimeChangeListener.java | 29 +- .../dmfs/tasks/utils/TimeChangeObserver.java | 163 +- .../org/dmfs/tasks/utils/ValidatingUri.java | 42 +- .../org/dmfs/tasks/utils/ViewDescriptor.java | 93 +- .../WidgetConfigurationDatabaseHelper.java | 251 +- .../tasks/utils/colors/AdjustedForFab.java | 60 +- .../dmfs/tasks/utils/colors/BlendColor.java | 73 +- .../utils/colors/DarkenedForStatusBar.java | 20 +- .../org/dmfs/tasks/utils/colors/Mixed.java | 61 +- .../utils/colors/SmoothLightnessCapped.java | 40 +- .../utils/permission/AppPermissions.java | 19 +- .../utils/permission/BasicAppPermissions.java | 34 +- .../permission/LegacyAppPermissions.java | 108 +- .../permission/MarshmallowPermissions.java | 168 +- .../permission/NoOpPermissionRequest.java | 26 +- .../tasks/utils/permission/Permission.java | 81 +- .../utils/permission/PermissionRequest.java | 33 +- .../PermissionRequestDialogFragment.java | 119 +- .../permission/utils/AppSettingsIntent.java | 43 +- .../utils/ManifestPermissionStrings.java | 46 +- .../tasks/widget/AbstractFieldEditor.java | 37 +- .../dmfs/tasks/widget/AbstractFieldView.java | 388 +-- .../org/dmfs/tasks/widget/BaseTaskView.java | 132 +- .../dmfs/tasks/widget/BooleanFieldEditor.java | 124 +- .../dmfs/tasks/widget/BooleanFieldView.java | 95 +- .../tasks/widget/CheckListFieldEditor.java | 424 ++- .../dmfs/tasks/widget/CheckListFieldView.java | 497 ++- .../widget/CheckListFieldViewLegacy.java | 228 +- .../dmfs/tasks/widget/ChoicesFieldEditor.java | 394 +-- .../dmfs/tasks/widget/ChoicesFieldView.java | 115 +- .../tasks/widget/DescriptionFieldView.java | 940 +++--- .../org/dmfs/tasks/widget/ListColorView.java | 72 +- .../tasks/widget/ListenableScrollView.java | 93 +- .../dmfs/tasks/widget/LocationFieldView.java | 182 +- .../tasks/widget/PercentageFieldEditor.java | 168 +- .../tasks/widget/PercentageFieldView.java | 99 +- .../dmfs/tasks/widget/RRuleFieldEditor.java | 277 +- .../tasks/widget/RecurrenceRuleEditor.java | 32 +- .../dmfs/tasks/widget/RecurrenceRuleView.java | 32 +- .../java/org/dmfs/tasks/widget/SmartView.java | 16 +- .../java/org/dmfs/tasks/widget/TaskEdit.java | 69 +- .../java/org/dmfs/tasks/widget/TaskView.java | 137 +- .../dmfs/tasks/widget/TextFieldEditor.java | 172 +- .../org/dmfs/tasks/widget/TextFieldView.java | 117 +- .../dmfs/tasks/widget/TimeFieldEditor.java | 788 +++-- .../org/dmfs/tasks/widget/TimeFieldView.java | 298 +- .../org/dmfs/tasks/widget/UrlFieldEditor.java | 188 +- .../org/dmfs/tasks/widget/UrlFieldView.java | 122 +- .../tasks/widget/recurrence/Conditional.java | 76 +- .../tasks/widget/recurrence/NotRepeating.java | 43 +- .../recurrence/RecurrencePopupGenerator.java | 57 +- .../tasks/widget/recurrence/RepeatByRule.java | 50 +- .../main/res/anim/openttasks_fade_exit.xml | 2 +- .../src/main/res/color/due_date_text.xml | 2 +- .../src/main/res/color/overdue_date_text.xml | 2 +- opentasks/src/main/res/color/tab_icon.xml | 2 +- .../res/color/task_list_subtitle_text.xml | 2 +- .../main/res/color/task_list_title_text.xml | 2 +- .../src/main/res/drawable/bg_searchview.xml | 2 +- .../complete_task_background_overlay.xml | 2 +- .../src/main/res/drawable/custom_seekbar.xml | 2 +- .../src/main/res/drawable/light_gray.xml | 2 +- .../drawable/list_background_activated.xml | 2 +- .../res/drawable/list_background_pressed.xml | 2 +- .../res/drawable/list_background_selected.xml | 2 +- ...dmfs_colorshape_checkbox_selector_dark.xml | 2 +- ...mfs_colorshape_checkbox_selector_light.xml | 2 +- ...lorshape_checkbox_white_shape_selector.xml | 2 +- .../src/main/res/drawable/overlay_bottom.xml | 2 +- .../src/main/res/drawable/overlay_top.xml | 2 +- .../main/res/drawable/rect_shape_corners.xml | 2 +- .../rect_shape_corners_white_insert.xml | 2 +- .../main/res/drawable/rounded_edittext.xml | 2 +- .../drawable/selectable_background_white.xml | 6 +- .../res/drawable/selectable_list_item.xml | 2 +- .../main/res/drawable/shape_color_circle.xml | 2 +- .../res/drawable/tab_indicator_ab_white.xml | 4 +- .../src/main/res/drawable/task_divider.xml | 2 +- opentasks/src/main/res/drawable/task_due.xml | 2 +- .../task_progress_background_shade.xml | 2 +- .../task_remaining_background_shade.xml | 2 +- .../src/main/res/drawable/task_start.xml | 2 +- .../main/res/drawable/text_cursor_white.xml | 2 +- .../src/main/res/drawable/transparent.xml | 2 +- .../res/drawable/vertical_shade_r_to_l.xml | 2 +- .../main/res/drawable/widget_list_item_bg.xml | 2 +- .../drawable/widget_task_add_button_bg.xml | 2 +- .../src/main/res/drawable/window_overlay.xml | 2 +- .../main/res/layout-v24/undo_notification.xml | 2 +- .../res/layout/account_list_item_dialog.xml | 2 +- .../src/main/res/layout/activity_about.xml | 2 +- .../res/layout/activity_manage_task_list.xml | 2 +- .../main/res/layout/activity_task_list.xml | 2 +- .../main/res/layout/activity_task_twopane.xml | 2 +- .../layout/activity_task_twopane_small.xml | 2 +- .../main/res/layout/boolean_field_editor.xml | 2 +- .../main/res/layout/boolean_field_view.xml | 2 +- .../res/layout/checklist_field_editor.xml | 2 +- .../layout/checklist_field_editor_element.xml | 2 +- .../main/res/layout/checklist_field_view.xml | 2 +- .../layout/checklist_field_view_element.xml | 2 +- .../main/res/layout/choices_field_editor.xml | 2 +- .../main/res/layout/choices_field_view.xml | 2 +- .../res/layout/description_field_editor.xml | 2 +- .../res/layout/description_field_view.xml | 2 +- .../layout/description_field_view_element.xml | 2 +- ...description_field_view_element_actions.xml | 2 +- .../src/main/res/layout/detail_label.xml | 2 +- .../src/main/res/layout/editor_header.xml | 2 +- .../layout/fragment_expandable_task_list.xml | 2 +- .../res/layout/fragment_input_text_dialog.xml | 2 +- .../res/layout/fragment_quick_add_dialog.xml | 2 +- .../fragment_quick_add_dialog_header.xml | 2 +- .../res/layout/fragment_synced_task_list.xml | 2 +- .../res/layout/fragment_task_edit_detail.xml | 2 +- .../fragment_task_edit_detail_twopane.xml | 2 +- .../layout/fragment_task_list_selection.xml | 2 +- .../res/layout/fragment_task_view_detail.xml | 2 +- .../res/layout/fragment_visiblelist_list.xml | 2 +- .../layout/integer_choices_spinner_item.xml | 2 +- .../integer_choices_spinner_selected_item.xml | 2 +- .../src/main/res/layout/list_color_view.xml | 2 +- .../main/res/layout/list_item_selection.xml | 2 +- .../res/layout/list_spinner_item_dropdown.xml | 2 +- .../res/layout/list_spinner_item_selected.xml | 2 +- .../list_spinner_item_selected_quick_add.xml | 2 +- .../layout/opentasks_activity_preferences.xml | 2 +- .../layout/opentasks_fragment_empty_task.xml | 2 +- .../layout/opentasks_location_field_view.xml | 2 +- .../layout/opentasks_rrule_field_editor.xml | 2 +- .../res/layout/percentage_field_editor.xml | 2 +- .../main/res/layout/percentage_field_view.xml | 2 +- .../src/main/res/layout/task_detail_view.xml | 2 +- opentasks/src/main/res/layout/task_edit.xml | 2 +- .../main/res/layout/task_editor_activity.xml | 2 +- .../src/main/res/layout/task_list_element.xml | 2 +- .../layout/task_list_group_single_line.xml | 2 +- .../res/layout/task_list_provider_bar.xml | 2 +- .../src/main/res/layout/task_list_widget.xml | 2 +- .../main/res/layout/task_list_widget_item.xml | 2 +- .../res/layout/task_list_widget_loading.xml | 2 +- opentasks/src/main/res/layout/task_view.xml | 2 +- .../src/main/res/layout/text_field_editor.xml | 2 +- .../src/main/res/layout/text_field_view.xml | 2 +- .../res/layout/text_field_view_nodivider.xml | 2 +- .../text_field_view_nodivider_large.xml | 2 +- .../text_field_view_nodivider_small.xml | 2 +- .../src/main/res/layout/time_field_editor.xml | 2 +- .../src/main/res/layout/time_field_view.xml | 2 +- .../src/main/res/layout/undo_notification.xml | 2 +- .../src/main/res/layout/url_field_editor.xml | 2 +- .../src/main/res/layout/url_field_view.xml | 2 +- .../res/layout/visible_task_list_item.xml | 2 +- .../main/res/menu/edit_task_activity_menu.xml | 2 +- .../src/main/res/menu/list_settings_menu.xml | 2 +- opentasks/src/main/res/menu/settings.xml | 2 +- .../main/res/menu/task_list_activity_menu.xml | 2 +- .../main/res/menu/task_list_fragment_menu.xml | 2 +- opentasks/src/main/res/menu/test_display.xml | 2 +- .../main/res/menu/view_task_fragment_menu.xml | 2 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- opentasks/src/main/res/values-de/strings.xml | 2 +- opentasks/src/main/res/values-es/strings.xml | 2 +- opentasks/src/main/res/values-eu/strings.xml | 2 +- opentasks/src/main/res/values-fi/strings.xml | 2 +- opentasks/src/main/res/values-fr/strings.xml | 2 +- opentasks/src/main/res/values-gd/strings.xml | 2 +- opentasks/src/main/res/values-gl/strings.xml | 2 +- opentasks/src/main/res/values-hu/strings.xml | 2 +- opentasks/src/main/res/values-is/strings.xml | 2 +- opentasks/src/main/res/values-it/strings.xml | 2 +- opentasks/src/main/res/values-ja/strings.xml | 2 +- .../src/main/res/values-night-v27/styles.xml | 2 +- .../src/main/res/values-night/styles.xml | 2 +- opentasks/src/main/res/values-nl/strings.xml | 2 +- opentasks/src/main/res/values-pl/strings.xml | 14 +- .../src/main/res/values-pt-rPT/strings.xml | 2 +- .../src/main/res/values-pt-rbr/strings.xml | 2 +- opentasks/src/main/res/values-ru/strings.xml | 2 +- opentasks/src/main/res/values-sk/strings.xml | 2 +- .../src/main/res/values-sv-rSE/strings.xml | 2 +- .../main/res/values-sw720dp-land/dimens.xml | 2 +- .../src/main/res/values-sw720dp-land/refs.xml | 4 +- .../main/res/values-sw720dp-port/dimens.xml | 2 +- .../src/main/res/values-sw720dp-port/refs.xml | 2 +- opentasks/src/main/res/values-tr/strings.xml | 2 +- opentasks/src/main/res/values-uk/strings.xml | 2 +- opentasks/src/main/res/values-v21/styles.xml | 2 +- opentasks/src/main/res/values-v27/styles.xml | 2 +- .../src/main/res/values-zh-rCN/strings.xml | 2 +- opentasks/src/main/res/values/arrays.xml | 2 +- opentasks/src/main/res/values/attrs.xml | 2 +- opentasks/src/main/res/values/bools.xml | 2 +- opentasks/src/main/res/values/colors.xml | 2 +- opentasks/src/main/res/values/dashclock.xml | 2 +- opentasks/src/main/res/values/dimens.xml | 2 +- opentasks/src/main/res/values/ids.xml | 2 +- opentasks/src/main/res/values/ints.xml | 2 +- .../main/res/values/opentasks_defaults.xml | 2 +- opentasks/src/main/res/values/prefs.xml | 2 +- opentasks/src/main/res/values/refs.xml | 2 +- opentasks/src/main/res/values/strings.xml | 2 +- opentasks/src/main/res/values/styles.xml | 2 +- .../src/main/res/xml/app_preferences.xml | 2 +- .../main/res/xml/dashclock_preferences.xml | 2 +- opentasks/src/main/res/xml/listview_tabs.xml | 2 +- .../main/res/xml/notification_preferences.xml | 2 +- opentasks/src/main/res/xml/searchable.xml | 2 +- .../src/main/res/xml/task_widget_info.xml | 2 +- .../main/res/xml/task_widget_info_large.xml | 2 +- .../adapters/DescriptionExtractingTest.java | 136 +- .../notification/signals/ToggledTest.java | 52 +- .../RecentlyUsedListsNullHandlingTest.java | 277 +- .../dmfs/tasks/utils/ValidatingUriTest.java | 55 +- opentaskspal/build.gradle | 2 + .../operations/SubtaskRelation.java | 56 +- .../opentaskspal/operations/TaskListTask.java | 48 +- .../opentaskspal/predicates/IsProperty.java | 14 +- .../opentaskspal/predicates/IsRelation.java | 14 +- .../opentaskspal/predicates/TaskOnList.java | 17 +- .../readdata/EffectiveDueDate.java | 42 +- .../readdata/EffectiveTaskColor.java | 31 +- .../readdata/EffectiveTimezone.java | 23 +- .../org/dmfs/opentaskspal/readdata/Id.java | 20 +- .../readdata/PercentComplete.java | 16 +- .../readdata/TaskCompletionTime.java | 25 +- .../opentaskspal/readdata/TaskDateTime.java | 32 +- .../opentaskspal/readdata/TaskDuration.java | 16 +- .../opentaskspal/readdata/TaskIsClosed.java | 17 +- .../dmfs/opentaskspal/readdata/TaskPin.java | 16 +- .../dmfs/opentaskspal/readdata/TaskStart.java | 17 +- .../opentaskspal/readdata/TaskStatus.java | 17 +- .../dmfs/opentaskspal/readdata/TaskTitle.java | 16 +- .../dmfs/opentaskspal/readdata/TaskUri.java | 45 +- .../opentaskspal/readdata/TaskVersion.java | 16 +- .../readdata/functions/StringToColor.java | 21 +- .../rowdata/DateTimeListData.java | 52 +- .../dmfs/opentaskspal/rowsets/Subtasks.java | 20 +- .../opentaskspal/tables/InstanceTable.java | 11 +- .../tables/LocalTaskListsTable.java | 28 +- .../opentaskspal/tables/PropertiesTable.java | 14 +- .../opentaskspal/tables/TaskListScoped.java | 101 +- .../opentaskspal/tables/TaskListsTable.java | 13 +- .../dmfs/opentaskspal/tables/TasksTable.java | 14 +- .../opentaskspal/tasklists/ColorData.java | 19 +- .../dmfs/opentaskspal/tasklists/NameData.java | 18 +- .../opentaskspal/tasklists/OwnerData.java | 20 +- .../tasklists/SyncStatusData.java | 28 +- .../tasklists/VisibilityData.java | 28 +- .../tasks/ChildTaskRelationData.java | 26 +- .../org/dmfs/opentaskspal/tasks/DueData.java | 50 +- .../opentaskspal/tasks/ExDatesTaskData.java | 16 +- .../dmfs/opentaskspal/tasks/NoTimeData.java | 36 +- .../tasks/OriginalInstanceData.java | 22 +- .../tasks/OriginalInstanceSyncIdData.java | 25 +- .../tasks/ParentTaskRelationData.java | 27 +- .../dmfs/opentaskspal/tasks/PinnedData.java | 43 +- .../dmfs/opentaskspal/tasks/PropertyData.java | 25 +- .../opentaskspal/tasks/RDatesTaskData.java | 26 +- .../opentaskspal/tasks/RRuleTaskData.java | 37 +- .../dmfs/opentaskspal/tasks/RelationData.java | 53 +- .../dmfs/opentaskspal/tasks/StatusData.java | 33 +- .../dmfs/opentaskspal/tasks/SyncIdData.java | 15 +- .../org/dmfs/opentaskspal/tasks/TimeData.java | 117 +- .../dmfs/opentaskspal/tasks/TitleData.java | 16 +- .../dmfs/opentaskspal/tasks/VersionData.java | 38 +- .../opentaskspal/views/InstancesView.java | 12 +- .../opentaskspal/views/TaskListScoped.java | 57 +- .../opentaskspal/views/TaskListsView.java | 12 +- .../dmfs/opentaskspal/views/TasksView.java | 12 +- .../opentaskstestpal/InstanceTestData.java | 170 +- .../readdata/EffectiveDueDateTest.java | 173 +- .../readdata/EffectiveTaskColorTest.java | 64 +- .../readdata/TaskDateTimeTest.java | 131 +- .../rowdata/DateTimeListDataTest.java | 143 +- .../opentaskspal/tasklists/ColorDataTest.java | 30 +- .../opentaskspal/tasklists/NameDataTest.java | 33 +- .../opentaskspal/tasklists/OwnerDataTest.java | 33 +- .../tasklists/SyncStatusDataTest.java | 44 +- .../tasklists/VisibilityDataTest.java | 44 +- .../dmfs/opentaskspal/tasks/DueDataTest.java | 105 +- .../tasks/ExDatesTaskDataTest.java | 141 +- .../opentaskspal/tasks/NoTimeDataTest.java | 53 +- .../tasks/OriginalInstanceSyncIdDataTest.java | 38 +- .../opentaskspal/tasks/PropertyDataTest.java | 41 +- .../tasks/RDatesTaskDataTest.java | 141 +- .../opentaskspal/tasks/RRuleTaskDataTest.java | 33 +- .../opentaskspal/tasks/RelationDataTest.java | 74 +- .../opentaskspal/tasks/SyncIdDataTest.java | 31 +- .../dmfs/opentaskspal/tasks/TimeDataTest.java | 224 +- .../opentaskspal/tasks/TitleDataTest.java | 31 +- .../InstanceTestDataTest.java | 281 +- 605 files changed, 37930 insertions(+), 43011 deletions(-) diff --git a/opentasks-contract/build.gradle b/opentasks-contract/build.gradle index 2556a61c..c618b1e0 100644 --- a/opentasks-contract/build.gradle +++ b/opentasks-contract/build.gradle @@ -13,17 +13,16 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { sourceCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21 } } - -dependencies { -} diff --git a/opentasks-contract/src/main/java/org/dmfs/tasks/contract/TaskContract.java b/opentasks-contract/src/main/java/org/dmfs/tasks/contract/TaskContract.java index 3ce329da..8bc5970b 100644 --- a/opentasks-contract/src/main/java/org/dmfs/tasks/contract/TaskContract.java +++ b/opentasks-contract/src/main/java/org/dmfs/tasks/contract/TaskContract.java @@ -21,1708 +21,1508 @@ import android.content.Intent; import android.net.Uri; import android.provider.BaseColumns; import android.provider.SyncStateContract; - import java.util.ArrayList; import java.util.HashMap; import java.util.Map; - /** * Task contract. This class defines the interface to the task provider. - *

- * TODO: Add missing javadoc. - *

- *

- * TODO: Specify extended properties - *

- *

- * TODO: Add CONTENT_URI for the attachment store. - *

- *

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

+ * + *

TODO: Add missing javadoc. + * + *

TODO: Specify extended properties + * + *

TODO: Add CONTENT_URI for the attachment store. + * + *

TODO: Also, we could use some refactoring... * * @author Marten Gajda * @author Tobias Reinsch */ -public final class TaskContract -{ +public final class TaskContract { + + 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 static final 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 static final String EXTRA_TASK_ALLDAY = "org.dmfs.provider.tasks.extra.ALLDAY"; + + /** A String extra containing the timezone id of the task. */ + public static final String EXTRA_TASK_TIMEZONE = "org.dmfs.provider.tasks.extra.TIMEZONE"; + + /** A String extra containing the title of the task. */ + public static final 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 static final 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: + * + *

+ */ + public static final 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 { + public static final String CONTENT_URI_PATH = "syncstate"; - private static Map sUriFactories = new HashMap(4); + /** + * Get the sync state content {@link Uri} using the given authority. + * + * @param authority The authority. + * @return A {@link Uri}. + */ + public 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 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 { /** - * URI parameter to signal that the caller is a sync adapter. + * A unique Sync ID as set by the sync adapter. + * + *

Value: String */ - public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; + String _SYNC_ID = "_sync_id"; /** - * URI parameter to signal the request of the extended properties of a task. + * Sync version as set by the sync adapter. + * + *

Value: String */ - public static final String LOAD_PROPERTIES = "load_properties"; + String SYNC_VERSION = "sync_version"; /** - * URI parameter to submit the account name of the account we operate on. + * Indicates that a task or a task list has been changed. + * + *

Value: Integer */ - public static final String ACCOUNT_NAME = "account_name"; + String _DIRTY = "_dirty"; /** - * URI parameter to submit the account type of the account we operate on. + * A general purpose column for the sync adapter. + * + *

Value: String */ - public static final String ACCOUNT_TYPE = "account_type"; + String SYNC1 = "sync1"; /** - * Account name for local, unsynced task lists. + * A general purpose column for the sync adapter. + * + *

Value: String */ - public static final String LOCAL_ACCOUNT_NAME = "Local"; + String SYNC2 = "sync2"; /** - * Account type for local, unsynced task lists. + * A general purpose column for the sync adapter. + * + *

Value: String */ - public static final String LOCAL_ACCOUNT_TYPE = "org.dmfs.account.LOCAL"; + String SYNC3 = "sync3"; /** - * 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}. + * A general purpose column for the sync adapter. + * + *

Value: String */ - public static final String ACTION_DATABASE_INITIALIZED = "org.dmfs.tasks.DATABASE_INITIALIZED"; + String SYNC4 = "sync4"; /** - * A MIME type of an authority. Authorities itself don't seem to have a MIME type in Android, so we just use our own. + * A general purpose column for the sync adapter. + * + *

Value: String */ - public static final String MIMETYPE_AUTHORITY = ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.org.dmfs.authority.mimetype"; + String SYNC5 = "sync5"; /** - * 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. + * A general purpose column for the sync adapter. + * + *

Value: String */ - public static final String ACTION_BROADCAST_TASK_DUE = "org.dmfs.android.tasks.TASK_DUE"; + String SYNC6 = "sync6"; /** - * 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. + * A general purpose column for the sync adapter. + * + *

Value: String */ - public static final String ACTION_BROADCAST_TASK_STARTING = "org.dmfs.android.tasks.TASK_START"; + String SYNC7 = "sync7"; + + /** + * A general purpose column for the sync adapter. + * + *

Value: String + */ + 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 + */ + String ACCOUNT_NAME = "account_name"; + + /** + * The type of the account this list belongs to. This field is write-once. + * + *

Value: String + */ + 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 + */ + 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 + */ + String _DELETED = "_deleted"; + } + + /** + * Data columns of task lists. + * + * @author Marten Gajda + */ + public interface TaskListColumns { /** - * 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. + * List ID. + * + *

Value: Long + * + *

read-only */ - public final static String EXTRA_TASK_TIMESTAMP = "org.dmfs.provider.tasks.extra.TIMESTAMP"; + String _ID = "_id"; /** - * A Boolean extra to indicate that the event that was triggered is an all-day date. + * The name of the task list. + * + *

Value: String */ - public final static String EXTRA_TASK_ALLDAY = "org.dmfs.provider.tasks.extra.ALLDAY"; + String LIST_NAME = "list_name"; /** - * A String extra containing the timezone id of the task. + * The color of this list as integer (0xaarrggbb). Only the sync adapter can change this. + * + *

Value: Integer */ - public final static String EXTRA_TASK_TIMEZONE = "org.dmfs.provider.tasks.extra.TIMEZONE"; + String LIST_COLOR = "list_color"; /** - * A String extra containing the title of the task. + * 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 final static String EXTRA_TASK_TITLE = "org.dmfs.provider.tasks.extra.TITLE"; + String ACCESS_LEVEL = "list_access_level"; /** - * 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. + * Indicates that a task list is set to be visible. + * + *

Value: Integer (0 or 1) */ - public final static String EXTRA_OPERATIONS_URIS = "org.dmfs.tasks.OPERATIONS_URIS"; + String VISIBLE = "visible"; /** - * The name of the {@link Intent#ACTION_PROVIDER_CHANGED} extra that contains the {@link ArrayList} of provider operation codes. The following codes are - * used: - *

+ * Indicates that a task list is set to be synced. + * + *

Value: Integer (0 or 1) */ - public final static String EXTRA_OPERATIONS = "org.dmfs.tasks.OPERATIONS"; + String SYNC_ENABLED = "sync_enabled"; + /** + * The email address of the list owner. + * + *

Value: String + */ + 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 { + public 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, + }; /** - * Private constructor to prevent instantiation. + * Get the task list content {@link Uri} using the given authority. + * + * @param authority The authority. + * @return A {@link Uri}. */ - private TaskContract() - { + public 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 extends BaseColumns { /** - * 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. + * The row id of a task. This value is read-only + * + *

Value: Integer */ - public static class SyncState implements SyncStateContract.Columns, BaseColumns - { - public final static String CONTENT_URI_PATH = "syncstate"; + String _ID = "_id"; + /** + * The local version number of this task. The only guarantee about the value is, it's + * incremented whenever the task changes (this includes any changes applied by sync adapters). + * + *

Note, there is no guarantee about how much it's incremented other than by at least 1. + * + *

Value: Integer + * + *

read-only + */ + String VERSION = "version"; - /** - * Get the sync state content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } + /** + * The id of the list this task belongs to. This value is write-once and must + * not be null. + * + *

Value: Integer + */ + String LIST_ID = "list_id"; - } + /** + * The title of the task. + * + *

Value: String + */ + String TITLE = "title"; + /** + * The location of the task. + * + *

Value: String + */ + String LOCATION = "location"; /** - * Get the base content {@link Uri} using the given authority. + * A geographic location related to the task. The should be a string in the format + * "longitude,latitude". * - * @param authority - * The authority. + *

Value: String + */ + String GEO = "geo"; + + /** + * The description of a task. * - * @return A {@link Uri}. + *

Value: String */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(); - } + String DESCRIPTION = "description"; + /** + * The URL iCalendar field for this task. Must be a valid URI if not null- + * + *

Value: String + */ + String URL = "url"; /** - * 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. + * The email address of the organizer if any, {@code null} otherwise. * - * @author Marten Gajda + *

Value: String */ - public interface CommonSyncColumns - { - - /** - * A unique Sync ID as set by the sync adapter. - *

- * Value: String - *

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

- * Value: String - *

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

- * Value: Integer - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: String - *

- */ - String SYNC8 = "sync8"; + 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 + */ + String PRIORITY = "priority"; + /** The default value of {@link #PRIORITY}. */ + int PRIORITY_DEFAULT = 0; /** - * Additional sync columns for task lists. + * The classification of a task. This value must be either null or one of {@link + * #CLASSIFICATION_PUBLIC}, {@link #CLASSIFICATION_PRIVATE}, {@link + * #CLASSIFICATION_CONFIDENTIAL}. * - * @author Marten Gajda + *

Value: Integer */ - public interface TaskListSyncColumns - { - - /** - * The name of the account this list belongs to. This field is write-once. - *

- * Value: String - *

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

- * Value: String - *

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

Value: Long */ - public interface TaskSyncColumns - { - /** - * The UID of a task. This is field can be changed by a sync adapter only. - *

- * Value: String - *

- */ - 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 - *

- */ - String _DELETED = "_deleted"; - } + String COMPLETED = "completed"; + /** + * Indicates that the date of completion is an all-day date. + * + *

Value: Integer + */ + String COMPLETED_IS_ALLDAY = "completed_is_allday"; /** - * Data columns of task lists. + * A number between 0 and 100 that indicates the progress of the task or null. * - * @author Marten Gajda + *

Value: Integer (0-100) */ - public interface TaskListColumns - { - - /** - * List ID. - *

- * Value: Long - *

- *

- * read-only - *

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

- * Value: String - *

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

- * Value: Integer - *

- */ - 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 - *

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

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

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

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

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

- * Value: String - *

- */ - String OWNER = "list_owner"; + 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 + */ + String STATUS = "status"; + + /** A specific status indicating that nothing has been done yet. */ + int STATUS_NEEDS_ACTION = 0; + + /** A specific status indicating that some work has been done. */ + int STATUS_IN_PROCESS = 1; + + /** A specific status indicating that the task is completed. */ + int STATUS_COMPLETED = 2; + + /** A specific status indicating that the task has been cancelled. */ + int STATUS_CANCELLED = 3; + /** The default status is "needs action". */ + int STATUS_DEFAULT = STATUS_NEEDS_ACTION; /** - * The task list table holds one entry for each task list. + * 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. * - * @author Marten Gajda + *

Value: Integer + * + *

read-only */ - public static final class TaskLists implements TaskListColumns, TaskListSyncColumns, CommonSyncColumns - { - public 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 static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } + 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 + */ + 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 + */ + String TASK_COLOR = "task_color"; /** - * Task data columns. Defines all the values a task can have at most once. + * When this task starts in milliseconds since the epoch. * - * @author Marten Gajda + *

Value: Long */ - public interface TaskColumns extends BaseColumns - { - - /** - * The row id of a task. This value is read-only - *

- * Value: Integer - *

- */ - String _ID = "_id"; - - /** - * The local version number of this task. The only guarantee about the value is, it's incremented whenever the task changes (this includes any - * changes applied by sync adapters). - *

- * Note, there is no guarantee about how much it's incremented other than by at least 1. - *

- * Value: Integer - *

- * read-only - */ - String VERSION = "version"; - - /** - * The id of the list this task belongs to. This value is write-once and must not be null. - *

- * Value: Integer - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: String - *

- */ - String DESCRIPTION = "description"; - - /** - * The URL iCalendar field for this task. Must be a valid URI if not null- - *

- * Value: String - *

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

- * Value: String - *

- */ - 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 - *

- */ - String PRIORITY = "priority"; - - /** - * The default value of {@link #PRIORITY}. - */ - 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 - *

- */ - String CLASSIFICATION = "class"; - - /** - * Classification value for public tasks. - */ - int CLASSIFICATION_PUBLIC = 0; - - /** - * Classification value for private tasks. - */ - int CLASSIFICATION_PRIVATE = 1; - - /** - * Classification value for confidential tasks. - */ - int CLASSIFICATION_CONFIDENTIAL = 2; - - /** - * Default value of {@link #CLASSIFICATION}. - */ - 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 - *

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

- * Value: Integer - *

- */ - 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) - *

- */ - 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 - *

- */ - String STATUS = "status"; - - /** - * A specific status indicating that nothing has been done yet. - */ - int STATUS_NEEDS_ACTION = 0; - - /** - * A specific status indicating that some work has been done. - */ - int STATUS_IN_PROCESS = 1; - - /** - * A specific status indicating that the task is completed. - */ - int STATUS_COMPLETED = 2; - - /** - * A specific status indicating that the task has been cancelled. - */ - int STATUS_CANCELLED = 3; - - /** - * The default status is "needs action". - */ - 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 - *

- */ - 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 - *

- */ - 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 - *

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

- * Value: Long - *

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

- * Value: Long - *

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

- * Value: Long - *

- */ - 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. - */ - 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 - *

- */ - 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 - *

- */ - 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 - *

- */ - 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 - *

- */ - 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 - *

- */ - 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 - *

- */ - 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 - *

- */ - 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 a - * recurring instance. - *

- * Value: Long - *

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

- * Value: Integer - *

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

- * Note, when writing this value the task {@link Property.Relation} properties are updated accordingly. Any parent or child relations which - * make this a child of another task are deleted and a new {@link Property.Relation#RELTYPE_PARENT} relation pointing to the new parent is created. - * Be aware that Siblings will be split, i.e. they are not moved to the new parent. Currently this might cause siblings to become orphans if they - * don't have a parent-child relationship. This behavior may change in future version. - *

- * - *

- * Value: Long - *

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

- * Value: String - *

- */ - 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 - */ - 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 - *

- */ - 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 - *

- */ - String PINNED = "pinned"; - } + String DTSTART = "dtstart"; + /** Boolean: flag that indicates that this is an all-day task. */ + String IS_ALLDAY = "is_allday"; /** - * Columns that are valid in a search query. + * When this task has been created in milliseconds since the epoch. * - * @author Marten Gajda + *

Value: Long */ - 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 - *

- */ - String SCORE = "score"; - } + String CREATED = "created"; + /** + * When this task had been modified the last time in milliseconds since the epoch. + * + *

Value: Long + */ + String LAST_MODIFIED = "last_modified"; /** - * The task table stores the data of all tasks. + * 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. + */ + 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). * - * @author Marten Gajda + *

Value: Long */ - 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"; - - public static final String CONTENT_URI_PATH = "tasks"; - - public static final String SEARCH_URI_PATH = "tasks_search"; - - public 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 static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - - public 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(); - } - } + 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 + */ + String DURATION = "duration"; /** - * Columns of a task instance. + * 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. * - * @author Yannic Ahrens - * @author Marten Gajda + *

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

Value: String */ - public interface InstanceColumns - { - /** - * _ID of task this instance belongs to. - *

- * Value: Long - *

- */ - 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 - *

- */ - 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 - *

- */ - 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 - *

- */ - 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 - *

- */ - 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 - *

- */ - String INSTANCE_DURATION = "instance_duration"; - - /** - * The start of the original instance as specified in the master task. For non-recurring task instances this is {@code null}. - *

- * For recurring tasks, these are the timestamps which have been derived from the recurrence rule or dates, except those specified as exdates. - */ - String INSTANCE_ORIGINAL_TIME = "instance_original_time"; - - /** - * The distance of the instance from the current one. For closed instances this is always {@code -1}, for the current instance this is {@code 0}. For - * the instance after the current one this is {@code 1}, for the instance after that one it's {@code 2}, etc.. - *

- * Value: Integer - *

- * read-only - */ - String DISTANCE_FROM_CURRENT = "distance_from_current"; - } + 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 + */ + String EXDATE = "exdate"; /** - * A table containing one entry per task instance. This table is writable in order to allow modification of single instances of a task. Write operations to - * this table will be converted into operations on overrides and forwarded to the task table. - *

- * Note: The {@link #DTSTART}, {@link #DUE} values of instances of recurring tasks represent the actual instance values, i.e. they are different for each - * instance ({@link #DURATION} is always {@code null}). - *

- * Also, none of the instances are recurring themselves, so {@link #RRULE}, {@link #RDATE} and {@link #EXDATE} are always {@code null}. - *

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

- * The following operations are supported: - *

- *

Insert

- *

- * Note, the data of an insert must not contain the fields {@link #RRULE}, {@link #RDATE} or {@link #EXDATE}. If the new instance belongs to an existing - * task the data must contain the fields {@link #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME}. Also note, this table supports writing {@link - * #DURATION} (if the instance has a {@link #DTSTART}), but reading it back will always return a {@code null} {@link #DURATION} and a non-{@code null} - * {@link #DUE} date. Reading the task in the tasks table will, however, return the original {@link #DURATION}. - *

- * If there already is an instance (with or without override) for the given {@link #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME} an exception - * is thrown. - *

- *
ORIGINAL_INSTANCE_ID valueResult
absent or emptyA new non-recurring task is created with the given - * values.
a valid {@link Tasks} row {@code _ID}An {@link #RDATE} for the given {@link #ORIGINAL_INSTANCE_TIME} time is added to - * the given master task, any {@link #EXDATE} for this time is removed. The task is inserted as an override to the given master. No fields are inherited - * though. {@link #ORIGINAL_INSTANCE_ALLDAY} will be set to {@link #IS_ALLDAY} of the master. - *

- * Note, if the given master is non-recurring, this operation will turn it into a recurring task.

invalid {@link Tasks} row {@code - * _ID}An exception is thrown.
- *

- *

Update

- *

- * Note, the data of an update must not contain any fields related to recurrence ({@link #RRULE}, {@link #RDATE}, {@link #EXDATE}, {@link - * #ORIGINAL_INSTANCE_ID}, {@link #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY}). Also note, this table supports writing {@link #DURATION} - * (if the instance has a {@link #DTSTART}), but reading it back will always return a {@code null} {@link #DURATION} and a non-{@code null} {@link #DUE} - * date. Reading the task in the tasks table will, however, return the original {@link #DURATION}. - *

- * - *
Target task typeResult
Recurring master taskA new override is created with the given data.

Note, - * any fields which are not provided are inherited from the master, except for {@link #DTSTART} and {@link #DUE} which will be inherited from the instance - * and {@link #DURATION}, {@link #RRULE}, {@link #RDATE} and {@link #EXDATE} which are set to {@code null}. {@link #ORIGINAL_INSTANCE_ID}, {@link - * #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY} will be set accordingly.

Single instance taskThe task is - * updated with the given values.
Recurrence override with existing masterThe task is updated with the given values.
Recurrence override without existing masterThe task is updated with the given values.
- *

- *

Delete

- *

- * - * - *
Target task typeResult
Recurring master taskAn {@link #EXDATE} for this instance is added, any {@link - * #RDATE} for this instance is removed. The instance row is removed.

TODO: mark the task deleted if the remaining recurrence set is empty

Single instance taskThe {@link Tasks#_DELETED} flag of the task is set.
Recurrence override with existing - * masterThe {@link Tasks#_DELETED} flag of the override is set, an {@link #EXDATE} for this instance is added to the master, any {@link #RDATE} - * for this instance is removed from the master. TODO: mark the master deleted if the remaining recurrence set of the master is empty
Recurrence override without existing masterThe {@link Tasks#_DELETED} flag of the task is set.
- * - * @author Yannic Ahrens - * @author Marten Gajda - */ - 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"; - - /** - * Flag indicating that ths is an instance of a recurring task. - *

- * Value: Integer - *

- * read-only - */ - public static final String IS_RECURRING = "is_recurring"; - - public 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 static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } + * A recurrence rule as specified in RFC 5545 Section 3.3.10. + * + *

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

Value: String + */ + 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 + */ + 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 + */ + 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 a recurring instance. + * + *

Value: Long + */ + String ORIGINAL_INSTANCE_TIME = "original_instance_time"; /** - * Available values in Categories. - *

- * Categories are per account. It's up to the front-end to ensure consistency of category colors across accounts. + * A flag indicating that the original instance was an all-day task. * - * @author Marten Gajda + *

Value: Integer */ - public interface CategoriesColumns - { + String ORIGINAL_INSTANCE_ALLDAY = "original_instance_allday"; - String _ID = "_id"; + /** + * The row id of the parent task. null if the task has no parent task. + * + *

Note, when writing this value the task {@link Property.Relation} properties are updated + * accordingly. Any parent or child relations which make this a child of another task are + * deleted and a new {@link Property.Relation#RELTYPE_PARENT} relation pointing to the new + * parent is created. Be aware that Siblings will be split, i.e. they are not moved to the new + * parent. Currently this might cause siblings to become orphans if they don't have a + * parent-child relationship. This behavior may change in future version. + * + *

Value: Long + */ + String PARENT_ID = "parent_id"; - String ACCOUNT_NAME = "account_name"; + /** + * The sorting of this task under it's parent task. + * + *

Value: String + */ + String SORTING = "sorting"; - String ACCOUNT_TYPE = "account_type"; + /** + * 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 + */ + String HAS_ALARMS = "has_alarms"; - String NAME = "name"; + /** + * 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 + */ + String HAS_PROPERTIES = "has_properties"; - String COLOR = "color"; + /** + * 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 + */ + 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 + */ + 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"; + + public static final String CONTENT_URI_PATH = "tasks"; + + public static final String SEARCH_URI_PATH = "tasks_search"; + + public 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 static Uri getContentUri(String authority) { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); + } + + public 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + String INSTANCE_DURATION = "instance_duration"; + + /** + * The start of the original instance as specified in the master task. For non-recurring task + * instances this is {@code null}. + * + *

For recurring tasks, these are the timestamps which have been derived from the recurrence + * rule or dates, except those specified as exdates. + */ + String INSTANCE_ORIGINAL_TIME = "instance_original_time"; + + /** + * The distance of the instance from the current one. For closed instances this is always {@code + * -1}, for the current instance this is {@code 0}. For the instance after the current one this + * is {@code 1}, for the instance after that one it's {@code 2}, etc.. + * + *

Value: Integer + * + *

read-only + */ + String DISTANCE_FROM_CURRENT = "distance_from_current"; + } + + /** + * A table containing one entry per task instance. This table is writable in order to allow + * modification of single instances of a task. Write operations to this table will be converted + * into operations on overrides and forwarded to the task table. + * + *

Note: The {@link #DTSTART}, {@link #DUE} values of instances of recurring tasks represent + * the actual instance values, i.e. they are different for each instance ({@link #DURATION} is + * always {@code null}). + * + *

Also, none of the instances are recurring themselves, so {@link #RRULE}, {@link #RDATE} and + * {@link #EXDATE} are always {@code null}. + * + *

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

The following operations are supported: + * + *

+ * + *

Insert

+ * + *

Note, the data of an insert must not contain the fields {@link #RRULE}, {@link #RDATE} or + * {@link #EXDATE}. If the new instance belongs to an existing task the data must contain the + * fields {@link #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME}. Also note, this table + * supports writing {@link #DURATION} (if the instance has a {@link #DTSTART}), but reading it + * back will always return a {@code null} {@link #DURATION} and a non-{@code null} {@link #DUE} + * date. Reading the task in the tasks table will, however, return the original {@link #DURATION}. + * + *

If there already is an instance (with or without override) for the given {@link + * #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME} an exception is thrown. + * + *

+ * + * + *
ORIGINAL_INSTANCE_ID valueResult
absent or emptyA new non-recurring task is created with the given + * values.
a valid {@link Tasks} row {@code _ID}An {@link #RDATE} for the given {@link #ORIGINAL_INSTANCE_TIME} time is added to + * the given master task, any {@link #EXDATE} for this time is removed. The task is inserted as an override to the given master. No fields are inherited + * though. {@link #ORIGINAL_INSTANCE_ALLDAY} will be set to {@link #IS_ALLDAY} of the master. + *

+ * Note, if the given master is non-recurring, this operation will turn it into a recurring task.

invalid {@link Tasks} row {@code + * _ID}An exception is thrown.
+ * + *

+ * + *

Update

+ * + *

Note, the data of an update must not contain any fields related to recurrence ({@link + * #RRULE}, {@link #RDATE}, {@link #EXDATE}, {@link #ORIGINAL_INSTANCE_ID}, {@link + * #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY}). Also note, this table supports + * writing {@link #DURATION} (if the instance has a {@link #DTSTART}), but reading it back will + * always return a {@code null} {@link #DURATION} and a non-{@code null} {@link #DUE} date. + * Reading the task in the tasks table will, however, return the original {@link #DURATION}. + * + *

+ * + * + * + * + *
Target task typeResult
Recurring master taskA new override is created with the given data.

Note, + * any fields which are not provided are inherited from the master, except for {@link #DTSTART} and {@link #DUE} which will be inherited from the instance + * and {@link #DURATION}, {@link #RRULE}, {@link #RDATE} and {@link #EXDATE} which are set to {@code null}. {@link #ORIGINAL_INSTANCE_ID}, {@link + * #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY} will be set accordingly.

Single instance taskThe task is + * updated with the given values.
Recurrence override with existing masterThe task is updated with the given values.
Recurrence override without existing masterThe task is updated with the given values.
+ * + *

+ * + *

Delete

+ * + *

+ * + * + * + * + * + *
Target task typeResult
Recurring master taskAn {@link #EXDATE} for this instance is added, any {@link + * #RDATE} for this instance is removed. The instance row is removed.

TODO: mark the task deleted if the remaining recurrence set is empty

Single instance taskThe {@link Tasks#_DELETED} flag of the task is set.
Recurrence override with existing + * masterThe {@link Tasks#_DELETED} flag of the override is set, an {@link #EXDATE} for this instance is added to the master, any {@link #RDATE} + * for this instance is removed from the master. TODO: mark the master deleted if the remaining recurrence set of the master is empty
Recurrence override without existing masterThe {@link Tasks#_DELETED} flag of the task is set.
+ * + * @author Yannic Ahrens + * @author Marten Gajda + */ + 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; - public static final class Categories implements CategoriesColumns - { + /** + * 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; - public static final String CONTENT_URI_PATH = "categories"; + /** + * 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; - public static final String DEFAULT_SORT_ORDER = NAME; + /** + * 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; - /** - * Get the categories content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } + /** + * 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"; + /** + * Flag indicating that ths is an instance of a recurring task. + * + *

Value: Integer + * + *

read-only + */ + public static final String IS_RECURRING = "is_recurring"; + + public 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 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 { + + String _ID = "_id"; + + String ACCOUNT_NAME = "account_name"; + String ACCOUNT_TYPE = "account_type"; - public interface AlarmsColumns - { - String ALARM_ID = "alarm_id"; + String NAME = "name"; - String LAST_TRIGGER = "last_trigger"; + String COLOR = "color"; + } - String NEXT_TRIGGER = "next_trigger"; + public static final class Categories implements CategoriesColumns { + + public static final String CONTENT_URI_PATH = "categories"; + + public static final String DEFAULT_SORT_ORDER = NAME; + + /** + * Get the categories content {@link Uri} using the given authority. + * + * @param authority The authority. + * @return A {@link Uri}. + */ + public static Uri getContentUri(String authority) { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); } + } + public interface AlarmsColumns { + String ALARM_ID = "alarm_id"; - public static final class Alarms implements AlarmsColumns - { + String LAST_TRIGGER = "last_trigger"; - public static final String CONTENT_URI_PATH = "alarms"; + String NEXT_TRIGGER = "next_trigger"; + } + public static final class Alarms implements AlarmsColumns { - /** - * Get the alarms content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } + public static final String CONTENT_URI_PATH = "alarms"; + /** + * Get the alarms content {@link Uri} using the given authority. + * + * @param authority The authority. + * @return A {@link Uri}. + */ + public static Uri getContentUri(String authority) { + return getUriFactory(authority).getUri(CONTENT_URI_PATH); } + } + public interface PropertySyncColumns { + String SYNC1 = "prop_sync1"; - public interface PropertySyncColumns - { - String SYNC1 = "prop_sync1"; + String SYNC2 = "prop_sync2"; - String SYNC2 = "prop_sync2"; + String SYNC3 = "prop_sync3"; - String SYNC3 = "prop_sync3"; + String SYNC4 = "prop_sync4"; - String SYNC4 = "prop_sync4"; + String SYNC5 = "prop_sync5"; - String SYNC5 = "prop_sync5"; + String SYNC6 = "prop_sync6"; - String SYNC6 = "prop_sync6"; + String SYNC7 = "prop_sync7"; - String SYNC7 = "prop_sync7"; + String SYNC8 = "prop_sync8"; + } - String SYNC8 = "prop_sync8"; - } + public interface PropertyColumns { + + String PROPERTY_ID = "property_id"; + + String TASK_ID = "task_id"; + String MIMETYPE = "mimetype"; - public interface PropertyColumns - { + String VERSION = "prop_version"; - String PROPERTY_ID = "property_id"; + String DATA0 = "data0"; - String TASK_ID = "task_id"; + String DATA1 = "data1"; - String MIMETYPE = "mimetype"; + String DATA2 = "data2"; - String VERSION = "prop_version"; + String DATA3 = "data3"; - String DATA0 = "data0"; + String DATA4 = "data4"; - String DATA1 = "data1"; + String DATA5 = "data5"; - String DATA2 = "data2"; + String DATA6 = "data6"; - String DATA3 = "data3"; + String DATA7 = "data7"; - String DATA4 = "data4"; + String DATA8 = "data8"; - String DATA5 = "data5"; + String DATA9 = "data9"; - String DATA6 = "data6"; + String DATA10 = "data10"; - String DATA7 = "data7"; + String DATA11 = "data11"; - String DATA8 = "data8"; + String DATA12 = "data12"; - String DATA9 = "data9"; + String DATA13 = "data13"; - String DATA10 = "data10"; + String DATA14 = "data14"; - String DATA11 = "data11"; + String DATA15 = "data15"; + } - String DATA12 = "data12"; + public static final class Properties implements PropertySyncColumns, PropertyColumns { - String DATA13 = "data13"; + public static final String CONTENT_URI_PATH = "properties"; - String DATA14 = "data14"; + public static final String DEFAULT_SORT_ORDER = DATA0; - String DATA15 = "data15"; + /** + * Get the properties content {@link Uri} using the given authority. + * + * @param authority The authority. + * @return A {@link Uri}. + */ + public 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 + */ + interface Attachment extends PropertyColumns { + /** The mime-type of this property. */ + 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 + */ + String URL = DATA1; + + /** + * The display name of the attachment, if any. + * + *

Value: String + */ + String DISPLAY_NAME = DATA2; + + /** + * Content-type of the attachment. + * + *

Value: String + */ + String FORMAT = DATA3; + + /** + * File size of the attachment or -1 if unknown. + * + *

Value: Long + */ + 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 + */ + String CONTENT_URI = DATA5; + } - public static final class Properties implements PropertySyncColumns, PropertyColumns - { + interface Attendee extends PropertyColumns { + /** The mime-type of this property. */ + String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/attendee"; - public static final String CONTENT_URI_PATH = "properties"; + /** + * Name of the contact, if known. + * + *

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

Value: String + */ + String EMAIL = DATA1; + String ROLE = DATA2; - /** - * Get the properties content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } + String STATUS = DATA3; + String RSVP = DATA4; } + /** Categories are immutable. For creation is either the category id or name necessary */ + interface Category extends PropertyColumns { + /** The mime-type of this property. */ + String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/category"; + + /** + * Row id of the category. + * + *

Value: Long + */ + String CATEGORY_ID = DATA0; + + /** + * The name of the category + * + *

Value: String + */ + String CATEGORY_NAME = DATA1; + + /** + * The decimal coded color of the category + * + *

Value: Integer + * + *

read-only + */ + String CATEGORY_COLOR = DATA2; + } - 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 - */ - interface Attachment extends PropertyColumns - { - /** - * The mime-type of this property. - */ - 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 - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: Long - *

- */ - 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 - *

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

- * Value: String - *

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

- * Value: String - *

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

- * Value: Long - *

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

- * Value: String - *

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

- * Value: Integer - *

- *

- * read-only - *

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

- * Value: String - *

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

- * Value: String - *

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

- * When writing a relation, exactly one of {@link #RELATED_ID} or {@link #RELATED_UID} must be present. The missing value and {@link - * #RELATED_CONTENT_URI} will be populated automatically if possible. - *

- * {@link Tasks#PARENT_ID} is updated automatically if possible. - */ - interface Relation extends PropertyColumns - { - /** - * The mime-type of this property. - */ - 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 - *

- */ - String RELATED_ID = DATA1; - - /** - * The relation type. This must be one of the {@code RELTYPE_*} values. - *

- * Value: int - *

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

- * Value: String - *

- */ - String RELATED_UID = DATA3; - - /** - * The content Uri of a related object in another Android content provider, if found. - *

- * Value: String (URI) - *

- *

- * This field is read-only. - *

- */ - String RELATED_CONTENT_URI = DATA5; - - /** - * The related object is the parent of the object owning this relation. - */ - int RELTYPE_PARENT = 0; - - /** - * The related object is the child of the object owning this relation. - */ - int RELTYPE_CHILD = 1; - - /** - * The related object is a sibling of the object owning this relation. - */ - int RELTYPE_SIBLING = 2; - - } - - - interface Alarm extends PropertyColumns - { - - int ALARM_TYPE_NOTHING = 0; - - int ALARM_TYPE_MESSAGE = 1; - - int ALARM_TYPE_EMAIL = 2; - - int ALARM_TYPE_SMS = 3; - - int ALARM_TYPE_SOUND = 4; - - int ALARM_REFERENCE_DUE_DATE = 1; - - int ALARM_REFERENCE_START_DATE = 2; - - /** - * The mime-type of this property. - */ - 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 - *

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

- * Value: Integer - *

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

- * Value: String - *

- */ - 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 - *

- */ - String ALARM_TYPE = DATA3; - } - + interface Comment extends PropertyColumns { + /** The mime-type of this property. */ + String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/comment"; + + /** + * Comment text. + * + *

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

Value: String + */ + String LANGUAGE = DATA1; } + interface Contact extends PropertyColumns { + /** The mime-type of this property. */ + String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/contact"; - 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; + String NAME = DATA0; + + String LANGUAGE = DATA1; } + /** + * Relations of a task. + * + *

When writing a relation, exactly one of {@link #RELATED_ID} or {@link #RELATED_UID} must + * be present. The missing value and {@link #RELATED_CONTENT_URI} will be populated + * automatically if possible. + * + *

{@link Tasks#PARENT_ID} is updated automatically if possible. + */ + interface Relation extends PropertyColumns { + /** The mime-type of this property. */ + 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 + */ + String RELATED_ID = DATA1; + + /** + * The relation type. This must be one of the {@code RELTYPE_*} values. + * + *

Value: int + */ + String RELATED_TYPE = DATA2; + + /** + * The UID of the related object. + * + *

Value: String + */ + String RELATED_UID = DATA3; + + /** + * The content Uri of a related object in another Android content provider, if found. + * + *

Value: String (URI) + * + *

This field is read-only. + */ + String RELATED_CONTENT_URI = DATA5; + + /** The related object is the parent of the object owning this relation. */ + int RELTYPE_PARENT = 0; + + /** The related object is the child of the object owning this relation. */ + int RELTYPE_CHILD = 1; + + /** The related object is a sibling of the object owning this relation. */ + int RELTYPE_SIBLING = 2; + } + + interface Alarm extends PropertyColumns { + + int ALARM_TYPE_NOTHING = 0; + + int ALARM_TYPE_MESSAGE = 1; + + int ALARM_TYPE_EMAIL = 2; + + int ALARM_TYPE_SMS = 3; + + int ALARM_TYPE_SOUND = 4; + + int ALARM_REFERENCE_DUE_DATE = 1; + + int ALARM_REFERENCE_START_DATE = 2; + + /** The mime-type of this property. */ + 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 + */ + String MINUTES_BEFORE = DATA0; + + /** + * The reference date for the alarm. Either {@link #ALARM_REFERENCE_DUE_DATE} or {@link + * #ALARM_REFERENCE_START_DATE}. + * + *

Value: Integer + */ + String REFERENCE = DATA1; + + /** + * A message that appears with the alarm. + * + *

Value: String + */ + 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 + */ + 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; + } } diff --git a/opentasks-contract/src/main/java/org/dmfs/tasks/contract/UriFactory.java b/opentasks-contract/src/main/java/org/dmfs/tasks/contract/UriFactory.java index 0092aca2..213f24f4 100644 --- a/opentasks-contract/src/main/java/org/dmfs/tasks/contract/UriFactory.java +++ b/opentasks-contract/src/main/java/org/dmfs/tasks/contract/UriFactory.java @@ -17,41 +17,28 @@ package org.dmfs.tasks.contract; import android.net.Uri; - import java.util.HashMap; import java.util.Map; +/** TODO */ +public final class UriFactory { + private final String mAuthority; + private final Map mUriMap = new HashMap(16); -/** - * TODO - */ -public final class UriFactory -{ - private final String mAuthority; - private final Map mUriMap = new HashMap(16); - - - UriFactory(String authority) - { - mAuthority = authority; - mUriMap.put(null, Uri.parse("content://" + authority)); - } - - - void addUri(String path) - { - mUriMap.put(path, Uri.parse("content://" + mAuthority + "/" + path)); - } - + UriFactory(String authority) { + mAuthority = authority; + mUriMap.put(null, Uri.parse("content://" + authority)); + } - Uri getUri() - { - return mUriMap.get(null); - } + void addUri(String path) { + mUriMap.put(path, Uri.parse("content://" + mAuthority + "/" + path)); + } + Uri getUri() { + return mUriMap.get(null); + } - Uri getUri(String path) - { - return mUriMap.get(path); - } + Uri getUri(String path) { + return mUriMap.get(path); + } } diff --git a/opentasks-provider/build.gradle b/opentasks-provider/build.gradle index 0ac44f88..561b813d 100644 --- a/opentasks-provider/build.gradle +++ b/opentasks-provider/build.gradle @@ -9,20 +9,21 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 36 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } + buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { sourceCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21 } + packagingOptions { exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderDetachInstancesTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderDetachInstancesTest.java index bd824a9a..b37b2974 100644 --- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderDetachInstancesTest.java +++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderDetachInstancesTest.java @@ -16,13 +16,19 @@ package org.dmfs.provider.tasks; +import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; +import static org.dmfs.jems.optional.elementary.Absent.absent; +import static org.junit.Assert.assertThat; + import android.accounts.Account; import android.content.ContentProviderClient; import android.content.Context; import android.content.OperationApplicationException; import android.os.Build; import android.os.RemoteException; - +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; +import java.util.TimeZone; import org.dmfs.android.contentpal.Operation; import org.dmfs.android.contentpal.OperationsQueue; import org.dmfs.android.contentpal.RowSnapshot; @@ -73,680 +79,794 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.TimeZone; - -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; -import static org.dmfs.jems.optional.elementary.Absent.absent; -import static org.junit.Assert.assertThat; - - /** * Test {@link TaskProvider} for correctly detaching completed instances. * * @author Marten Gajda */ @RunWith(AndroidJUnit4.class) -public class TaskProviderDetachInstancesTest -{ - private String mAuthority; - private ContentProviderClient mClient; - private Account mTestAccount = new Account("testname", "testtype"); - - - @Before - public void setUp() throws Exception - { - Context context = InstrumentationRegistry.getTargetContext(); - mAuthority = AuthorityUtil.taskAuthority(context); - mClient = context.getContentResolver().acquireContentProviderClient(mAuthority); - - // Assert that tables are empty: - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.flush(); - queue.enqueue(new Seq>( - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new TaskListsTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)))); - queue.flush(); - } - - - @After - public void tearDown() throws Exception - { - /* - TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation - https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html - https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator - */ - - // Clear the DB: - BasicOperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new SingletonIterable>(new BulkDelete<>(new LocalTaskListsTable(mAuthority)))); - queue.flush(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - { - mClient.close(); - } - else - { - mClient.release(); - } - } - - - /** - * Test if the first instance of a task with a DTSTART, DUE and an RRULE is correctly detached when completed. - */ - @Test - public void testRRule() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - - Duration day = new Duration(1, 1, 0); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))) - )); - queue.flush(); - - assertThat(new Seq<>( - // update the first non-closed instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - ), - resultsIn(queue, - /* - * We expect three tasks: - * - the original master with updated RRULE, DTSTART and DUE - * - a deleted instance - * - a detached task - */ - - // the original master - new Assert<>(task, - new Composite<>( - new TimeData<>(start.addDuration(day), due.addDuration(day)), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=4"))), - // there is one instance referring to the master (the old second instance, now first) - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // the detached task instance: - new Counted<>(1, new BulkAssert<>(new Synced<>(mTestAccount, instancesTable), - new Composite<>( - new InstanceTestData(localStart, localDue, absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task)))), - // the deleted task (doesn't have an instance) - new Counted<>(1, new BulkAssert<>(new Synced<>(mTestAccount, new TasksTable(mAuthority)), - new Composite<>(new TimeData<>(start, due)), - new AllOf<>( - new ReferringTo<>(Tasks.ORIGINAL_INSTANCE_ID, task), - new EqArg<>(Tasks._DELETED, 1)))), - // the former 2nd instance (now first) - new AssertRelated<>(new Synced<>(mTestAccount, instancesTable), Instances.TASK_ID, task, - new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())))); - } - - - /** - * Test if two instances of a task with a DTSTART, DUE and an RRULE are detached correctly. - */ - @Test - public void testRRuleCompleteAll() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - - Duration day = new Duration(1, 1, 0); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TitleData("Test-Task"), - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)))), - // complete the first non-closed instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - )); - queue.flush(); - - Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); - Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); - assertThat(new Seq<>( - // update the second instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - ), - resultsIn(queue, - /* - * We expect five tasks: - * - the original master with updated RRULE, DTSTART and DUE, deleted - * - a completed and deleted overrides for the first and second instance - * - a detached first and second instance - */ - - // the original master - new Assert<>(task, - new Composite<>( - // points to former second instance before being deleted - new TimeData<>(start.addDuration(day), due.addDuration(day)), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=1"), - new CharSequenceRowData<>(Tasks._DELETED, "1"))), - // there is no instance referring to the master because it has been fully completed (and deleted) - new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // the first detached task instance: - new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(localStart, localDue, absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, start.getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // the second detached task instance: - new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(second, second.addDuration(new Duration(1, 0, 3600)), absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, second.getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // two instances total, both completed - new Counted<>(2, - new BulkAssert<>( - syncedInstances, - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), - new AnyOf<>())), - // five tasks in total - new Counted<>(5, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new AnyOf<>())), - // three deleted tasks in total - new Counted<>(3, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new EqArg<>(Tasks._DELETED, 1))))); - } - - - /** - * Test if two instances of a task with a DTSTART, DUE, RRULE and RDATE are detached correctly. - */ - @Test - public void testRRuleRDateCompleteFirstTwo() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - - Duration day = new Duration(1, 1, 0); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TitleData("Test-Task"), - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)), - new RDatesTaskData( - new Seq<>( - DateTime.parse("20180103T123456Z"), - DateTime.parse("20180105T123456Z"), - DateTime.parse("20180107T123456Z"))))), - // update the first non-closed instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - )); - queue.flush(); - - Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); - Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); - assertThat(new Seq<>( - // update the second instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - ), - resultsIn(queue, - /* - * We expect five tasks: - * - the original master with updated RRULE, RDATES, DTSTART and DUE, deleted - * - completed and deleted overrides for the first and second instance - * - a detached first and second instance - */ - - // the first detached task instance: - new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(DateTime.parse("20180103T123456Z"), DateTime.parse("20180103T133456Z"), absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180103T123456Z").getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // the original master has been updated - new Assert<>(task, - new Composite<>( - // points to former third instance before being deleted - new TimeData<>(start.addDuration(day).addDuration(day), due.addDuration(day).addDuration(day)), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;INTERVAL=2;COUNT=1"), - new CharSequenceRowData<>(Tasks._DELETED, "0"), - new RDatesTaskData( - new Seq<>( - DateTime.parse("20180105T123456Z"), - DateTime.parse("20180107T123456Z"))))), - // there is one instance referring to the master - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new CharSequenceRowData<>(Instances.INSTANCE_ORIGINAL_TIME, - String.valueOf(DateTime.parse("20180105T123456Z").getTimestamp())))), - // the second detached task instance: - new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(start, due, absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, start.getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // two completed instances, neither of them referring to the master - new Counted<>(2, - new BulkAssert<>( - syncedInstances, - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), - new AllOf<>( - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // one incomplete instance , the first instance of the new master - new Counted<>(1, - new BulkAssert<>( - syncedInstances, - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_NEEDS_ACTION)), - new AllOf<>( - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0), - new ReferringTo<>(Instances.TASK_ID, task)))), - // five tasks in total (two deleted overrides, two detached ones and the new master) - new Counted<>(5, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new AnyOf<>())), - // two deleted tasks in total (the old overrides) - new Counted<>(2, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new EqArg<>(Tasks._DELETED, 1))))); +public class TaskProviderDetachInstancesTest { + private String mAuthority; + private ContentProviderClient mClient; + private Account mTestAccount = new Account("testname", "testtype"); + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getTargetContext(); + mAuthority = AuthorityUtil.taskAuthority(context); + mClient = context.getContentResolver().acquireContentProviderClient(mAuthority); + + // Assert that tables are empty: + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.flush(); + queue.enqueue( + new Seq>( + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new TaskListsTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + queue.flush(); + } + + @After + public void tearDown() throws Exception { + /* + TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation + https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html + https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator + */ + + // Clear the DB: + BasicOperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new SingletonIterable>(new BulkDelete<>(new LocalTaskListsTable(mAuthority)))); + queue.flush(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mClient.close(); + } else { + mClient.release(); } - - - /** - * Test if two instances of a task with a DTSTART, DUE, RRULE, RDATE and EXDATE are detached correctly. - */ - @Test - public void testRRuleRDateCompleteWithExdates() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - - Duration day = new Duration(1, 1, 0); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TitleData("Test-Task"), - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)), - new RDatesTaskData( - new Seq<>( - DateTime.parse("20180105T123456Z"), - DateTime.parse("20180107T123456Z"))), - new ExDatesTaskData( - new Seq<>( - DateTime.parse("20180104T123456Z"), - DateTime.parse("20180105T123456Z"))))), - // update the first non-closed instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - )); - queue.flush(); - - Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); - Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); - assertThat(new Seq<>( - // update the second instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - ), - resultsIn(queue, - /* - * We expect five tasks: - * - the original master deleted - * - completed and deleted overrides for the first and second instance - * - detached first and second instances - */ - - // the first detached task instance: - new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(DateTime.parse("20180106T123456Z"), DateTime.parse("20180106T133456Z"), absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180106T123456Z").getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // the original master has been deleted - new Counted<>(0, new Assert<>(task, new Composite<>(new EmptyRowData<>()))), - // there is no instance referring to the master - new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // the second detached task instance: - new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(DateTime.parse("20180107T123456Z"), DateTime.parse("20180107T133456Z"), absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180107T123456Z").getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // two completed instances, neither of them referring to the master - new Counted<>(2, - new BulkAssert<>( - syncedInstances, - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), - new AllOf<>( - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // five tasks in total (two deleted overrides, two detached ones and the old master) - new Counted<>(5, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new AnyOf<>())), - // three deleted tasks in total (the old overrides and the old master) - new Counted<>(3, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new EqArg<>(Tasks._DELETED, 1))))); - } - - - /** - * Test if two instances of a task with a DTSTART, DUE, RRULE, RDATE and EXDATE are detached correctly. - */ - @Test - public void testRRuleRDateCompleteOnlyRRuleInstances() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - - Duration day = new Duration(1, 1, 0); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TitleData("Test-Task"), - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)), - new RDatesTaskData( - new Seq<>( - DateTime.parse("20180105T123456Z"), - DateTime.parse("20180107T123456Z"))), - new ExDatesTaskData( - new Seq<>( - DateTime.parse("20180104T123456Z"))))) -/* // update the first non-closed instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))*/ - )); - queue.flush(); - - Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); - Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); - assertThat(new Seq<>( - // update the second instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - ), - resultsIn(queue, - /* - * We expect five tasks: - * - the original master deleted - * - completed and deleted overrides for the first and second instance - * - detached first and second instances - */ - - // the first detached task instance: - new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(DateTime.parse("20180105T123456Z"), DateTime.parse("20180105T133456Z"), absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180105T123456Z").getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // the original master has been updated - new Assert<>(task, - new Composite<>( - new TimeData<>(DateTime.parse("20180106T123456Z"), DateTime.parse("20180106T133456Z")), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;INTERVAL=2;COUNT=1"), - new CharSequenceRowData<>(Tasks._DELETED, "0"), - new RDatesTaskData( - new Seq<>( - DateTime.parse("20180107T123456Z"))))), - // the second detached task instance: - /* new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(DateTime.parse("20180106T123456Z"), DateTime.parse("20180106T133456Z"), absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180106T123456Z").getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),*/ - // one completed instance, not referring to the master - new Counted<>(1, - new BulkAssert<>( - syncedInstances, - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), - new AllOf<>( - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // three tasks in total (one deleted override, one detached one and the master) - new Counted<>(3, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new AnyOf<>())), - // three deleted tasks in total (the old overrides and the old master) - new Counted<>(1, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new EqArg<>(Tasks._DELETED, 1))))); - } - - - /** - * Test if two all-day instances of a task with a DTSTART, DUE, RRULE, RDATE and EXDATE are detached correctly. - */ - @Test - public void testRRuleRDateCompleteWithExdatesAllDay() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 1, 0); - DateTime start = DateTime.parse("20180104"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TitleData("Test-Task"), - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)), - new RDatesTaskData( - new Seq<>( - DateTime.parse("20180105"), - DateTime.parse("20180107"))), - new ExDatesTaskData( - new Seq<>( - DateTime.parse("20180104"), - DateTime.parse("20180105"))))), - // update the first non-closed instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - )); - queue.flush(); - - Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); - Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); - assertThat(new Seq<>( - // update the second instance - new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))) - ), - resultsIn(queue, - /* - * We expect five tasks: - * - the original master deleted - * - completed and deleted overrides for the first and second instance - * - detached first and second instances - */ - - // the first detached task instance: - new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(DateTime.parse("20180106"), DateTime.parse("20180107"), absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180106").getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // the original master has been deleted - new Counted<>(0, new Assert<>(task, new Composite<>(new EmptyRowData<>()))), - // there is no instance referring to the master - new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // the second detached task instance: - new Counted<>(1, new BulkAssert<>(syncedInstances, - new Composite<>( - new InstanceTestData(DateTime.parse("20180107"), DateTime.parse("20180108"), absent(), -1), - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), - new AllOf<>( - new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180107").getTimestamp()), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // two completed instances, neither of them referring to the master - new Counted<>(2, - new BulkAssert<>( - syncedInstances, - new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), - new AllOf<>( - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1), - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // five tasks in total (two deleted overrides, two detached ones and the old master) - new Counted<>(5, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new AnyOf<>())), - // three deleted tasks in total (the old overrides and the old master) - new Counted<>(3, - new BulkAssert<>( - tasksTable, - new TitleData("Test-Task"), - new EqArg<>(Tasks._DELETED, 1))))); - } - + } + + /** + * Test if the first instance of a task with a DTSTART, DUE and an RRULE is correctly detached + * when completed. + */ + @Test + public void testRRule() + throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException { + RowSnapshot taskList = + new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + + Duration day = new Duration(1, 1, 0); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))))); + queue.flush(); + + assertThat( + new Seq<>( + // update the first non-closed instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))), + resultsIn( + queue, + /* + * We expect three tasks: + * - the original master with updated RRULE, DTSTART and DUE + * - a deleted instance + * - a detached task + */ + + // the original master + new Assert<>( + task, + new Composite<>( + new TimeData<>(start.addDuration(day), due.addDuration(day)), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=4"))), + // there is one instance referring to the master (the old second instance, now first) + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // the detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + new Synced<>(mTestAccount, instancesTable), + new Composite<>( + new InstanceTestData(localStart, localDue, absent(), -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task)))), + // the deleted task (doesn't have an instance) + new Counted<>( + 1, + new BulkAssert<>( + new Synced<>(mTestAccount, new TasksTable(mAuthority)), + new Composite<>(new TimeData<>(start, due)), + new AllOf<>( + new ReferringTo<>(Tasks.ORIGINAL_INSTANCE_ID, task), + new EqArg<>(Tasks._DELETED, 1)))), + // the former 2nd instance (now first) + new AssertRelated<>( + new Synced<>(mTestAccount, instancesTable), + Instances.TASK_ID, + task, + new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())))); + } + + /** Test if two instances of a task with a DTSTART, DUE and an RRULE are detached correctly. */ + @Test + public void testRRuleCompleteAll() + throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException { + RowSnapshot taskList = + new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + + Duration day = new Duration(1, 1, 0); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TitleData("Test-Task"), + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)))), + // complete the first non-closed instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))))); + queue.flush(); + + Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); + Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); + assertThat( + new Seq<>( + // update the second instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))), + resultsIn( + queue, + /* + * We expect five tasks: + * - the original master with updated RRULE, DTSTART and DUE, deleted + * - a completed and deleted overrides for the first and second instance + * - a detached first and second instance + */ + + // the original master + new Assert<>( + task, + new Composite<>( + // points to former second instance before being deleted + new TimeData<>(start.addDuration(day), due.addDuration(day)), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=1"), + new CharSequenceRowData<>(Tasks._DELETED, "1"))), + // there is no instance referring to the master because it has been fully completed (and + // deleted) + new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // the first detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new Composite<>( + new InstanceTestData(localStart, localDue, absent(), -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>(Instances.INSTANCE_START, start.getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // the second detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new Composite<>( + new InstanceTestData( + second, second.addDuration(new Duration(1, 0, 3600)), absent(), -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>(Instances.INSTANCE_START, second.getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // two instances total, both completed + new Counted<>( + 2, + new BulkAssert<>( + syncedInstances, + new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), + new AnyOf<>())), + // five tasks in total + new Counted<>( + 5, new BulkAssert<>(tasksTable, new TitleData("Test-Task"), new AnyOf<>())), + // three deleted tasks in total + new Counted<>( + 3, + new BulkAssert<>( + tasksTable, new TitleData("Test-Task"), new EqArg<>(Tasks._DELETED, 1))))); + } + + /** + * Test if two instances of a task with a DTSTART, DUE, RRULE and RDATE are detached correctly. + */ + @Test + public void testRRuleRDateCompleteFirstTwo() + throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException { + RowSnapshot taskList = + new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + + Duration day = new Duration(1, 1, 0); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TitleData("Test-Task"), + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)), + new RDatesTaskData( + new Seq<>( + DateTime.parse("20180103T123456Z"), + DateTime.parse("20180105T123456Z"), + DateTime.parse("20180107T123456Z"))))), + // update the first non-closed instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))))); + queue.flush(); + + Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); + Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); + assertThat( + new Seq<>( + // update the second instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))), + resultsIn( + queue, + /* + * We expect five tasks: + * - the original master with updated RRULE, RDATES, DTSTART and DUE, deleted + * - completed and deleted overrides for the first and second instance + * - a detached first and second instance + */ + + // the first detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new Composite<>( + new InstanceTestData( + DateTime.parse("20180103T123456Z"), + DateTime.parse("20180103T133456Z"), + absent(), + -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>( + Instances.INSTANCE_START, + DateTime.parse("20180103T123456Z").getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // the original master has been updated + new Assert<>( + task, + new Composite<>( + // points to former third instance before being deleted + new TimeData<>( + start.addDuration(day).addDuration(day), + due.addDuration(day).addDuration(day)), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;INTERVAL=2;COUNT=1"), + new CharSequenceRowData<>(Tasks._DELETED, "0"), + new RDatesTaskData( + new Seq<>( + DateTime.parse("20180105T123456Z"), + DateTime.parse("20180107T123456Z"))))), + // there is one instance referring to the master + new Counted<>( + 1, + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new CharSequenceRowData<>( + Instances.INSTANCE_ORIGINAL_TIME, + String.valueOf(DateTime.parse("20180105T123456Z").getTimestamp())))), + // the second detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new Composite<>( + new InstanceTestData(start, due, absent(), -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>(Instances.INSTANCE_START, start.getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // two completed instances, neither of them referring to the master + new Counted<>( + 2, + new BulkAssert<>( + syncedInstances, + new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), + new AllOf<>( + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // one incomplete instance , the first instance of the new master + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_NEEDS_ACTION)), + new AllOf<>( + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0), + new ReferringTo<>(Instances.TASK_ID, task)))), + // five tasks in total (two deleted overrides, two detached ones and the new master) + new Counted<>( + 5, new BulkAssert<>(tasksTable, new TitleData("Test-Task"), new AnyOf<>())), + // two deleted tasks in total (the old overrides) + new Counted<>( + 2, + new BulkAssert<>( + tasksTable, new TitleData("Test-Task"), new EqArg<>(Tasks._DELETED, 1))))); + } + + /** + * Test if two instances of a task with a DTSTART, DUE, RRULE, RDATE and EXDATE are detached + * correctly. + */ + @Test + public void testRRuleRDateCompleteWithExdates() + throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException { + RowSnapshot taskList = + new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + + Duration day = new Duration(1, 1, 0); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TitleData("Test-Task"), + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)), + new RDatesTaskData( + new Seq<>( + DateTime.parse("20180105T123456Z"), + DateTime.parse("20180107T123456Z"))), + new ExDatesTaskData( + new Seq<>( + DateTime.parse("20180104T123456Z"), + DateTime.parse("20180105T123456Z"))))), + // update the first non-closed instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))))); + queue.flush(); + + Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); + Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); + assertThat( + new Seq<>( + // update the second instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))), + resultsIn( + queue, + /* + * We expect five tasks: + * - the original master deleted + * - completed and deleted overrides for the first and second instance + * - detached first and second instances + */ + + // the first detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new Composite<>( + new InstanceTestData( + DateTime.parse("20180106T123456Z"), + DateTime.parse("20180106T133456Z"), + absent(), + -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>( + Instances.INSTANCE_START, + DateTime.parse("20180106T123456Z").getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // the original master has been deleted + new Counted<>(0, new Assert<>(task, new Composite<>(new EmptyRowData<>()))), + // there is no instance referring to the master + new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // the second detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new Composite<>( + new InstanceTestData( + DateTime.parse("20180107T123456Z"), + DateTime.parse("20180107T133456Z"), + absent(), + -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>( + Instances.INSTANCE_START, + DateTime.parse("20180107T123456Z").getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // two completed instances, neither of them referring to the master + new Counted<>( + 2, + new BulkAssert<>( + syncedInstances, + new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), + new AllOf<>( + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // five tasks in total (two deleted overrides, two detached ones and the old master) + new Counted<>( + 5, new BulkAssert<>(tasksTable, new TitleData("Test-Task"), new AnyOf<>())), + // three deleted tasks in total (the old overrides and the old master) + new Counted<>( + 3, + new BulkAssert<>( + tasksTable, new TitleData("Test-Task"), new EqArg<>(Tasks._DELETED, 1))))); + } + + /** + * Test if two instances of a task with a DTSTART, DUE, RRULE, RDATE and EXDATE are detached + * correctly. + */ + @Test + public void testRRuleRDateCompleteOnlyRRuleInstances() + throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException { + RowSnapshot taskList = + new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + + Duration day = new Duration(1, 1, 0); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TitleData("Test-Task"), + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)), + new RDatesTaskData( + new Seq<>( + DateTime.parse("20180105T123456Z"), + DateTime.parse("20180107T123456Z"))), + new ExDatesTaskData(new Seq<>(DateTime.parse("20180104T123456Z"))))) + /* // update the first non-closed instance + new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))*/ + )); + queue.flush(); + + Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); + Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); + assertThat( + new Seq<>( + // update the second instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))), + resultsIn( + queue, + /* + * We expect five tasks: + * - the original master deleted + * - completed and deleted overrides for the first and second instance + * - detached first and second instances + */ + + // the first detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new Composite<>( + new InstanceTestData( + DateTime.parse("20180105T123456Z"), + DateTime.parse("20180105T133456Z"), + absent(), + -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>( + Instances.INSTANCE_START, + DateTime.parse("20180105T123456Z").getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // the original master has been updated + new Assert<>( + task, + new Composite<>( + new TimeData<>( + DateTime.parse("20180106T123456Z"), DateTime.parse("20180106T133456Z")), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;INTERVAL=2;COUNT=1"), + new CharSequenceRowData<>(Tasks._DELETED, "0"), + new RDatesTaskData(new Seq<>(DateTime.parse("20180107T123456Z"))))), + // the second detached task instance: + /* new Counted<>(1, new BulkAssert<>(syncedInstances, + new Composite<>( + new InstanceTestData(DateTime.parse("20180106T123456Z"), DateTime.parse("20180106T133456Z"), absent(), -1), + new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180106T123456Z").getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),*/ + // one completed instance, not referring to the master + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), + new AllOf<>( + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // three tasks in total (one deleted override, one detached one and the master) + new Counted<>( + 3, new BulkAssert<>(tasksTable, new TitleData("Test-Task"), new AnyOf<>())), + // three deleted tasks in total (the old overrides and the old master) + new Counted<>( + 1, + new BulkAssert<>( + tasksTable, new TitleData("Test-Task"), new EqArg<>(Tasks._DELETED, 1))))); + } + + /** + * Test if two all-day instances of a task with a DTSTART, DUE, RRULE, RDATE and EXDATE are + * detached correctly. + */ + @Test + public void testRRuleRDateCompleteWithExdatesAllDay() + throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException { + RowSnapshot taskList = + new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority))); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 1, 0); + DateTime start = DateTime.parse("20180104"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TitleData("Test-Task"), + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)), + new RDatesTaskData( + new Seq<>(DateTime.parse("20180105"), DateTime.parse("20180107"))), + new ExDatesTaskData( + new Seq<>(DateTime.parse("20180104"), DateTime.parse("20180105"))))), + // update the first non-closed instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0))))); + queue.flush(); + + Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority)); + Synced syncedInstances = new Synced<>(mTestAccount, instancesTable); + assertThat( + new Seq<>( + // update the second instance + new BulkUpdate<>( + instancesTable, + new StatusData<>(Tasks.STATUS_COMPLETED), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))), + resultsIn( + queue, + /* + * We expect five tasks: + * - the original master deleted + * - completed and deleted overrides for the first and second instance + * - detached first and second instances + */ + + // the first detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new Composite<>( + new InstanceTestData( + DateTime.parse("20180106"), DateTime.parse("20180107"), absent(), -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>( + Instances.INSTANCE_START, DateTime.parse("20180106").getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // the original master has been deleted + new Counted<>(0, new Assert<>(task, new Composite<>(new EmptyRowData<>()))), + // there is no instance referring to the master + new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // the second detached task instance: + new Counted<>( + 1, + new BulkAssert<>( + syncedInstances, + new Composite<>( + new InstanceTestData( + DateTime.parse("20180107"), DateTime.parse("20180108"), absent(), -1), + new CharSequenceRowData<>( + Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))), + new AllOf<>( + new EqArg<>( + Instances.INSTANCE_START, DateTime.parse("20180107").getTimestamp()), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // two completed instances, neither of them referring to the master + new Counted<>( + 2, + new BulkAssert<>( + syncedInstances, + new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), + new AllOf<>( + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1), + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // five tasks in total (two deleted overrides, two detached ones and the old master) + new Counted<>( + 5, new BulkAssert<>(tasksTable, new TitleData("Test-Task"), new AnyOf<>())), + // three deleted tasks in total (the old overrides and the old master) + new Counted<>( + 3, + new BulkAssert<>( + tasksTable, new TitleData("Test-Task"), new EqArg<>(Tasks._DELETED, 1))))); + } } diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderInstancesTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderInstancesTest.java index af12ef36..6f039b40 100644 --- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderInstancesTest.java +++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderInstancesTest.java @@ -16,6 +16,9 @@ package org.dmfs.provider.tasks; +import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; +import static org.junit.Assert.assertThat; + import android.content.ContentProviderClient; import android.content.Context; import android.content.OperationApplicationException; @@ -23,7 +26,6 @@ import android.os.Build; import android.os.RemoteException; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; - import org.dmfs.android.contentpal.Operation; import org.dmfs.android.contentpal.OperationsQueue; import org.dmfs.android.contentpal.RowSnapshot; @@ -62,10 +64,6 @@ import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; -import static org.junit.Assert.assertThat; - - /** * Tests for {@link TaskProvider}. These tests check various operations on the instances table. * @@ -73,290 +71,310 @@ import static org.junit.Assert.assertThat; * @author Marten Gajda */ @RunWith(AndroidJUnit4.class) -public class TaskProviderInstancesTest -{ - private String mAuthority; - private Context mContext; - private ContentProviderClient mClient; - - - @Before - public void setUp() throws Exception - { - mContext = InstrumentationRegistry.getTargetContext(); - mAuthority = AuthorityUtil.taskAuthority(mContext); - mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); - - // Assert that tables are empty: - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new TaskListsTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)))); - queue.flush(); - } - - - @After - public void tearDown() throws Exception - { - /* - TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation - https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html - https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator - */ - - // Clear the DB: - BasicOperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new SingletonIterable>(new BulkDelete<>(new LocalTaskListsTable(mAuthority)))); - queue.flush(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - { - mClient.close(); - } - else - { - mClient.release(); - } - } - - - /** - * Create a single instance. - */ - @Ignore("Inserting instances is currently unsupported.") - @Test - public void testInsertSingleInstance() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot instance = new VirtualRowSnapshot<>(new InstanceTable(mAuthority)); - - assertThat(new Seq<>( - // create a local list - new Put<>(taskList, new NameData("list1")), - // insert a new task straight into the instances table - new Put<>(instance, new Referring<>(Tasks.LIST_ID, taskList, new CharSequenceRowData<>(Tasks.TITLE, "task1"))) - - ), resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - // the task list contains exactly one task with the title "task1" - new Counted<>(1, new BulkAssert<>(new TasksTable(mAuthority))), - new AssertRelated<>(new TasksTable(mAuthority), Tasks.LIST_ID, taskList, - new Composite<>( - new CharSequenceRowData<>(Tasks.TITLE, "task1"))), - // the instances table contains one instance - new Counted<>(1, new BulkAssert<>(new InstanceTable(mAuthority))), - // the instances table contains the given instance - new Assert<>(instance, new Composite<>( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TITLE, "task1"))))); - } - - - /** - * Create a single instance and update it. - */ - @Test - public void testUpdateSingleInstance() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); - - assertThat(new Seq<>( - // create a local list - new Put<>(taskList, new NameData("list1")), - // insert a new task straight into the instances table - new Put<>(task, new Referring<>(Tasks.LIST_ID, taskList, new CharSequenceRowData<>(Tasks.TITLE, "task1"))), - // update the instance - new BulkUpdate<>( - new InstanceTable(mAuthority), - new CharSequenceRowData<>(Tasks.TITLE, "Updated"), - new ReferringTo<>(Instances.TASK_ID, task)) - ), resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - // the task list contains exactly one task with the title "Updated" - new Counted<>(1, new BulkAssert<>(new TasksTable(mAuthority))), - new AssertRelated<>(new TasksTable(mAuthority), Tasks.LIST_ID, taskList, - new Composite<>( - new CharSequenceRowData<>(Tasks.TITLE, "Updated"))), - // the instances table contains one instance - new Counted<>(1, new BulkAssert<>(new InstanceTable(mAuthority))), - // the instances table contains the given instance - new Counted<>(1, new BulkAssert<>( - new InstanceTable(mAuthority), - new Composite<>( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TITLE, "Updated")), - new ReferringTo<>(Instances.TASK_ID, task))))); - } - - - /** - * Create a single instance and complete it. - */ - @Test - public void testCompleteSingleInstance() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); - - assertThat(new Seq<>( - // create a local list - new Put<>(taskList, new NameData("list1")), - // insert a new task straight into the instances table - new Put<>(task, new Referring<>(Tasks.LIST_ID, taskList, new CharSequenceRowData<>(Tasks.TITLE, "task1"))), - // update the instance status - new BulkUpdate<>( - new InstanceTable(mAuthority), - (transactionContext, builder) -> builder.withValue(Tasks.STATUS, Tasks.STATUS_COMPLETED), - new ReferringTo<>(Instances.TASK_ID, task)) - ), resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - // the task list contains exactly one task with the title "Updated" - new Counted<>(1, new BulkAssert<>(new TasksTable(mAuthority))), - new AssertRelated<>(new TasksTable(mAuthority), Tasks.LIST_ID, taskList, - new Composite<>( - new CharSequenceRowData<>(Tasks.TITLE, "task1"))), - // the instances table contains one instance - new Counted<>(1, new BulkAssert<>(new InstanceTable(mAuthority))), - // the instances table contains the given instance - new Counted<>(1, new BulkAssert<>( - new InstanceTable(mAuthority), - new Composite<>( - new InstanceTestData(-1), - new CharSequenceRowData<>(Tasks.TITLE, "task1")), - new ReferringTo<>(Instances.TASK_ID, task))))); - } - - - /** - * Create a single instance and delete it. - */ - @Test - public void testDeleteSingleInstance() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); - - assertThat(new Seq<>( - // create a local list - new Put<>(taskList, new NameData("list1")), - // insert a new task - new Put<>(task, new Referring<>(Tasks.LIST_ID, taskList, new CharSequenceRowData<>(Tasks.TITLE, "task1"))), - // delete the instance - new BulkDelete<>(new InstanceTable(mAuthority), new ReferringTo<>(Instances.TASK_ID, task)) - ), resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - // the list does not contain a single task - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)))); - +public class TaskProviderInstancesTest { + private String mAuthority; + private Context mContext; + private ContentProviderClient mClient; + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getTargetContext(); + mAuthority = AuthorityUtil.taskAuthority(mContext); + mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); + + // Assert that tables are empty: + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new TaskListsTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + queue.flush(); + } + + @After + public void tearDown() throws Exception { + /* + TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation + https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html + https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator + */ + + // Clear the DB: + BasicOperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new SingletonIterable>(new BulkDelete<>(new LocalTaskListsTable(mAuthority)))); + queue.flush(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mClient.close(); + } else { + mClient.release(); } - - - /** - * Create a single instance and insert an override for exactly the same instance. - */ - @Test(expected = IllegalArgumentException.class) - public void testInsertSingleInstanceTwice() throws RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); - RowSnapshot instance = new VirtualRowSnapshot<>(new InstanceTable(mAuthority)); - - DateTime dateTime = DateTime.parse("20180110T224500Z"); - String dtstart = Long.toString(dateTime.getTimestamp()); - - new BaseTransaction().with(new Seq<>( - // create a local list - new Put<>(taskList, new NameData("list1")), - // insert a new task into the tasks table (we insert a task to get a RowReference to the new row) - new Put<>(task, - new Composite<>( - new Referring<>(Tasks.LIST_ID, taskList), - new TimeData<>(dateTime), - new TitleData("task1"))), - new Put<>(instance, - new Composite<>( - new Referring<>(Tasks.LIST_ID, taskList), - new CharSequenceRowData<>(Tasks.DTSTART, "1234"), - // insert an instance which would override the original instance, which already exists - new Referring<>(Tasks.ORIGINAL_INSTANCE_ID, task), - new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_TIME, dtstart), - new CharSequenceRowData<>(Tasks.TZ, "UTC"), - new CharSequenceRowData<>(Tasks.TITLE, "task1"))) - - )).commit(mClient); - } - - - /** - * Create a single instance and insert an override for a new instance, turning the event into a recurring event. - */ - @Ignore("Inserting instances is currently not supported.") - @Test - public void testInsertSingleInstanceAddAnother() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); - RowSnapshot instance = new VirtualRowSnapshot<>(new InstanceTable(mAuthority)); - - DateTime dateTimeOriginal = DateTime.parse("20180110T224500Z"); - // override is one day later - DateTime dateTimeOverride = DateTime.parse("20180111T224500Z"); - String startOverride = Long.toString(dateTimeOverride.getTimestamp()); - - assertThat(new Seq<>( + } + + /** Create a single instance. */ + @Ignore("Inserting instances is currently unsupported.") + @Test + public void testInsertSingleInstance() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot instance = new VirtualRowSnapshot<>(new InstanceTable(mAuthority)); + + assertThat( + new Seq<>( + // create a local list + new Put<>(taskList, new NameData("list1")), + // insert a new task straight into the instances table + new Put<>( + instance, + new Referring<>( + Tasks.LIST_ID, taskList, new CharSequenceRowData<>(Tasks.TITLE, "task1")))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + // the task list contains exactly one task with the title "task1" + new Counted<>(1, new BulkAssert<>(new TasksTable(mAuthority))), + new AssertRelated<>( + new TasksTable(mAuthority), + Tasks.LIST_ID, + taskList, + new Composite<>(new CharSequenceRowData<>(Tasks.TITLE, "task1"))), + // the instances table contains one instance + new Counted<>(1, new BulkAssert<>(new InstanceTable(mAuthority))), + // the instances table contains the given instance + new Assert<>( + instance, + new Composite<>( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TITLE, "task1"))))); + } + + /** Create a single instance and update it. */ + @Test + public void testUpdateSingleInstance() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); + + assertThat( + new Seq<>( + // create a local list + new Put<>(taskList, new NameData("list1")), + // insert a new task straight into the instances table + new Put<>( + task, + new Referring<>( + Tasks.LIST_ID, taskList, new CharSequenceRowData<>(Tasks.TITLE, "task1"))), + // update the instance + new BulkUpdate<>( + new InstanceTable(mAuthority), + new CharSequenceRowData<>(Tasks.TITLE, "Updated"), + new ReferringTo<>(Instances.TASK_ID, task))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + // the task list contains exactly one task with the title "Updated" + new Counted<>(1, new BulkAssert<>(new TasksTable(mAuthority))), + new AssertRelated<>( + new TasksTable(mAuthority), + Tasks.LIST_ID, + taskList, + new Composite<>(new CharSequenceRowData<>(Tasks.TITLE, "Updated"))), + // the instances table contains one instance + new Counted<>(1, new BulkAssert<>(new InstanceTable(mAuthority))), + // the instances table contains the given instance + new Counted<>( + 1, + new BulkAssert<>( + new InstanceTable(mAuthority), + new Composite<>( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TITLE, "Updated")), + new ReferringTo<>(Instances.TASK_ID, task))))); + } + + /** Create a single instance and complete it. */ + @Test + public void testCompleteSingleInstance() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); + + assertThat( + new Seq<>( + // create a local list + new Put<>(taskList, new NameData("list1")), + // insert a new task straight into the instances table + new Put<>( + task, + new Referring<>( + Tasks.LIST_ID, taskList, new CharSequenceRowData<>(Tasks.TITLE, "task1"))), + // update the instance status + new BulkUpdate<>( + new InstanceTable(mAuthority), + (transactionContext, builder) -> + builder.withValue(Tasks.STATUS, Tasks.STATUS_COMPLETED), + new ReferringTo<>(Instances.TASK_ID, task))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + // the task list contains exactly one task with the title "Updated" + new Counted<>(1, new BulkAssert<>(new TasksTable(mAuthority))), + new AssertRelated<>( + new TasksTable(mAuthority), + Tasks.LIST_ID, + taskList, + new Composite<>(new CharSequenceRowData<>(Tasks.TITLE, "task1"))), + // the instances table contains one instance + new Counted<>(1, new BulkAssert<>(new InstanceTable(mAuthority))), + // the instances table contains the given instance + new Counted<>( + 1, + new BulkAssert<>( + new InstanceTable(mAuthority), + new Composite<>( + new InstanceTestData(-1), new CharSequenceRowData<>(Tasks.TITLE, "task1")), + new ReferringTo<>(Instances.TASK_ID, task))))); + } + + /** Create a single instance and delete it. */ + @Test + public void testDeleteSingleInstance() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); + + assertThat( + new Seq<>( + // create a local list + new Put<>(taskList, new NameData("list1")), + // insert a new task + new Put<>( + task, + new Referring<>( + Tasks.LIST_ID, taskList, new CharSequenceRowData<>(Tasks.TITLE, "task1"))), + // delete the instance + new BulkDelete<>( + new InstanceTable(mAuthority), new ReferringTo<>(Instances.TASK_ID, task))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + // the list does not contain a single task + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + } + + /** Create a single instance and insert an override for exactly the same instance. */ + @Test(expected = IllegalArgumentException.class) + public void testInsertSingleInstanceTwice() + throws RemoteException, OperationApplicationException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); + RowSnapshot instance = new VirtualRowSnapshot<>(new InstanceTable(mAuthority)); + + DateTime dateTime = DateTime.parse("20180110T224500Z"); + String dtstart = Long.toString(dateTime.getTimestamp()); + + new BaseTransaction() + .with( + new Seq<>( // create a local list new Put<>(taskList, new NameData("list1")), - // insert a new task into the tasks table (we insert a task to get a RowReference to the new row) - new Put<>(task, - new Composite( - new Referring<>(Tasks.LIST_ID, taskList), - new TimeData(dateTimeOriginal), - new TitleData("task1"))), - new Put<>(instance, - new Composite( - new Referring<>(Tasks.LIST_ID, taskList), - new CharSequenceRowData<>(Tasks.DTSTART, "1234"), - new CharSequenceRowData<>(Tasks.IS_ALLDAY, "0"), - // insert an override instance - new Referring<>(Tasks.ORIGINAL_INSTANCE_ID, task), - new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_TIME, startOverride), - new CharSequenceRowData<>(Tasks.TZ, "UTC"), - new CharSequenceRowData<>(Tasks.TITLE, "task override"))) - - ), resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - // the task list contains exactly two tasks - new Counted<>(2, new BulkAssert<>(new TasksTable(mAuthority))), - // check that the original task has RDATES now, one for the original start and one for the new override - new Assert<>(task, - new Composite( - new Referring<>(Tasks.LIST_ID, taskList), - new TimeData(dateTimeOriginal), - new CharSequenceRowData<>(Tasks.RDATE, "20180110T224500Z,20180111T224500Z"), - new TitleData("task1"))), - // and check there is a task for the override - new AssertRelated<>(new TasksTable(mAuthority), Tasks.ORIGINAL_INSTANCE_ID, task, - new Composite( - new Referring<>(Tasks.LIST_ID, taskList), - new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_TIME, startOverride), - new TimeData(new DateTime(1234)), - new TitleData("task override"))) - // TODO: enable tests below once recurrence has been implemented -// // the instances table contains two instances as well -// new Counted<>(2, new BulkAssert<>(new InstanceTable(mAuthority))), -// // one instance is related to the task -// new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, task, -// new Composite<>( -// new InstanceTestData(new Present<>(dateTimeOriginal), new Absent<>(), new Present<>(dateTimeOriginal), 0), -// new CharSequenceRowData<>(Tasks.TITLE, "task1"))), -// // the other instance is for the override -// new Assert<>(instance, new Composite<>( -// new InstanceTestData(new Present<>(new DateTime(1234)), new Absent<>(), new Present<>(dateTimeOverride), 0), -// new CharSequenceRowData<>(Tasks.TITLE, "task override"))) - )); - } + // insert a new task into the tasks table (we insert a task to get a RowReference to + // the new row) + new Put<>( + task, + new Composite<>( + new Referring<>(Tasks.LIST_ID, taskList), + new TimeData<>(dateTime), + new TitleData("task1"))), + new Put<>( + instance, + new Composite<>( + new Referring<>(Tasks.LIST_ID, taskList), + new CharSequenceRowData<>(Tasks.DTSTART, "1234"), + // insert an instance which would override the original instance, which + // already exists + new Referring<>(Tasks.ORIGINAL_INSTANCE_ID, task), + new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_TIME, dtstart), + new CharSequenceRowData<>(Tasks.TZ, "UTC"), + new CharSequenceRowData<>(Tasks.TITLE, "task1"))))) + .commit(mClient); + } + + /** + * Create a single instance and insert an override for a new instance, turning the event into a + * recurring event. + */ + @Ignore("Inserting instances is currently not supported.") + @Test + public void testInsertSingleInstanceAddAnother() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); + RowSnapshot instance = new VirtualRowSnapshot<>(new InstanceTable(mAuthority)); + + DateTime dateTimeOriginal = DateTime.parse("20180110T224500Z"); + // override is one day later + DateTime dateTimeOverride = DateTime.parse("20180111T224500Z"); + String startOverride = Long.toString(dateTimeOverride.getTimestamp()); + + assertThat( + new Seq<>( + // create a local list + new Put<>(taskList, new NameData("list1")), + // insert a new task into the tasks table (we insert a task to get a RowReference to the + // new row) + new Put<>( + task, + new Composite( + new Referring<>(Tasks.LIST_ID, taskList), + new TimeData(dateTimeOriginal), + new TitleData("task1"))), + new Put<>( + instance, + new Composite( + new Referring<>(Tasks.LIST_ID, taskList), + new CharSequenceRowData<>(Tasks.DTSTART, "1234"), + new CharSequenceRowData<>(Tasks.IS_ALLDAY, "0"), + // insert an override instance + new Referring<>(Tasks.ORIGINAL_INSTANCE_ID, task), + new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_TIME, startOverride), + new CharSequenceRowData<>(Tasks.TZ, "UTC"), + new CharSequenceRowData<>(Tasks.TITLE, "task override")))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + // the task list contains exactly two tasks + new Counted<>(2, new BulkAssert<>(new TasksTable(mAuthority))), + // check that the original task has RDATES now, one for the original start and one for + // the new override + new Assert<>( + task, + new Composite( + new Referring<>(Tasks.LIST_ID, taskList), + new TimeData(dateTimeOriginal), + new CharSequenceRowData<>(Tasks.RDATE, "20180110T224500Z,20180111T224500Z"), + new TitleData("task1"))), + // and check there is a task for the override + new AssertRelated<>( + new TasksTable(mAuthority), + Tasks.ORIGINAL_INSTANCE_ID, + task, + new Composite( + new Referring<>(Tasks.LIST_ID, taskList), + new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_TIME, startOverride), + new TimeData(new DateTime(1234)), + new TitleData("task override"))) + // TODO: enable tests below once recurrence has been implemented + // // the instances table contains two instances as well + // new Counted<>(2, new BulkAssert<>(new InstanceTable(mAuthority))), + // // one instance is related to the task + // new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, + // task, + // new Composite<>( + // new InstanceTestData(new Present<>(dateTimeOriginal), + // new Absent<>(), new Present<>(dateTimeOriginal), 0), + // new CharSequenceRowData<>(Tasks.TITLE, "task1"))), + // // the other instance is for the override + // new Assert<>(instance, new Composite<>( + // new InstanceTestData(new Present<>(new DateTime(1234)), new + // Absent<>(), new Present<>(dateTimeOverride), 0), + // new CharSequenceRowData<>(Tasks.TITLE, "task override"))) + )); + } } diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderObserverTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderObserverTest.java index 4784e53f..5fc8447e 100644 --- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderObserverTest.java +++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderObserverTest.java @@ -16,13 +16,26 @@ package org.dmfs.provider.tasks; +import static org.dmfs.android.contentpal.testing.android.uri.UriMatcher.hasParam; +import static org.dmfs.provider.tasks.matchers.NotifiesMatcher.notifies; +import static org.dmfs.provider.tasks.matchers.UriMatcher.authority; +import static org.dmfs.provider.tasks.matchers.UriMatcher.path; +import static org.dmfs.provider.tasks.matchers.UriMatcher.scheme; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; + import android.accounts.Account; import android.content.ContentProviderClient; import android.content.Context; import android.content.OperationApplicationException; import android.os.Build; import android.os.RemoteException; - +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import org.dmfs.android.contentpal.Operation; import org.dmfs.android.contentpal.OperationsQueue; import org.dmfs.android.contentpal.RowSnapshot; @@ -48,192 +61,130 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import static org.dmfs.android.contentpal.testing.android.uri.UriMatcher.hasParam; -import static org.dmfs.provider.tasks.matchers.NotifiesMatcher.notifies; -import static org.dmfs.provider.tasks.matchers.UriMatcher.authority; -import static org.dmfs.provider.tasks.matchers.UriMatcher.path; -import static org.dmfs.provider.tasks.matchers.UriMatcher.scheme; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.emptyIterable; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.startsWith; - - /** * Tests for {@link TaskProvider}. * * @author Marten Gajda */ @RunWith(AndroidJUnit4.class) -public class TaskProviderObserverTest -{ - private String mAuthority; - private Context mContext; - private ContentProviderClient mClient; - private final Account testAccount = new Account("foo", "bar"); - - - @Before - public void setUp() throws Exception - { - mContext = InstrumentationRegistry.getTargetContext(); - mAuthority = AuthorityUtil.taskAuthority(mContext); - mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); - - // Assert that tables are empty: - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new TaskListsTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)))); - queue.flush(); - } - - - @After - public void tearDown() throws Exception - { - /* - TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation - https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html - https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator - */ - - // Clear the DB: - BasicOperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new BulkDelete<>(new LocalTaskListsTable(mAuthority)), - new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))))); - queue.flush(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - { - mClient.close(); - } - else - { - mClient.release(); - } - } - - - /** - * Test notifications for creating one task list and task. - */ - @Test - public void testSingleInsert() throws RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - OperationsQueue queue = new BasicOperationsQueue(mClient); - - assertThat(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new TitleData("task1"))), - notifies( - TaskContract.getContentUri(mAuthority), - queue, - containsInAnyOrder( - allOf( - scheme("content"), - authority(mAuthority), - path(is("/tasks")) - ), - allOf( - scheme("content"), - authority(mAuthority), - path(startsWith("/tasks/")) - ), - allOf( - scheme("content"), - authority(mAuthority), - path(startsWith("/instances")) - ), - allOf( - scheme("content"), - authority(mAuthority), - path(startsWith("/tasklists/")) - ), - allOf( - scheme("content"), - authority(mAuthority), - path(is("/tasklists")), - hasParam(TaskContract.CALLER_IS_SYNCADAPTER, "true"), - hasParam(TaskContract.ACCOUNT_NAME, TaskContract.LOCAL_ACCOUNT_NAME), - hasParam(TaskContract.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE) - )))); +public class TaskProviderObserverTest { + private String mAuthority; + private Context mContext; + private ContentProviderClient mClient; + private final Account testAccount = new Account("foo", "bar"); + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getTargetContext(); + mAuthority = AuthorityUtil.taskAuthority(mContext); + mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); + + // Assert that tables are empty: + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new TaskListsTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + queue.flush(); + } + + @After + public void tearDown() throws Exception { + /* + TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation + https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html + https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator + */ + + // Clear the DB: + BasicOperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new BulkDelete<>(new LocalTaskListsTable(mAuthority)), + new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))))); + queue.flush(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mClient.close(); + } else { + mClient.release(); } - - - /** - * Update a task and check the notifications. - */ - @Test - public void testSingleUpdate() throws RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - OperationsQueue queue = new BasicOperationsQueue(mClient); - - queue.enqueue( - new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new TitleData("task1")))); - queue.flush(); - - assertThat(new Seq<>( - new Put<>(task, new TitleData("task1b"))), - notifies( - TaskContract.getContentUri(mAuthority), - queue, - // taskprovider should notity the tasks URI iself, the task diretory and the instances directory - containsInAnyOrder( - allOf( - scheme("content"), - authority(mAuthority), - path(is("/tasks")) - ), - allOf( - scheme("content"), - authority(mAuthority), - path(startsWith("/tasks/")) - ), - allOf( - scheme("content"), - authority(mAuthority), - path(is("/instances")) - )))); - } - - - /** - * Test that an update that doesn't change anything doesn't trigger a notification. - */ - @Test - public void testNoOpUpdate() throws RemoteException, OperationApplicationException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - OperationsQueue queue = new BasicOperationsQueue(mClient); - - queue.enqueue( - new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new TitleData("task1")))); - queue.flush(); - - assertThat(new Seq<>( - new Put<>(task, new TitleData("task1"))), - notifies( - TaskContract.getContentUri(mAuthority), - queue, - // there should no notification - emptyIterable())); - } - + } + + /** Test notifications for creating one task list and task. */ + @Test + public void testSingleInsert() throws RemoteException, OperationApplicationException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + OperationsQueue queue = new BasicOperationsQueue(mClient); + + assertThat( + new Seq<>( + new Put<>(taskList, new NameData("list1")), new Put<>(task, new TitleData("task1"))), + notifies( + TaskContract.getContentUri(mAuthority), + queue, + containsInAnyOrder( + allOf(scheme("content"), authority(mAuthority), path(is("/tasks"))), + allOf(scheme("content"), authority(mAuthority), path(startsWith("/tasks/"))), + allOf(scheme("content"), authority(mAuthority), path(startsWith("/instances"))), + allOf(scheme("content"), authority(mAuthority), path(startsWith("/tasklists/"))), + allOf( + scheme("content"), + authority(mAuthority), + path(is("/tasklists")), + hasParam(TaskContract.CALLER_IS_SYNCADAPTER, "true"), + hasParam(TaskContract.ACCOUNT_NAME, TaskContract.LOCAL_ACCOUNT_NAME), + hasParam(TaskContract.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE))))); + } + + /** Update a task and check the notifications. */ + @Test + public void testSingleUpdate() throws RemoteException, OperationApplicationException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + OperationsQueue queue = new BasicOperationsQueue(mClient); + + queue.enqueue( + new Seq<>( + new Put<>(taskList, new NameData("list1")), new Put<>(task, new TitleData("task1")))); + queue.flush(); + + assertThat( + new Seq<>(new Put<>(task, new TitleData("task1b"))), + notifies( + TaskContract.getContentUri(mAuthority), + queue, + // taskprovider should notity the tasks URI iself, the task diretory and the instances + // directory + containsInAnyOrder( + allOf(scheme("content"), authority(mAuthority), path(is("/tasks"))), + allOf(scheme("content"), authority(mAuthority), path(startsWith("/tasks/"))), + allOf(scheme("content"), authority(mAuthority), path(is("/instances")))))); + } + + /** Test that an update that doesn't change anything doesn't trigger a notification. */ + @Test + public void testNoOpUpdate() throws RemoteException, OperationApplicationException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + OperationsQueue queue = new BasicOperationsQueue(mClient); + + queue.enqueue( + new Seq<>( + new Put<>(taskList, new NameData("list1")), new Put<>(task, new TitleData("task1")))); + queue.flush(); + + assertThat( + new Seq<>(new Put<>(task, new TitleData("task1"))), + notifies( + TaskContract.getContentUri(mAuthority), + queue, + // there should no notification + emptyIterable())); + } } diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRecurrenceTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRecurrenceTest.java index 727bf532..15fcc3df 100644 --- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRecurrenceTest.java +++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRecurrenceTest.java @@ -16,10 +16,16 @@ package org.dmfs.provider.tasks; +import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; +import static org.dmfs.optional.Absent.absent; +import static org.junit.Assert.assertThat; + import android.content.ContentProviderClient; import android.content.Context; import android.os.Build; - +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; +import java.util.TimeZone; import org.dmfs.android.contentpal.Operation; import org.dmfs.android.contentpal.OperationsQueue; import org.dmfs.android.contentpal.RowSnapshot; @@ -74,113 +80,103 @@ import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.TimeZone; - -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; -import static org.dmfs.optional.Absent.absent; -import static org.junit.Assert.assertThat; - - /** * Recurrence Tests for {@link TaskProvider}. * * @author Marten Gajda */ @RunWith(AndroidJUnit4.class) -public class TaskProviderRecurrenceTest -{ - private String mAuthority; - private Context mContext; - private ContentProviderClient mClient; - - - @Before - public void setUp() throws Exception - { - mContext = InstrumentationRegistry.getTargetContext(); - mAuthority = AuthorityUtil.taskAuthority(mContext); - mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); - - // Assert that tables are empty: - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new TaskListsTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)))); - queue.flush(); - } - - - @After - public void tearDown() throws Exception - { - /* - TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation - https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html - https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator - */ - - // Clear the DB: - BasicOperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new SingletonIterable>(new BulkDelete<>(new LocalTaskListsTable(mAuthority)))); - queue.flush(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - { - mClient.close(); - } - else - { - mClient.release(); - } +public class TaskProviderRecurrenceTest { + private String mAuthority; + private Context mContext; + private ContentProviderClient mClient; + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getTargetContext(); + mAuthority = AuthorityUtil.taskAuthority(mContext); + mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); + + // Assert that tables are empty: + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new TaskListsTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + queue.flush(); + } + + @After + public void tearDown() throws Exception { + /* + TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation + https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html + https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator + */ + + // Clear the DB: + BasicOperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new SingletonIterable>(new BulkDelete<>(new LocalTaskListsTable(mAuthority)))); + queue.flush(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mClient.close(); + } else { + mClient.release(); } - - - /** - * Test if instances of a task with a DTSTART, DUE and an RRULE. - */ - @Test - public void testRRule() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - - Duration day = new Duration(1, 1, 0); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), -// new Counted<>(5, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*, + } + + /** Test if instances of a task with a DTSTART, DUE and an RRULE. */ + @Test + public void testRRule() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + + Duration day = new Duration(1, 1, 0); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), + // new Counted<>(5, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), @@ -196,55 +192,63 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */) - ); - } - - - /** - * Test if instances of a task with a timed DTSTART, DUE and a floating RRULE UNTIL. - *

- * Note, this combination should not be accepted by the provider. For the time being, however, it should be tolerated instead of causing a crash. - */ - @Test - public void testRRuleWithFloatingMismatch() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - - Duration day = new Duration(1, 1, 0); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;UNTIL=20180106", RecurrenceRule.RfcMode.RFC2445_LAX)))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;UNTIL=20180106"))), -// new Counted<>(5, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)); + } + + /** + * Test if instances of a task with a timed DTSTART, DUE and a floating RRULE UNTIL. + * + *

Note, this combination should not be accepted by the provider. For the time being, however, + * it should be tolerated instead of causing a crash. + */ + @Test + public void testRRuleWithFloatingMismatch() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + + Duration day = new Duration(1, 1, 0); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;UNTIL=20180106", RecurrenceRule.RfcMode.RFC2445_LAX))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;UNTIL=20180106"))), + // new Counted<>(5, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), @@ -260,53 +264,58 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */) - ); - } - - - /** - * Test if instances of a task with an all-day DTSTART, DUE and an RRULE. - */ - @Test - public void testAllDayRRule() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration days = new Duration(1, 2, 0); - DateTime start = DateTime.parse("20180104"); - DateTime due = start.addDuration(days); - DateTime localStart = start; - - Duration day = new Duration(1, 1, 0); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - DateTime localDue = due; - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), -// new Counted<>(5, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)); + } + + /** Test if instances of a task with an all-day DTSTART, DUE and an RRULE. */ + @Test + public void testAllDayRRule() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration days = new Duration(1, 2, 0); + DateTime start = DateTime.parse("20180104"); + DateTime due = start.addDuration(days); + DateTime localStart = start; + + Duration day = new Duration(1, 1, 0); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + DateTime localDue = due; + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), + // new Counted<>(5, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), @@ -322,55 +331,64 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */) - ); - } - - - /** - * Test if instances of a task with an all-day DTSTART, DUE and an RRULE with an absolute UNTIL - *

- * Note, this combination should not be accepted by the provider. For the time being, however, it should be tolerated instead of causing a crash. - */ - @Test - public void testAllDayRRuleFloatingMismatch() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration days = new Duration(1, 2, 0); - DateTime start = DateTime.parse("20180104"); - DateTime due = start.addDuration(days); - DateTime localStart = start; - - Duration day = new Duration(1, 1, 0); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - DateTime localDue = due; - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;UNTIL=20180106T120000Z", RecurrenceRule.RfcMode.RFC2445_LAX)))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;UNTIL=20180106T120000Z"))), -// new Counted<>(5, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)); + } + + /** + * Test if instances of a task with an all-day DTSTART, DUE and an RRULE with an absolute UNTIL + * + *

Note, this combination should not be accepted by the provider. For the time being, however, + * it should be tolerated instead of causing a crash. + */ + @Test + public void testAllDayRRuleFloatingMismatch() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration days = new Duration(1, 2, 0); + DateTime start = DateTime.parse("20180104"); + DateTime due = start.addDuration(days); + DateTime localStart = start; + + Duration day = new Duration(1, 1, 0); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + DateTime localDue = due; + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;UNTIL=20180106T120000Z", + RecurrenceRule.RfcMode.RFC2445_LAX))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;UNTIL=20180106T120000Z"))), + // new Counted<>(5, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), @@ -386,50 +404,55 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */) - ); - } - - - /** - * Test if instances of a task with a DUE and an RRULE but no DTSTART. - */ - @Test - public void testRRuleNoDtStart() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime due = DateTime.parse("20180104T123456Z"); - - Duration day = new Duration(1, 1, 0); - - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localDue.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new DueData<>(due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new DueData<>(due), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), -// new Counted<>(5, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(absent(), new Present<>(localDue), new Present<>(due), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, due.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)); + } + + /** Test if instances of a task with a DUE and an RRULE but no DTSTART. */ + @Test + public void testRRuleNoDtStart() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime due = DateTime.parse("20180104T123456Z"); + + Duration day = new Duration(1, 1, 0); + + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localDue.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new DueData<>(due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new DueData<>(due), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), + // new Counted<>(5, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(absent(), new Present<>(localDue), new Present<>(due), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, due.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(absent(), new Present<>(second), new Present<>(second), 1), @@ -445,50 +468,55 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(absent(), new Present<>(fifth), new Present<>(fifth), 4), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/) - ); - } - - - /** - * Test if instances of a task with a DTSTART and an RRULE but no DUE - */ - @Test - public void testRRuleNoDue() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.parse("20180104T123456Z"); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), -// new Counted<>(5, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(new Present<>(localStart), absent(), new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)); + } + + /** Test if instances of a task with a DTSTART and an RRULE but no DUE */ + @Test + public void testRRuleNoDue() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.parse("20180104T123456Z"); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), + // new Counted<>(5, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(new Present<>(localStart), absent(), new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(new Present<>(second), absent(), new Present<>(second), 1), @@ -504,129 +532,159 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(new Present<>(fifth), absent(), new Present<>(fifth), 4), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/) - ); - } - - - /** - * Remove an instance from a task with an RRULE. - */ - @Ignore("Test tries to delete 3rd instance which has not been created because currently only 1 instance is expanded") - @Test - public void testRRuleRemoveInstance() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), - // remove the third instance - new BulkDelete<>(instancesTable, - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp()))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"), - new CharSequenceRowData<>(Tasks.EXDATE, "20180106T123456Z"))), - new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())), - // 2nd instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())), - // 4th instance (now 3rd): - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 2), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())), - // 5th instance (now 4th): - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))) - ); - } - - - /** - * Test RRULE with overridden instance (inserted into the tasks table) - */ - @Test - public void testRRuleWithOverride() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskOverride = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new TitleData("original"), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), - // the override moves the instance by an hour - new Put<>(taskOverride, new Composite<>( - new TimeData<>(third.addDuration(hour), third.addDuration(hour).addDuration(hour)), - new TitleData("override"), - new OriginalInstanceData(task, third))) - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.TITLE, "original"), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), - new Assert<>(taskOverride, - new Composite<>( - new TimeData<>(third.addDuration(hour), third.addDuration(hour).addDuration(hour)), - new CharSequenceRowData<>(Tasks.TITLE, "override"), - new OriginalInstanceData(task, third))), -// new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, taskOverride)), - new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, taskOverride)), -// new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)); + } + + /** Remove an instance from a task with an RRULE. */ + @Ignore( + "Test tries to delete 3rd instance which has not been created because currently only 1 instance is expanded") + @Test + public void testRRuleRemoveInstance() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), + // remove the third instance + new BulkDelete<>( + instancesTable, + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"), + new CharSequenceRowData<>(Tasks.EXDATE, "20180106T123456Z"))), + new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())), + // 2nd instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())), + // 4th instance (now 3rd): + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 2), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())), + // 5th instance (now 4th): + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())))); + } + + /** Test RRULE with overridden instance (inserted into the tasks table) */ + @Test + public void testRRuleWithOverride() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskOverride = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new TitleData("original"), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), + // the override moves the instance by an hour + new Put<>( + taskOverride, + new Composite<>( + new TimeData<>( + third.addDuration(hour), third.addDuration(hour).addDuration(hour)), + new TitleData("override"), + new OriginalInstanceData(task, third)))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.TITLE, "original"), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), + new Assert<>( + taskOverride, + new Composite<>( + new TimeData<>( + third.addDuration(hour), third.addDuration(hour).addDuration(hour)), + new CharSequenceRowData<>(Tasks.TITLE, "override"), + new OriginalInstanceData(task, third))), + // new Counted<>(1, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, taskOverride)), + new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, taskOverride)), + // new Counted<>(4, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), @@ -643,260 +701,336 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/) - ); - } - - - /** - * Test RRULE with overridden instance (inserted into the tasks table) and a completed 1st instance. - */ - @Test - public void testRRuleWith2ndOverrideAndCompleted1st() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskOverride = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new TitleData("original"), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), - // the override moves the instance by an hour - new Put<>(taskOverride, new Composite<>( - new TimeData<>(second.addDuration(hour), second.addDuration(hour).addDuration(hour)), - new TitleData("override"), - new OriginalInstanceData(task, second))), - new Put<>(task, new StatusData<>(Tasks.STATUS_COMPLETED))), - resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.TITLE, "original"), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"), - new StatusData<>(Tasks.STATUS_COMPLETED))), - new Assert<>(taskOverride, - new Composite<>( - new TimeData<>(second.addDuration(hour), second.addDuration(hour).addDuration(hour)), - new CharSequenceRowData<>(Tasks.TITLE, "override"), - new OriginalInstanceData(task, second))), - // 1st (completed) instance: - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), -1), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))), - // 2nd instance (now the current one): - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, taskOverride, - new InstanceTestData( - second.addDuration(hour), - second.addDuration(hour).addDuration(hour), - new Present<>(second), 0))))); - } - - - /** - * Test RRULE with overridden instance (inserted into the tasks table) and a deleted 1st instance. - */ - @Test - public void testRRuleWith2ndOverrideAndDeleted1st() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskOverride = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new TitleData("original"), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), - // the override moves the instance by an hour - new Put<>(taskOverride, new Composite<>( - new TimeData<>(second.addDuration(hour), second.addDuration(hour).addDuration(hour)), - new TitleData("override"), - new OriginalInstanceData(task, second))), - // delete 1st instance - new BulkDelete<>(instancesTable, new AllOf<>( - new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.DISTANCE_FROM_CURRENT, "0")))), - resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.TITLE, "original"), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"), - new CharSequenceRowData<>(Tasks.EXDATE, start.toString()), - new StatusData<>(Tasks.STATUS_DEFAULT))), - new Assert<>(taskOverride, - new Composite<>( - new TimeData<>(second.addDuration(hour), second.addDuration(hour).addDuration(hour)), - new CharSequenceRowData<>(Tasks.TITLE, "override"), - new OriginalInstanceData(task, second))), - // no instances point to the original task - new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 2nd instance (now the current one): - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, taskOverride, - new InstanceTestData( - second.addDuration(hour), - second.addDuration(hour).addDuration(hour), - new Present<>(second), 0))))); - } - - - /** - * Test RRULE with overridden instance (via update on the instances table). This time we don't override the date time fields and expect the instance to - * inherit the original instance start and due (instead of the master start and due) - */ - @Ignore("Test tries to override the 3rd instance which has not been created because we currently only expand one instance.") - @Test - public void testRRuleWithOverride2() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table tasksTable = new TasksTable(mAuthority); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, tasksTable)); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new TitleData("original"), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), - // the override just changes the title - new BulkUpdate<>(instancesTable, - new Composite<>( - new CharSequenceRowData(Tasks.TITLE, "override")), - new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp()))) - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.TITLE, "original"), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), - new AssertRelated<>(tasksTable, Tasks.ORIGINAL_INSTANCE_ID, task, - new Composite<>( - // note the task table contains the original time zone, not the default one - new TimeData<>(third.shiftTimeZone(start.getTimeZone()), third.shiftTimeZone(start.getTimeZone()).addDuration(hour)), - new CharSequenceRowData<>(Tasks.TITLE, "override"), - new OriginalInstanceData(task, third))), - new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.ORIGINAL_INSTANCE_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())), - // 2nd instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())), - // 3th instance (the overridden one). We don't have a row reference to this row, so we select it by the ORIGINAL_INSTANCE-ID - new AssertRelated<>(instancesTable, Tasks.ORIGINAL_INSTANCE_ID, task, - new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 2), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())), - // 4th instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 3), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())), - // 5th instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))) - ); - } - - - /** - * Test if instances of a task with a DTSTART, an RRULE and EXDATEs. - */ - @Test - public void testRRuleWithExDates() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)), - new ExDatesTaskData(new Seq<>(third, fifth)))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"), - new CharSequenceRowData<>(Tasks.EXDATE, "20180106T123456Z,20180108T123456Z"))), -// new Counted<>(3, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)); + } + + /** + * Test RRULE with overridden instance (inserted into the tasks table) and a completed 1st + * instance. + */ + @Test + public void testRRuleWith2ndOverrideAndCompleted1st() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskOverride = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new TitleData("original"), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), + // the override moves the instance by an hour + new Put<>( + taskOverride, + new Composite<>( + new TimeData<>( + second.addDuration(hour), second.addDuration(hour).addDuration(hour)), + new TitleData("override"), + new OriginalInstanceData(task, second))), + new Put<>(task, new StatusData<>(Tasks.STATUS_COMPLETED))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.TITLE, "original"), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"), + new StatusData<>(Tasks.STATUS_COMPLETED))), + new Assert<>( + taskOverride, + new Composite<>( + new TimeData<>( + second.addDuration(hour), second.addDuration(hour).addDuration(hour)), + new CharSequenceRowData<>(Tasks.TITLE, "override"), + new OriginalInstanceData(task, second))), + // 1st (completed) instance: + new Counted<>( + 1, + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), -1), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))), + // 2nd instance (now the current one): + new Counted<>( + 1, + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + taskOverride, + new InstanceTestData( + second.addDuration(hour), + second.addDuration(hour).addDuration(hour), + new Present<>(second), + 0))))); + } + + /** + * Test RRULE with overridden instance (inserted into the tasks table) and a deleted 1st instance. + */ + @Test + public void testRRuleWith2ndOverrideAndDeleted1st() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskOverride = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new TitleData("original"), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), + // the override moves the instance by an hour + new Put<>( + taskOverride, + new Composite<>( + new TimeData<>( + second.addDuration(hour), second.addDuration(hour).addDuration(hour)), + new TitleData("override"), + new OriginalInstanceData(task, second))), + // delete 1st instance + new BulkDelete<>( + instancesTable, + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.DISTANCE_FROM_CURRENT, "0")))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.TITLE, "original"), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"), + new CharSequenceRowData<>(Tasks.EXDATE, start.toString()), + new StatusData<>(Tasks.STATUS_DEFAULT))), + new Assert<>( + taskOverride, + new Composite<>( + new TimeData<>( + second.addDuration(hour), second.addDuration(hour).addDuration(hour)), + new CharSequenceRowData<>(Tasks.TITLE, "override"), + new OriginalInstanceData(task, second))), + // no instances point to the original task + new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 2nd instance (now the current one): + new Counted<>( + 1, + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + taskOverride, + new InstanceTestData( + second.addDuration(hour), + second.addDuration(hour).addDuration(hour), + new Present<>(second), + 0))))); + } + + /** + * Test RRULE with overridden instance (via update on the instances table). This time we don't + * override the date time fields and expect the instance to inherit the original instance start + * and due (instead of the master start and due) + */ + @Ignore( + "Test tries to override the 3rd instance which has not been created because we currently only expand one instance.") + @Test + public void testRRuleWithOverride2() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table tasksTable = new TasksTable(mAuthority); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, tasksTable)); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new TitleData("original"), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))), + // the override just changes the title + new BulkUpdate<>( + instancesTable, + new Composite<>(new CharSequenceRowData(Tasks.TITLE, "override")), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.TITLE, "original"), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"))), + new AssertRelated<>( + tasksTable, + Tasks.ORIGINAL_INSTANCE_ID, + task, + new Composite<>( + // note the task table contains the original time zone, not the default one + new TimeData<>( + third.shiftTimeZone(start.getTimeZone()), + third.shiftTimeZone(start.getTimeZone()).addDuration(hour)), + new CharSequenceRowData<>(Tasks.TITLE, "override"), + new OriginalInstanceData(task, third))), + new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + new Counted<>( + 1, new AssertRelated<>(instancesTable, Instances.ORIGINAL_INSTANCE_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())), + // 2nd instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())), + // 3th instance (the overridden one). We don't have a row reference to this row, so we + // select it by the ORIGINAL_INSTANCE-ID + new AssertRelated<>( + instancesTable, + Tasks.ORIGINAL_INSTANCE_ID, + task, + new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 2), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())), + // 4th instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 3), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())), + // 5th instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())))); + } + + /** Test if instances of a task with a DTSTART, an RRULE and EXDATEs. */ + @Test + public void testRRuleWithExDates() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)), + new ExDatesTaskData(new Seq<>(third, fifth))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"), + new CharSequenceRowData<>(Tasks.EXDATE, "20180106T123456Z,20180108T123456Z"))), + // new Counted<>(3, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), @@ -904,59 +1038,62 @@ public class TaskProviderRecurrenceTest // 4th instance (now 3rd): new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 2), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp()))*/) - ); - } - - - /** - * Test if instances of a task with a DTSTART and RDATEs. - */ - @Test - public void testRDate() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RDatesTaskData(new Seq<>(start, second, third, fourth, fifth)))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.RDATE, - "20180104T123456Z," + - "20180105T123456Z," + - "20180106T123456Z," + - "20180107T123456Z," + - "20180108T123456Z" - ))), -// new Counted<>(5, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp()))*/)); + } + + /** Test if instances of a task with a DTSTART and RDATEs. */ + @Test + public void testRDate() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RDatesTaskData(new Seq<>(start, second, third, fourth, fifth))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>( + Tasks.RDATE, + "20180104T123456Z," + + "20180105T123456Z," + + "20180106T123456Z," + + "20180107T123456Z," + + "20180108T123456Z"))), + // new Counted<>(5, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), @@ -972,64 +1109,65 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/) - ); - } - - - /** - * Test if instances of a task with a DTSTART and RDATEs, add exdate afterwards. - */ - @Test - public void testRDateAddExDate() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RDatesTaskData(new Seq<>(start, second, third, fourth, fifth)))), - // the third instance becomed an exdate now - new Put<>(task, - new Composite<>( - new ExDatesTaskData(new Seq<>(third)))) - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.RDATE, - "20180104T123456Z," + - "20180105T123456Z," + - "20180106T123456Z," + - "20180107T123456Z," + - "20180108T123456Z"), - new CharSequenceRowData<>(Tasks.EXDATE, - "20180106T123456Z" - ))), -// new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(localStart, localDue, new Present<>(start), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)); + } + + /** Test if instances of a task with a DTSTART and RDATEs, add exdate afterwards. */ + @Test + public void testRDateAddExDate() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RDatesTaskData(new Seq<>(start, second, third, fourth, fifth)))), + // the third instance becomed an exdate now + new Put<>(task, new Composite<>(new ExDatesTaskData(new Seq<>(third))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>( + Tasks.RDATE, + "20180104T123456Z," + + "20180105T123456Z," + + "20180106T123456Z," + + "20180107T123456Z," + + "20180108T123456Z"), + new CharSequenceRowData<>(Tasks.EXDATE, "20180106T123456Z"))), + // new Counted<>(4, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(localStart, localDue, new Present<>(start), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())) /*, // 2nd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1), @@ -1045,72 +1183,80 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/) - ); - } - - - /** - * Test if instances of a task with a DTSTART and RDATEs, complete first. - */ - @Test - public void testRDateFirstComplete() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot override = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - // first insert new task, - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RDatesTaskData(new Seq<>(start, second, third, fourth, fifth)))), - // next, insert override - new Put<>(override, - new Composite<>( - new TimeData<>(start, due), - new OriginalInstanceData(task, start), - new StatusData<>(Tasks.STATUS_COMPLETED))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new CharSequenceRowData<>(Tasks.RDATE, - "20180104T123456Z," + - "20180105T123456Z," + - "20180106T123456Z," + - "20180107T123456Z," + - "20180108T123456Z" - ))), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, override)), -// new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, override, - new InstanceTestData(localStart, localDue, new Present<>(start), -1), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())), - // 2nd instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)); + } + + /** Test if instances of a task with a DTSTART and RDATEs, complete first. */ + @Test + public void testRDateFirstComplete() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot override = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + // first insert new task, + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RDatesTaskData(new Seq<>(start, second, third, fourth, fifth)))), + // next, insert override + new Put<>( + override, + new Composite<>( + new TimeData<>(start, due), + new OriginalInstanceData(task, start), + new StatusData<>(Tasks.STATUS_COMPLETED)))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new CharSequenceRowData<>( + Tasks.RDATE, + "20180104T123456Z," + + "20180105T123456Z," + + "20180106T123456Z," + + "20180107T123456Z," + + "20180108T123456Z"))), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, override)), + // new Counted<>(4, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + override, + new InstanceTestData(localStart, localDue, new Present<>(start), -1), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())), + // 2nd instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())) /*, // 3rd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 1), @@ -1122,79 +1268,88 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/) - ); - } - - - /** - * Test if instances of a task with a DTSTART and RDATEs, complete first inserted first. - */ - @Test - public void testRDateFirstCompleteFirstInserted() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot override = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - // first insert override - new Put<>(override, - new Composite<>( - new TimeData<>(start, due), - new OriginalInstanceSyncIdData("xyz", start), - new StatusData<>(Tasks.STATUS_COMPLETED))), - // then insert task - new Put<>(task, - new Composite<>( - new SyncIdData("xyz"), - new TimeData<>(start, due), - new RDatesTaskData(new Seq<>(start, second, third, fourth, fifth)))) - - ), resultsIn(mClient, - new Assert<>(task, - new Composite<>( - new TimeData<>(start, due), - new SyncIdData("xyz"), - new CharSequenceRowData<>(Tasks.RDATE, - "20180104T123456Z," + - "20180105T123456Z," + - "20180106T123456Z," + - "20180107T123456Z," + - "20180108T123456Z" - ))), - new Assert<>(override, - new Composite<>( - new TimeData<>(start, due), - new OriginalInstanceSyncIdData("xyz", start), - new StatusData<>(Tasks.STATUS_COMPLETED))), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, override)), -// new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance, overridden and completed - new AssertRelated<>(instancesTable, Instances.TASK_ID, override, - new InstanceTestData(localStart, localDue, new Present<>(start), -1), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())), - // 2nd instance: - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp()))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)); + } + + /** Test if instances of a task with a DTSTART and RDATEs, complete first inserted first. */ + @Test + public void testRDateFirstCompleteFirstInserted() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot override = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + // first insert override + new Put<>( + override, + new Composite<>( + new TimeData<>(start, due), + new OriginalInstanceSyncIdData("xyz", start), + new StatusData<>(Tasks.STATUS_COMPLETED))), + // then insert task + new Put<>( + task, + new Composite<>( + new SyncIdData("xyz"), + new TimeData<>(start, due), + new RDatesTaskData(new Seq<>(start, second, third, fourth, fifth))))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new SyncIdData("xyz"), + new CharSequenceRowData<>( + Tasks.RDATE, + "20180104T123456Z," + + "20180105T123456Z," + + "20180106T123456Z," + + "20180107T123456Z," + + "20180108T123456Z"))), + new Assert<>( + override, + new Composite<>( + new TimeData<>(start, due), + new OriginalInstanceSyncIdData("xyz", start), + new StatusData<>(Tasks.STATUS_COMPLETED))), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, override)), + // new Counted<>(4, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance, overridden and completed + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + override, + new InstanceTestData(localStart, localDue, new Present<>(start), -1), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())), + // 2nd instance: + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())) /*, // 3rd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 1), @@ -1206,87 +1361,107 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/) - ); - } - - - /** - * Test if instances of a task with a DTSTART and RDATEs, complete first via instances table. - */ - @Test - public void testRDateFirstCompleteViaInstances() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table tasksTable = new TasksTable(mAuthority); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, tasksTable)); - - Duration hour = new Duration(1, 0, 3600 /* 1 hour */); - DateTime start = DateTime.parse("20180104T123456Z"); - DateTime due = start.addDuration(hour); - - Duration day = new Duration(1, 1, 0); - - DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); - DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); - - DateTime second = localStart.addDuration(day); - DateTime third = second.addDuration(day); - DateTime fourth = third.addDuration(day); - DateTime fifth = fourth.addDuration(day); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - // first insert the task - new Put<>(task, - new Composite<>( - new TimeData<>(start, due), - new RDatesTaskData(start, second, third, fourth, fifth))), - // then complete the first instance - new BulkUpdate<>(instancesTable, new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), - new AllOf<>( - new ReferringTo<>(Instances.TASK_ID, task), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())))), - resultsIn(mClient, - // we've already closed the first instance which has been detached, the master now points to the second instance - new Counted<>(1, - new Assert<>(task, - new Composite<>( - new TimeData<>(DateTime.parse("20180105T123456Z"), DateTime.parse("20180105T133456Z")), - new RDatesTaskData( - // "20180104T123456Z" // the detached instance - DateTime.parse("20180105T123456Z"), - DateTime.parse("20180106T123456Z"), - DateTime.parse("20180107T123456Z"), - DateTime.parse("20180108T123456Z"))))), - // there must be one task which is not equal to the original task, it's the detached instance - new Counted<>(1, - new BulkAssert<>(tasksTable, - new Composite<>( - new TimeData<>(start, due), - new StatusData<>(Tasks.STATUS_COMPLETED), - new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_ID, null), - new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_SYNC_ID, null), - new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_TIME, null)), - new Not<>(new ReferringTo<>(Tasks._ID, task)))), - // and one instance which doesn't refer to the original task - new Counted<>(1, new BulkAssert<>(instancesTable, new Not<>(new ReferringTo<>(Instances.TASK_ID, task)))), - // but 4 instances of that original task -// new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), - // 1st instance, detached and completed - new Counted<>(1, new BulkAssert<>(instancesTable, - new Composite<>( - new InstanceTestData(localStart, localDue, absent(), -1)), - new AllOf<>( - new IsNull<>(Instances.INSTANCE_ORIGINAL_TIME), // the detached instance has no INSTANCE_ORIGINAL_TIME - new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), - // 2nd instance: - new Counted<>(1, - new AssertRelated<>(instancesTable, Instances.TASK_ID, task, - new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())))/*, + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)); + } + + /** Test if instances of a task with a DTSTART and RDATEs, complete first via instances table. */ + @Test + public void testRDateFirstCompleteViaInstances() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table tasksTable = new TasksTable(mAuthority); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, tasksTable)); + + Duration hour = new Duration(1, 0, 3600 /* 1 hour */); + DateTime start = DateTime.parse("20180104T123456Z"); + DateTime due = start.addDuration(hour); + + Duration day = new Duration(1, 1, 0); + + DateTime localStart = start.shiftTimeZone(TimeZone.getDefault()); + DateTime localDue = due.shiftTimeZone(TimeZone.getDefault()); + + DateTime second = localStart.addDuration(day); + DateTime third = second.addDuration(day); + DateTime fourth = third.addDuration(day); + DateTime fifth = fourth.addDuration(day); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + // first insert the task + new Put<>( + task, + new Composite<>( + new TimeData<>(start, due), + new RDatesTaskData(start, second, third, fourth, fifth))), + // then complete the first instance + new BulkUpdate<>( + instancesTable, + new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)), + new AllOf<>( + new ReferringTo<>(Instances.TASK_ID, task), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())))), + resultsIn( + mClient, + // we've already closed the first instance which has been detached, the master now + // points to the second instance + new Counted<>( + 1, + new Assert<>( + task, + new Composite<>( + new TimeData<>( + DateTime.parse("20180105T123456Z"), DateTime.parse("20180105T133456Z")), + new RDatesTaskData( + // "20180104T123456Z" // the detached instance + DateTime.parse("20180105T123456Z"), + DateTime.parse("20180106T123456Z"), + DateTime.parse("20180107T123456Z"), + DateTime.parse("20180108T123456Z"))))), + // there must be one task which is not equal to the original task, it's the detached + // instance + new Counted<>( + 1, + new BulkAssert<>( + tasksTable, + new Composite<>( + new TimeData<>(start, due), + new StatusData<>(Tasks.STATUS_COMPLETED), + new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_ID, null), + new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_SYNC_ID, null), + new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_TIME, null)), + new Not<>(new ReferringTo<>(Tasks._ID, task)))), + // and one instance which doesn't refer to the original task + new Counted<>( + 1, + new BulkAssert<>( + instancesTable, new Not<>(new ReferringTo<>(Instances.TASK_ID, task)))), + // but 4 instances of that original task + // new Counted<>(4, new AssertRelated<>(instancesTable, + // Instances.TASK_ID, task)), + new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)), + // 1st instance, detached and completed + new Counted<>( + 1, + new BulkAssert<>( + instancesTable, + new Composite<>(new InstanceTestData(localStart, localDue, absent(), -1)), + new AllOf<>( + new IsNull<>( + Instances.INSTANCE_ORIGINAL_TIME), // the detached instance has no + // INSTANCE_ORIGINAL_TIME + new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))), + // 2nd instance: + new Counted<>( + 1, + new AssertRelated<>( + instancesTable, + Instances.TASK_ID, + task, + new InstanceTestData( + second, second.addDuration(hour), new Present<>(second), 0), + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp()))) /*, // 3rd instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 1), @@ -1298,8 +1473,6 @@ public class TaskProviderRecurrenceTest // 5th instance: new AssertRelated<>(instancesTable, Instances.TASK_ID, task, new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3), - new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/) - ); - } - + new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)); + } } diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRelatingTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRelatingTest.java index f7595805..4a3c8815 100644 --- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRelatingTest.java +++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRelatingTest.java @@ -16,12 +16,16 @@ package org.dmfs.provider.tasks; +import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; +import static org.junit.Assert.assertThat; + import android.accounts.Account; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.os.Build; - +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import org.dmfs.android.contentpal.Operation; import org.dmfs.android.contentpal.OperationsQueue; import org.dmfs.android.contentpal.RowSnapshot; @@ -58,127 +62,136 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; -import static org.junit.Assert.assertThat; - - /** * Tests for {@link TaskProvider} reparenting feature. * * @author Marten Gajda */ @RunWith(AndroidJUnit4.class) -public class TaskProviderRelatingTest -{ - private ContentResolver mResolver; - private String mAuthority; - private Context mContext; - private ContentProviderClient mClient; - private final Account testAccount = new Account("foo", "bar"); - - - @Before - public void setUp() throws Exception - { - mContext = InstrumentationRegistry.getTargetContext(); - mResolver = mContext.getContentResolver(); - mAuthority = AuthorityUtil.taskAuthority(mContext); - mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); - - // Assert that tables are empty: - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new TaskListsTable(mAuthority)), - new AssertEmptyTable<>(new PropertiesTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)))); - queue.flush(); - } - - - @After - public void tearDown() throws Exception - { - /* - TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation - https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html - https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator - */ - - // Clear the DB: - BasicOperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new BulkDelete<>(new LocalTaskListsTable(mAuthority)), - new BulkDelete<>(new PropertiesTable(mAuthority)), - new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))))); - queue.flush(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - { - mClient.close(); - } - else - { - mClient.release(); - } - } - - - /** - * Create 1 local task list, then create a child Task, related to a parent UID and finally the parent. - */ - @Test - public void testRelateTask() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))); - RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new Synced<>(testAccount, new TasksTable(mAuthority)))); - RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new Synced<>(testAccount, new TasksTable(mAuthority)))); - - assertThat(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(taskChild, new Composite<>( - new TitleData("child"), - new CharSequenceRowData<>(Tasks._UID, "childUID"))), - new Insert<>(new PropertiesTable(mAuthority), new Composite<>( - new CharSequenceRowData<>(TaskContract.Property.Relation.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new Referring<>(TaskContract.Property.Relation.TASK_ID, taskChild), - new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_UID, "parentUID"), - new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)) - )), - new Put<>(taskParent, new Composite<>( - new TitleData("parent"), - new CharSequenceRowData<>(Tasks._UID, "parentUID"))) - ), - resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - new Assert<>(taskChild, new Composite<>( - new TitleData("child"), - new CharSequenceRowData<>(Tasks._UID, "childUID"), - new Referring<>(Tasks.PARENT_ID, taskParent))), - new Assert<>(taskParent, new Composite<>( - new CharSequenceRowData<>(Tasks._UID, "parentUID"), - new TitleData("parent"))), - new Counted<>(1, new BulkAssert<>( - new PropertiesTable(mAuthority), - new Composite<>( - new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, - String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), - new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_UID, "parentUID"), - new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent) - ), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild) - ))), - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent) - ))) - )); +public class TaskProviderRelatingTest { + private ContentResolver mResolver; + private String mAuthority; + private Context mContext; + private ContentProviderClient mClient; + private final Account testAccount = new Account("foo", "bar"); + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getTargetContext(); + mResolver = mContext.getContentResolver(); + mAuthority = AuthorityUtil.taskAuthority(mContext); + mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); + + // Assert that tables are empty: + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new TaskListsTable(mAuthority)), + new AssertEmptyTable<>(new PropertiesTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + queue.flush(); + } + + @After + public void tearDown() throws Exception { + /* + TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation + https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html + https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator + */ + + // Clear the DB: + BasicOperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new BulkDelete<>(new LocalTaskListsTable(mAuthority)), + new BulkDelete<>(new PropertiesTable(mAuthority)), + new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))))); + queue.flush(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mClient.close(); + } else { + mClient.release(); } + } + + /** + * Create 1 local task list, then create a child Task, related to a parent UID and finally the + * parent. + */ + @Test + public void testRelateTask() { + RowSnapshot taskList = + new VirtualRowSnapshot<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))); + RowSnapshot taskChild = + new VirtualRowSnapshot<>( + new TaskListScoped(taskList, new Synced<>(testAccount, new TasksTable(mAuthority)))); + RowSnapshot taskParent = + new VirtualRowSnapshot<>( + new TaskListScoped(taskList, new Synced<>(testAccount, new TasksTable(mAuthority)))); + + assertThat( + new Seq<>( + new Put<>(taskList, new NameData("list1")), + new Put<>( + taskChild, + new Composite<>( + new TitleData("child"), new CharSequenceRowData<>(Tasks._UID, "childUID"))), + new Insert<>( + new PropertiesTable(mAuthority), + new Composite<>( + new CharSequenceRowData<>( + TaskContract.Property.Relation.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new Referring<>(TaskContract.Property.Relation.TASK_ID, taskChild), + new CharSequenceRowData<>( + TaskContract.Property.Relation.RELATED_UID, "parentUID"), + new CharSequenceRowData<>( + TaskContract.Property.Relation.RELATED_TYPE, + String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)))), + new Put<>( + taskParent, + new Composite<>( + new TitleData("parent"), new CharSequenceRowData<>(Tasks._UID, "parentUID")))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + new Assert<>( + taskChild, + new Composite<>( + new TitleData("child"), + new CharSequenceRowData<>(Tasks._UID, "childUID"), + new Referring<>(Tasks.PARENT_ID, taskParent))), + new Assert<>( + taskParent, + new Composite<>( + new CharSequenceRowData<>(Tasks._UID, "parentUID"), new TitleData("parent"))), + new Counted<>( + 1, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new Composite<>( + new CharSequenceRowData<>( + TaskContract.Property.Relation.RELATED_TYPE, + String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), + new CharSequenceRowData<>( + TaskContract.Property.Relation.RELATED_UID, "parentUID"), + new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent)), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)))), + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)))))); + } } diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderReparentingTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderReparentingTest.java index 57eedc1f..38c8ba6d 100644 --- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderReparentingTest.java +++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderReparentingTest.java @@ -16,12 +16,16 @@ package org.dmfs.provider.tasks; +import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; +import static org.junit.Assert.assertThat; + import android.accounts.Account; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.os.Build; - +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; import org.dmfs.android.contentpal.Operation; import org.dmfs.android.contentpal.OperationsQueue; import org.dmfs.android.contentpal.RowSnapshot; @@ -58,335 +62,364 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; -import static org.junit.Assert.assertThat; - - /** * Tests for {@link TaskProvider} reparenting feature. * * @author Marten Gajda */ @RunWith(AndroidJUnit4.class) -public class TaskProviderReparentingTest -{ - private ContentResolver mResolver; - private String mAuthority; - private Context mContext; - private ContentProviderClient mClient; - private final Account testAccount = new Account("foo", "bar"); - - - @Before - public void setUp() throws Exception - { - mContext = InstrumentationRegistry.getTargetContext(); - mResolver = mContext.getContentResolver(); - mAuthority = AuthorityUtil.taskAuthority(mContext); - mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); - - // Assert that tables are empty: - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new TaskListsTable(mAuthority)), - new AssertEmptyTable<>(new PropertiesTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)))); - queue.flush(); - } - - - @After - public void tearDown() throws Exception - { - /* - TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation - https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html - https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator - */ - - // Clear the DB: - BasicOperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new BulkDelete<>(new LocalTaskListsTable(mAuthority)), - new BulkDelete<>(new PropertiesTable(mAuthority)), - new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))))); - queue.flush(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - { - mClient.close(); - } - else - { - mClient.release(); - } - } - - - /** - * Create 1 local task list and a parent and a child task. - */ - @Test - public void testRelateTask() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - assertThat(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(taskParent, new TitleData("parent")), - new Put<>(taskChild, new Composite<>( - new TitleData("child"), - new Referring<>(Tasks.PARENT_ID, taskParent))) - ), - resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - new Assert<>(taskChild, new Composite<>( - new TitleData("child"), - new Referring<>(Tasks.PARENT_ID, taskParent))), - new Assert<>(taskParent, new Composite<>( - new TitleData("parent"))), - new Counted<>(1, new BulkAssert<>( - new PropertiesTable(mAuthority), - new Composite<>( - new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, - String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), - new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent) - ), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild) - ))), - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent) - ))) - )); - } - - - /** - * Create 1 local task list and 2 tasks, in a second operation make the second one parent of the first one. - */ - @Test - public void testAdoptTask() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - assertThat(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(taskChild, new TitleData("child")), - new Put<>(taskParent, new TitleData("parent")), - new Put<>(taskChild, new Referring<>(Tasks.PARENT_ID, taskParent)) - ), - resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - new Assert<>(taskChild, new Composite<>( - new TitleData("child"), - new Referring<>(Tasks.PARENT_ID, taskParent))), - new Assert<>(taskParent, new Composite<>( - new TitleData("parent"))), - new Counted<>(1, new BulkAssert<>( - new PropertiesTable(mAuthority), - new Composite<>( - new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, - String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), - new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent) - ), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild) - ))), - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent) - ))) - )); - } - - - /** - * Create 1 local task list and 3 tasks, create parent child relationship and change it afterwards - */ - @Test - public void testReparentTask() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskNewParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - assertThat(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(taskParent, new TitleData("parent")), - new Put<>(taskChild, new Composite<>( - new TitleData("child"), - new Referring<>(Tasks.PARENT_ID, taskParent))), - new Put<>(taskNewParent, new TitleData("newParent")), - new Put<>(taskChild, new Referring<>(Tasks.PARENT_ID, taskNewParent)) - ), - resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - new Assert<>(taskChild, new Composite<>( - new TitleData("child"), - new Referring<>(Tasks.PARENT_ID, taskNewParent))), - new Assert<>(taskParent, new Composite<>( - new TitleData("parent"))), - new Assert<>(taskNewParent, new Composite<>( - new TitleData("newParent"))), - - new Counted<>(1, new BulkAssert<>( - new PropertiesTable(mAuthority), - new Composite<>( - new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, - String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), - new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskNewParent) - ), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild) - ))), - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent) - ))), - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskNewParent) - ))) - )); - } - - - /** - * Create 1 local task list and 4 tasks, create parent child relationship with a sibling and change it afterwards - */ - @Test - public void testReparentTaskWithSibling() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskNewParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskSibling = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - assertThat(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(taskParent, new TitleData("parent")), - new Put<>(taskChild, new Composite<>( - new TitleData("child"), - new Referring<>(Tasks.PARENT_ID, taskParent))), - new Put<>(taskNewParent, new TitleData("newParent")), - new Put<>(taskSibling, new TitleData("sibling")), - new Insert<>(new PropertiesTable(mAuthority), new Composite<>( - new CharSequenceRowData<>(TaskContract.Property.Relation.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new Referring<>(TaskContract.Property.Relation.TASK_ID, taskSibling), - new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskChild), - new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, String.valueOf( - TaskContract.Property.Relation.RELTYPE_SIBLING)) - )), - new Put<>(taskChild, new Referring<>(Tasks.PARENT_ID, taskNewParent)) - ), - resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - new Assert<>(taskChild, new Composite<>( - new TitleData("child"), - new Referring<>(Tasks.PARENT_ID, taskNewParent))), - new Assert<>(taskParent, new Composite<>( - new TitleData("parent"))), - new Assert<>(taskNewParent, new Composite<>( - new TitleData("newParent"))), - new Assert<>(taskSibling, new Composite<>( - new TitleData("sibling"))), - - new Counted<>(1, new BulkAssert<>( - new PropertiesTable(mAuthority), - new Composite<>( - new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, - String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), - new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskNewParent) - ), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild) - ))), - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent) - ))), - // yikes the sibling became an orphan because it has no relation to its parent anymore. - // this should be fixed, see https://github.com/dmfs/opentasks/issues/932 - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskSibling) - ))), - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskNewParent) - ))) - )); - } - - - /** - * Create 1 local task list and 2 tasks, create parent child relationship and remove it - */ - @Test - public void testOrphanTask() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - assertThat(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(taskParent, new TitleData("parent")), - new Put<>(taskChild, new Composite<>( - new TitleData("child"), - new Referring<>(Tasks.PARENT_ID, taskParent))), - new Put<>(taskChild, new CharSequenceRowData<>(Tasks.PARENT_ID, null)) - ), - resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - new Assert<>(taskChild, new Composite<>( - new TitleData("child"), - new CharSequenceRowData<>(Tasks.PARENT_ID, null))), - new Assert<>(taskParent, new Composite<>( - new TitleData("parent"))), - - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild) - ))), - new Counted<>(0, new BulkAssert<>( - new PropertiesTable(mAuthority), - new AllOf<>( - new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE), - new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent) - ))) - )); +public class TaskProviderReparentingTest { + private ContentResolver mResolver; + private String mAuthority; + private Context mContext; + private ContentProviderClient mClient; + private final Account testAccount = new Account("foo", "bar"); + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getTargetContext(); + mResolver = mContext.getContentResolver(); + mAuthority = AuthorityUtil.taskAuthority(mContext); + mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); + + // Assert that tables are empty: + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new TaskListsTable(mAuthority)), + new AssertEmptyTable<>(new PropertiesTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + queue.flush(); + } + + @After + public void tearDown() throws Exception { + /* + TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation + https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html + https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator + */ + + // Clear the DB: + BasicOperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new BulkDelete<>(new LocalTaskListsTable(mAuthority)), + new BulkDelete<>(new PropertiesTable(mAuthority)), + new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))))); + queue.flush(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mClient.close(); + } else { + mClient.release(); } + } + + /** Create 1 local task list and a parent and a child task. */ + @Test + public void testRelateTask() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot taskChild = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskParent = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + assertThat( + new Seq<>( + new Put<>(taskList, new NameData("list1")), + new Put<>(taskParent, new TitleData("parent")), + new Put<>( + taskChild, + new Composite<>( + new TitleData("child"), new Referring<>(Tasks.PARENT_ID, taskParent)))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + new Assert<>( + taskChild, + new Composite<>( + new TitleData("child"), new Referring<>(Tasks.PARENT_ID, taskParent))), + new Assert<>(taskParent, new Composite<>(new TitleData("parent"))), + new Counted<>( + 1, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new Composite<>( + new CharSequenceRowData<>( + TaskContract.Property.Relation.RELATED_TYPE, + String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), + new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent)), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)))), + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)))))); + } + + /** + * Create 1 local task list and 2 tasks, in a second operation make the second one parent of the + * first one. + */ + @Test + public void testAdoptTask() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot taskChild = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskParent = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + assertThat( + new Seq<>( + new Put<>(taskList, new NameData("list1")), + new Put<>(taskChild, new TitleData("child")), + new Put<>(taskParent, new TitleData("parent")), + new Put<>(taskChild, new Referring<>(Tasks.PARENT_ID, taskParent))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + new Assert<>( + taskChild, + new Composite<>( + new TitleData("child"), new Referring<>(Tasks.PARENT_ID, taskParent))), + new Assert<>(taskParent, new Composite<>(new TitleData("parent"))), + new Counted<>( + 1, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new Composite<>( + new CharSequenceRowData<>( + TaskContract.Property.Relation.RELATED_TYPE, + String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), + new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent)), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)))), + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)))))); + } + + /** + * Create 1 local task list and 3 tasks, create parent child relationship and change it afterwards + */ + @Test + public void testReparentTask() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot taskChild = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskParent = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskNewParent = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + assertThat( + new Seq<>( + new Put<>(taskList, new NameData("list1")), + new Put<>(taskParent, new TitleData("parent")), + new Put<>( + taskChild, + new Composite<>( + new TitleData("child"), new Referring<>(Tasks.PARENT_ID, taskParent))), + new Put<>(taskNewParent, new TitleData("newParent")), + new Put<>(taskChild, new Referring<>(Tasks.PARENT_ID, taskNewParent))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + new Assert<>( + taskChild, + new Composite<>( + new TitleData("child"), new Referring<>(Tasks.PARENT_ID, taskNewParent))), + new Assert<>(taskParent, new Composite<>(new TitleData("parent"))), + new Assert<>(taskNewParent, new Composite<>(new TitleData("newParent"))), + new Counted<>( + 1, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new Composite<>( + new CharSequenceRowData<>( + TaskContract.Property.Relation.RELATED_TYPE, + String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), + new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskNewParent)), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)))), + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)))), + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskNewParent)))))); + } + + /** + * Create 1 local task list and 4 tasks, create parent child relationship with a sibling and + * change it afterwards + */ + @Test + public void testReparentTaskWithSibling() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot taskChild = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskParent = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskNewParent = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskSibling = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + assertThat( + new Seq<>( + new Put<>(taskList, new NameData("list1")), + new Put<>(taskParent, new TitleData("parent")), + new Put<>( + taskChild, + new Composite<>( + new TitleData("child"), new Referring<>(Tasks.PARENT_ID, taskParent))), + new Put<>(taskNewParent, new TitleData("newParent")), + new Put<>(taskSibling, new TitleData("sibling")), + new Insert<>( + new PropertiesTable(mAuthority), + new Composite<>( + new CharSequenceRowData<>( + TaskContract.Property.Relation.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new Referring<>(TaskContract.Property.Relation.TASK_ID, taskSibling), + new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskChild), + new CharSequenceRowData<>( + TaskContract.Property.Relation.RELATED_TYPE, + String.valueOf(TaskContract.Property.Relation.RELTYPE_SIBLING)))), + new Put<>(taskChild, new Referring<>(Tasks.PARENT_ID, taskNewParent))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + new Assert<>( + taskChild, + new Composite<>( + new TitleData("child"), new Referring<>(Tasks.PARENT_ID, taskNewParent))), + new Assert<>(taskParent, new Composite<>(new TitleData("parent"))), + new Assert<>(taskNewParent, new Composite<>(new TitleData("newParent"))), + new Assert<>(taskSibling, new Composite<>(new TitleData("sibling"))), + new Counted<>( + 1, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new Composite<>( + new CharSequenceRowData<>( + TaskContract.Property.Relation.RELATED_TYPE, + String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)), + new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskNewParent)), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)))), + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)))), + // yikes the sibling became an orphan because it has no relation to its parent anymore. + // this should be fixed, see https://github.com/dmfs/opentasks/issues/932 + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskSibling)))), + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskNewParent)))))); + } + + /** Create 1 local task list and 2 tasks, create parent child relationship and remove it */ + @Test + public void testOrphanTask() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot taskChild = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + RowSnapshot taskParent = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + assertThat( + new Seq<>( + new Put<>(taskList, new NameData("list1")), + new Put<>(taskParent, new TitleData("parent")), + new Put<>( + taskChild, + new Composite<>( + new TitleData("child"), new Referring<>(Tasks.PARENT_ID, taskParent))), + new Put<>(taskChild, new CharSequenceRowData<>(Tasks.PARENT_ID, null))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + new Assert<>( + taskChild, + new Composite<>( + new TitleData("child"), new CharSequenceRowData<>(Tasks.PARENT_ID, null))), + new Assert<>(taskParent, new Composite<>(new TitleData("parent"))), + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)))), + new Counted<>( + 0, + new BulkAssert<>( + new PropertiesTable(mAuthority), + new AllOf<>( + new EqArg<>( + TaskContract.Properties.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE), + new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)))))); + } } diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderTest.java index 47a8d9d3..cb3739b9 100644 --- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderTest.java +++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderTest.java @@ -16,13 +16,19 @@ package org.dmfs.provider.tasks; +import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; +import static org.dmfs.optional.Absent.absent; +import static org.junit.Assert.assertThat; + import android.accounts.Account; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.os.Build; - +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; +import java.util.TimeZone; import org.dmfs.android.contentpal.Operation; import org.dmfs.android.contentpal.OperationsQueue; import org.dmfs.android.contentpal.RowSnapshot; @@ -72,16 +78,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.TimeZone; - -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn; -import static org.dmfs.optional.Absent.absent; -import static org.junit.Assert.assertThat; - - /** * Tests for {@link TaskProvider}. * @@ -90,856 +86,835 @@ import static org.junit.Assert.assertThat; * @author Marten Gajda */ @RunWith(AndroidJUnit4.class) -public class TaskProviderTest -{ - private ContentResolver mResolver; - private String mAuthority; - private Context mContext; - private ContentProviderClient mClient; - private final Account testAccount = new Account("foo", "bar"); - - - @Before - public void setUp() throws Exception - { - mContext = InstrumentationRegistry.getTargetContext(); - mResolver = mContext.getContentResolver(); - mAuthority = AuthorityUtil.taskAuthority(mContext); - mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); - - // Assert that tables are empty: - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new TaskListsTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)))); - queue.flush(); - } - - - @After - public void tearDown() throws Exception - { - /* - TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation - https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html - https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator - */ - - // Clear the DB: - BasicOperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new Seq>( - new BulkDelete<>(new LocalTaskListsTable(mAuthority)), - new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))))); - queue.flush(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - { - mClient.close(); - } - else - { - mClient.release(); - } - } - - - /** - * Create 1 local task list and 1 task, check values in TaskLists, Tasks, Instances tables. - */ - @Test - public void testSingleInsert() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - assertThat(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new TitleData("task1")) - - ), resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - new Assert<>(task, new Composite<>( - new TitleData("task1"), - new VersionData(0))), - new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TZ, null)) - ))); - } - - - /** - * Create 1 local task list and 1 task, update task via instances table and check values in TaskLists, Tasks, Instances tables. - */ - @Test - public void testSingleInsertUpdateInstance() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - Table instancesTable = new InstanceTable(mAuthority); - - assertThat(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new TitleData("task1")), - new BulkUpdate<>(instancesTable, new CharSequenceRowData<>(Tasks.TITLE, "task updated"), new ReferringTo<>(Instances.TASK_ID, task)) - - ), resultsIn(mClient, - new Assert<>(taskList, new NameData("list1")), - new Assert<>(task, new Composite<>( - new TitleData("task updated"), - new VersionData(1))), - new AssertRelated<>(new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TZ, null)) - ))); - } - - - /** - * Create 2 task list and 3 tasks, check values. - */ - @Test - public void testMultipleInserts() - { - Table taskListsTable = new LocalTaskListsTable(mAuthority); - RowSnapshot taskList1 = new VirtualRowSnapshot<>(taskListsTable); - RowSnapshot taskList2 = new VirtualRowSnapshot<>(taskListsTable); - RowSnapshot task1 = new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority))); - RowSnapshot task2 = new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority))); - RowSnapshot task3 = new VirtualRowSnapshot<>(new TaskListScoped(taskList2, new TasksTable(mAuthority))); - - assertThat(new Seq<>( - new Put<>(taskList1, new NameData("list1")), - new Put<>(taskList2, new NameData("list2")), - new Put<>(task1, new TitleData("task1")), - new Put<>(task2, new TitleData("task2")), - new Put<>(task3, new TitleData("task3")) - - ), resultsIn(mClient, - new Assert<>(taskList1, new NameData("list1")), - new Assert<>(taskList2, new NameData("list2")), - new Assert<>(task1, new Composite<>( - new TitleData("task1"), - new VersionData(0))), - new Assert<>(task2, new Composite<>( - new TitleData("task2"), - new VersionData(0))), - new Assert<>(task3, new Composite<>( - new TitleData("task3"), - new VersionData(0))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task1, - new Composite( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TZ, null))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task2, - new Composite( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TZ, null))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task3, - new Composite( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TZ, null)) - ))); - } - - - /** - * Create 2 task list and 3 tasks with updates, check values. - */ - @Test - public void testMultipleInsertsAndUpdates() - { - Table taskListsTable = new LocalTaskListsTable(mAuthority); - RowSnapshot taskList1 = new VirtualRowSnapshot<>(taskListsTable); - RowSnapshot taskList2 = new VirtualRowSnapshot<>(taskListsTable); - RowSnapshot task1 = new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority))); - RowSnapshot task2 = new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority))); - RowSnapshot task3 = new VirtualRowSnapshot<>(new TaskListScoped(taskList2, new TasksTable(mAuthority))); - - assertThat(new Seq<>( - new Put<>(taskList1, new NameData("list1")), - new Put<>(taskList2, new NameData("list2")), - new Put<>(task1, new TitleData("task1a")), - new Put<>(task2, new TitleData("task2a")), - new Put<>(task3, new TitleData("task3a")), - // update task 1 and 2 - new Put<>(task1, new TitleData("task1b")), - new Put<>(task2, new TitleData("task2b")), - // update task 1 once more - new Put<>(task1, new TitleData("task1c")) - - ), resultsIn(mClient, - new Assert<>(taskList1, new NameData("list1")), - new Assert<>(taskList2, new NameData("list2")), - new Assert<>(task1, new Composite<>( - new TitleData("task1c"), - new VersionData(2))), - new Assert<>(task2, new Composite<>( - new TitleData("task2b"), - new VersionData(1))), - new Assert<>(task3, new Composite<>( - new TitleData("task3a"), - new VersionData(0))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task1, - new Composite( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TZ, null))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task2, - new Composite( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TZ, null))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task3, - new Composite( - new InstanceTestData(0), - new CharSequenceRowData<>(Tasks.TZ, null)) - ))); - } - - - /** - * Create task with start and due, check datetime values including generated duration. - */ - @Test - public void testInsertTaskWithStartAndDue() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now(); - DateTime due = start.addDuration(new Duration(1, 1, 0)); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData()), - new Put<>(task, new TimeData<>(start, due)) - - ), resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TimeData<>(start, due), - new VersionData(0))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - start.shiftTimeZone(TimeZone.getDefault()), - due.shiftTimeZone(TimeZone.getDefault()), - absent(), - 0), - new CharSequenceRowData<>(Tasks.TZ, "UTC")) - ))); +public class TaskProviderTest { + private ContentResolver mResolver; + private String mAuthority; + private Context mContext; + private ContentProviderClient mClient; + private final Account testAccount = new Account("foo", "bar"); + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getTargetContext(); + mResolver = mContext.getContentResolver(); + mAuthority = AuthorityUtil.taskAuthority(mContext); + mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority); + + // Assert that tables are empty: + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new TaskListsTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + queue.flush(); + } + + @After + public void tearDown() throws Exception { + /* + TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation + https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html + https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator + */ + + // Clear the DB: + BasicOperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue( + new Seq>( + new BulkDelete<>(new LocalTaskListsTable(mAuthority)), + new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority))))); + queue.flush(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mClient.close(); + } else { + mClient.release(); } - - - - /** - * Create task with start and due, check datetime values including generated duration. - */ - @Test - public void testInsertTaskWithAlldayStartAndDue() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now().toAllDay(); - DateTime due = start.addDuration(new Duration(1, 2, 0)); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData()), - new Put<>(task, new TimeData<>(start, due)) - - ), resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TimeData<>(start, due), - new VersionData(0))), + } + + /** Create 1 local task list and 1 task, check values in TaskLists, Tasks, Instances tables. */ + @Test + public void testSingleInsert() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + assertThat( + new Seq<>( + new Put<>(taskList, new NameData("list1")), new Put<>(task, new TitleData("task1"))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + new Assert<>(task, new Composite<>(new TitleData("task1"), new VersionData(0))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TZ, null))))); + } + + /** + * Create 1 local task list and 1 task, update task via instances table and check values in + * TaskLists, Tasks, Instances tables. + */ + @Test + public void testSingleInsertUpdateInstance() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + Table instancesTable = new InstanceTable(mAuthority); + + assertThat( + new Seq<>( + new Put<>(taskList, new NameData("list1")), + new Put<>(task, new TitleData("task1")), + new BulkUpdate<>( + instancesTable, + new CharSequenceRowData<>(Tasks.TITLE, "task updated"), + new ReferringTo<>(Instances.TASK_ID, task))), + resultsIn( + mClient, + new Assert<>(taskList, new NameData("list1")), + new Assert<>(task, new Composite<>(new TitleData("task updated"), new VersionData(1))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TZ, null))))); + } + + /** Create 2 task list and 3 tasks, check values. */ + @Test + public void testMultipleInserts() { + Table taskListsTable = new LocalTaskListsTable(mAuthority); + RowSnapshot taskList1 = new VirtualRowSnapshot<>(taskListsTable); + RowSnapshot taskList2 = new VirtualRowSnapshot<>(taskListsTable); + RowSnapshot task1 = + new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority))); + RowSnapshot task2 = + new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority))); + RowSnapshot task3 = + new VirtualRowSnapshot<>(new TaskListScoped(taskList2, new TasksTable(mAuthority))); + + assertThat( + new Seq<>( + new Put<>(taskList1, new NameData("list1")), + new Put<>(taskList2, new NameData("list2")), + new Put<>(task1, new TitleData("task1")), + new Put<>(task2, new TitleData("task2")), + new Put<>(task3, new TitleData("task3"))), + resultsIn( + mClient, + new Assert<>(taskList1, new NameData("list1")), + new Assert<>(taskList2, new NameData("list2")), + new Assert<>(task1, new Composite<>(new TitleData("task1"), new VersionData(0))), + new Assert<>(task2, new Composite<>(new TitleData("task2"), new VersionData(0))), + new Assert<>(task3, new Composite<>(new TitleData("task3"), new VersionData(0))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task1, + new Composite( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TZ, null))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task2, + new Composite( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TZ, null))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task3, + new Composite( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TZ, null))))); + } + + /** Create 2 task list and 3 tasks with updates, check values. */ + @Test + public void testMultipleInsertsAndUpdates() { + Table taskListsTable = new LocalTaskListsTable(mAuthority); + RowSnapshot taskList1 = new VirtualRowSnapshot<>(taskListsTable); + RowSnapshot taskList2 = new VirtualRowSnapshot<>(taskListsTable); + RowSnapshot task1 = + new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority))); + RowSnapshot task2 = + new VirtualRowSnapshot<>(new TaskListScoped(taskList1, new TasksTable(mAuthority))); + RowSnapshot task3 = + new VirtualRowSnapshot<>(new TaskListScoped(taskList2, new TasksTable(mAuthority))); + + assertThat( + new Seq<>( + new Put<>(taskList1, new NameData("list1")), + new Put<>(taskList2, new NameData("list2")), + new Put<>(task1, new TitleData("task1a")), + new Put<>(task2, new TitleData("task2a")), + new Put<>(task3, new TitleData("task3a")), + // update task 1 and 2 + new Put<>(task1, new TitleData("task1b")), + new Put<>(task2, new TitleData("task2b")), + // update task 1 once more + new Put<>(task1, new TitleData("task1c"))), + resultsIn( + mClient, + new Assert<>(taskList1, new NameData("list1")), + new Assert<>(taskList2, new NameData("list2")), + new Assert<>(task1, new Composite<>(new TitleData("task1c"), new VersionData(2))), + new Assert<>(task2, new Composite<>(new TitleData("task2b"), new VersionData(1))), + new Assert<>(task3, new Composite<>(new TitleData("task3a"), new VersionData(0))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task1, + new Composite( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TZ, null))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task2, + new Composite( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TZ, null))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task3, + new Composite( + new InstanceTestData(0), new CharSequenceRowData<>(Tasks.TZ, null))))); + } + + /** Create task with start and due, check datetime values including generated duration. */ + @Test + public void testInsertTaskWithStartAndDue() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now(); + DateTime due = start.addDuration(new Duration(1, 1, 0)); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData()), + new Put<>(task, new TimeData<>(start, due))), + resultsIn( + mClient, + new Assert<>(task, new Composite<>(new TimeData<>(start, due), new VersionData(0))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData( + start.shiftTimeZone(TimeZone.getDefault()), + due.shiftTimeZone(TimeZone.getDefault()), + absent(), + 0), + new CharSequenceRowData<>(Tasks.TZ, "UTC"))))); + } + + /** Create task with start and due, check datetime values including generated duration. */ + @Test + public void testInsertTaskWithAlldayStartAndDue() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now().toAllDay(); + DateTime due = start.addDuration(new Duration(1, 2, 0)); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData()), + new Put<>(task, new TimeData<>(start, due))), + resultsIn( + mClient, + new Assert<>(task, new Composite<>(new TimeData<>(start, due), new VersionData(0))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData(start, due, absent(), 0), + new CharSequenceRowData<>(Tasks.TZ, "UTC"))))); + } + + /** + * Create task with start and due, check datetime and INSTANCE_STATUS values after updating the + * status. + */ + @Test + public void testInsertTaskWithStartAndDueUpdateStatus() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now(); + DateTime due = start.addDuration(new Duration(1, 1, 0)); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>(task, new TimeData<>(start, due)), + // update the status of the new task + new Put<>(task, new StatusData<>(Tasks.STATUS_COMPLETED))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), new VersionData(1))), // task has been updated once + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData( + start.shiftTimeZone(TimeZone.getDefault()), + due.shiftTimeZone(TimeZone.getDefault()), + absent(), + -1), + new CharSequenceRowData<>(Tasks.TZ, "UTC"))))); + } + + /** + * Create task with start and due, check datetime and INSTANCE_STATUS values after updating the + * task twice. + */ + @Test + public void testInsertTaskWithStartAndDueUpdateTwice() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now(); + DateTime due = start.addDuration(new Duration(1, 1, 0)); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>(task, new TimeData<>(start, due)), + // update the status of the new task + new Put<>(task, new StatusData<>(Tasks.STATUS_COMPLETED)), + // update the title of the new task + new Put<>(task, new TitleData("Task Title"))), + resultsIn( + mClient, + new Assert<>( + task, + new Composite<>( + new TimeData<>(start, due), + new TitleData("Task Title"), + new VersionData(2))), // task has been updated twice + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData( + start.shiftTimeZone(TimeZone.getDefault()), + due.shiftTimeZone(TimeZone.getDefault()), + absent(), + -1), + new CharSequenceRowData<>(Tasks.TZ, "UTC"))))); + } + + /** + * Create task with start and due and update it with new values, check datetime values including + * generated duration. + */ + @Test + public void testInsertTaskWithStartAndDueMovedForward() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now(); + DateTime due = start.addDuration(new Duration(1, 1, 0)); + Duration duration = new Duration(1, 2, 0); + + DateTime startNew = start.addDuration(duration); + DateTime dueNew = due.addDuration(duration); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData()), + new Put<>(task, new TimeData<>(start, due)), + new Put<>(task, new TimeData<>(startNew, dueNew))), + resultsIn( + mClient, + new Assert<>( + task, new Composite<>(new TimeData<>(startNew, dueNew), new VersionData(1))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData( + startNew.shiftTimeZone(TimeZone.getDefault()), + dueNew.shiftTimeZone(TimeZone.getDefault()), + absent(), + 0), + new CharSequenceRowData<>(Tasks.TZ, "UTC"))))); + } + + /** + * Create task with start and due and update it with new values, check datetime values including + * generated duration. + */ + @Test + public void testInsertTaskWithStartAndDueMovedBackwards() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now(); + DateTime due = start.addDuration(new Duration(1, 1, 0)); + Duration duration = new Duration(-1, 2, 0); + + DateTime startNew = start.addDuration(duration); + DateTime dueNew = due.addDuration(duration); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData()), + new Put<>(task, new TimeData<>(start, due)), + new Put<>(task, new TimeData<>(startNew, dueNew))), + resultsIn( + mClient, + new Assert<>( + task, new Composite<>(new TimeData<>(startNew, dueNew), new VersionData(1))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData( + startNew.shiftTimeZone(TimeZone.getDefault()), + dueNew.shiftTimeZone(TimeZone.getDefault()), + absent(), + 0), + new CharSequenceRowData<>(Tasks.TZ, "UTC"))))); + } + + /** + * Create task without dates and set start and due afterwards, check datetime values including + * generated duration. + */ + @Test + public void testInsertTaskWithStartAndDueAddedAfterwards() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now(); + DateTime due = start.addDuration(new Duration(1, 1, 0)); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData()), + new Put<>(task, new TitleData("Test")), + new Put<>(task, new TimeData<>(start, due))), + resultsIn( + mClient, + new Assert<>(task, new Composite<>(new TimeData<>(start, due), new VersionData(1))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData( + start.shiftTimeZone(TimeZone.getDefault()), + due.shiftTimeZone(TimeZone.getDefault()), + absent(), + 0), + new CharSequenceRowData<>(Tasks.TZ, "UTC"))))); + } + + /** Create task with start and duration, check datetime values including generated due. */ + @Test + public void testInsertWithStartAndDuration() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now(); + Duration duration = Duration.parse("PT1H"); + long durationMillis = duration.toMillis(); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData()), + new Put<>(task, new TimeData<>(start, duration))), + resultsIn( + mClient, + new Assert<>( + task, new Composite<>(new TimeData<>(start, duration), new VersionData(0))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData( + start.shiftTimeZone(TimeZone.getDefault()), + start.shiftTimeZone(TimeZone.getDefault()).addDuration(duration), + absent(), + 0), + new CharSequenceRowData<>(Tasks.TZ, "UTC"))))); + } + + /** Create task with start and duration, check datetime values including generated due. */ + @Test + public void testInsertWithStartAndDurationChangeTimeZone() { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now(); + Duration duration = Duration.parse("PT1H"); + long durationMillis = duration.toMillis(); + DateTime startNew = start.shiftTimeZone(TimeZone.getTimeZone("America/New_York")); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData()), + new Put<>(task, new TimeData<>(start, duration)), + // update the task with a the same start in a different time zone + new Put<>(task, new TimeData<>(startNew, duration))), + resultsIn( + mClient, + new Assert<>( + task, new Composite<>(new TimeData<>(startNew, duration), new VersionData(1))), + // note that, apart from the time zone, all values stay the same + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData( + start.shiftTimeZone(TimeZone.getDefault()), + start.shiftTimeZone(TimeZone.getDefault()).addDuration(duration), + absent(), + 0), + new CharSequenceRowData<>(Tasks.TZ, "America/New_York"))))); + } + + /** + * Having a task with start and due. Update it with different due, check datetime values correct + * in Tasks and Instances. + */ + @Test + public void testUpdateDue() throws Exception { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + OperationsQueue queue = new BasicOperationsQueue(mClient); + + DateTime start = DateTime.now(); + DateTime due = start.addDuration(new Duration(1, 0, 1)); + + queue.enqueue( + new Seq<>( + new Put<>(taskList, new NameData("list1")), + new Put<>(task, new TimeData<>(start, due)))); + queue.flush(); + + DateTime due2 = due.addDuration(new Duration(1, 0, 2)); + + assertThat( + new SingletonIterable<>(new Put<>(task, new TimeData<>(start, due2))), + resultsIn( + queue, + new Assert<>(task, new Composite<>(new TimeData<>(start, due2), new VersionData(1))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite( + new InstanceTestData( + start.shiftTimeZone(TimeZone.getDefault()), + due2.shiftTimeZone(TimeZone.getDefault()), + absent(), + 0), + new CharSequenceRowData<>(Tasks.TZ, "UTC"))))); + } + + /** + * Having a single task. Delete task, check that it is removed from Tasks and Instances tables. + */ + @Test + public void testInstanceDelete() throws Exception { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table taskTable = new TaskListScoped(taskList, new TasksTable(mAuthority)); + RowSnapshot task = new VirtualRowSnapshot<>(taskTable); + OperationsQueue queue = new BasicOperationsQueue(mClient); + + queue.enqueue( + new Seq<>( + new Put<>(taskList, new NameData("list1")), new Put<>(task, new TitleData("task1")))); + queue.flush(); + + assertThat( + new SingletonIterable<>(new Delete<>(task)), + resultsIn( + queue, + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + } + + /** + * Having a single task. Delete the instance of that task, check that it is removed from Tasks and + * Instances tables. + */ + @Test + public void testDeleteInstance() throws Exception { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table taskTable = new TaskListScoped(taskList, new TasksTable(mAuthority)); + Table instancesTable = new InstanceTable(mAuthority); + RowSnapshot task = new VirtualRowSnapshot<>(taskTable); + OperationsQueue queue = new BasicOperationsQueue(mClient); + + queue.enqueue( + new Seq<>( + new Put<>(taskList, new NameData("list1")), new Put<>(task, new TitleData("task1")))); + queue.flush(); + + // check that removing the instance removes task and instance + assertThat( + new SingletonIterable<>( + new BulkDelete<>(instancesTable, new ReferringTo<>(Instances.TASK_ID, task))), + resultsIn( + queue, + new AssertEmptyTable<>(new TasksTable(mAuthority)), + new AssertEmptyTable<>(new InstanceTable(mAuthority)))); + } + + /** + * Contract: LIST_ID is required on task creation. + * + *

Create task without LIST_ID, check for IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void testInsertWithOutListId() throws Exception { + RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); + OperationsQueue queue = new BasicOperationsQueue(mClient); + queue.enqueue(new SingletonIterable>(new Put<>(task, new TitleData("task1")))); + queue.flush(); + } + + /** + * Contract: LIST_ID has to refer to existing TaskList. + * + *

Create task with a non-exsiting LIST_ID, check for IllegalArgumentException. + */ + @Test(expected = IllegalArgumentException.class) + public void testInsertWithInvalidTaskListId() { + ContentValues values = new ContentValues(); + values.put(Tasks.LIST_ID, 5); + mResolver.insert(Tasks.getContentUri(mAuthority), values); + } + + /** + * Contract: Setting ORIGINAL_INSTANCE_SYNC_ID for an exception task, provider must fill + * ORIGINAL_INSTANCE_ID with corresponding original task's _ID. + */ + @Test + public void testExceptionalInstance_settingSyncId_shouldUpdateRegularId() throws Exception { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table taskTable = new TaskListScoped(taskList, new TasksTable(mAuthority)); + RowSnapshot task = new VirtualRowSnapshot<>(taskTable); + RowSnapshot exceptionTask = new VirtualRowSnapshot<>(taskTable); + + OperationsQueue queue = new BasicOperationsQueue(mClient); + + queue.enqueue( + new Seq>( + new Put<>(taskList, new NameData("list1")), + new Put<>(task, new Composite<>(new TitleData("task1"), new SyncIdData("syncId1"))))); + queue.flush(); + + assertThat( + new SingletonIterable<>( + new Put<>( + exceptionTask, + new Composite<>( + new TitleData("task1exception"), + new OriginalInstanceSyncIdData("syncId1", new DateTime(0))))), + resultsIn( + queue, + new AssertRelated<>( + new TasksTable(mAuthority), + Tasks.ORIGINAL_INSTANCE_ID, + task, + new TitleData("task1exception")))); + } + + /** + * Contract: Setting ORIGINAL_INSTANCE_ID for an exception task, provider must fill + * ORIGINAL_INSTANCE_SYNC_ID with corresponding original task's _SYNC_ID. + */ + @Test + public void testExceptionalInstance_settingRegularId_shouldUpdateSyncId() throws Exception { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + Table taskTable = new TaskListScoped(taskList, new TasksTable(mAuthority)); + RowSnapshot task = new VirtualRowSnapshot<>(taskTable); + RowSnapshot exceptionTask = new VirtualRowSnapshot<>(taskTable); + + OperationsQueue queue = new BasicOperationsQueue(mClient); + + queue.enqueue( + new Seq>( + new Put<>(taskList, new NameData("list1")), + new Put<>(task, new Composite<>(new TitleData("task1"), new SyncIdData("syncId1"))))); + queue.flush(); + + DateTime now = DateTime.now(); + + assertThat( + new SingletonIterable<>( + new Put<>( + exceptionTask, + new Composite<>( + new TitleData("task1exception"), new OriginalInstanceData(task, now)))), + resultsIn( + queue, + new AssertRelated<>( + new TasksTable(mAuthority), + Tasks.ORIGINAL_INSTANCE_ID, + task, + new Composite<>( + new TitleData("task1exception"), + new OriginalInstanceSyncIdData("syncId1", now))))); + } + + /** Move a non-recurring task to another list. */ + @Test + public void testMoveTaskInstance() throws Exception { + RowSnapshot taskListOld = + new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot taskListNew = + new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskListOld, new TasksTable(mAuthority))); + OperationsQueue queue = new BasicOperationsQueue(mClient); + + // create two lists and a single task in the first list + queue.enqueue( + new Seq<>( + new Put<>(taskListOld, new NameData("list1")), + new Put<>(taskListNew, new NameData("list2")), + new Put<>(task, new TitleData("title")))); + queue.flush(); + + assertThat( + new SingletonIterable<>( + // update the sole task instance to the new list + new BulkUpdate<>( + new InstanceTable(mAuthority), + new Referring<>(Tasks.LIST_ID, taskListNew), + new ReferringTo<>(Tasks.LIST_ID, taskListOld))), + resultsIn( + queue, + // assert the old list is empty + new Counted<>( + 0, new AssertRelated<>(new InstanceTable(mAuthority), Tasks.LIST_ID, taskListOld)), + new Counted<>( + 0, new AssertRelated<>(new TasksTable(mAuthority), Tasks.LIST_ID, taskListOld)), + // assert the new list contains a single entry + new Counted<>( + 1, new AssertRelated<>(new InstanceTable(mAuthority), Tasks.LIST_ID, taskListNew)), + new Counted<>( + 1, new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - start, - due, - absent(), - 0), - new CharSequenceRowData<>(Tasks.TZ, "UTC")) - ))); - } - - - /** - * Create task with start and due, check datetime and INSTANCE_STATUS values after updating the status. - */ - @Test - public void testInsertTaskWithStartAndDueUpdateStatus() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now(); - DateTime due = start.addDuration(new Duration(1, 1, 0)); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, new TimeData<>(start, due)), - // update the status of the new task - new Put<>(task, new StatusData<>(Tasks.STATUS_COMPLETED)) - ), resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TimeData<>(start, due), - new VersionData(1))), // task has been updated once + new TasksTable(mAuthority), + Tasks.LIST_ID, + taskListNew, + new TitleData("title"))))); + } + + /** Move a non-recurring task to another list. */ + @Test + public void testMoveTaskInstanceAsSyncAdapter() throws Exception { + Table taskListsTable = new Synced<>(testAccount, new TaskListsTable(mAuthority)); + Table instancesTable = new Synced<>(testAccount, new InstanceTable(mAuthority)); + Table tasksTable = new Synced<>(testAccount, new TasksTable(mAuthority)); + + RowSnapshot taskListOld = new VirtualRowSnapshot<>(taskListsTable); + RowSnapshot taskListNew = new VirtualRowSnapshot<>(taskListsTable); + RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskListOld, tasksTable)); + OperationsQueue queue = new BasicOperationsQueue(mClient); + + // create two lists and a single task in the first list + queue.enqueue( + new Seq<>( + new Put<>(taskListOld, new NameData("list1")), + new Put<>(taskListNew, new NameData("list2")), + new Put<>( + task, + new Composite<>( + new SyncIdData("syncid"), // give it a sync id, so it counts as synced + new TitleData("title"))))); + queue.flush(); + + assertThat( + new SingletonIterable<>( + // update the sole task instance to the new list + new BulkUpdate<>( + new InstanceTable(mAuthority), + new Referring<>(Tasks.LIST_ID, taskListNew), + new ReferringTo<>(Tasks.LIST_ID, taskListOld))), + resultsIn( + queue, + // assert the old list contains a deleted entry for the task + new Counted<>(0, new AssertRelated<>(instancesTable, Tasks.LIST_ID, taskListOld)), + new Counted<>( + 1, new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - start.shiftTimeZone(TimeZone.getDefault()), - due.shiftTimeZone(TimeZone.getDefault()), - absent(), - -1), - new CharSequenceRowData<>(Tasks.TZ, "UTC")) - ))); - } - - - /** - * Create task with start and due, check datetime and INSTANCE_STATUS values after updating the task twice. - */ - @Test - public void testInsertTaskWithStartAndDueUpdateTwice() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now(); - DateTime due = start.addDuration(new Duration(1, 1, 0)); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, new TimeData<>(start, due)), - // update the status of the new task - new Put<>(task, new StatusData<>(Tasks.STATUS_COMPLETED)), - // update the title of the new task - new Put<>(task, new TitleData("Task Title")) - ), resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TimeData<>(start, due), - new TitleData("Task Title"), - new VersionData(2))), // task has been updated twice + tasksTable, + Tasks.LIST_ID, + taskListOld, + new Composite<>( + new TitleData("title"), new CharSequenceRowData<>(Tasks._DELETED, "1")))), + // assert the new list contains a single entry + new Counted<>(1, new AssertRelated<>(instancesTable, Tasks.LIST_ID, taskListNew)), + new Counted<>( + 1, new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - start.shiftTimeZone(TimeZone.getDefault()), - due.shiftTimeZone(TimeZone.getDefault()), - absent(), - -1), - new CharSequenceRowData<>(Tasks.TZ, "UTC")) - ))); - } - - - /** - * Create task with start and due and update it with new values, check datetime values including generated duration. - */ - @Test - public void testInsertTaskWithStartAndDueMovedForward() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now(); - DateTime due = start.addDuration(new Duration(1, 1, 0)); - Duration duration = new Duration(1, 2, 0); - - DateTime startNew = start.addDuration(duration); - DateTime dueNew = due.addDuration(duration); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData()), - new Put<>(task, new TimeData<>(start, due)), - new Put<>(task, new TimeData<>(startNew, dueNew)) - ), resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TimeData<>(startNew, dueNew), - new VersionData(1))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - startNew.shiftTimeZone(TimeZone.getDefault()), - dueNew.shiftTimeZone(TimeZone.getDefault()), - absent(), - 0), - new CharSequenceRowData<>(Tasks.TZ, "UTC")) - ))); - } - - - /** - * Create task with start and due and update it with new values, check datetime values including generated duration. - */ - @Test - public void testInsertTaskWithStartAndDueMovedBackwards() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now(); - DateTime due = start.addDuration(new Duration(1, 1, 0)); - Duration duration = new Duration(-1, 2, 0); - - DateTime startNew = start.addDuration(duration); - DateTime dueNew = due.addDuration(duration); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData()), - new Put<>(task, new TimeData<>(start, due)), - new Put<>(task, new TimeData<>(startNew, dueNew)) - ), resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TimeData<>(startNew, dueNew), - new VersionData(1))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - startNew.shiftTimeZone(TimeZone.getDefault()), - dueNew.shiftTimeZone(TimeZone.getDefault()), - absent(), - 0), - new CharSequenceRowData<>(Tasks.TZ, "UTC")) - ))); - } - - - /** - * Create task without dates and set start and due afterwards, check datetime values including generated duration. - */ - @Test - public void testInsertTaskWithStartAndDueAddedAfterwards() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now(); - DateTime due = start.addDuration(new Duration(1, 1, 0)); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData()), - new Put<>(task, new TitleData("Test")), - new Put<>(task, new TimeData<>(start, due)) - ), resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TimeData<>(start, due), - new VersionData(1))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - start.shiftTimeZone(TimeZone.getDefault()), - due.shiftTimeZone(TimeZone.getDefault()), - absent(), - 0), - new CharSequenceRowData<>(Tasks.TZ, "UTC")) - ))); - } - - - /** - * Create task with start and duration, check datetime values including generated due. - */ - @Test - public void testInsertWithStartAndDuration() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now(); - Duration duration = Duration.parse("PT1H"); - long durationMillis = duration.toMillis(); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData()), - new Put<>(task, new TimeData<>(start, duration)) - - ), resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TimeData<>(start, duration), - new VersionData(0))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - start.shiftTimeZone(TimeZone.getDefault()), - start.shiftTimeZone(TimeZone.getDefault()).addDuration(duration), - absent(), - 0), - new CharSequenceRowData<>(Tasks.TZ, "UTC")) - ))); - } - - - /** - * Create task with start and duration, check datetime values including generated due. - */ - @Test - public void testInsertWithStartAndDurationChangeTimeZone() - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now(); - Duration duration = Duration.parse("PT1H"); - long durationMillis = duration.toMillis(); - DateTime startNew = start.shiftTimeZone(TimeZone.getTimeZone("America/New_York")); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData()), - new Put<>(task, new TimeData<>(start, duration)), - // update the task with a the same start in a different time zone - new Put<>(task, new TimeData<>(startNew, duration)) - - ), resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TimeData<>(startNew, duration), - new VersionData(1))), - // note that, apart from the time zone, all values stay the same - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - start.shiftTimeZone(TimeZone.getDefault()), - start.shiftTimeZone(TimeZone.getDefault()).addDuration(duration), - absent(), - 0), - new CharSequenceRowData<>(Tasks.TZ, "America/New_York")) - ))); - } - - - /** - * Having a task with start and due. - * Update it with different due, check datetime values correct in Tasks and Instances. - */ - @Test - public void testUpdateDue() throws Exception - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - OperationsQueue queue = new BasicOperationsQueue(mClient); - - DateTime start = DateTime.now(); - DateTime due = start.addDuration(new Duration(1, 0, 1)); - - queue.enqueue(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new TimeData<>(start, due)) - )); - queue.flush(); - - DateTime due2 = due.addDuration(new Duration(1, 0, 2)); - - assertThat(new SingletonIterable<>( - new Put<>(task, new TimeData<>(start, due2)) - - ), resultsIn(queue, - new Assert<>(task, new Composite<>( - new TimeData<>(start, due2), - new VersionData(1))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite( - new InstanceTestData( - start.shiftTimeZone(TimeZone.getDefault()), - due2.shiftTimeZone(TimeZone.getDefault()), - absent(), - 0), - new CharSequenceRowData<>(Tasks.TZ, "UTC")) - ))); - } - - - /** - * Having a single task. - * Delete task, check that it is removed from Tasks and Instances tables. - */ - @Test - public void testInstanceDelete() throws Exception - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table taskTable = new TaskListScoped(taskList, new TasksTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(taskTable); - OperationsQueue queue = new BasicOperationsQueue(mClient); - - queue.enqueue(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new TitleData("task1")) - )); - queue.flush(); - - assertThat(new SingletonIterable<>( - new Delete<>(task) - - ), resultsIn(queue, - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)) - )); - } - - - /** - * Having a single task. - * Delete the instance of that task, check that it is removed from Tasks and Instances tables. - */ - @Test - public void testDeleteInstance() throws Exception - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table taskTable = new TaskListScoped(taskList, new TasksTable(mAuthority)); - Table instancesTable = new InstanceTable(mAuthority); - RowSnapshot task = new VirtualRowSnapshot<>(taskTable); - OperationsQueue queue = new BasicOperationsQueue(mClient); - - queue.enqueue(new Seq<>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new TitleData("task1")) - )); - queue.flush(); - - // check that removing the instance removes task and instance - assertThat(new SingletonIterable<>( - new BulkDelete<>(instancesTable, new ReferringTo<>(Instances.TASK_ID, task)) - - ), resultsIn(queue, - new AssertEmptyTable<>(new TasksTable(mAuthority)), - new AssertEmptyTable<>(new InstanceTable(mAuthority)) - )); - } - - - /** - * Contract: LIST_ID is required on task creation. - *

- * Create task without LIST_ID, check for IllegalArgumentException. - */ - @Test(expected = IllegalArgumentException.class) - public void testInsertWithOutListId() throws Exception - { - RowSnapshot task = new VirtualRowSnapshot<>(new TasksTable(mAuthority)); - OperationsQueue queue = new BasicOperationsQueue(mClient); - queue.enqueue(new SingletonIterable>(new Put<>(task, new TitleData("task1")))); - queue.flush(); - } - - - /** - * Contract: LIST_ID has to refer to existing TaskList. - *

- * Create task with a non-exsiting LIST_ID, check for IllegalArgumentException. - */ - @Test(expected = IllegalArgumentException.class) - public void testInsertWithInvalidTaskListId() - { - ContentValues values = new ContentValues(); - values.put(Tasks.LIST_ID, 5); - mResolver.insert(Tasks.getContentUri(mAuthority), values); - } - - - /** - * Contract: Setting ORIGINAL_INSTANCE_SYNC_ID for an exception task, - * provider must fill ORIGINAL_INSTANCE_ID with corresponding original task's _ID. - */ - @Test - public void testExceptionalInstance_settingSyncId_shouldUpdateRegularId() throws Exception - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table taskTable = new TaskListScoped(taskList, new TasksTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(taskTable); - RowSnapshot exceptionTask = new VirtualRowSnapshot<>(taskTable); - - OperationsQueue queue = new BasicOperationsQueue(mClient); - - queue.enqueue(new Seq>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new Composite<>( - new TitleData("task1"), - new SyncIdData("syncId1")) - ) - )); - queue.flush(); - - assertThat(new SingletonIterable<>( - new Put<>(exceptionTask, new Composite<>( - new TitleData("task1exception"), - new OriginalInstanceSyncIdData("syncId1", new DateTime(0))) - ) - - ), resultsIn(queue, - new AssertRelated<>(new TasksTable(mAuthority), Tasks.ORIGINAL_INSTANCE_ID, task, new TitleData("task1exception")) - )); - } - - - /** - * Contract: Setting ORIGINAL_INSTANCE_ID for an exception task, - * provider must fill ORIGINAL_INSTANCE_SYNC_ID with corresponding original task's _SYNC_ID. - */ - @Test - public void testExceptionalInstance_settingRegularId_shouldUpdateSyncId() throws Exception - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - Table taskTable = new TaskListScoped(taskList, new TasksTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(taskTable); - RowSnapshot exceptionTask = new VirtualRowSnapshot<>(taskTable); - - OperationsQueue queue = new BasicOperationsQueue(mClient); - - queue.enqueue(new Seq>( - new Put<>(taskList, new NameData("list1")), - new Put<>(task, new Composite<>( - new TitleData("task1"), - new SyncIdData("syncId1"))) - )); - queue.flush(); - - DateTime now = DateTime.now(); - - assertThat(new SingletonIterable<>( - new Put<>(exceptionTask, - new Composite<>( - new TitleData("task1exception"), - new OriginalInstanceData(task, now))) - - ), resultsIn(queue, - new AssertRelated<>(new TasksTable(mAuthority), Tasks.ORIGINAL_INSTANCE_ID, task, - new Composite<>( - new TitleData("task1exception"), - new OriginalInstanceSyncIdData("syncId1", now) - )) - )); - } - - - /** - * Move a non-recurring task to another list. - */ - @Test - public void testMoveTaskInstance() throws Exception - { - RowSnapshot taskListOld = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot taskListNew = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskListOld, new TasksTable(mAuthority))); - OperationsQueue queue = new BasicOperationsQueue(mClient); - - // create two lists and a single task in the first list - queue.enqueue(new Seq<>( - new Put<>(taskListOld, new NameData("list1")), - new Put<>(taskListNew, new NameData("list2")), - new Put<>(task, new TitleData("title")) - )); - queue.flush(); - - assertThat(new SingletonIterable<>( - // update the sole task instance to the new list - new BulkUpdate<>(new InstanceTable(mAuthority), new Referring<>(Tasks.LIST_ID, taskListNew), new ReferringTo<>(Tasks.LIST_ID, taskListOld)) - ), resultsIn(queue, - // assert the old list is empty - new Counted<>(0, new AssertRelated<>(new InstanceTable(mAuthority), Tasks.LIST_ID, taskListOld)), - new Counted<>(0, new AssertRelated<>(new TasksTable(mAuthority), Tasks.LIST_ID, taskListOld)), - // assert the new list contains a single entry - new Counted<>(1, new AssertRelated<>(new InstanceTable(mAuthority), Tasks.LIST_ID, taskListNew)), - new Counted<>(1, new AssertRelated<>(new TasksTable(mAuthority), Tasks.LIST_ID, taskListNew, new TitleData("title"))) - )); - } - - - /** - * Move a non-recurring task to another list. - */ - @Test - public void testMoveTaskInstanceAsSyncAdapter() throws Exception - { - Table taskListsTable = new Synced<>(testAccount, new TaskListsTable(mAuthority)); - Table instancesTable = new Synced<>(testAccount, new InstanceTable(mAuthority)); - Table tasksTable = new Synced<>(testAccount, new TasksTable(mAuthority)); - - RowSnapshot taskListOld = new VirtualRowSnapshot<>(taskListsTable); - RowSnapshot taskListNew = new VirtualRowSnapshot<>(taskListsTable); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskListOld, tasksTable)); - OperationsQueue queue = new BasicOperationsQueue(mClient); - - // create two lists and a single task in the first list - queue.enqueue(new Seq<>( - new Put<>(taskListOld, new NameData("list1")), - new Put<>(taskListNew, new NameData("list2")), - new Put<>(task, new Composite<>( - new SyncIdData("syncid"), // give it a sync id, so it counts as synced - new TitleData("title"))))); - queue.flush(); - - assertThat(new SingletonIterable<>( - // update the sole task instance to the new list - new BulkUpdate<>(new InstanceTable(mAuthority), new Referring<>(Tasks.LIST_ID, taskListNew), new ReferringTo<>(Tasks.LIST_ID, taskListOld)) - ), resultsIn(queue, - // assert the old list contains a deleted entry for the task - new Counted<>(0, - new AssertRelated<>( - instancesTable, - Tasks.LIST_ID, - taskListOld)), - new Counted<>(1, - new AssertRelated<>( - tasksTable, - Tasks.LIST_ID, - taskListOld, - new Composite<>( - new TitleData("title"), - new CharSequenceRowData<>(Tasks._DELETED, "1")))), - // assert the new list contains a single entry - new Counted<>(1, new AssertRelated<>(instancesTable, Tasks.LIST_ID, taskListNew)), - new Counted<>(1, new AssertRelated<>(tasksTable, Tasks.LIST_ID, taskListNew, new TitleData("title"))) - )); - } - - - /** - * Create task with start and due, check datetime values including generated duration. - */ - @Test - public void testInsertTaskWithoutStartAndDueButRRULE() throws InvalidRecurrenceRuleException - { - RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); - RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); - - DateTime start = DateTime.now(); - DateTime due = start.addDuration(new Duration(1, 1, 0)); - - assertThat(new Seq<>( - new Put<>(taskList, new EmptyRowData<>()), - new Put<>(task, new Composite<>( - new TitleData("test"), - new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX))))), - resultsIn(mClient, - new Assert<>(task, new Composite<>( - new TitleData("test"), - new VersionData(0))), - new AssertRelated<>( - new InstanceTable(mAuthority), Instances.TASK_ID, task, - new Composite<>( - new CharSequenceRowData<>(Tasks.TITLE, "test"), - new InstanceTestData( - absent(), - absent(), - absent(), - 0), - new CharSequenceRowData<>(Tasks.TZ, null)) - ))); - } - + tasksTable, Tasks.LIST_ID, taskListNew, new TitleData("title"))))); + } + + /** Create task with start and due, check datetime values including generated duration. */ + @Test + public void testInsertTaskWithoutStartAndDueButRRULE() throws InvalidRecurrenceRuleException { + RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority)); + RowSnapshot task = + new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority))); + + DateTime start = DateTime.now(); + DateTime due = start.addDuration(new Duration(1, 1, 0)); + + assertThat( + new Seq<>( + new Put<>(taskList, new EmptyRowData<>()), + new Put<>( + task, + new Composite<>( + new TitleData("test"), + new RRuleTaskData( + new RecurrenceRule( + "FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX))))), + resultsIn( + mClient, + new Assert<>(task, new Composite<>(new TitleData("test"), new VersionData(0))), + new AssertRelated<>( + new InstanceTable(mAuthority), + Instances.TASK_ID, + task, + new Composite<>( + new CharSequenceRowData<>(Tasks.TITLE, "test"), + new InstanceTestData(absent(), absent(), absent(), 0), + new CharSequenceRowData<>(Tasks.TZ, null))))); + } } diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/matchers/NotifiesMatcher.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/matchers/NotifiesMatcher.java index 39014d6c..834b9bed 100644 --- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/matchers/NotifiesMatcher.java +++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/matchers/NotifiesMatcher.java @@ -21,102 +21,87 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; - +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import org.dmfs.android.contentpal.Operation; import org.dmfs.android.contentpal.OperationsQueue; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; - -import androidx.annotation.NonNull; -import androidx.test.platform.app.InstrumentationRegistry; - - /** * @author Marten Gajda */ -public final class NotifiesMatcher extends TypeSafeDiagnosingMatcher>> -{ - private final Uri mUri; - private final OperationsQueue mOperationsQueue; - private final Matcher> mDelegate; - - - public static Matcher>> notifies(@NonNull Uri uri, @NonNull OperationsQueue operationsQueue, @NonNull Matcher> delegate) - { - return new NotifiesMatcher(uri, operationsQueue, delegate); - } - - - public NotifiesMatcher(Uri uri, @NonNull OperationsQueue operationsQueue, @NonNull Matcher> delegate) - { - mUri = uri; - mOperationsQueue = operationsQueue; - mDelegate = delegate; - } - - - @Override - protected boolean matchesSafely(Iterable> item, Description mismatchDescription) - { - Collection notifications = Collections.synchronizedCollection(new HashSet<>()); - HandlerThread handlerThread = new HandlerThread("ObserverHandlerThread"); - handlerThread.start(); - - ContentObserver observer = new ContentObserver(new Handler(handlerThread.getLooper())) - { - @Override - public void onChange(boolean selfChange, Uri uri) - { - super.onChange(selfChange, uri); - System.out.println("Notifcation: " + uri); - notifications.add(uri); - } +public final class NotifiesMatcher + extends TypeSafeDiagnosingMatcher>> { + private final Uri mUri; + private final OperationsQueue mOperationsQueue; + private final Matcher> mDelegate; + + public static Matcher>> notifies( + @NonNull Uri uri, + @NonNull OperationsQueue operationsQueue, + @NonNull Matcher> delegate) { + return new NotifiesMatcher(uri, operationsQueue, delegate); + } + + public NotifiesMatcher( + Uri uri, + @NonNull OperationsQueue operationsQueue, + @NonNull Matcher> delegate) { + mUri = uri; + mOperationsQueue = operationsQueue; + mDelegate = delegate; + } + + @Override + protected boolean matchesSafely( + Iterable> item, Description mismatchDescription) { + Collection notifications = Collections.synchronizedCollection(new HashSet<>()); + HandlerThread handlerThread = new HandlerThread("ObserverHandlerThread"); + handlerThread.start(); + + ContentObserver observer = + new ContentObserver(new Handler(handlerThread.getLooper())) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + System.out.println("Notifcation: " + uri); + notifications.add(uri); + } }; - Context context = InstrumentationRegistry.getInstrumentation().getContext(); - context.getContentResolver().registerContentObserver(mUri, true, observer); - try - { - try - { - mOperationsQueue.enqueue(item); - mOperationsQueue.flush(); - } - catch (Exception e) - { - throw new RuntimeException("Exception during executing the target OperationBatch", e); - } - - Thread.sleep(100); - if (!mDelegate.matches(notifications)) - { - mismatchDescription.appendText("Wrong notifications "); - mDelegate.describeMismatch(notifications, mismatchDescription); - return false; - } - return true; - } - catch (InterruptedException e) - { - e.printStackTrace(); - return false; - } - finally - { - context.getContentResolver().unregisterContentObserver(observer); - handlerThread.quit(); - } + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + context.getContentResolver().registerContentObserver(mUri, true, observer); + try { + try { + mOperationsQueue.enqueue(item); + mOperationsQueue.flush(); + } catch (Exception e) { + throw new RuntimeException("Exception during executing the target OperationBatch", e); + } + + Thread.sleep(100); + if (!mDelegate.matches(notifications)) { + mismatchDescription.appendText("Wrong notifications "); + mDelegate.describeMismatch(notifications, mismatchDescription); + return false; + } + return true; + } catch (InterruptedException e) { + e.printStackTrace(); + return false; + } finally { + context.getContentResolver().unregisterContentObserver(observer); + handlerThread.quit(); } + } - - @Override - public void describeTo(Description description) - { - description.appendText("Notifies ").appendDescriptionOf(mDelegate); - } + @Override + public void describeTo(Description description) { + description.appendText("Notifies ").appendDescriptionOf(mDelegate); + } } diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/matchers/UriMatcher.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/matchers/UriMatcher.java index 8af65df0..60b2e65a 100644 --- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/matchers/UriMatcher.java +++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/matchers/UriMatcher.java @@ -16,34 +16,25 @@ package org.dmfs.provider.tasks.matchers; -import android.net.Uri; - -import org.hamcrest.Matcher; - import static org.dmfs.jems.hamcrest.matchers.LambdaMatcher.having; import static org.hamcrest.Matchers.is; +import android.net.Uri; +import org.hamcrest.Matcher; /** * @author Marten Gajda */ -public final class UriMatcher -{ - public static Matcher scheme(String scheme) - { - return having(Uri::getScheme, is(scheme)); - } - - - public static Matcher authority(String authority) - { - return having(Uri::getEncodedAuthority, is(authority)); - } - - - public static Matcher path(Matcher patchMatcher) - { - return having(Uri::getEncodedPath, patchMatcher); - } - +public final class UriMatcher { + public static Matcher scheme(String scheme) { + return having(Uri::getScheme, is(scheme)); + } + + public static Matcher authority(String authority) { + return having(Uri::getEncodedAuthority, is(authority)); + } + + public static Matcher path(Matcher patchMatcher) { + return having(Uri::getEncodedPath, patchMatcher); + } } diff --git a/opentasks-provider/src/androidTest/res/values/opentasks_defaults.xml b/opentasks-provider/src/androidTest/res/values/opentasks_defaults.xml index d3b53833..589a32da 100644 --- a/opentasks-provider/src/androidTest/res/values/opentasks_defaults.xml +++ b/opentasks-provider/src/androidTest/res/values/opentasks_defaults.xml @@ -6,4 +6,4 @@ translatable="false">org.dmfs.tasks.test - \ No newline at end of file + diff --git a/opentasks-provider/src/main/java/org/dmfs/ngrams/NGramGenerator.java b/opentasks-provider/src/main/java/org/dmfs/ngrams/NGramGenerator.java index 58e30648..bf472fed 100644 --- a/opentasks-provider/src/main/java/org/dmfs/ngrams/NGramGenerator.java +++ b/opentasks-provider/src/main/java/org/dmfs/ngrams/NGramGenerator.java @@ -22,147 +22,116 @@ import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; - /** * Generator for N-grams from a given String. * * @author Marten Gajda */ -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(); - - - public NGramGenerator(int n) - { - this(n, 1); +public final class NGramGenerator { + /** A {@link Pattern} that matches anything that doesn't belong to a word or number. */ + private static final Pattern SEPARATOR_PATTERN = Pattern.compile("[^\\p{L}\\p{M}\\d]+"); + + /** A {@link Pattern} that matches anything that doesn't belong to a word. */ + private static final 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(); + + public NGramGenerator(int n) { + this(n, 1); + } + + public NGramGenerator(int n, int minWordLen) { + mN = n; + mMinWordLen = minWordLen; + } + + /** + * 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 The {@link Set} containing the N-grams. + */ + public Set getNgrams(String data) { + if (data == null) { + return Collections.emptySet(); } - - public NGramGenerator(int n, int minWordLen) - { - mN = n; - mMinWordLen = minWordLen; + if (mAllLowercase) { + data = data.toLowerCase(mLocale); } + String[] words = + mReturnNumbers ? SEPARATOR_PATTERN.split(data) : SEPARATOR_PATTERN_NO_NUMBERS.split(data); - /** - * 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 set = new HashSet(128); - - /** - * 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; + for (String word : words) { + getNgrams(word, set); } + return set; + } - /** - * 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; - } - + private void getNgrams(String word, Set ngrams) { + final int len = word.length(); - /** - * Get all N-grams contained in the given String. - * - * @param data - * The String to analyze. - * - * @return The {@link Set} containing the N-grams. - */ - public Set getNgrams(String data) - { - if (data == null) - { - return Collections.emptySet(); - } - - if (mAllLowercase) - { - data = data.toLowerCase(mLocale); - } - - String[] words = mReturnNumbers ? SEPARATOR_PATTERN.split(data) : SEPARATOR_PATTERN_NO_NUMBERS.split(data); - - Set set = new HashSet(128); - - for (String word : words) - { - getNgrams(word, set); - } - - return set; + if (len < mMinWordLen) { + return; } + final int n = mN; + final int last = Math.max(1, len - n + 1); - private void getNgrams(String word, Set ngrams) - { - final int len = word.length(); - - if (len < mMinWordLen) - { - 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))); - } + 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. - */ - ngrams.add(" " + word.substring(0, Math.min(len, n - 1))); - } + if (mAddSpaceInFront) { + /* + * Add another String with a space and the first n-1 characters of the word. + */ + ngrams.add(" " + word.substring(0, Math.min(len, n - 1))); } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/AuthorityUtil.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/AuthorityUtil.java index 81182b49..d12d7ad6 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/AuthorityUtil.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/AuthorityUtil.java @@ -17,27 +17,22 @@ package org.dmfs.provider.tasks; import android.content.Context; - import org.dmfs.tasks.provider.R; - /** * Access for the authority name of the tasks content provider. * * @author Gabor Keszthelyi */ -// TODO Figure out better design or at least rename to TaskAuthority.get(context) (results in changes in many files) -public final class AuthorityUtil -{ - private static String sCachedValue; - +// TODO Figure out better design or at least rename to TaskAuthority.get(context) (results in +// changes in many files) +public final class AuthorityUtil { + private static String sCachedValue; - public static String taskAuthority(Context context) - { - if (sCachedValue == null) - { - sCachedValue = context.getString(R.string.opentasks_authority); - } - return sCachedValue; + public static String taskAuthority(Context context) { + if (sCachedValue == null) { + sCachedValue = context.getString(R.string.opentasks_authority); } + return sCachedValue; + } } 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 622533c4..4951db2c 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 @@ -28,7 +28,7 @@ import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Handler; import android.util.Log; - +import java.util.TimeZone; import org.dmfs.provider.tasks.model.CursorContentValuesInstanceAdapter; import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter; import org.dmfs.provider.tasks.model.InstanceAdapter; @@ -39,381 +39,392 @@ import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.contract.TaskContract.Tasks; -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() - { +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); - Instantiating.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); + 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); + Instantiating.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() - { + /** 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 instancesCursor = 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 (instancesCursor.moveToNext()) - { - InstanceAdapter task = new CursorContentValuesInstanceAdapter(InstanceAdapter._ID.getFrom(instancesCursor), instancesCursor, null); - - DateTime instanceDue = task.valueOf(InstanceAdapter.INSTANCE_DUE); - if (instanceDue != null && !instanceDue.isFloating()) - { - // make sure we compare instances in local time - instanceDue = instanceDue.shiftTimeZone(localTimeZone); - } - - DateTime instanceStart = task.valueOf(InstanceAdapter.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())); - } - 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())); - } - } - } - finally - { - instancesCursor.close(); + 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 instancesCursor = + 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 (instancesCursor.moveToNext()) { + InstanceAdapter task = + new CursorContentValuesInstanceAdapter( + InstanceAdapter._ID.getFrom(instancesCursor), instancesCursor, null); + + DateTime instanceDue = task.valueOf(InstanceAdapter.INSTANCE_DUE); + if (instanceDue != null && !instanceDue.isFloating()) { + // make sure we compare instances in local time + instanceDue = instanceDue.shiftTimeZone(localTimeZone); + } + + DateTime instanceStart = task.valueOf(InstanceAdapter.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())); + } 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())); + } } + } finally { + instancesCursor.close(); + } - // all notifications up to now have been triggered - saveLastAlarmTime(context, now); + // all notifications up to now have been triggered + saveLastAlarmTime(context, now); - // set the alarm for the next notification - UPDATE_NOTIFICATION_ALARM.fire(context, null); + // 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()); - editor.apply(); + 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()); + editor.apply(); } - - 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())); + 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 instance that has started or became due. * - * @param context - * A {@link Context}. - * @param action - * The broadcast action. - * @param uri - * The task uri. + * @param context A {@link Context}. + * @param action The broadcast action. + * @param uri The task uri. */ - private void sendBroadcast(Context context, String action, Uri uri) - { - Intent intent = new Intent(action); - intent.setData(uri); - // only notify our own package - intent.setPackage(context.getPackageName()); - context.sendBroadcast(intent); + private void sendBroadcast(Context context, String action, Uri uri) { + Intent intent = new Intent(action); + intent.setData(uri); + // only notify our own package + intent.setPackage(context.getPackageName()); + 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() - { + /** + * 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 nextInstanceStartCursor = 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 (nextInstanceStartCursor.moveToNext()) - { - TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(nextInstanceStartCursor), - nextInstanceStartCursor, null); - nextAlarm = task.valueOf(TaskAdapter.INSTANCE_START); - if (!nextAlarm.isFloating()) - { - nextAlarm = nextAlarm.shiftTimeZone(localTimeZone); - } - } - } - finally - { - nextInstanceStartCursor.close(); - } - - // find the next task that's due - Cursor nextInstanceDueCursor = 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 (nextInstanceDueCursor.moveToNext()) - { - TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(nextInstanceDueCursor), nextInstanceDueCursor, - null); - DateTime nextDue = task.valueOf(TaskAdapter.INSTANCE_DUE); - if (!nextDue.isFloating()) - { - nextDue = nextDue.shiftTimeZone(localTimeZone); - } - - if (nextAlarm == null || nextAlarm.getInstance() > nextDue.getInstance()) - { - nextAlarm = nextDue; - } - } + 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 nextInstanceStartCursor = + 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 (nextInstanceStartCursor.moveToNext()) { + TaskAdapter task = + new CursorContentValuesTaskAdapter( + TaskAdapter.INSTANCE_TASK_ID.getFrom(nextInstanceStartCursor), + nextInstanceStartCursor, + null); + nextAlarm = task.valueOf(TaskAdapter.INSTANCE_START); + if (!nextAlarm.isFloating()) { + nextAlarm = nextAlarm.shiftTimeZone(localTimeZone); + } } - finally - { - nextInstanceDueCursor.close(); + } finally { + nextInstanceStartCursor.close(); + } + + // find the next task that's due + Cursor nextInstanceDueCursor = + 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 (nextInstanceDueCursor.moveToNext()) { + TaskAdapter task = + new CursorContentValuesTaskAdapter( + TaskAdapter.INSTANCE_TASK_ID.getFrom(nextInstanceDueCursor), + nextInstanceDueCursor, + 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 { + nextInstanceDueCursor.close(); + } - if (nextAlarm != null) - { - TaskProviderBroadcastReceiver.planNotificationUpdate(context, nextAlarm); - } - else - { - saveLastAlarmTime(context, now); - } + 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()); - editor.apply(); + 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()); + editor.apply(); } - - 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())); + 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"; - - - 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(AuthorityUtil.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); - } + }); + + /** A lock object to serialize the execution of all incoming {@link ContentOperation}. */ + private static final Object mLock = new Object(); + + /** The base path of the Uri to trigger content operations. */ + private static final 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"; + + 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(AuthorityUtil.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()); } - - - /** - * 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; } - - /** - * 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]; + if (id - firstId >= values().length) { + return null; } + return values()[id - firstId]; + } - public interface OperationHandler - { - void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values); - } - + public interface OperationHandler { + 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 1093a4f5..716f6248 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 @@ -20,7 +20,11 @@ import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; - +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import org.dmfs.jems.iterable.decorators.Chunked; import org.dmfs.ngrams.NGramGenerator; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; @@ -30,601 +34,596 @@ import org.dmfs.tasks.contract.TaskContract.Properties; import org.dmfs.tasks.contract.TaskContract.TaskColumns; import org.dmfs.tasks.contract.TaskContract.Tasks; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - - /** * Supports the {@link TaskDatabaseHelper} in the matter of full-text-search. * * @author Tobias Reinsch * @author Marten Gajda */ -public class FTSDatabaseHelper -{ - /** - * We search the ngram table in chunks of 500. This should be good enough for an average task but still well below - * the SQLITE expression length limit and the variable count limit. - */ - private final static int NGRAM_SEARCH_CHUNK_SIZE = 500; - - private final static float SEARCH_RESULTS_MIN_SCORE = 0.33f; - - /** - * A Generator for 3-grams. - */ - private final static NGramGenerator TRIGRAM_GENERATOR = new NGramGenerator(3, 1).setAddSpaceInFront(true); +public class FTSDatabaseHelper { + /** + * We search the ngram table in chunks of 500. This should be good enough for an average task but + * still well below the SQLITE expression length limit and the variable count limit. + */ + private static final int NGRAM_SEARCH_CHUNK_SIZE = 500; + + private static final float SEARCH_RESULTS_MIN_SCORE = 0.33f; + + /** A Generator for 3-grams. */ + private static final NGramGenerator TRIGRAM_GENERATOR = + new NGramGenerator(3, 1).setAddSpaceInFront(true); + + /** A Generator for 4-grams. */ + private static final NGramGenerator TETRAGRAM_GENERATOR = + new NGramGenerator(4, 3 /* shorter words are fully covered by trigrams */) + .setAddSpaceInFront(true); + + private static final String PROPERTY_NGRAM_SELECTION = + String.format( + "%s = ? AND %s = ? AND %s = ?", + FTSContentColumns.TASK_ID, FTSContentColumns.TYPE, FTSContentColumns.PROPERTY_ID); + private static final String NON_PROPERTY_NGRAM_SELECTION = + String.format( + "%s = ? AND %s = ? AND %s is null", + FTSContentColumns.TASK_ID, FTSContentColumns.TYPE, FTSContentColumns.PROPERTY_ID); + private static final String[] NGRAM_SYNC_COLUMNS = {"_rowid_", FTSContentColumns.NGRAM_ID}; + + /** + * 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. */ + String TASK_ID = "fts_task_id"; /** - * A Generator for 4-grams. + * The the property id of the searchable entry or null if the entry is not related + * to a property. */ - private final static NGramGenerator TETRAGRAM_GENERATOR = new NGramGenerator(4, 3 /* shorter words are fully covered by trigrams */).setAddSpaceInFront( - true); - private static final String PROPERTY_NGRAM_SELECTION = String.format("%s = ? AND %s = ? AND %s = ?", FTSContentColumns.TASK_ID, FTSContentColumns.TYPE, - FTSContentColumns.PROPERTY_ID); - private static final String NON_PROPERTY_NGRAM_SELECTION = String.format("%s = ? AND %s = ? AND %s is null", FTSContentColumns.TASK_ID, + String PROPERTY_ID = "fts_property_id"; + + /** The the type of the searchable entry */ + String TYPE = "fts_type"; + + /** An n-gram for a task. */ + 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. */ + String NGRAM_ID = "ngram_id"; + + /** The content of the N-gram */ + 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 static final 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 static final 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 static final String SQL_RAW_QUERY_SEARCH_TASK = + "SELECT %s " + + ", (1.0*count(DISTINCT " + + NGramColumns.NGRAM_ID + + ")/?) 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 + + " and " + + Tasks.VISIBLE + + " = 1 order by %s;"; + + private static final String SQL_RAW_QUERY_SEARCH_TASK_DEFAULT_PROJECTION = + Tables.INSTANCE_VIEW + ".* ," + FTS_NGRAM_TABLE + "." + NGramColumns.TEXT; + + private static final 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 static final 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. */ + int TITLE = 1; + + /** This is an entry for the description of a task. */ + int DESCRIPTION = 2; + + /** This is an entry for the location of a task. */ + int LOCATION = 3; + + /** This is an entry for a property of a task. */ + 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.PROPERTY_ID); - private static final String[] NGRAM_SYNC_COLUMNS = { "_rowid_", FTSContentColumns.NGRAM_ID }; - - - /** - * 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. - */ - 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. - */ - String PROPERTY_ID = "fts_property_id"; - - /** - * The the type of the searchable entry - */ - String TYPE = "fts_type"; - - /** - * An n-gram for a task. - */ - String NGRAM_ID = "fts_ngram_id"; - + 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)); } - - - /** - * The columns of the N-gram table for the FTS search - * - * @author Tobias Reinsch - */ - public interface NGramColumns - { - /** - * The row id of the N-gram. - */ - String NGRAM_ID = "ngram_id"; - - /** - * The content of the N-gram - */ - String TEXT = "ngram_text"; - + 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); } - - 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 " + ", (1.0*count(DISTINCT " + NGramColumns.NGRAM_ID + ")/?) 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 - + " and " + Tasks.VISIBLE + " = 1 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. - */ - int TITLE = 1; - - /** - * This is an entry for the description of a task. - */ - int DESCRIPTION = 2; - - /** - * This is an entry for the location of a task. - */ - int LOCATION = 3; - - /** - * This is an entry for a property of a task. - */ - int PROPERTY = 4; - + // location + if (location != null && location.length() > 0) { + updateEntry(db, taskId, -1, SearchableTypes.LOCATION, location); } - - public static void onCreate(SQLiteDatabase db) - { - initializeFTS(db); + // description + if (description != null && description.length() > 0) { + updateEntry(db, taskId, -1, SearchableTypes.DESCRIPTION, description); } - - - 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)); - } + } + + /** + * 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)); } - - /** - * 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)); - + // location + if (task.isUpdated(TaskAdapter.LOCATION)) { + updateEntry(db, task.id(), -1, SearchableTypes.LOCATION, task.valueOf(TaskAdapter.LOCATION)); } - - /** - * 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(); + // description + if (task.isUpdated(TaskAdapter.DESCRIPTION)) { + updateEntry( + db, task.id(), -1, SearchableTypes.DESCRIPTION, task.valueOf(TaskAdapter.DESCRIPTION)); } - - - /** - * 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 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); + } + + /** + * Returns the IDs of each of the provided ngrams, creating them in th database if necessary. + * + * @param db A writable {@link SQLiteDatabase}. + * @param ngrams The NGrams. + * @return The ids of the ngrams in the given set. + */ + private static Set ngramIds(SQLiteDatabase db, Set ngrams) { + if (ngrams.size() == 0) { + return Collections.emptySet(); } - - /** - * 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)); + Set missingNgrams = new HashSet<>(ngrams); + Set ngramIds = new HashSet<>(ngrams.size() * 2); + + for (Iterable chunk : new Chunked<>(NGRAM_SEARCH_CHUNK_SIZE, ngrams)) { + // build selection and arguments for each chunk + // we can't do this in a single query because the length of sql statement and number of + // arguments is limited. + + StringBuilder selection = new StringBuilder(NGramColumns.TEXT); + selection.append(" in ("); + boolean first = true; + List arguments = new ArrayList<>(NGRAM_SEARCH_CHUNK_SIZE); + for (String ngram : chunk) { + if (first) { + first = false; + } else { + selection.append(","); } - - // 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)); + selection.append("?"); + arguments.add(ngram); + } + selection.append(" )"); + + try (Cursor c = + db.query( + FTS_NGRAM_TABLE, + new String[] {NGramColumns.NGRAM_ID, NGramColumns.TEXT}, + selection.toString(), + arguments.toArray(new String[0]), + null, + null, + null)) { + while (c.moveToNext()) { + // remove the ngrams we already have in the table + missingNgrams.remove(c.getString(1)); + // remember its id + ngramIds.add(c.getLong(0)); } - + } } + ContentValues values = new ContentValues(1); - /** - * 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); + // now insert the missing ngrams and store their ids + for (String ngram : missingNgrams) { + values.put(NGramColumns.TEXT, ngram); + ngramIds.add(db.insert(FTS_NGRAM_TABLE, null, values)); } - - - /** - * Returns the IDs of each of the provided ngrams, creating them in th database if necessary. - * - * @param db - * A writable {@link SQLiteDatabase}. - * @param ngrams - * The NGrams. - * - * @return The ids of the ngrams in the given set. - */ - private static Set ngramIds(SQLiteDatabase db, Set ngrams) - { - if (ngrams.size() == 0) - { - return Collections.emptySet(); - } - - Set missingNgrams = new HashSet<>(ngrams); - Set ngramIds = new HashSet<>(ngrams.size() * 2); - - for (Iterable chunk : new Chunked<>(NGRAM_SEARCH_CHUNK_SIZE, ngrams)) - { - // build selection and arguments for each chunk - // we can't do this in a single query because the length of sql statement and number of arguments is limited. - - StringBuilder selection = new StringBuilder(NGramColumns.TEXT); - selection.append(" in ("); - boolean first = true; - List arguments = new ArrayList<>(NGRAM_SEARCH_CHUNK_SIZE); - for (String ngram : chunk) - { - if (first) - { - first = false; - } - else - { - selection.append(","); - } - selection.append("?"); - arguments.add(ngram); - } - selection.append(" )"); - - try (Cursor c = db.query(FTS_NGRAM_TABLE, new String[] { NGramColumns.NGRAM_ID, NGramColumns.TEXT }, selection.toString(), - arguments.toArray(new String[0]), null, null, null)) - { - while (c.moveToNext()) - { - // remove the ngrams we already have in the table - missingNgrams.remove(c.getString(1)); - // remember its id - ngramIds.add(c.getLong(0)); - } - } - } - - ContentValues values = new ContentValues(1); - - // now insert the missing ngrams and store their ids - for (String ngram : missingNgrams) - { - values.put(NGramColumns.TEXT, ngram); - ngramIds.add(db.insert(FTS_NGRAM_TABLE, null, values)); - } - return ngramIds; - + return ngramIds; + } + + private static void updateEntry( + SQLiteDatabase db, long taskId, long propertyId, int type, String searchableText) { + // generate nGrams + Set propertyNgrams = TRIGRAM_GENERATOR.getNgrams(searchableText); + propertyNgrams.addAll(TETRAGRAM_GENERATOR.getNgrams(searchableText)); + + // get an ID for each of the Ngrams. + Set ngramIds = ngramIds(db, propertyNgrams); + + // unlink unused ngrams from the task and get the missing ones we have to link to the tak + Set missing = syncNgrams(db, taskId, propertyId, type, ngramIds); + + // insert ngram relations for all new ngrams + addNgrams(db, missing, 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. + */ + private static void addNgrams( + 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.insert(FTS_CONTENT_TABLE, null, values); } - - - private static void updateEntry(SQLiteDatabase db, long taskId, long propertyId, int type, String searchableText) - { - // generate nGrams - Set propertyNgrams = TRIGRAM_GENERATOR.getNgrams(searchableText); - propertyNgrams.addAll(TETRAGRAM_GENERATOR.getNgrams(searchableText)); - - // get an ID for each of the Ngrams. - Set ngramIds = ngramIds(db, propertyNgrams); - - // unlink unused ngrams from the task and get the missing ones we have to link to the tak - Set missing = syncNgrams(db, taskId, propertyId, type, ngramIds); - - // insert ngram relations for all new ngrams - addNgrams(db, missing, taskId, propertyId, type); + } + + /** + * Synchronizes 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. + * @param ngramsIds The set of ngrams ids which should be linked to the task + * @return The number of deleted relations. + */ + private static Set syncNgrams( + SQLiteDatabase db, long taskId, long propertyId, int contentType, Set ngramsIds) { + String selection; + String[] selectionArgs; + if (SearchableTypes.PROPERTY == contentType) { + selection = PROPERTY_NGRAM_SELECTION; + selectionArgs = + new String[] { + String.valueOf(taskId), String.valueOf(contentType), String.valueOf(propertyId) + }; + } else { + selection = NON_PROPERTY_NGRAM_SELECTION; + selectionArgs = new String[] {String.valueOf(taskId), String.valueOf(contentType)}; } - - /** - * 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. - */ - private static void addNgrams(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.insert(FTS_CONTENT_TABLE, null, values); + // In order to sync the ngrams, we go over each existing ngram and delete ngram relations not in + // the set of new ngrams + // Then we return the set of ngrams we didn't find + Set missing = new HashSet<>(ngramsIds); + try (Cursor c = + db.query( + FTS_CONTENT_TABLE, NGRAM_SYNC_COLUMNS, selection, selectionArgs, null, null, null)) { + while (c.moveToNext()) { + Long ngramId = c.getLong(1); + if (!ngramsIds.contains(ngramId)) { + db.delete(FTS_CONTENT_TABLE, "_rowid_ = ?", new String[] {c.getString(0)}); + } else { + // this ngram wasn't missing + missing.remove(ngramId); } - + } } - - - /** - * Synchronizes 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. - * @param ngramsIds - * The set of ngrams ids which should be linked to the task - * - * @return The number of deleted relations. - */ - private static Set syncNgrams(SQLiteDatabase db, long taskId, long propertyId, int contentType, Set ngramsIds) - { - String selection; - String[] selectionArgs; - if (SearchableTypes.PROPERTY == contentType) - { - selection = PROPERTY_NGRAM_SELECTION; - selectionArgs = new String[] { String.valueOf(taskId), String.valueOf(contentType), String.valueOf(propertyId) }; - } - else - { - selection = NON_PROPERTY_NGRAM_SELECTION; - selectionArgs = new String[] { String.valueOf(taskId), String.valueOf(contentType) }; - } - - // In order to sync the ngrams, we go over each existing ngram and delete ngram relations not in the set of new ngrams - // Then we return the set of ngrams we didn't find - Set missing = new HashSet<>(ngramsIds); - try (Cursor c = db.query(FTS_CONTENT_TABLE, NGRAM_SYNC_COLUMNS, selection, selectionArgs, null, null, null)) - { - while (c.moveToNext()) - { - Long ngramId = c.getLong(1); - if (!ngramsIds.contains(ngramId)) - { - db.delete(FTS_CONTENT_TABLE, "_rowid_ = ?", new String[] { c.getString(0) }); - } - else - { - // this ngram wasn't missing - missing.remove(ngramId); - } - } - } - return missing; + return missing; + } + + /** + * 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); + ngrams.addAll(TETRAGRAM_GENERATOR.getNgrams(searchString)); - /** - * 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); + String[] queryArgs; - if (!TextUtils.isEmpty(selection)) - { - selectionBuilder.append(" ("); - selectionBuilder.append(selection); - selectionBuilder.append(") AND ("); - } - else - { - selectionBuilder.append(" ("); - } + if (searchString != null && searchString.length() > 1) { - Set ngrams = TRIGRAM_GENERATOR.getNgrams(searchString); - ngrams.addAll(TETRAGRAM_GENERATOR.getNgrams(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(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"); + 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; + 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 2d50bfdb..e8903fb6 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 @@ -19,21 +19,14 @@ package org.dmfs.provider.tasks; /** * @author Marten Gajda */ -public enum ProviderOperation -{ +public enum ProviderOperation { - /** - * Insert operations. - */ - INSERT, + /** Insert operations. */ + INSERT, - /** - * Update operations. - */ - UPDATE, + /** Update operations. */ + UPDATE, - /** - * Delete operations. - */ - DELETE + /** Delete operations. */ + DELETE } 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 21c54ae0..8de0bd39 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 @@ -26,339 +26,287 @@ import android.content.OperationApplicationException; 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.Locale; +import java.util.Set; import org.dmfs.iterables.SingletonIterable; import org.dmfs.jems.fragile.Fragile; import org.dmfs.jems.iterable.composite.Joined; import org.dmfs.jems.single.Single; import org.dmfs.provider.tasks.utils.Profiled; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Locale; -import java.util.Set; - - -/** - * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage. - */ +/** General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage. */ /* * Changed by marten@dmfs.org: - * + * * removed protected mDb field and replaced it by local fields. There is no reason to store the database if we get a new one for every transaction. Instead we * also pass the database to the *InTransaction methods. - * + * * update visibility of class and methods */ -abstract class SQLiteContentProvider extends ContentProvider -{ - - interface TransactionEndTask - { - void execute(SQLiteDatabase database); - } - - - @SuppressWarnings("unused") - private static final String TAG = "SQLiteContentProvider"; - - private SQLiteOpenHelper mOpenHelper; - private final Set mChangedUris = new HashSet<>(); - - 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; - - private final Iterable mTransactionEndTasks; - - - protected SQLiteContentProvider(Iterable transactionEndTasks) - { - // append a task to set the transaction to successful - mTransactionEndTasks = new Joined<>(transactionEndTasks, new SingletonIterable<>(new SuccessfulTransactionEndTask())); - } - - - @Override - public boolean onCreate() - { - mOpenHelper = getDatabaseHelper(getContext()); - 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; +abstract class SQLiteContentProvider extends ContentProvider { + + interface TransactionEndTask { + void execute(SQLiteDatabase database); + } + + @SuppressWarnings("unused") + private static final String TAG = "SQLiteContentProvider"; + + private SQLiteOpenHelper mOpenHelper; + private final Set mChangedUris = new HashSet<>(); + + 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; + + private final Iterable mTransactionEndTasks; + + protected SQLiteContentProvider(Iterable transactionEndTasks) { + // append a task to set the transaction to successful + mTransactionEndTasks = + new Joined<>( + transactionEndTasks, new SingletonIterable<>(new SuccessfulTransactionEndTask())); + } + + @Override + public boolean onCreate() { + mOpenHelper = getDatabaseHelper(getContext()); + 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 SQLiteOpenHelper getDatabaseHelper() - { - return mOpenHelper; - } - - - private boolean applyingBatch() - { - return mApplyingBatch.get() != null && mApplyingBatch.get(); - } - - - @Override - public Uri insert(Uri uri, ContentValues values) - { - return new Profiled("Insert").run((Single) () -> - { - Uri result; - boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); - boolean applyingBatch = applyingBatch(); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - if (!applyingBatch) - { - db.beginTransaction(); - try - { + } + + 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) { + return new Profiled("Insert") + .run( + (Single) + () -> { + Uri result; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + if (!applyingBatch) { + db.beginTransaction(); + try { + result = insertInTransaction(db, uri, values, callerIsSyncAdapter); + endTransaction(db); + } finally { + db.endTransaction(); + } + onEndTransaction(callerIsSyncAdapter); + } else { result = insertInTransaction(db, uri, values, callerIsSyncAdapter); + } + return result; + }); + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + return new Profiled("BulkInsert") + .run( + (Single) + () -> { + 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(); + } endTransaction(db); - } - finally - { - db.endTransaction(); - } - onEndTransaction(callerIsSyncAdapter); - } - else - { - result = insertInTransaction(db, uri, values, callerIsSyncAdapter); - } - return result; - }); - } - - - @Override - public int bulkInsert(Uri uri, ContentValues[] values) - { - return new Profiled("BulkInsert").run((Single) () -> - { - 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(); - } - endTransaction(db); - } - finally - { - db.endTransaction(); - } - onEndTransaction(callerIsSyncAdapter); - return numValues; - }); - } - - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) - { - return new Profiled("Update").run((Single) () -> - { - int count; - boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); - boolean applyingBatch = applyingBatch(); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - if (!applyingBatch) - { - db.beginTransaction(); - try - { - count = updateInTransaction(db, uri, values, selection, selectionArgs, callerIsSyncAdapter); - endTransaction(db); - } - 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) - { - return new Profiled("Delete").run((Single) () -> - { - int count; - boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); - boolean applyingBatch = applyingBatch(); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - if (!applyingBatch) - { - db.beginTransaction(); - try - { - count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter); - endTransaction(db); - } - finally - { + } finally { db.endTransaction(); - } - onEndTransaction(callerIsSyncAdapter); - } - else - { - count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter); - } - return count; - }); - } - - - @Override - public ContentProviderResult[] applyBatch(ArrayList operations) throws OperationApplicationException - { - return new Profiled(String.format(Locale.ENGLISH, "Batch of %d operations", operations.size())).run( - (Fragile) () -> - { - int ypCount = 0; - int opCount = 0; - boolean callerIsSyncAdapter = false; - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + } + onEndTransaction(callerIsSyncAdapter); + return numValues; + }); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return new Profiled("Update") + .run( + (Single) + () -> { + int count; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + if (!applyingBatch) { 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); - } - endTransaction(db); - return results; + try { + count = + updateInTransaction( + db, uri, values, selection, selectionArgs, callerIsSyncAdapter); + endTransaction(db); + } finally { + db.endTransaction(); } - finally - { - mApplyingBatch.set(false); - db.endTransaction(); - onEndTransaction(callerIsSyncAdapter); + onEndTransaction(callerIsSyncAdapter); + } else { + count = + updateInTransaction( + db, uri, values, selection, selectionArgs, callerIsSyncAdapter); + } + return count; + }); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return new Profiled("Delete") + .run( + (Single) + () -> { + int count; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + if (!applyingBatch) { + db.beginTransaction(); + try { + count = + deleteInTransaction( + db, uri, selection, selectionArgs, callerIsSyncAdapter); + endTransaction(db); + } finally { + db.endTransaction(); } + onEndTransaction(callerIsSyncAdapter); + } else { + count = + deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter); + } + return count; }); - } - + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList operations) + throws OperationApplicationException { + return new Profiled(String.format(Locale.ENGLISH, "Batch of %d operations", operations.size())) + .run( + (Fragile) + () -> { + 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); + } + endTransaction(db); + 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 void onEndTransaction(boolean callerIsSyncAdapter) { + Set changed; + synchronized (mChangedUris) { + changed = new HashSet(mChangedUris); + mChangedUris.clear(); } - - - protected boolean syncToNetwork(Uri uri) - { - return false; + 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; + } - private void endTransaction(SQLiteDatabase database) - { - for (TransactionEndTask task : mTransactionEndTasks) - { - task.execute(database); - } + private void endTransaction(SQLiteDatabase database) { + for (TransactionEndTask task : mTransactionEndTasks) { + task.execute(database); } + } - - /** - * A {@link TransactionEndTask} which sets the transaction to be successful. - */ - private static class SuccessfulTransactionEndTask implements TransactionEndTask - { - @Override - public void execute(SQLiteDatabase database) - { - database.setTransactionSuccessful(); - } + /** A {@link TransactionEndTask} which sets the transaction to be successful. */ + private static class SuccessfulTransactionEndTask implements TransactionEndTask { + @Override + public void execute(SQLiteDatabase database) { + database.setTransactionSuccessful(); } + } } 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 a78d8c76..3fecb621 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 @@ -22,7 +22,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; - +import java.util.Locale; import org.dmfs.jems.optional.adapters.First; import org.dmfs.jems.predicate.elementary.Equals; import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter; @@ -38,858 +38,1532 @@ import org.dmfs.tasks.contract.TaskContract.Property.Category; import org.dmfs.tasks.contract.TaskContract.TaskLists; import org.dmfs.tasks.contract.TaskContract.Tasks; -import java.util.Locale; - - /** - * Task database helper takes care of creating and updating the task database, including tables, indices and triggers. + * 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 - { - void onDatabaseCreated(SQLiteDatabase db); - - void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion); +public class TaskDatabaseHelper extends SQLiteOpenHelper { + + /** Interface of a listener that's called when the database has been created or migrated. */ + public interface OnDatabaseOperationListener { + void onDatabaseCreated(SQLiteDatabase db); + + void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion); + } + + private static final String TAG = "TaskDatabaseHelper"; + + /** The name of our database file. */ + private static final String DATABASE_NAME = "tasks.db"; + + /** The database version. */ + private static final int DATABASE_VERSION = 23; + + /** List of all tables we provide. */ + public interface Tables { + String LISTS = "Lists"; + + String WRITEABLE_LISTS = "Writeable_Lists"; + + String TASKS = "Tasks"; + + String TASKS_VIEW = "Task_View"; + + String TASKS_PROPERTY_VIEW = "Task_Property_View"; + + String INSTANCES = "Instances"; + + String INSTANCE_VIEW = "Instance_View"; + + String INSTANCE_CLIENT_VIEW = "Instance_Client_View"; + + String INSTANCE_PROPERTY_VIEW = "Instance_Property_View"; + + String INSTANCE_CATEGORY_VIEW = "Instance_Cagetory_View"; + + String CATEGORIES = "Categories"; + + String CATEGORIES_MAPPING = "Categories_Mapping"; + + String PROPERTIES = "Properties"; + + String ALARMS = "Alarms"; + + String SYNCSTATE = "SyncState"; + } + + /** Columns of internal table for the category mapping. */ + public interface CategoriesMapping { + String TASK_ID = "task_id"; + + String CATEGORY_ID = "category_id"; + + String PROPERTY_ID = "property_id"; + } + + /** + * SQL command to create a view that combines tasks with some data from the list they belong to. + */ + private static final String SQL_CREATE_TASK_VIEW = + "create view " + + Tables.TASKS_VIEW + + " as select " + + Tables.TASKS + + ".*, " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_TYPE + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_OWNER + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_ACCESS_LEVEL + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_COLOR + + ", " + + Tables.LISTS + + "." + + Tasks.VISIBLE + + " from " + + Tables.TASKS + + " join " + + Tables.LISTS + + " on (" + + Tables.TASKS + + "." + + Tasks.LIST_ID + + "=" + + Tables.LISTS + + "." + + TaskLists._ID + + ");"; + + /** + * SQL command to create a view that combines tasks with some data from the list they belong to. + */ + private static final String SQL_CREATE_TASK_PROPERTY_VIEW = + "create view " + + Tables.TASKS_PROPERTY_VIEW + + " as select " + + Tables.TASKS + + ".*, " + + Tables.PROPERTIES + + ".*, " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_TYPE + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_OWNER + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_ACCESS_LEVEL + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_COLOR + + ", " + + Tables.LISTS + + "." + + Tasks.VISIBLE + + " from " + + Tables.TASKS + + " join " + + Tables.LISTS + + " on (" + + Tables.TASKS + + "." + + Tasks.LIST_ID + + "=" + + Tables.LISTS + + "." + + TaskLists._ID + + ") " + + "left join " + + Tables.PROPERTIES + + " on (" + + Tables.TASKS + + "." + + Tasks._ID + + "=" + + Tables.PROPERTIES + + "." + + Properties.TASK_ID + + ");"; + + /** SQL command to drop the task view. */ + private static final String SQL_DROP_TASK_VIEW = "DROP VIEW " + Tables.TASKS_VIEW + ";"; + + /** + * SQL command to create a view that combines task instances with some data from the list they + * belong to. + */ + private static final String SQL_CREATE_INSTANCE_VIEW = + "CREATE VIEW " + + Tables.INSTANCE_VIEW + + " AS SELECT " + + Tables.INSTANCES + + ".*, " + + Tables.TASKS + + ".*, " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_TYPE + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_OWNER + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_ACCESS_LEVEL + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_COLOR + + ", " + + Tables.LISTS + + "." + + Tasks.VISIBLE + + " FROM " + + Tables.TASKS + + " JOIN " + + Tables.LISTS + + " ON (" + + Tables.TASKS + + "." + + TaskContract.Tasks.LIST_ID + + "=" + + Tables.LISTS + + "." + + TaskContract.Tasks._ID + + ")" + + " JOIN " + + Tables.INSTANCES + + " ON (" + + Tables.TASKS + + "." + + TaskContract.Tasks._ID + + "=" + + Tables.INSTANCES + + "." + + TaskContract.Instances.TASK_ID + + ");"; + + /** + * SQL command to create a view that combines task instances with some data from the list they + * belong to. This replaces the task DTSTART, DUE and ORIGINAL_INSTANCE_TIME values with + * respective values of the instance. + * + *

This is the instances view as seen by the content provider clients. + */ + private static final String SQL_CREATE_INSTANCE_CLIENT_VIEW = + "CREATE VIEW " + + Tables.INSTANCE_CLIENT_VIEW + + " AS SELECT " + + Tables.INSTANCES + + ".*, " + // override task due, start and original times with the instance values + + Tables.INSTANCES + + "." + + TaskContract.Instances.INSTANCE_START + + " as " + + Tasks.DTSTART + + ", " + + Tables.INSTANCES + + "." + + TaskContract.Instances.INSTANCE_DUE + + " as " + + Tasks.DUE + + ", " + + Tables.INSTANCES + + "." + + TaskContract.Instances.INSTANCE_ORIGINAL_TIME + + " as " + + Tasks.ORIGINAL_INSTANCE_TIME + + ", " + // override task duration with null, we already have a due + + "null as " + + Tasks.DURATION + + ", " + // override recurrence values with null, instances themselves are not recurring + + "null as " + + Tasks.RRULE + + ", " + + "null as " + + Tasks.RDATE + + ", " + + "null as " + + Tasks.EXDATE + + ", " + // this instance is part of a recurring task if either it has recurrence values or + // overrides an instance + + "not (" + + Tasks.RRULE + + " is null and " + + Tasks.RDATE + + " is null and " + + Tasks.ORIGINAL_INSTANCE_ID + + " is null and " + + Tasks.ORIGINAL_INSTANCE_SYNC_ID + + " is null) as " + + TaskContract.Instances.IS_RECURRING + + ", " + + Tables.TASKS + + ".*, " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_TYPE + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_OWNER + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_ACCESS_LEVEL + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_COLOR + + ", " + + Tables.LISTS + + "." + + Tasks.VISIBLE + + " FROM " + + Tables.TASKS + + " JOIN " + + Tables.LISTS + + " ON (" + + Tables.TASKS + + "." + + TaskContract.Tasks.LIST_ID + + "=" + + Tables.LISTS + + "." + + TaskContract.TaskLists._ID + + ")" + + " JOIN " + + Tables.INSTANCES + + " ON (" + + Tables.TASKS + + "." + + TaskContract.Tasks._ID + + "=" + + Tables.INSTANCES + + "." + + TaskContract.Instances.TASK_ID + + ");"; + + /** + * SQL command to create a view that combines task instances view with the belonging properties. + */ + private static final String SQL_CREATE_INSTANCE_PROPERTY_VIEW = + "CREATE VIEW " + + Tables.INSTANCE_PROPERTY_VIEW + + " AS SELECT " + + Tables.INSTANCES + + ".*, " + + Tables.PROPERTIES + + ".*, " + + Tables.TASKS + + ".*, " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_TYPE + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_OWNER + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_ACCESS_LEVEL + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_COLOR + + ", " + + Tables.LISTS + + "." + + Tasks.VISIBLE + + " FROM " + + Tables.TASKS + + " JOIN " + + Tables.LISTS + + " ON (" + + Tables.TASKS + + "." + + TaskContract.Tasks.LIST_ID + + "=" + + Tables.LISTS + + "." + + TaskContract.Tasks._ID + + ")" + + " JOIN " + + Tables.INSTANCES + + " ON (" + + Tables.TASKS + + "." + + TaskContract.Tasks._ID + + "=" + + Tables.INSTANCES + + "." + + TaskContract.Instances.TASK_ID + + ")" + + " LEFT JOIN " + + Tables.PROPERTIES + + " ON (" + + Tables.TASKS + + "." + + Tasks._ID + + "=" + + Tables.PROPERTIES + + "." + + Properties.TASK_ID + + ");"; + + /** + * SQL command to create a view that combines task instances with some data from the list they + * belong to. + */ + private static final String SQL_CREATE_INSTANCE_CATEGORY_VIEW = + "CREATE VIEW " + + Tables.INSTANCE_CATEGORY_VIEW + + " AS SELECT " + + Tables.INSTANCES + + ".*, " + + Tables.CATEGORIES_MAPPING + + "." + + CategoriesMapping.CATEGORY_ID + + ", " + + Tables.TASKS + + ".*, " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.ACCOUNT_TYPE + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_OWNER + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_NAME + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_ACCESS_LEVEL + + ", " + + Tables.LISTS + + "." + + Tasks.LIST_COLOR + + ", " + + Tables.LISTS + + "." + + Tasks.VISIBLE + + " FROM " + + Tables.TASKS + + " JOIN " + + Tables.LISTS + + " ON (" + + Tables.TASKS + + "." + + TaskContract.Tasks.LIST_ID + + "=" + + Tables.LISTS + + "." + + TaskContract.Tasks._ID + + ")" + + " JOIN " + + Tables.INSTANCES + + " ON (" + + Tables.TASKS + + "." + + TaskContract.Tasks._ID + + "=" + + Tables.INSTANCES + + "." + + TaskContract.Instances.TASK_ID + + ")" + + " LEFT JOIN " + + Tables.CATEGORIES_MAPPING + + " ON (" + + Tables.CATEGORIES_MAPPING + + "." + + CategoriesMapping.TASK_ID + + "=" + + Tables.INSTANCES + + "." + + TaskContract.Instances.TASK_ID + + ");"; + + /** SQL command to drop the instance view. */ + private static final String SQL_DROP_INSTANCE_VIEW = "DROP VIEW " + Tables.INSTANCE_VIEW + ";"; + + /** SQL command to drop the instance property view. */ + // private final static String SQL_DROP_INSTANCE_PROPERTY_VIEW = "DROP VIEW " + + // Tables.INSTANCE_PROPERTY_VIEW + ";"; + + /** SQL command to create the instances table. */ + private static final String SQL_CREATE_SYNCSTATE_TABLE = + "CREATE TABLE " + + Tables.SYNCSTATE + + " ( " + + TaskContract.SyncState._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + TaskContract.SyncState.ACCOUNT_NAME + + " TEXT, " + + TaskContract.SyncState.ACCOUNT_TYPE + + " TEXT, " + + TaskContract.SyncState.DATA + + " TEXT " + + ");"; + + /** SQL command to create the instances table. */ + private static final String SQL_CREATE_INSTANCES_TABLE = + "CREATE TABLE " + + Tables.INSTANCES + + " ( " + + TaskContract.Instances._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + TaskContract.Instances.TASK_ID + + " INTEGER NOT NULL, " // NOT NULL + + TaskContract.Instances.INSTANCE_START + + " INTEGER, " + + TaskContract.Instances.INSTANCE_DUE + + " INTEGER, " + + TaskContract.Instances.INSTANCE_START_SORTING + + " INTEGER, " + + TaskContract.Instances.INSTANCE_DUE_SORTING + + " INTEGER, " + + TaskContract.Instances.INSTANCE_DURATION + + " INTEGER, " + + TaskContract.Instances.INSTANCE_ORIGINAL_TIME + + " INTEGER DEFAULT 0, " + + TaskContract.Instances.DISTANCE_FROM_CURRENT + + " INTEGER DEFAULT 0);"; + + /** SQL command to create a trigger to clean up data of removed tasks. */ + private static final String SQL_CREATE_TASKS_CLEANUP_TRIGGER = + "CREATE TRIGGER task_cleanup_trigger AFTER DELETE ON " + + Tables.TASKS + + " BEGIN " + + " DELETE FROM " + + Tables.PROPERTIES + + " WHERE " + + TaskContract.Properties.TASK_ID + + "= old." + + TaskContract.Tasks._ID + + ";" + + " DELETE FROM " + + Tables.INSTANCES + + " WHERE " + + TaskContract.Instances.TASK_ID + + "=old." + + TaskContract.Tasks._ID + + ";" + + " END;"; + + /** SQL command to create a trigger to clean up data of removed lists. */ + private static final String SQL_CREATE_LISTS_CLEANUP_TRIGGER = + "CREATE TRIGGER list_cleanup_trigger AFTER DELETE ON " + + Tables.LISTS + + " BEGIN " + + " DELETE FROM " + + Tables.TASKS + + " WHERE " + + Tasks.LIST_ID + + "= old." + + TaskLists._ID + + ";" + + " END;"; + + /** SQL command to drop the clean up trigger. */ + private static final String SQL_DROP_TASKS_CLEANUP_TRIGGER = "DROP TRIGGER task_cleanup_trigger;"; + + /** SQL command that counts and sets the alarm on deletion */ + private static final String SQL_COUNT_ALARMS_ON_DELETE = + " BEGIN UPDATE " + + Tables.TASKS + + " SET " + + Tasks.HAS_ALARMS + + " = (SELECT COUNT (*) FROM " + + Tables.PROPERTIES + + " WHERE " + + Properties.MIMETYPE + + " = '" + + Alarm.CONTENT_ITEM_TYPE + + "' AND " + + Alarm.ALARM_TYPE + + " <> " + + Alarm.ALARM_TYPE_NOTHING + + " AND " + + Properties.TASK_ID + + " = OLD." + + Properties.TASK_ID + + ") WHERE " + + Tasks._ID + + " = OLD." + + Properties.TASK_ID + + "; END;"; + + /** SQL command that counts and sets the alarm on insert and update */ + private static final String SQL_COUNT_ALARMS = + " BEGIN UPDATE " + + Tables.TASKS + + " SET " + + Tasks.HAS_ALARMS + + " = (SELECT COUNT (*) FROM " + + Tables.PROPERTIES + + " WHERE " + + Properties.MIMETYPE + + " = '" + + Alarm.CONTENT_ITEM_TYPE + + "' AND " + + Alarm.ALARM_TYPE + + " <> " + + Alarm.ALARM_TYPE_NOTHING + + " AND " + + Properties.TASK_ID + + " = NEW." + + Properties.TASK_ID + + ") WHERE " + + Tasks._ID + + " = NEW." + + Properties.TASK_ID + + "; END;"; + + /** SQL command to create a trigger that counts the alarms for a task on create */ + private static final String SQL_CREATE_ALARM_COUNT_CREATE_TRIGGER = + "CREATE TRIGGER alarm_count_create_trigger AFTER INSERT ON " + + Tables.PROPERTIES + + " WHEN NEW." + + Properties.MIMETYPE + + " = '" + + Alarm.CONTENT_ITEM_TYPE + + "'" + + SQL_COUNT_ALARMS; + + /** SQL command to create a trigger that counts the alarms for a task on update */ + private static final String SQL_CREATE_ALARM_COUNT_UPDATE_TRIGGER = + "CREATE TRIGGER alarm_count_update_trigger AFTER UPDATE ON " + + Tables.PROPERTIES + + " WHEN NEW." + + Properties.MIMETYPE + + " = '" + + Alarm.CONTENT_ITEM_TYPE + + "'" + + SQL_COUNT_ALARMS; + + /** SQL command to create a trigger that counts the alarms for a task on delete */ + private static final String SQL_CREATE_ALARM_COUNT_DELETE_TRIGGER = + "CREATE TRIGGER alarm_count_delete_trigger AFTER DELETE ON " + + Tables.PROPERTIES + + " WHEN OLD." + + Properties.MIMETYPE + + " = '" + + Alarm.CONTENT_ITEM_TYPE + + "'" + + SQL_COUNT_ALARMS_ON_DELETE; + + /** SQL command to create a trigger to clean up data of removed property. */ + private static final String SQL_CREATE_ALARM_PROPERTY_CLEANUP_TRIGGER = + "CREATE TRIGGER alarm_property_cleanup_trigger AFTER DELETE ON " + + Tables.PROPERTIES + + " WHEN OLD." + + Properties.MIMETYPE + + " = '" + + Alarm.CONTENT_ITEM_TYPE + + "'" + + " BEGIN " + + " DELETE FROM " + + Tables.ALARMS + + " WHERE " + + TaskContract.Alarms.ALARM_ID + + "= OLD." + + TaskContract.Properties.PROPERTY_ID + + ";" + + " END;"; + + /** SQL command to create a trigger to clean up data of removed property. */ + private static final String SQL_CREATE_CATEGORY_PROPERTY_CLEANUP_TRIGGER = + "CREATE TRIGGER category_property_cleanup_trigger AFTER DELETE ON " + + Tables.PROPERTIES + + " WHEN OLD." + + Properties.MIMETYPE + + " = '" + + Category.CONTENT_ITEM_TYPE + + "'" + + " BEGIN " + + " DELETE FROM " + + Tables.CATEGORIES_MAPPING + + " WHERE " + + CategoriesMapping.PROPERTY_ID + + "= OLD." + + TaskContract.Properties.PROPERTY_ID + + ";" + + " END;"; + + /** SQL command to create a trigger to clean up property data of removed task. */ + private static final String SQL_CREATE_TASK_PROPERTY_CLEANUP_TRIGGER = + "CREATE TRIGGER task_property_cleanup_trigger AFTER DELETE ON " + + Tables.TASKS + + " BEGIN " + + " DELETE FROM " + + Tables.PROPERTIES + + " WHERE " + + Properties.TASK_ID + + "= OLD." + + Tasks._ID + + ";" + + " END;"; + + /** SQL command to create a trigger to increment task version number on every update. */ + private static final String SQL_CREATE_TASK_VERSION_TRIGGER = + "CREATE TRIGGER task_version_trigger BEFORE UPDATE ON " + + Tables.TASKS + + " BEGIN " + + " UPDATE " + + Tables.TASKS + + " SET " + + Tasks.VERSION + + " = OLD." + + Tasks.VERSION + + " + 1 where " + + Tasks._ID + + " = NEW." + + Tasks._ID + + ";" + + " END;"; + + /** SQL command to create the task list table. */ + private static final String SQL_CREATE_LISTS_TABLE = + "CREATE TABLE " + + Tables.LISTS + + " ( " + + TaskContract.TaskLists._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + TaskContract.TaskLists.ACCOUNT_NAME + + " TEXT," + + TaskContract.TaskLists.ACCOUNT_TYPE + + " TEXT," + + TaskContract.TaskLists.LIST_NAME + + " TEXT," + + TaskContract.TaskLists.LIST_COLOR + + " INTEGER," + + TaskContract.TaskLists.ACCESS_LEVEL + + " INTEGER," + + TaskContract.TaskLists.VISIBLE + + " INTEGER," + + TaskContract.TaskLists.SYNC_ENABLED + + " INTEGER," + + TaskContract.TaskLists.OWNER + + " TEXT," + + TaskContract.TaskLists._DIRTY + + " INTEGER DEFAULT 0," + + TaskContract.TaskLists._SYNC_ID + + " TEXT," + + TaskContract.TaskLists.SYNC_VERSION + + " TEXT," + + TaskContract.TaskLists.SYNC1 + + " TEXT," + + TaskContract.TaskLists.SYNC2 + + " TEXT," + + TaskContract.TaskLists.SYNC3 + + " TEXT," + + TaskContract.TaskLists.SYNC4 + + " TEXT," + + TaskContract.TaskLists.SYNC5 + + " TEXT," + + TaskContract.TaskLists.SYNC6 + + " TEXT," + + TaskContract.TaskLists.SYNC7 + + " TEXT," + + TaskContract.TaskLists.SYNC8 + + " TEXT);"; + + /** SQL command to create the task table. */ + private static final String SQL_CREATE_TASKS_TABLE = + "CREATE TABLE " + + Tables.TASKS + + " ( " + + TaskContract.Tasks._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + TaskContract.Tasks.VERSION + + " INTEGER DEFAULT 0," + + TaskContract.Tasks.LIST_ID + + " INTEGER NOT NULL, " + + TaskContract.Tasks.TITLE + + " TEXT," + + TaskContract.Tasks.LOCATION + + " TEXT," + + TaskContract.Tasks.GEO + + " TEXT," + + TaskContract.Tasks.DESCRIPTION + + " TEXT," + + TaskContract.Tasks.URL + + " TEXT," + + TaskContract.Tasks.ORGANIZER + + " TEXT," + + TaskContract.Tasks.PRIORITY + + " INTEGER, " + + TaskContract.Tasks.TASK_COLOR + + " INTEGER," + + TaskContract.Tasks.CLASSIFICATION + + " INTEGER," + + TaskContract.Tasks.COMPLETED + + " INTEGER," + + TaskContract.Tasks.COMPLETED_IS_ALLDAY + + " INTEGER," + + TaskContract.Tasks.PERCENT_COMPLETE + + " INTEGER," + + TaskContract.Tasks.STATUS + + " INTEGER DEFAULT " + + TaskContract.Tasks.STATUS_DEFAULT + + "," + + TaskContract.Tasks.IS_NEW + + " INTEGER," + + TaskContract.Tasks.IS_CLOSED + + " INTEGER," + + TaskContract.Tasks.DTSTART + + " INTEGER," + + TaskContract.Tasks.CREATED + + " INTEGER," + + TaskContract.Tasks.LAST_MODIFIED + + " INTEGER," + + TaskContract.Tasks.IS_ALLDAY + + " INTEGER," + + TaskContract.Tasks.TZ + + " TEXT," + + TaskContract.Tasks.DUE + + " INTEGER," + + TaskContract.Tasks.DURATION + + " TEXT," + + TaskContract.Tasks.RDATE + + " TEXT," + + TaskContract.Tasks.EXDATE + + " TEXT," + + TaskContract.Tasks.RRULE + + " TEXT," + + TaskContract.Tasks.PARENT_ID + + " INTEGER," + + TaskContract.Tasks.SORTING + + " TEXT," + + TaskContract.Tasks.HAS_ALARMS + + " INTEGER," + + TaskContract.Tasks.HAS_PROPERTIES + + " INTEGER," + + TaskContract.Tasks.PINNED + + " INTEGER," + + TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID + + " TEXT," + + TaskContract.Tasks.ORIGINAL_INSTANCE_ID + + " INTEGER," + + TaskContract.Tasks.ORIGINAL_INSTANCE_TIME + + " INTEGER," + + TaskContract.Tasks.ORIGINAL_INSTANCE_ALLDAY + + " INTEGER," + + TaskContract.Tasks._DIRTY + + " INTEGER DEFAULT 1," // a new task is always dirty + + TaskContract.Tasks._DELETED + + " INTEGER DEFAULT 0," // new tasks are not deleted by default + + TaskContract.Tasks._SYNC_ID + + " TEXT," + + TaskContract.Tasks._UID + + " TEXT," + + TaskContract.Tasks.SYNC_VERSION + + " TEXT," + + TaskContract.Tasks.SYNC1 + + " TEXT," + + TaskContract.Tasks.SYNC2 + + " TEXT," + + TaskContract.Tasks.SYNC3 + + " TEXT," + + TaskContract.Tasks.SYNC4 + + " TEXT," + + TaskContract.Tasks.SYNC5 + + " TEXT," + + TaskContract.Tasks.SYNC6 + + " TEXT," + + TaskContract.Tasks.SYNC7 + + " TEXT," + + TaskContract.Tasks.SYNC8 + + " TEXT);"; + + /** SQL command to create the categories table. */ + private static final String SQL_CREATE_CATEGORIES_TABLE = + "CREATE TABLE " + + Tables.CATEGORIES + + " ( " + + TaskContract.Categories._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + TaskContract.Categories.ACCOUNT_NAME + + " TEXT," + + TaskContract.Categories.ACCOUNT_TYPE + + " TEXT," + + TaskContract.Categories.NAME + + " TEXT," + + TaskContract.Categories.COLOR + + " INTEGER);"; + + /** SQL command to create the categories table. */ + private static final String SQL_CREATE_CATEGORIES_MAPPING_TABLE = + "CREATE TABLE " + + Tables.CATEGORIES_MAPPING + + " ( " + + CategoriesMapping.TASK_ID + + " INTEGER," + + CategoriesMapping.CATEGORY_ID + + " INTEGER," + + CategoriesMapping.PROPERTY_ID + + " INTEGER," + + "FOREIGN KEY (" + + CategoriesMapping.TASK_ID + + ") REFERENCES " + + Tables.TASKS + + "(" + + TaskContract.Tasks._ID + + ")," + + "FOREIGN KEY (" + + CategoriesMapping.PROPERTY_ID + + ") REFERENCES " + + Tables.PROPERTIES + + "(" + + TaskContract.Properties.PROPERTY_ID + + ")," + + "FOREIGN KEY (" + + CategoriesMapping.CATEGORY_ID + + ") REFERENCES " + + Tables.CATEGORIES + + "(" + + TaskContract.Categories._ID + + "));"; + + /** SQL command to create the alarms table the stores the already triggered alarms. */ + private static final String SQL_CREATE_ALARMS_TABLE = + "CREATE TABLE " + + Tables.ALARMS + + " ( " + + TaskContract.Alarms.ALARM_ID + + " INTEGER," + + TaskContract.Alarms.LAST_TRIGGER + + " TEXT," + + TaskContract.Alarms.NEXT_TRIGGER + + " TEXT);"; + + /** SQL command to create the table for extended properties. */ + private static final String SQL_CREATE_PROPERTIES_TABLE = + "CREATE TABLE " + + Tables.PROPERTIES + + " ( " + + TaskContract.Properties.PROPERTY_ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + TaskContract.Properties.TASK_ID + + " INTEGER," + + TaskContract.Properties.MIMETYPE + + " INTEGER," + + TaskContract.Properties.VERSION + + " INTEGER," + + TaskContract.Properties.DATA0 + + " TEXT," + + TaskContract.Properties.DATA1 + + " TEXT," + + TaskContract.Properties.DATA2 + + " TEXT," + + TaskContract.Properties.DATA3 + + " TEXT," + + TaskContract.Properties.DATA4 + + " TEXT," + + TaskContract.Properties.DATA5 + + " TEXT," + + TaskContract.Properties.DATA6 + + " TEXT," + + TaskContract.Properties.DATA7 + + " TEXT," + + TaskContract.Properties.DATA8 + + " TEXT," + + TaskContract.Properties.DATA9 + + " TEXT," + + TaskContract.Properties.DATA10 + + " TEXT," + + TaskContract.Properties.DATA11 + + " TEXT," + + TaskContract.Properties.DATA12 + + " TEXT," + + TaskContract.Properties.DATA13 + + " TEXT," + + TaskContract.Properties.DATA14 + + " TEXT," + + TaskContract.Properties.DATA15 + + " TEXT," + + TaskContract.Properties.SYNC1 + + " TEXT," + + TaskContract.Properties.SYNC2 + + " TEXT," + + TaskContract.Properties.SYNC3 + + " TEXT," + + TaskContract.Properties.SYNC4 + + " TEXT," + + TaskContract.Properties.SYNC5 + + " TEXT," + + TaskContract.Properties.SYNC6 + + " TEXT," + + TaskContract.Properties.SYNC7 + + " TEXT," + + TaskContract.Properties.SYNC8 + + " TEXT);"; + + /** SQL command to drop the task view. */ + private static final String SQL_DROP_PROPERTIES_TABLE = "DROP TABLE " + Tables.PROPERTIES + ";"; + + /** + * 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 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(); - private static final String TAG = "TaskDatabaseHelper"; - - /** - * The name of our database file. - */ - private static final String DATABASE_NAME = "tasks.db"; - - /** - * The database version. - */ - private static final int DATABASE_VERSION = 23; - - - /** - * List of all tables we provide. - */ - public interface Tables - { - String LISTS = "Lists"; - - String WRITEABLE_LISTS = "Writeable_Lists"; - - String TASKS = "Tasks"; - - String TASKS_VIEW = "Task_View"; - - String TASKS_PROPERTY_VIEW = "Task_Property_View"; - - String INSTANCES = "Instances"; - - String INSTANCE_VIEW = "Instance_View"; - - String INSTANCE_CLIENT_VIEW = "Instance_Client_View"; - - String INSTANCE_PROPERTY_VIEW = "Instance_Property_View"; - - String INSTANCE_CATEGORY_VIEW = "Instance_Cagetory_View"; - - String CATEGORIES = "Categories"; - - String CATEGORIES_MAPPING = "Categories_Mapping"; - - String PROPERTIES = "Properties"; - - String ALARMS = "Alarms"; - - String SYNCSTATE = "SyncState"; + // Index name is constructed like this: tablename_fields[0]_idx + buffer.append("CREATE "); + if (unique) { + buffer.append(" UNIQUE "); } - - - /** - * Columns of internal table for the category mapping. - */ - public interface CategoriesMapping - { - String TASK_ID = "task_id"; - - String CATEGORY_ID = "category_id"; - - String PROPERTY_ID = "property_id"; - + buffer.append("INDEX IF NOT EXISTS "); + 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]); } - - - /** - * SQL command to create a view that combines tasks with some data from the list they belong to. - */ - private final static String SQL_CREATE_TASK_VIEW = "create view " + Tables.TASKS_VIEW + " as select " + - Tables.TASKS + ".*, " + - Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", " + - Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", " + - Tables.LISTS + "." + Tasks.LIST_OWNER + ", " + - Tables.LISTS + "." + Tasks.LIST_NAME + ", " + - Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", " + - Tables.LISTS + "." + Tasks.LIST_COLOR + ", " + - Tables.LISTS + "." + Tasks.VISIBLE + - " from " + Tables.TASKS + " join " + Tables.LISTS + - " on (" + Tables.TASKS + "." + Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskLists._ID + ");"; - - /** - * SQL command to create a view that combines tasks with some data from the list they belong to. - */ - private final static String SQL_CREATE_TASK_PROPERTY_VIEW = "create view " + Tables.TASKS_PROPERTY_VIEW + " as select " + - Tables.TASKS + ".*, " + - Tables.PROPERTIES + ".*, " + - Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", " + - Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", " + - Tables.LISTS + "." + Tasks.LIST_OWNER + ", " + - Tables.LISTS + "." + Tasks.LIST_NAME + ", " + - Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", " + - Tables.LISTS + "." + Tasks.LIST_COLOR + ", " + - Tables.LISTS + "." + Tasks.VISIBLE + - " from " + Tables.TASKS + " join " + Tables.LISTS + - " on (" + Tables.TASKS + "." + Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskLists._ID + ") " + - "left join " + Tables.PROPERTIES + " on (" + Tables.TASKS + "." + Tasks._ID + "=" + Tables.PROPERTIES + "." + Properties.TASK_ID + ");"; - - /** - * SQL command to drop the task view. - */ - private final static String SQL_DROP_TASK_VIEW = "DROP VIEW " + Tables.TASKS_VIEW + ";"; - - /** - * SQL command to create a view that combines task instances with some data from the list they belong to. - */ - private final static String SQL_CREATE_INSTANCE_VIEW = "CREATE VIEW " + Tables.INSTANCE_VIEW + " AS SELECT " - + Tables.INSTANCES + ".*, " - + Tables.TASKS + ".*, " - + Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", " - + Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", " - + Tables.LISTS + "." + Tasks.LIST_OWNER + ", " - + Tables.LISTS + "." + Tasks.LIST_NAME + ", " - + Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", " - + Tables.LISTS + "." + Tasks.LIST_COLOR + ", " - + Tables.LISTS + "." + Tasks.VISIBLE - + " FROM " + Tables.TASKS - + " JOIN " + Tables.LISTS + " ON (" + Tables.TASKS + "." + TaskContract.Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskContract.Tasks._ID + ")" - + " JOIN " + Tables.INSTANCES + " ON (" + Tables.TASKS + "." + TaskContract.Tasks._ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ");"; - - /** - * SQL command to create a view that combines task instances with some data from the list they belong to. This replaces the task DTSTART, DUE and - * ORIGINAL_INSTANCE_TIME values with respective values of the instance. - *

- * This is the instances view as seen by the content provider clients. - */ - private final static String SQL_CREATE_INSTANCE_CLIENT_VIEW = "CREATE VIEW " + Tables.INSTANCE_CLIENT_VIEW + " AS SELECT " - + Tables.INSTANCES + ".*, " - // override task due, start and original times with the instance values - + Tables.INSTANCES + "." + TaskContract.Instances.INSTANCE_START + " as " + Tasks.DTSTART + ", " - + Tables.INSTANCES + "." + TaskContract.Instances.INSTANCE_DUE + " as " + Tasks.DUE + ", " - + Tables.INSTANCES + "." + TaskContract.Instances.INSTANCE_ORIGINAL_TIME + " as " + Tasks.ORIGINAL_INSTANCE_TIME + ", " - // override task duration with null, we already have a due - + "null as " + Tasks.DURATION + ", " - // override recurrence values with null, instances themselves are not recurring - + "null as " + Tasks.RRULE + ", " - + "null as " + Tasks.RDATE + ", " - + "null as " + Tasks.EXDATE + ", " - // this instance is part of a recurring task if either it has recurrence values or overrides an instance - + "not (" + Tasks.RRULE + " is null and " + Tasks.RDATE + " is null and " + Tasks.ORIGINAL_INSTANCE_ID + " is null and " + Tasks.ORIGINAL_INSTANCE_SYNC_ID + " is null) as " + TaskContract.Instances.IS_RECURRING + ", " - + Tables.TASKS + ".*, " - + Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", " - + Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", " - + Tables.LISTS + "." + Tasks.LIST_OWNER + ", " - + Tables.LISTS + "." + Tasks.LIST_NAME + ", " - + Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", " - + Tables.LISTS + "." + Tasks.LIST_COLOR + ", " - + Tables.LISTS + "." + Tasks.VISIBLE - + " FROM " + Tables.TASKS - + " JOIN " + Tables.LISTS + " ON (" + Tables.TASKS + "." + TaskContract.Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskContract.TaskLists._ID + ")" - + " JOIN " + Tables.INSTANCES + " ON (" + Tables.TASKS + "." + TaskContract.Tasks._ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ");"; - - /** - * SQL command to create a view that combines task instances view with the belonging properties. - */ - private final static String SQL_CREATE_INSTANCE_PROPERTY_VIEW = "CREATE VIEW " + Tables.INSTANCE_PROPERTY_VIEW + " AS SELECT " - + Tables.INSTANCES + ".*, " - + Tables.PROPERTIES + ".*, " - + Tables.TASKS + ".*, " - + Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", " - + Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", " - + Tables.LISTS + "." + Tasks.LIST_OWNER + ", " - + Tables.LISTS + "." + Tasks.LIST_NAME + ", " - + Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", " - + Tables.LISTS + "." + Tasks.LIST_COLOR + ", " - + Tables.LISTS + "." + Tasks.VISIBLE - + " FROM " + Tables.TASKS - + " JOIN " + Tables.LISTS + " ON (" + Tables.TASKS + "." + TaskContract.Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskContract.Tasks._ID + ")" - + " JOIN " + Tables.INSTANCES + " ON (" + Tables.TASKS + "." + TaskContract.Tasks._ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ")" - + " LEFT JOIN " + Tables.PROPERTIES + " ON (" + Tables.TASKS + "." + Tasks._ID + "=" + Tables.PROPERTIES + "." + Properties.TASK_ID + ");"; - - /** - * SQL command to create a view that combines task instances with some data from the list they belong to. - */ - private final static String SQL_CREATE_INSTANCE_CATEGORY_VIEW = "CREATE VIEW " + Tables.INSTANCE_CATEGORY_VIEW + " AS SELECT " - + Tables.INSTANCES + ".*, " - + Tables.CATEGORIES_MAPPING + "." + CategoriesMapping.CATEGORY_ID + ", " - + Tables.TASKS + ".*, " - + Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", " - + Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", " - + Tables.LISTS + "." + Tasks.LIST_OWNER + ", " - + Tables.LISTS + "." + Tasks.LIST_NAME + ", " - + Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", " - + Tables.LISTS + "." + Tasks.LIST_COLOR + ", " - + Tables.LISTS + "." + Tasks.VISIBLE - + " FROM " + Tables.TASKS - + " JOIN " + Tables.LISTS + " ON (" + Tables.TASKS + "." + TaskContract.Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskContract.Tasks._ID + ")" - + " JOIN " + Tables.INSTANCES + " ON (" + Tables.TASKS + "." + TaskContract.Tasks._ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ")" - + " LEFT JOIN " + Tables.CATEGORIES_MAPPING + " ON (" + Tables.CATEGORIES_MAPPING + "." + CategoriesMapping.TASK_ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ");"; - - /** - * SQL command to drop the instance view. - */ - private final static String SQL_DROP_INSTANCE_VIEW = "DROP VIEW " + Tables.INSTANCE_VIEW + ";"; - - /** - * SQL command to drop the instance property view. - */ - //private final static String SQL_DROP_INSTANCE_PROPERTY_VIEW = "DROP VIEW " + Tables.INSTANCE_PROPERTY_VIEW + ";"; - - /** - * SQL command to create the instances table. - */ - private final static String SQL_CREATE_SYNCSTATE_TABLE = - "CREATE TABLE " + Tables.SYNCSTATE + " ( " + - TaskContract.SyncState._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " - + TaskContract.SyncState.ACCOUNT_NAME + " TEXT, " - + TaskContract.SyncState.ACCOUNT_TYPE + " TEXT, " - + TaskContract.SyncState.DATA + " TEXT " - + ");"; - - /** - * SQL command to create the instances table. - */ - private final static String SQL_CREATE_INSTANCES_TABLE = - "CREATE TABLE " + Tables.INSTANCES + " ( " + - TaskContract.Instances._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " - + TaskContract.Instances.TASK_ID + " INTEGER NOT NULL, " // NOT NULL - + TaskContract.Instances.INSTANCE_START + " INTEGER, " - + TaskContract.Instances.INSTANCE_DUE + " INTEGER, " - + TaskContract.Instances.INSTANCE_START_SORTING + " INTEGER, " - + TaskContract.Instances.INSTANCE_DUE_SORTING + " INTEGER, " - + TaskContract.Instances.INSTANCE_DURATION + " INTEGER, " - + TaskContract.Instances.INSTANCE_ORIGINAL_TIME + " INTEGER DEFAULT 0, " - + TaskContract.Instances.DISTANCE_FROM_CURRENT + " INTEGER DEFAULT 0);"; - - /** - * SQL command to create a trigger to clean up data of removed tasks. - */ - private final static String SQL_CREATE_TASKS_CLEANUP_TRIGGER = - "CREATE TRIGGER task_cleanup_trigger AFTER DELETE ON " + Tables.TASKS - + " BEGIN " - + " DELETE FROM " + Tables.PROPERTIES + " WHERE " + TaskContract.Properties.TASK_ID + "= old." + TaskContract.Tasks._ID + ";" - + " DELETE FROM " + Tables.INSTANCES + " WHERE " + TaskContract.Instances.TASK_ID + "=old." + TaskContract.Tasks._ID + ";" - + " END;"; - - /** - * SQL command to create a trigger to clean up data of removed lists. - */ - private final static String SQL_CREATE_LISTS_CLEANUP_TRIGGER = - "CREATE TRIGGER list_cleanup_trigger AFTER DELETE ON " + Tables.LISTS - + " BEGIN " - + " DELETE FROM " + Tables.TASKS + " WHERE " + Tasks.LIST_ID + "= old." + TaskLists._ID + ";" - + " END;"; - - /** - * SQL command to drop the clean up trigger. - */ - private final static String SQL_DROP_TASKS_CLEANUP_TRIGGER = - "DROP TRIGGER task_cleanup_trigger;"; - - /** - * SQL command that counts and sets the alarm on deletion - */ - private final static String SQL_COUNT_ALARMS_ON_DELETE = - " BEGIN UPDATE " + Tables.TASKS + " SET " + Tasks.HAS_ALARMS - + " = (SELECT COUNT (*) FROM " + Tables.PROPERTIES - + " WHERE " + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "' AND " + Alarm.ALARM_TYPE + " <> " + Alarm.ALARM_TYPE_NOTHING + " AND " + Properties.TASK_ID + " = OLD." + Properties.TASK_ID - + ") WHERE " + Tasks._ID + " = OLD." + Properties.TASK_ID - + "; END;"; - - /** - * SQL command that counts and sets the alarm on insert and update - */ - private final static String SQL_COUNT_ALARMS = - " BEGIN UPDATE " + Tables.TASKS + " SET " + Tasks.HAS_ALARMS - + " = (SELECT COUNT (*) FROM " + Tables.PROPERTIES - + " WHERE " + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "' AND " + Alarm.ALARM_TYPE + " <> " + Alarm.ALARM_TYPE_NOTHING + " AND " + Properties.TASK_ID + " = NEW." + Properties.TASK_ID - + ") WHERE " + Tasks._ID + " = NEW." + Properties.TASK_ID - + "; END;"; - - /** - * SQL command to create a trigger that counts the alarms for a task on create - */ - private final static String SQL_CREATE_ALARM_COUNT_CREATE_TRIGGER = - "CREATE TRIGGER alarm_count_create_trigger AFTER INSERT ON " + Tables.PROPERTIES + " WHEN NEW." + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "'" - + SQL_COUNT_ALARMS; - - /** - * SQL command to create a trigger that counts the alarms for a task on update - */ - private final static String SQL_CREATE_ALARM_COUNT_UPDATE_TRIGGER = - "CREATE TRIGGER alarm_count_update_trigger AFTER UPDATE ON " + Tables.PROPERTIES + " WHEN NEW." + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "'" - + SQL_COUNT_ALARMS; - - /** - * SQL command to create a trigger that counts the alarms for a task on delete - */ - private final static String SQL_CREATE_ALARM_COUNT_DELETE_TRIGGER = - "CREATE TRIGGER alarm_count_delete_trigger AFTER DELETE ON " + Tables.PROPERTIES + " WHEN OLD." + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "'" - + SQL_COUNT_ALARMS_ON_DELETE; - - /** - * SQL command to create a trigger to clean up data of removed property. - */ - private final static String SQL_CREATE_ALARM_PROPERTY_CLEANUP_TRIGGER = - "CREATE TRIGGER alarm_property_cleanup_trigger AFTER DELETE ON " + Tables.PROPERTIES + " WHEN OLD." + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "'" - + " BEGIN " - + " DELETE FROM " + Tables.ALARMS + " WHERE " + TaskContract.Alarms.ALARM_ID + "= OLD." + TaskContract.Properties.PROPERTY_ID + ";" - + " END;"; - - /** - * SQL command to create a trigger to clean up data of removed property. - */ - private final static String SQL_CREATE_CATEGORY_PROPERTY_CLEANUP_TRIGGER = - "CREATE TRIGGER category_property_cleanup_trigger AFTER DELETE ON " + Tables.PROPERTIES + " WHEN OLD." + Properties.MIMETYPE + " = '" + Category.CONTENT_ITEM_TYPE + "'" - + " BEGIN " - + " DELETE FROM " + Tables.CATEGORIES_MAPPING + " WHERE " + CategoriesMapping.PROPERTY_ID + "= OLD." + TaskContract.Properties.PROPERTY_ID + ";" - + " END;"; - - /** - * SQL command to create a trigger to clean up property data of removed task. - */ - private final static String SQL_CREATE_TASK_PROPERTY_CLEANUP_TRIGGER = - "CREATE TRIGGER task_property_cleanup_trigger AFTER DELETE ON " + Tables.TASKS + " BEGIN " - + " DELETE FROM " + Tables.PROPERTIES + " WHERE " + Properties.TASK_ID + "= OLD." + Tasks._ID + ";" - + " END;"; - - /** - * SQL command to create a trigger to increment task version number on every update. - */ - private final static String SQL_CREATE_TASK_VERSION_TRIGGER = - "CREATE TRIGGER task_version_trigger BEFORE UPDATE ON " + Tables.TASKS + " BEGIN " - + " UPDATE " + Tables.TASKS + " SET " + Tasks.VERSION + " = OLD." + Tasks.VERSION + " + 1 where " + Tasks._ID + " = NEW." + Tasks._ID + ";" - + " END;"; - - /** - * SQL command to create the task list table. - */ - private final static String SQL_CREATE_LISTS_TABLE = - "CREATE TABLE " + Tables.LISTS + " ( " - + TaskContract.TaskLists._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," - + TaskContract.TaskLists.ACCOUNT_NAME + " TEXT," - + TaskContract.TaskLists.ACCOUNT_TYPE + " TEXT," - + TaskContract.TaskLists.LIST_NAME + " TEXT," - + TaskContract.TaskLists.LIST_COLOR + " INTEGER," - + TaskContract.TaskLists.ACCESS_LEVEL + " INTEGER," - + TaskContract.TaskLists.VISIBLE + " INTEGER," - + TaskContract.TaskLists.SYNC_ENABLED + " INTEGER," - + TaskContract.TaskLists.OWNER + " TEXT," - + TaskContract.TaskLists._DIRTY + " INTEGER DEFAULT 0," - + TaskContract.TaskLists._SYNC_ID + " TEXT," - + TaskContract.TaskLists.SYNC_VERSION + " TEXT," - + TaskContract.TaskLists.SYNC1 + " TEXT," - + TaskContract.TaskLists.SYNC2 + " TEXT," - + TaskContract.TaskLists.SYNC3 + " TEXT," - + TaskContract.TaskLists.SYNC4 + " TEXT," - + TaskContract.TaskLists.SYNC5 + " TEXT," - + TaskContract.TaskLists.SYNC6 + " TEXT," - + TaskContract.TaskLists.SYNC7 + " TEXT," - + TaskContract.TaskLists.SYNC8 + " TEXT);"; - - /** - * SQL command to create the task table. - */ - private final static String SQL_CREATE_TASKS_TABLE = - "CREATE TABLE " + Tables.TASKS + " ( " - + TaskContract.Tasks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," - + TaskContract.Tasks.VERSION + " INTEGER DEFAULT 0," - + TaskContract.Tasks.LIST_ID + " INTEGER NOT NULL, " - + TaskContract.Tasks.TITLE + " TEXT," - + TaskContract.Tasks.LOCATION + " TEXT," - + TaskContract.Tasks.GEO + " TEXT," - + TaskContract.Tasks.DESCRIPTION + " TEXT," - + TaskContract.Tasks.URL + " TEXT," - + TaskContract.Tasks.ORGANIZER + " TEXT," - + TaskContract.Tasks.PRIORITY + " INTEGER, " - + TaskContract.Tasks.TASK_COLOR + " INTEGER," - + TaskContract.Tasks.CLASSIFICATION + " INTEGER," - + TaskContract.Tasks.COMPLETED + " INTEGER," - + TaskContract.Tasks.COMPLETED_IS_ALLDAY + " INTEGER," - + TaskContract.Tasks.PERCENT_COMPLETE + " INTEGER," - + TaskContract.Tasks.STATUS + " INTEGER DEFAULT " + TaskContract.Tasks.STATUS_DEFAULT + "," - + TaskContract.Tasks.IS_NEW + " INTEGER," - + TaskContract.Tasks.IS_CLOSED + " INTEGER," - + TaskContract.Tasks.DTSTART + " INTEGER," - + TaskContract.Tasks.CREATED + " INTEGER," - + TaskContract.Tasks.LAST_MODIFIED + " INTEGER," - + TaskContract.Tasks.IS_ALLDAY + " INTEGER," - + TaskContract.Tasks.TZ + " TEXT," - + TaskContract.Tasks.DUE + " INTEGER," - + TaskContract.Tasks.DURATION + " TEXT," - + TaskContract.Tasks.RDATE + " TEXT," - + TaskContract.Tasks.EXDATE + " TEXT," - + TaskContract.Tasks.RRULE + " TEXT," - + TaskContract.Tasks.PARENT_ID + " INTEGER," - + TaskContract.Tasks.SORTING + " TEXT," - + TaskContract.Tasks.HAS_ALARMS + " INTEGER," - + TaskContract.Tasks.HAS_PROPERTIES + " INTEGER," - + TaskContract.Tasks.PINNED + " INTEGER," - + TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID + " TEXT," - + TaskContract.Tasks.ORIGINAL_INSTANCE_ID + " INTEGER," - + TaskContract.Tasks.ORIGINAL_INSTANCE_TIME + " INTEGER," - + TaskContract.Tasks.ORIGINAL_INSTANCE_ALLDAY + " INTEGER," - + TaskContract.Tasks._DIRTY + " INTEGER DEFAULT 1," // a new task is always dirty - + TaskContract.Tasks._DELETED + " INTEGER DEFAULT 0," // new tasks are not deleted by default - + TaskContract.Tasks._SYNC_ID + " TEXT," - + TaskContract.Tasks._UID + " TEXT," - + TaskContract.Tasks.SYNC_VERSION + " TEXT," - + TaskContract.Tasks.SYNC1 + " TEXT," - + TaskContract.Tasks.SYNC2 + " TEXT," - + TaskContract.Tasks.SYNC3 + " TEXT," - + TaskContract.Tasks.SYNC4 + " TEXT," - + TaskContract.Tasks.SYNC5 + " TEXT," - + TaskContract.Tasks.SYNC6 + " TEXT," - + TaskContract.Tasks.SYNC7 + " TEXT," - + TaskContract.Tasks.SYNC8 + " TEXT);"; - - /** - * SQL command to create the categories table. - */ - private final static String SQL_CREATE_CATEGORIES_TABLE = - "CREATE TABLE " + Tables.CATEGORIES - + " ( " + TaskContract.Categories._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," - + TaskContract.Categories.ACCOUNT_NAME + " TEXT," - + TaskContract.Categories.ACCOUNT_TYPE + " TEXT," - + TaskContract.Categories.NAME + " TEXT," - + TaskContract.Categories.COLOR + " INTEGER);"; - - /** - * SQL command to create the categories table. - */ - private final static String SQL_CREATE_CATEGORIES_MAPPING_TABLE = - "CREATE TABLE " + Tables.CATEGORIES_MAPPING - + " ( " + CategoriesMapping.TASK_ID + " INTEGER," - + CategoriesMapping.CATEGORY_ID + " INTEGER," - + CategoriesMapping.PROPERTY_ID + " INTEGER," - + "FOREIGN KEY (" + CategoriesMapping.TASK_ID + ") REFERENCES " + Tables.TASKS + "(" + TaskContract.Tasks._ID + ")," - + "FOREIGN KEY (" + CategoriesMapping.PROPERTY_ID + ") REFERENCES " + Tables.PROPERTIES + "(" + TaskContract.Properties.PROPERTY_ID + ")," - + "FOREIGN KEY (" + CategoriesMapping.CATEGORY_ID + ") REFERENCES " + Tables.CATEGORIES + "(" + TaskContract.Categories._ID + "));"; - - /** - * SQL command to create the alarms table the stores the already triggered alarms. - */ - private final static String SQL_CREATE_ALARMS_TABLE = - "CREATE TABLE " + Tables.ALARMS - + " ( " + TaskContract.Alarms.ALARM_ID + " INTEGER," - + TaskContract.Alarms.LAST_TRIGGER + " TEXT," - + TaskContract.Alarms.NEXT_TRIGGER + " TEXT);"; - - /** - * SQL command to create the table for extended properties. - */ - private final static String SQL_CREATE_PROPERTIES_TABLE = - "CREATE TABLE " + Tables.PROPERTIES + " ( " - + TaskContract.Properties.PROPERTY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," - + TaskContract.Properties.TASK_ID + " INTEGER," - + TaskContract.Properties.MIMETYPE + " INTEGER," - + TaskContract.Properties.VERSION + " INTEGER," - + TaskContract.Properties.DATA0 + " TEXT," - + TaskContract.Properties.DATA1 + " TEXT," - + TaskContract.Properties.DATA2 + " TEXT," - + TaskContract.Properties.DATA3 + " TEXT," - + TaskContract.Properties.DATA4 + " TEXT," - + TaskContract.Properties.DATA5 + " TEXT," - + TaskContract.Properties.DATA6 + " TEXT," - + TaskContract.Properties.DATA7 + " TEXT," - + TaskContract.Properties.DATA8 + " TEXT," - + TaskContract.Properties.DATA9 + " TEXT," - + TaskContract.Properties.DATA10 + " TEXT," - + TaskContract.Properties.DATA11 + " TEXT," - + TaskContract.Properties.DATA12 + " TEXT," - + TaskContract.Properties.DATA13 + " TEXT," - + TaskContract.Properties.DATA14 + " TEXT," - + TaskContract.Properties.DATA15 + " TEXT," - + TaskContract.Properties.SYNC1 + " TEXT," - + TaskContract.Properties.SYNC2 + " TEXT," - + TaskContract.Properties.SYNC3 + " TEXT," - + TaskContract.Properties.SYNC4 + " TEXT," - + TaskContract.Properties.SYNC5 + " TEXT," - + TaskContract.Properties.SYNC6 + " TEXT," - + TaskContract.Properties.SYNC7 + " TEXT," - + TaskContract.Properties.SYNC8 + " TEXT);"; - - /** - * SQL command to drop the task view. - */ - private final static String SQL_DROP_PROPERTIES_TABLE = "DROP TABLE " + Tables.PROPERTIES + ";"; - - - /** - * 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 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 IF NOT EXISTS "); - 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(); - + 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 task version update trigger + db.execSQL(SQL_CREATE_TASK_VERSION_TRIGGER); + + // 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_CLIENT_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.INSTANCES, false, TaskContract.Instances.INSTANCE_ORIGINAL_TIME)); + 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); } - - private final OnDatabaseOperationListener mListener; - - - TaskDatabaseHelper(Context context, OnDatabaseOperationListener listener) - { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - mListener = listener; + 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"}); + } - /** - * Creates the tables, views, triggers and indices. - *

- * TODO: move all strings to separate final static variables. - */ - @Override - public void onCreate(SQLiteDatabase db) - { + if (oldVersion < 13) { + db.execSQL(SQL_CREATE_SYNCSTATE_TABLE); + } - // create task list table - db.execSQL(SQL_CREATE_LISTS_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)); + } - // 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"); + 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)); + } - // create task table - db.execSQL(SQL_CREATE_TASKS_TABLE); + if (oldVersion < 17) { + db.execSQL( + "alter table " + + Tables.INSTANCES + + " add column " + + TaskContract.Instances.INSTANCE_ORIGINAL_TIME + + " integer default 0;"); + db.execSQL( + createIndexString( + Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_ORIGINAL_TIME)); + } - // 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"); + if (oldVersion < 18) { + db.execSQL( + "alter table " + + Tables.INSTANCES + + " add column " + + TaskContract.Instances.DISTANCE_FROM_CURRENT + + " integer default 0;"); + } - // 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"); + if (oldVersion < 19) { + db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW); + } - // create task version update trigger + if (oldVersion < 22) { + // create version column, unless it already exists + if (!new First<>(new TableColumns(Tables.TASKS).value(db), new Equals<>(Tasks.VERSION)) + .isPresent()) { + // create task version column and update trigger + db.execSQL( + "alter table " + Tables.TASKS + " add column " + Tasks.VERSION + " Integer default 0;"); db.execSQL(SQL_CREATE_TASK_VERSION_TRIGGER); - - // 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_CLIENT_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.INSTANCES, false, TaskContract.Instances.INSTANCE_ORIGINAL_TIME)); - 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)); - } - - if (oldVersion < 17) - { - db.execSQL("alter table " + Tables.INSTANCES + " add column " + TaskContract.Instances.INSTANCE_ORIGINAL_TIME + " integer default 0;"); - db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_ORIGINAL_TIME)); - } - - if (oldVersion < 18) - { - db.execSQL("alter table " + Tables.INSTANCES + " add column " + TaskContract.Instances.DISTANCE_FROM_CURRENT + " integer default 0;"); - } - - if (oldVersion < 19) - { - db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW); - } - - if (oldVersion < 22) - { - // create version column, unless it already exists - if (!new First<>(new TableColumns(Tables.TASKS).value(db), new Equals<>(Tasks.VERSION)).isPresent()) - { - // create task version column and update trigger - db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.VERSION + " Integer default 0;"); - db.execSQL(SQL_CREATE_TASK_VERSION_TRIGGER); - } - } - - if (oldVersion < 22) - { - db.beginTransaction(); - try - { - // make sure we upgrade the instances of every recurring task - EntityProcessor processor = new Instantiating(new NoOpProcessor<>()); - try (Cursor c = db.query(Tables.TASKS, - new String[] { - TaskContract.Tasks._ID, Tasks.ORIGINAL_INSTANCE_ID, Tasks.DTSTART, Tasks.DUE, Tasks.DURATION, Tasks.IS_CLOSED, Tasks.TZ, - Tasks.IS_ALLDAY, Tasks.RRULE, Tasks.RDATE, Tasks.EXDATE, Tasks.ORIGINAL_INSTANCE_TIME, Tasks.ORIGINAL_INSTANCE_ALLDAY }, - String.format(Locale.ENGLISH, "%s is null", TaskContract.Tasks.ORIGINAL_INSTANCE_ID), - null, null, null, null)) - { - while (c.moveToNext()) - { - ContentValues values = new ContentValues(); - Instantiating.addUpdateRequest(values); - TaskAdapter adapter = new CursorContentValuesTaskAdapter(c, values); - processor.update(db, adapter, false); - } - } - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } + if (oldVersion < 22) { + db.beginTransaction(); + try { + // make sure we upgrade the instances of every recurring task + EntityProcessor processor = new Instantiating(new NoOpProcessor<>()); + try (Cursor c = + db.query( + Tables.TASKS, + new String[] { + TaskContract.Tasks._ID, + Tasks.ORIGINAL_INSTANCE_ID, + Tasks.DTSTART, + Tasks.DUE, + Tasks.DURATION, + Tasks.IS_CLOSED, + Tasks.TZ, + Tasks.IS_ALLDAY, + Tasks.RRULE, + Tasks.RDATE, + Tasks.EXDATE, + Tasks.ORIGINAL_INSTANCE_TIME, + Tasks.ORIGINAL_INSTANCE_ALLDAY + }, + String.format( + Locale.ENGLISH, "%s is null", TaskContract.Tasks.ORIGINAL_INSTANCE_ID), + null, + null, + null, + null)) { + while (c.moveToNext()) { + ContentValues values = new ContentValues(); + Instantiating.addUpdateRequest(values); + TaskAdapter adapter = new CursorContentValuesTaskAdapter(c, values); + processor.update(db, adapter, false); + } } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } - if (oldVersion < 23) - { - db.execSQL("drop view " + Tables.INSTANCE_CLIENT_VIEW + ";"); - db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW); - } + if (oldVersion < 23) { + db.execSQL("drop view " + Tables.INSTANCE_CLIENT_VIEW + ";"); + db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW); + } - // upgrade FTS - FTSDatabaseHelper.onUpgrade(db, oldVersion, newVersion); + // upgrade FTS + FTSDatabaseHelper.onUpgrade(db, oldVersion, newVersion); - if (mListener != null) - { - mListener.onDatabaseUpdate(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 d78d9a3a..35730989 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 @@ -37,7 +37,12 @@ import android.os.Handler; import android.os.HandlerThread; import android.text.TextUtils; import android.util.Log; - +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.dmfs.iterables.EmptyIterable; import org.dmfs.provider.tasks.TaskDatabaseHelper.OnDatabaseOperationListener; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; @@ -78,1318 +83,1164 @@ import org.dmfs.tasks.contract.TaskContract.TaskListSyncColumns; import org.dmfs.tasks.contract.TaskContract.TaskLists; import org.dmfs.tasks.contract.TaskContract.Tasks; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - - /** * 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 + * + *

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 OPERATIONS = 100000; - - private final static Set TASK_LIST_SYNC_COLUMNS = new HashSet(Arrays.asList(TaskLists.SYNC_ADAPTER_COLUMNS)); - private static final String TAG = "TaskProvider"; - - /** - * A list of {@link EntityProcessor}s to execute when doing operations on the instances table. - */ - private EntityProcessor mInstanceProcessorChain; - - /** - * A list of {@link EntityProcessor}s to execute when doing operations on the tasks table. - */ - private EntityProcessor mTaskProcessorChain; - - /** - * A list of {@link EntityProcessor}s to execute when doing operations on the task lists table. - */ - private EntityProcessor mListProcessorChain; - - /** - * Our authority. - */ - String mAuthority; - - /** - * The {@link UriMatcher} we use. - */ - private UriMatcher mUriMatcher; - - /** - * A handler to execute asynchronous jobs. - */ - Handler mAsyncHandler; - - /** - * Boolean to track if there are changes within a transaction. - *

- * This can be shared by multiple threads, hence the {@link AtomicBoolean}. - */ - private AtomicBoolean mChanged = new AtomicBoolean(false); - - /** - * This is a per transaction/thread flag which indicates whether new lists with an unknown account have been added. - * If this holds true at the end of a transaction a window should be shown to ask the user for access to that account. - */ - private ThreadLocal mStaleListCreated = new ThreadLocal<>(); - - /** - * The currently known accounts. This may be accessed from various threads, hence the AtomicReference. - * By statring with an empty set, we can always guarantee a non-null reference. - */ - private AtomicReference> mAccountCache = new AtomicReference<>(Collections.emptySet()); - - - public TaskProvider() - { - // for now we don't have anything specific to execute before the transaction ends. - super(EmptyIterable.instance()); - } - - - @Override - public boolean onCreate() - { - mAuthority = AuthorityUtil.taskAuthority(getContext()); - - mTaskProcessorChain = new Validating( - new AutoCompleting(new Relating(new Reparenting(new Instantiating(new Searchable(new Moving(new Originating(new TaskCommitProcessor())))))))); - - mListProcessorChain = new org.dmfs.provider.tasks.processors.lists.Validating(new ListCommitProcessor()); +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 OPERATIONS = 100000; + + private static final Set TASK_LIST_SYNC_COLUMNS = + new HashSet(Arrays.asList(TaskLists.SYNC_ADAPTER_COLUMNS)); + private static final String TAG = "TaskProvider"; + + /** A list of {@link EntityProcessor}s to execute when doing operations on the instances table. */ + private EntityProcessor mInstanceProcessorChain; + + /** A list of {@link EntityProcessor}s to execute when doing operations on the tasks table. */ + private EntityProcessor mTaskProcessorChain; - mInstanceProcessorChain = new org.dmfs.provider.tasks.processors.instances.Validating( - new Detaching(new TaskValueDelegate(mTaskProcessorChain), mTaskProcessorChain)); - - 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()); + /** + * A list of {@link EntityProcessor}s to execute when doing operations on the task lists table. + */ + private EntityProcessor mListProcessorChain; + + /** Our authority. */ + String mAuthority; + + /** The {@link UriMatcher} we use. */ + private UriMatcher mUriMatcher; + + /** A handler to execute asynchronous jobs. */ + Handler mAsyncHandler; + + /** + * Boolean to track if there are changes within a transaction. + * + *

This can be shared by multiple threads, hence the {@link AtomicBoolean}. + */ + private AtomicBoolean mChanged = new AtomicBoolean(false); + + /** + * This is a per transaction/thread flag which indicates whether new lists with an unknown account + * have been added. If this holds true at the end of a transaction a window should be shown to ask + * the user for access to that account. + */ + private ThreadLocal mStaleListCreated = new ThreadLocal<>(); + + /** + * The currently known accounts. This may be accessed from various threads, hence the + * AtomicReference. By statring with an empty set, we can always guarantee a non-null reference. + */ + private AtomicReference> mAccountCache = + new AtomicReference<>(Collections.emptySet()); + + public TaskProvider() { + // for now we don't have anything specific to execute before the transaction ends. + super(EmptyIterable.instance()); + } + + @Override + public boolean onCreate() { + mAuthority = AuthorityUtil.taskAuthority(getContext()); + + mTaskProcessorChain = + new Validating( + new AutoCompleting( + new Relating( + new Reparenting( + new Instantiating( + new Searchable( + new Moving(new Originating(new TaskCommitProcessor())))))))); + + mListProcessorChain = + new org.dmfs.provider.tasks.processors.lists.Validating(new ListCommitProcessor()); + + mInstanceProcessorChain = + new org.dmfs.provider.tasks.processors.instances.Validating( + new Detaching(new TaskValueDelegate(mTaskProcessorChain), mTaskProcessorChain)); + + 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 "); + } - AccountManager accountManager = AccountManager.get(getContext()); - accountManager.addOnAccountsUpdatedListener(this, mAsyncHandler, true); + sb.append(TaskListSyncColumns.ACCOUNT_NAME); + sb.append("="); + DatabaseUtils.appendEscapedSQLString(sb, accountName); + } + if (accountType != null) { - updateNotifications(); + if (sb.length() > 0) { + sb.append(" AND "); + } - return result; + sb.append(TaskListSyncColumns.ACCOUNT_TYPE); + sb.append("="); + DatabaseUtils.appendEscapedSQLString(sb, accountType); + } } - - - /** - * 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 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); } - - - /** - * 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); + if (accountType != null) { + sqlBuilder.appendWhere(" AND "); + sqlBuilder.appendWhere(TaskListSyncColumns.ACCOUNT_TYPE); + sqlBuilder.appendWhere("="); + sqlBuilder.appendWhereEscapeString(accountType); } + } - - /** - * 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); + private StringBuilder _selectId(StringBuilder sb, long id, String key) { + if (sb.length() > 0) { + sb.append(" AND "); } - - - /** - * 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); + 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(" ) "); + } } - - - /** - * 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) + 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 (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); - } + 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; } - 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); + 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; } - if (accountType != null) - { - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(TaskListSyncColumns.ACCOUNT_TYPE); - sqlBuilder.appendWhere("="); - sqlBuilder.appendWhereEscapeString(accountType); + 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; } - } - - - private StringBuilder _selectId(StringBuilder sb, long id, String key) - { - if (sb.length() > 0) - { - sb.append(" AND "); + 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); } - sb.append(key); - sb.append("="); - sb.append(id); - return sb; - } + 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_CLIENT_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_CLIENT_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; - protected StringBuilder selectId(Uri uri) - { - StringBuilder sb = new StringBuilder(128); - return selectId(sb, uri); + default: + throw new IllegalArgumentException("Unknown URI " + uri); } + Cursor c = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); - protected StringBuilder selectId(StringBuilder sb, Uri uri) - { - return _selectId(sb, getId(uri), TaskListColumns._ID); + 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 + final Cursor cursor = + db.query(Tables.LISTS, null, selection, selectionArgs, null, null, null, null); - protected StringBuilder selectTaskId(Uri uri) - { - StringBuilder sb = new StringBuilder(128); - return selectTaskId(sb, uri); - } + try { + while (cursor.moveToNext()) { + final ListAdapter list = + new CursorContentValuesListAdapter( + ListAdapter._ID.getFrom(cursor), cursor, new ContentValues()); + mListProcessorChain.delete(db, list, isSyncAdapter); + mChanged.set(true); + count++; + } + } finally { + cursor.close(); + } - protected StringBuilder selectTaskId(long id) - { - StringBuilder sb = new StringBuilder(128); - return selectTaskId(sb, id); - } + 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); + } + } - protected StringBuilder selectTaskId(StringBuilder sb, Uri uri) - { - return selectTaskId(sb, getId(uri)); - } + // iterate over all tasks that match the selection + 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()); - protected StringBuilder selectTaskId(StringBuilder sb, long id) - { - return _selectId(sb, id, Instances.TASK_ID); + mTaskProcessorChain.delete(db, task, isSyncAdapter); - } + mChanged.set(true); + count++; + } + } finally { + cursor.close(); + } + break; + } - protected StringBuilder selectPropertyId(Uri uri) - { - StringBuilder sb = new StringBuilder(128); - return selectPropertyId(sb, uri); - } + case INSTANCE_ID: + // add id to selection and fall through + selection = updateSelection(selectId(uri), selection); + case INSTANCES: + { + // iterate over all instances that match the selection + try (Cursor cursor = + db.query( + Tables.INSTANCE_VIEW, null, selection, selectionArgs, null, null, null, null)) { + while (cursor.moveToNext()) { + mInstanceProcessorChain.delete( + db, + new CursorContentValuesInstanceAdapter(cursor, new ContentValues()), + isSyncAdapter); + mChanged.set(true); + count++; + } + } - protected StringBuilder selectPropertyId(StringBuilder sb, Uri uri) - { - return selectPropertyId(sb, getId(uri)); - } + 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; - protected StringBuilder selectPropertyId(long id) - { - StringBuilder sb = new StringBuilder(128); - return selectPropertyId(sb, id); + default: + throw new IllegalArgumentException("Unknown URI " + uri); } - - protected StringBuilder selectPropertyId(StringBuilder sb, long id) - { - return _selectId(sb, id, PropertyColumns.PROPERTY_ID); + 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; + Uri result_uri; - /** - * 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))); - } - + String accountName = getAccountName(uri); + String accountType = getAccountType(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) + switch (mUriMatcher.match(uri)) { + case SYNCSTATE: { - if (sb.length() > 0) - { - sb.append(" AND ( ").append(selection).append(" ) "); - } - else - { - sb.append(" ( ").append(selection).append(" ) "); - } + 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; } - 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 LISTS: { - 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_CLIENT_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_CLIENT_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); + final ListAdapter list = new ContentValuesListAdapter(values); + list.set(ListAdapter.ACCOUNT_NAME, accountName); + list.set(ListAdapter.ACCOUNT_TYPE, accountType); + + mListProcessorChain.insert(db, list, isSyncAdapter); + mChanged.set(true); + + rowId = list.id(); + result_uri = TaskContract.TaskLists.getContentUri(mAuthority); + // if the account is unknown we need to ask the user + if (Build.VERSION.SDK_INT >= 26 + && !TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType) + && !mAccountCache.get().contains(new Account(accountName, accountType))) { + // store the fact that we have an unknown account in this transaction + mStaleListCreated.set(true); + Log.d( + TAG, + String.format( + "List with unknown account %s inserted.", + new Account(accountName, accountType))); + } + break; } - - Cursor c = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); - - if (c != null) - { - c.setNotificationUri(getContext().getContentResolver(), uri); + case TASKS: + final TaskAdapter task = new ContentValuesTaskAdapter(values); + + mTaskProcessorChain.insert(db, task, isSyncAdapter); + + mChanged.set(true); + + rowId = task.id(); + result_uri = TaskContract.Tasks.getContentUri(mAuthority); + + postNotifyUri(Instances.getContentUri(mAuthority)); + postNotifyUri(Tasks.getContentUri(mAuthority)); + + break; + + // inserting instances is currently disabled because we only expand one instance, + // so even though a new task (exception) would be created, no instance might show up + // we need to resolve this discrepancy. Until then this feature remains disabled. + // case INSTANCES: + // { + // InstanceAdapter instance = mInstanceProcessorChain.insert(db, new + // ContentValuesInstanceAdapter(values), isSyncAdapter); + // rowId = instance.id(); + // result_uri = TaskContract.Instances.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"); } - 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 - 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()); - - mListProcessorChain.delete(db, list, isSyncAdapter); - mChanged.set(true); - 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 - 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()); - - mTaskProcessorChain.delete(db, task, isSyncAdapter); - - mChanged.set(true); - count++; - } - } - finally - { - cursor.close(); - } - - break; - } - case INSTANCE_ID: - // add id to selection and fall through - selection = updateSelection(selectId(uri), selection); - - case INSTANCES: - { - // iterate over all instances that match the selection - try (Cursor cursor = db.query(Tables.INSTANCE_VIEW, null, selection, selectionArgs, null, null, null, null)) - { - while (cursor.moveToNext()) - { - mInstanceProcessorChain.delete(db, new CursorContentValuesInstanceAdapter(cursor, new ContentValues()), isSyncAdapter); - mChanged.set(true); - count++; - } - } - - 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); + Long taskId = values.getAsLong(Properties.TASK_ID); + if (taskId == null) { + throw new IllegalArgumentException("missing task id in property values"); } - if (count > 0) - { - postNotifyUri(uri); - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); + if (values.containsKey(Properties.PROPERTY_ID)) { + throw new IllegalArgumentException("property id can not be written"); } - return count; - } + 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; - @Override - public Uri insertInTransaction(final SQLiteDatabase db, Uri uri, final ContentValues values, final boolean isSyncAdapter) - { - long rowId; - Uri result_uri; - - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } - switch (mUriMatcher.match(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); + } + + @Override + public int updateInTransaction( + final SQLiteDatabase db, + Uri uri, + final ContentValues values, + String selection, + String[] selectionArgs, + final boolean isSyncAdapter) { + int count = 0; + boolean dataChanged = false; + 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: { - 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); - - mListProcessorChain.insert(db, list, isSyncAdapter); - mChanged.set(true); - - rowId = list.id(); - result_uri = TaskContract.TaskLists.getContentUri(mAuthority); - // if the account is unknown we need to ask the user - if (Build.VERSION.SDK_INT >= 26 && - !TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType) && - !mAccountCache.get().contains(new Account(accountName, accountType))) - { - // store the fact that we have an unknown account in this transaction - mStaleListCreated.set(true); - Log.d(TAG, String.format("List with unknown account %s inserted.", new Account(accountName, accountType))); - } - break; - } - case TASKS: - final TaskAdapter task = new ContentValuesTaskAdapter(values); - - mTaskProcessorChain.insert(db, task, isSyncAdapter); - - mChanged.set(true); - - rowId = task.id(); - result_uri = TaskContract.Tasks.getContentUri(mAuthority); - - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); - - break; - - // inserting instances is currently disabled because we only expand one instance, - // so even though a new task (exception) would be created, no instance might show up - // we need to resolve this discrepancy. Until then this feature remains disabled. -// case INSTANCES: -// { -// InstanceAdapter instance = mInstanceProcessorChain.insert(db, new ContentValuesInstanceAdapter(values), isSyncAdapter); -// rowId = instance.id(); -// result_uri = TaskContract.Instances.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 (!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); - if (rowId > 0 && result_uri != null) + case LISTS: { - result_uri = ContentUris.withAppendedId(result_uri, rowId); - postNotifyUri(result_uri); - postNotifyUri(uri); - return result_uri; + // iterate over all task lists that match the selection + 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); + + if (list.hasUpdates()) { + mListProcessorChain.update(db, list, isSyncAdapter); + dataChanged |= !TASK_LIST_SYNC_COLUMNS.containsAll(values.keySet()); + } + // note we still count the row even if no update was necessary + count++; + } + } finally { + cursor.close(); + } + break; } - throw new SQLException("Failed to insert row into " + uri); - } + case TASK_ID: + // update selection and fall through + selection = updateSelection(selectId(uri), selection); - - @Override - public int updateInTransaction(final SQLiteDatabase db, Uri uri, final ContentValues values, String selection, String[] selectionArgs, - final boolean isSyncAdapter) - { - int count = 0; - boolean dataChanged = false; - switch (mUriMatcher.match(uri)) + case TASKS: { - 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 - 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); - - if (list.hasUpdates()) - { - mListProcessorChain.update(db, list, isSyncAdapter); - dataChanged |= !TASK_LIST_SYNC_COLUMNS.containsAll(values.keySet()); - } - // note we still count the row even if no update was necessary - 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 - 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); - - if (task.hasUpdates()) - { - mTaskProcessorChain.update(db, task, isSyncAdapter); - dataChanged |= !TASK_LIST_SYNC_COLUMNS.containsAll(values.keySet()); - } - // note we still count the row even if no update was necessary - count++; - } - } - finally - { - cursor.close(); - } - - if (dataChanged) - { - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); - } - break; + // iterate over all tasks that match the selection + 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); + + if (task.hasUpdates()) { + mTaskProcessorChain.update(db, task, isSyncAdapter); + dataChanged |= !TASK_LIST_SYNC_COLUMNS.containsAll(values.keySet()); + } + // note we still count the row even if no update was necessary + count++; } + } finally { + cursor.close(); + } - case INSTANCE_ID: - // update selection and fall through - selection = updateSelection(selectId(uri), selection); - - case INSTANCES: - { - // iterate over all instances that match the selection - - try (Cursor cursor = db.query(Tables.INSTANCE_VIEW, null, selection, selectionArgs, null, null, null, null)) - { - 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 InstanceAdapter instance = new CursorContentValuesInstanceAdapter(cursor, - cursor.getCount() > 1 ? new ContentValues(values) : values); - - if (instance.hasUpdates()) - { - mInstanceProcessorChain.update(db, instance, isSyncAdapter); - dataChanged = true; - } - // note we still count the row even if no update was necessary - count++; - } - } - - if (dataChanged) - { - 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); + if (dataChanged) { + postNotifyUri(Instances.getContentUri(mAuthority)); + postNotifyUri(Tasks.getContentUri(mAuthority)); + } + break; } - if (dataChanged) - { - // send notifications, because non-sync columns have been updated - postNotifyUri(uri); - mChanged.set(true); - } + case INSTANCE_ID: + // update selection and fall through + selection = updateSelection(selectId(uri), selection); - return count; - } - - - /** - * Update task due and task start notifications. - */ - private void updateNotifications() - { - mAsyncHandler.post(new Runnable() + case INSTANCES: { - - @Override - public void run() - { - ContentOperation.UPDATE_NOTIFICATION_ALARM.fire(getContext(), null); + // iterate over all instances that match the selection + + try (Cursor cursor = + db.query( + Tables.INSTANCE_VIEW, null, selection, selectionArgs, null, null, null, null)) { + 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 InstanceAdapter instance = + new CursorContentValuesInstanceAdapter( + cursor, cursor.getCount() > 1 ? new ContentValues(values) : values); + + if (instance.hasUpdates()) { + mInstanceProcessorChain.update(db, instance, isSyncAdapter); + dataChanged = true; + } + // note we still count the row even if no update was necessary + count++; } - }); - } - + } - /** - * 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 (dataChanged) { + postNotifyUri(Instances.getContentUri(mAuthority)); + postNotifyUri(Tasks.getContentUri(mAuthority)); + } + break; } + case PROPERTY_ID: + selection = updateSelection(selectPropertyId(uri), selection); - 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"); + case PROPERTIES: + if (values.containsKey(Properties.MIMETYPE)) { + throw new IllegalArgumentException("property mimetypes can not be modified"); } - 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"); + if (values.containsKey(Properties.TASK_ID)) { + throw new IllegalArgumentException("task id can not be changed"); } - } - - /** - * 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"); + if (values.containsKey(Properties.PROPERTY_ID)) { + throw new IllegalArgumentException("property id can not be changed"); } - } - - @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; - case INSTANCE_ID: - return ContentResolver.CURSOR_ITEM_BASE_TYPE + "/org.dmfs.tasks." + Instances.CONTENT_URI_PATH; - default: - throw new IllegalArgumentException("Unsupported URI: " + uri); + // 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); + } - @Override - protected void onEndTransaction(boolean callerIsSyncAdapter) - { - super.onEndTransaction(callerIsSyncAdapter); - if (mChanged.compareAndSet(true, false)) - { - updateNotifications(); - Utils.sendActionProviderChangedBroadCast(getContext(), mAuthority); - } - + if (dataChanged) { + // send notifications, because non-sync columns have been updated + postNotifyUri(uri); + mChanged.set(true); } + return count; + } - @Override - public SQLiteOpenHelper getDatabaseHelper(Context context) - { - TaskDatabaseHelper helper = new TaskDatabaseHelper(context, this); + /** Update task due and task start notifications. */ + private void updateNotifications() { + mAsyncHandler.post( + new Runnable() { - return helper; + @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"); } - - @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); - // Android SDK 26 doesn't allow us to send implicit broadcasts, this particular brodcast is only for internal use, so just make it explicit by setting our package name - dbInitializedIntent.setPackage(getContext().getPackageName()); - getContext().sendBroadcast(dbInitializedIntent); + 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"); } - - @Override - public void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion) - { - if (oldVersion < 15) - { - mAsyncHandler.post(() -> ContentOperation.UPDATE_TIMEZONE.fire(getContext(), null)); - } + 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"); } - - - @Override - protected boolean syncToNetwork(Uri uri) - { - return true; + } + + /** + * 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 void onAccountsUpdated(Account[] accounts) - { - // cache the known accounts so we can check whether we know accounts for which new lists are added - mAccountCache.set(new HashSet<>(Arrays.asList(accounts))); - // TODO: we probably can move the cleanup code here and get rid of the Utils class - Utils.cleanUpLists(getContext(), getDatabaseHelper().getWritableDatabase(), accounts, mAuthority); + } + + @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; + case INSTANCE_ID: + return ContentResolver.CURSOR_ITEM_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); + if (mChanged.compareAndSet(true, false)) { + updateNotifications(); + Utils.sendActionProviderChangedBroadCast(getContext(), mAuthority); + } + } + + @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); + // Android SDK 26 doesn't allow us to send implicit broadcasts, this particular brodcast is only + // for internal use, so just make it explicit by setting our package name + dbInitializedIntent.setPackage(getContext().getPackageName()); + getContext().sendBroadcast(dbInitializedIntent); + } + + @Override + public void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 15) { + mAsyncHandler.post(() -> ContentOperation.UPDATE_TIMEZONE.fire(getContext(), null)); } + } + + @Override + protected boolean syncToNetwork(Uri uri) { + return true; + } + + @Override + public void onAccountsUpdated(Account[] accounts) { + // cache the known accounts so we can check whether we know accounts for which new lists are + // added + mAccountCache.set(new HashSet<>(Arrays.asList(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 06764ac6..f700e469 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 @@ -22,80 +22,79 @@ import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.os.Build; - -import org.dmfs.rfc5545.DateTime; - import java.util.TimeZone; - +import org.dmfs.rfc5545.DateTime; /** - * 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. + * 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 String ACTION_NOTIFICATION_ALARM = "org.dmfs.tasks.provider.NOTIFICATION_ALARM"; +public class TaskProviderBroadcastReceiver extends BroadcastReceiver { + private static final int REQUEST_CODE_ALARM = 1337; + private static final 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()); - } - - // AlarmManager API changed in v19 (KitKat) and the "set" method is not called at the exact time anymore - am.setExact(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), 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()); } + // AlarmManager API changed in v19 (KitKat) and the "set" method is not called at the exact time + // anymore + am.setExact(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), pendingIntent); + } - @Override - public void onReceive(Context context, Intent intent) - { - String action = intent.getAction(); - switch (action) + @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: { - 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); - } + // 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/Utils.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/Utils.java index 997fddbd..db7b1b3b 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 @@ -22,7 +22,9 @@ 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; import org.dmfs.iterables.SingletonIterable; import org.dmfs.jems.iterable.composite.Joined; import org.dmfs.jems.iterable.decorators.Mapped; @@ -39,107 +41,104 @@ import org.dmfs.tasks.contract.TaskContract.TaskLists; import org.dmfs.tasks.contract.TaskContract.Tasks; import org.dmfs.tasks.provider.R; -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 - // TODO: coalesce fast consecutive broadcasts, a delay of up to 1 second should be acceptable - - new With<>(new Intent(Intent.ACTION_PROVIDER_CHANGED, TaskContract.getContentUri(authority))) - .process(providerChangedIntent -> - new Batch(context::sendBroadcast) - .process(new Mapped<>( - packageName -> new Intent(providerChangedIntent).setPackage(packageName), - // TODO: fow now we hard code 3rd party package names, this should be replaced by some sort or registry - // see https://github.com/dmfs/opentasks/issues/824 - new Joined<>( - new SingletonIterable<>(context.getPackageName()), - new ResourceArray(context, R.array.opentasks_provider_changed_receivers))))); - } - - - 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); - } +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 + // TODO: coalesce fast consecutive broadcasts, a delay of up to 1 second should be acceptable + + new With<>(new Intent(Intent.ACTION_PROVIDER_CHANGED, TaskContract.getContentUri(authority))) + .process( + providerChangedIntent -> + new Batch(context::sendBroadcast) + .process( + new Mapped<>( + packageName -> + new Intent(providerChangedIntent).setPackage(packageName), + // TODO: fow now we hard code 3rd party package names, this should be + // replaced by some sort or registry + // see https://github.com/dmfs/opentasks/issues/824 + new Joined<>( + new SingletonIterable<>(context.getPackageName()), + new ResourceArray( + context, R.array.opentasks_provider_changed_receivers))))); + } + + 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}); } - db.setTransactionSuccessful(); + } } - finally - { - db.endTransaction(); + } 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); } - // 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); + } + 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 4882d219..4096d16f 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 @@ -19,115 +19,95 @@ package org.dmfs.provider.tasks.handler; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.tasks.contract.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; +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"); } - - /** - * 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); + 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 "); + } - /** - * 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); + 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 8c129cb0..0ec6dd8d 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 @@ -19,7 +19,6 @@ package org.dmfs.provider.tasks.handler; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper.CategoriesMapping; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; import org.dmfs.tasks.contract.TaskContract.Categories; @@ -27,251 +26,238 @@ import org.dmfs.tasks.contract.TaskContract.Properties; import org.dmfs.tasks.contract.TaskContract.Property.Category; import org.dmfs.tasks.contract.TaskContract.Tasks; - /** * 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; +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."); } - - /** - * 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; + // 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"); } - - - /** - * 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); + 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(); + } } - - /** - * 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); + 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); } - - // remove redundant values - values.remove(IS_NEW_CATEGORY); - values.remove(Categories.ACCOUNT_NAME); - values.remove(Categories.ACCOUNT_TYPE); - - return values; + } 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)); + } - /** - * 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); + 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 52d32d38..3d3b4134 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 @@ -19,36 +19,31 @@ package org.dmfs.provider.tasks.handler; import android.content.ContentValues; 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; - } +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; + } } 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 a01e39d8..7fa80c9c 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 @@ -19,137 +19,105 @@ package org.dmfs.provider.tasks.handler; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.FTSDatabaseHelper; import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; import org.dmfs.tasks.contract.TaskContract.Properties; - /** * 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); - } - - - public ContentValues cloneForNewTask(long newTaskId, ContentValues values) - { - ContentValues newValues = new ContentValues(values); - newValues.remove(Properties.PROPERTY_ID); - newValues.put(Properties.TASK_ID, newTaskId); - return newValues; - } - - - ; +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); + } + + public ContentValues cloneForNewTask(long newTaskId, ContentValues values) { + ContentValues newValues = new ContentValues(values); + newValues.remove(Properties.PROPERTY_ID); + newValues.put(Properties.TASK_ID, newTaskId); + return newValues; + } + ; } 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 0e194633..6573dd45 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 @@ -20,42 +20,33 @@ import org.dmfs.tasks.contract.TaskContract.Property.Alarm; import org.dmfs.tasks.contract.TaskContract.Property.Category; import org.dmfs.tasks.contract.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(); - +public class PropertyHandlerFactory { + private static final PropertyHandler CATEGORY_HANDLER = new CategoryHandler(); + private static final PropertyHandler ALARM_HANDLER = new AlarmHandler(); + private static final PropertyHandler RELATION_HANDLER = new RelationHandler(); + private static final 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 45677bec..480c8eab 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 @@ -19,251 +19,233 @@ package org.dmfs.provider.tasks.handler; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.tasks.contract.TaskContract.Property.Relation; import org.dmfs.tasks.contract.TaskContract.Tasks; - /** * 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); - - if (id == null && uid != null) - { - values.putNull(Relation.RELATED_ID); - } - else if (id != null && uid == null) - { - 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; +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); - @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); + if (id == null && uid != null) { + values.putNull(Relation.RELATED_ID); + } else if (id != null && uid == null) { + values.putNull(Relation.RELATED_UID); + } else { + throw new IllegalArgumentException( + "exactly one of RELATED_ID, RELATED_UID and RELATED_URI must be non-null"); } - - @Override - public ContentValues cloneForNewTask(long newTaskId, ContentValues values) - { - ContentValues newValues = super.cloneForNewTask(newTaskId, values); - newValues.remove(Relation.RELATED_CONTENT_URI); - return newValues; + 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 ContentValues cloneForNewTask(long newTaskId, ContentValues values) { + ContentValues newValues = super.cloneForNewTask(newTaskId, values); + newValues.remove(Relation.RELATED_CONTENT_URI); + return newValues; + } + + @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. + * + *

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)); } + } - - @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); + 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); } - - - @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. - *

- * 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)); + 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(); + } } - - - 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; + 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)); } - - 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; + if (type == Relation.RELTYPE_PARENT) { + // 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 == Relation.RELTYPE_CHILD) { + // 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 == Relation.RELTYPE_SIBLING) { + // 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 } - - - /** - * Update {@link Tasks#PARENT_ID} when a parent is assigned to a child. + } + + /** + * 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 + * parent relationship has been created and the old one is removed afterwards. In that case we can not simply clear the PARENT_ID. * - * @param db - * @param taskId - * @param values - * @param oldValues + * FIXME: For now we ignore that fact. But we should fix it. */ - 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 == Relation.RELTYPE_PARENT) - { - // 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 == Relation.RELTYPE_CHILD) - { - // 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 == Relation.RELTYPE_SIBLING) - { - // 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 - } + if (type == Relation.RELTYPE_PARENT) { + // 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 == Relation.RELTYPE_CHILD) { + // 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); + } } - - - /** - * Clear {@link Tasks#PARENT_ID} if a link is removed. + // else if (type == Relation.RELTYPE_SIBLING) + // { + /* + * 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. * - * @param db - * @param taskId - * @param oldValues + * FIXME: properly handle this case */ - 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 - * 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 == Relation.RELTYPE_PARENT) - { - // 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 == Relation.RELTYPE_CHILD) - { - // 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 == Relation.RELTYPE_SIBLING) - // { - /* - * 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/AbstractInstanceAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractInstanceAdapter.java index 4cae1c81..fcf3de7a 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractInstanceAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/AbstractInstanceAdapter.java @@ -18,20 +18,17 @@ package org.dmfs.provider.tasks.model; import android.content.ContentUris; import android.net.Uri; - import org.dmfs.tasks.contract.TaskContract; - /** - * An abstract implementation of a {@link InstanceAdapter} to server as the base for more concrete adapters. + * An abstract implementation of a {@link InstanceAdapter} to server as the base for more concrete + * adapters. * * @author Marten Gajda */ -public abstract class AbstractInstanceAdapter implements InstanceAdapter -{ - @Override - public final Uri uri(String authority) - { - return ContentUris.withAppendedId(TaskContract.Instances.getContentUri(authority), id()); - } +public abstract class AbstractInstanceAdapter implements InstanceAdapter { + @Override + public final Uri uri(String authority) { + return ContentUris.withAppendedId(TaskContract.Instances.getContentUri(authority), id()); + } } 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 80478e3e..3efeb67d 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 @@ -19,38 +19,30 @@ package org.dmfs.provider.tasks.model; import android.content.ContentUris; import android.content.ContentValues; import android.net.Uri; - import org.dmfs.provider.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.contract.TaskContract; - /** - * An abstract implementation of a {@link ListAdapter} to server as the base for more concrete adapters. + * 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); - - - @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 void setState(FieldAdapter stateFieldAdater, T value) - { - stateFieldAdater.setIn(mState, value); - } +public abstract class AbstractListAdapter implements ListAdapter { + private final ContentValues mState = new ContentValues(10); + + @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 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 1f23f773..a73d6374 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 @@ -19,53 +19,47 @@ package org.dmfs.provider.tasks.model; import android.content.ContentUris; import android.content.ContentValues; import android.net.Uri; - import org.dmfs.provider.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.contract.TaskContract; - /** - * An abstract implementation of a {@link TaskAdapter} to server as the base for more concrete adapters. + * 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); - - - @Override - public Uri uri(String authority) - { - return ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(authority), id()); - } - - - @Override - public boolean isRecurring() - { - // recurring tasks must have an RRULE or RDATEs and at least one of DTSTART and DUE date - return (valueOf(RRULE) != null || valueOf(RDATE).iterator().hasNext()) && (valueOf(DTSTART) != null || valueOf(DUE) != null); - } - +public abstract class AbstractTaskAdapter implements TaskAdapter { + private final ContentValues mState = new ContentValues(10); - @Override - public boolean recurrenceUpdated() - { - return isUpdated(RRULE) || isUpdated(DTSTART) || isUpdated(DUE) || isUpdated(DURATION) || isUpdated(RDATE) || isUpdated(EXDATE); - } + @Override + public Uri uri(String authority) { + return ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(authority), id()); + } + @Override + public boolean isRecurring() { + // recurring tasks must have an RRULE or RDATEs and at least one of DTSTART and DUE date + return (valueOf(RRULE) != null || valueOf(RDATE).iterator().hasNext()) + && (valueOf(DTSTART) != null || valueOf(DUE) != null); + } - @Override - public T getState(FieldAdapter stateFieldAdater) - { - return stateFieldAdater.getFrom(mState); - } + @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 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/ContentValuesInstanceAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesInstanceAdapter.java index 58a7e54c..339d8b36 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesInstanceAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesInstanceAdapter.java @@ -18,144 +18,109 @@ package org.dmfs.provider.tasks.model; import android.content.ContentValues; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.jems.single.elementary.Reduced; import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAdapter} for tasks that are stored in a {@link ContentValues}. * * @author Marten Gajda */ -public class ContentValuesInstanceAdapter extends AbstractInstanceAdapter -{ - private long mId; - private final ContentValues mValues; - - - public ContentValuesInstanceAdapter(ContentValues values) - { - this(-1L, values); - } - - - public ContentValuesInstanceAdapter(long id, ContentValues values) - { - mId = id; - mValues = values; - } - - - @Override - public long id() - { - return mId; +public class ContentValuesInstanceAdapter extends AbstractInstanceAdapter { + private long mId; + private final ContentValues mValues; + + public ContentValuesInstanceAdapter(ContentValues values) { + this(-1L, values); + } + + public ContentValuesInstanceAdapter(long id, ContentValues values) { + mId = id; + mValues = values; + } + + @Override + public long id() { + return mId; + } + + @Override + public T valueOf(FieldAdapter fieldAdapter) { + return fieldAdapter.getFrom(mValues); + } + + @Override + public T oldValueOf(FieldAdapter fieldAdapter) { + return null; + } + + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) { + return fieldAdapter.isSetIn(mValues); + } + + @Override + public boolean isWriteable() { + return true; + } + + @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 unset(FieldAdapter fieldAdapter) throws IllegalStateException { + fieldAdapter.removeFrom(mValues); + } + + @Override + public int commit(SQLiteDatabase db) { + if (mValues.size() == 0) { + return 0; } - - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return null; - } - - - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return fieldAdapter.isSetIn(mValues); - } - - - @Override - public boolean isWriteable() - { - return true; - } - - - @Override - public boolean hasUpdates() - { - return mValues.size() > 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); } - - - @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; - } - - 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 T getState(FieldAdapter stateFieldAdater) - { - return null; - } - - - @Override - public void setState(FieldAdapter stateFieldAdater, T value) - { - - } - - - @Override - public InstanceAdapter duplicate() - { - return new ContentValuesInstanceAdapter(new ContentValues(mValues)); - } - - - @Override - public TaskAdapter taskAdapter() - { - // make sure we remove any instance fields - return new ContentValuesTaskAdapter(new Reduced( + } + + @Override + public T getState(FieldAdapter stateFieldAdater) { + return null; + } + + @Override + public void setState(FieldAdapter stateFieldAdater, T value) {} + + @Override + public InstanceAdapter duplicate() { + return new ContentValuesInstanceAdapter(new ContentValues(mValues)); + } + + @Override + public TaskAdapter taskAdapter() { + // make sure we remove any instance fields + return new ContentValuesTaskAdapter( + new Reduced( () -> new ContentValues(mValues), (contentValues, column) -> { - contentValues.remove(column); - return contentValues; + contentValues.remove(column); + return contentValues; }, - INSTANCE_COLUMN_NAMES).value()); - } + INSTANCE_COLUMN_NAMES) + .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 d4418481..7c191bd4 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 @@ -18,113 +18,87 @@ package org.dmfs.provider.tasks.model; import android.content.ContentValues; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.contract.TaskContract; - /** * @author Marten Gajda */ -public class ContentValuesListAdapter extends AbstractListAdapter -{ - private long mId; - private final ContentValues mValues; - - - public ContentValuesListAdapter(ContentValues values) - { - this(-1L, values); - } - - - public ContentValuesListAdapter(long id, ContentValues values) - { - mId = id; - mValues = values; - } - - - @Override - public long id() - { - return mId; - } - - - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return null; - } - - - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return fieldAdapter.isSetIn(mValues); - } - - - @Override - public boolean isWriteable() - { - return true; +public class ContentValuesListAdapter extends AbstractListAdapter { + private long mId; + private final ContentValues mValues; + + public ContentValuesListAdapter(ContentValues values) { + this(-1L, values); + } + + public ContentValuesListAdapter(long id, ContentValues values) { + mId = id; + mValues = values; + } + + @Override + public long id() { + return mId; + } + + @Override + public T valueOf(FieldAdapter fieldAdapter) { + return fieldAdapter.getFrom(mValues); + } + + @Override + public T oldValueOf(FieldAdapter fieldAdapter) { + return null; + } + + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) { + return fieldAdapter.isSetIn(mValues); + } + + @Override + public boolean isWriteable() { + return true; + } + + @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 unset(FieldAdapter fieldAdapter) throws IllegalStateException { + fieldAdapter.removeFrom(mValues); + } + + @Override + public int commit(SQLiteDatabase db) { + if (mValues.size() == 0) { + return 0; } - - @Override - public boolean hasUpdates() - { - return mValues.size() > 0; - } - - - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); + 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 void unset(FieldAdapter fieldAdapter) throws IllegalStateException - { - fieldAdapter.removeFrom(mValues); - } - - - @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); - } - } - - - @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 c8a3b8d1..6fb3d33b 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 @@ -18,115 +18,86 @@ package org.dmfs.provider.tasks.model; import android.content.ContentValues; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.contract.TaskContract; - /** * 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; - - - public ContentValuesTaskAdapter(ContentValues values) - { - this(-1L, values); - } - - - public ContentValuesTaskAdapter(long id, ContentValues values) - { - mId = id; - mValues = values; - } - - - @Override - public long id() - { - return mId; - } - - - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return null; - } - - - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return fieldAdapter.isSetIn(mValues); - } - - - @Override - public boolean isWriteable() - { - return true; +public class ContentValuesTaskAdapter extends AbstractTaskAdapter { + private long mId; + private final ContentValues mValues; + + public ContentValuesTaskAdapter(ContentValues values) { + this(-1L, values); + } + + public ContentValuesTaskAdapter(long id, ContentValues values) { + mId = id; + mValues = values; + } + + @Override + public long id() { + return mId; + } + + @Override + public T valueOf(FieldAdapter fieldAdapter) { + return fieldAdapter.getFrom(mValues); + } + + @Override + public T oldValueOf(FieldAdapter fieldAdapter) { + return null; + } + + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) { + return fieldAdapter.isSetIn(mValues); + } + + @Override + public boolean isWriteable() { + return true; + } + + @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 unset(FieldAdapter fieldAdapter) throws IllegalStateException { + fieldAdapter.removeFrom(mValues); + } + + @Override + public int commit(SQLiteDatabase db) { + if (mValues.size() == 0) { + return 0; } - - @Override - public boolean hasUpdates() - { - return mValues.size() > 0; - } - - - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); + 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 void unset(FieldAdapter fieldAdapter) throws IllegalStateException - { - fieldAdapter.removeFrom(mValues); - } - - - @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); - } - } - - - @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/CursorContentValuesInstanceAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java index 6213cb37..dc041e2e 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java @@ -20,7 +20,7 @@ import android.content.ContentValues; import android.database.Cursor; import android.database.MatrixCursor; import android.database.sqlite.SQLiteDatabase; - +import java.util.ArrayList; import org.dmfs.iterables.decorators.Sieved; import org.dmfs.iterables.elementary.Seq; import org.dmfs.jems.iterable.decorators.Mapped; @@ -31,182 +31,150 @@ import org.dmfs.provider.tasks.model.adapters.FieldAdapter; import org.dmfs.provider.tasks.utils.ContainsValues; import org.dmfs.tasks.contract.TaskContract; -import java.util.ArrayList; - - /** - * An {@link InstanceAdapter} 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)}. + * An {@link InstanceAdapter} 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 CursorContentValuesInstanceAdapter extends AbstractInstanceAdapter -{ - private final long mId; - private final Cursor mCursor; - private final ContentValues mValues; - - - public CursorContentValuesInstanceAdapter(Cursor cursor, ContentValues values) - { - if (cursor == null && !_ID.existsIn(values)) - { - mId = -1L; - } - else - { - mId = _ID.getFrom(cursor); - } - mCursor = cursor; - mValues = values; +public class CursorContentValuesInstanceAdapter extends AbstractInstanceAdapter { + private final long mId; + private final Cursor mCursor; + private final ContentValues mValues; + + public CursorContentValuesInstanceAdapter(Cursor cursor, ContentValues values) { + if (cursor == null && !_ID.existsIn(values)) { + mId = -1L; + } else { + mId = _ID.getFrom(cursor); } - - - public CursorContentValuesInstanceAdapter(long id, Cursor cursor, ContentValues values) - { - mId = id; - mCursor = cursor; - mValues = values; + mCursor = cursor; + mValues = values; + } + + public CursorContentValuesInstanceAdapter(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); } - - - @Override - public long id() - { - return mId; + return fieldAdapter.getFrom(mCursor, mValues); + } + + @Override + public T oldValueOf(FieldAdapter fieldAdapter) { + return fieldAdapter.getFrom(mCursor); + } + + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) { + if (mValues == null || !fieldAdapter.isSetIn(mValues)) { + return false; } - - - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - if (mValues == null) - { - return fieldAdapter.getFrom(mCursor); - } - return fieldAdapter.getFrom(mCursor, mValues); + Object oldValue = fieldAdapter.getFrom(mCursor); + Object newValue = fieldAdapter.getFrom(mValues); + + return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue); + } + + @Override + public boolean isWriteable() { + return mValues != null; + } + + @Override + public boolean hasUpdates() { + return mValues != null + && mValues.size() > 0 + && !new ContainsValues(mValues).satisfiedBy(mCursor); + } + + @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 T oldValueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor); - } - - - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - if (mValues == null || !fieldAdapter.isSetIn(mValues)) - { - return false; - } - Object oldValue = fieldAdapter.getFrom(mCursor); - Object newValue = fieldAdapter.getFrom(mValues); - - return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue); - } + @Override + public T getState(FieldAdapter stateFieldAdater) { + return null; + } + @Override + public void setState(FieldAdapter stateFieldAdater, T value) {} - @Override - public boolean isWriteable() - { - return mValues != null; - } - - - @Override - public boolean hasUpdates() - { - return mValues != null && mValues.size() > 0 && !new ContainsValues(mValues).satisfiedBy(mCursor); - } - + @Override + public InstanceAdapter duplicate() { + ContentValues newValues = new ContentValues(mValues); - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); + // 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.Instances._ID.equals(column)) { + newValues.put(column, mCursor.getString(i)); + } } + return new ContentValuesInstanceAdapter(newValues); + } - @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 T getState(FieldAdapter stateFieldAdater) - { - return null; - } - - - @Override - public void setState(FieldAdapter stateFieldAdater, T value) - { - - } - - - @Override - public InstanceAdapter 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.Instances._ID.equals(column)) - { - newValues.put(column, mCursor.getString(i)); - } - } - - return new ContentValuesInstanceAdapter(newValues); - } - - - @Override - public TaskAdapter taskAdapter() - { - // make sure we remove any instance fields - ContentValues values = new Reduced( + @Override + public TaskAdapter taskAdapter() { + // make sure we remove any instance fields + ContentValues values = + new Reduced( () -> new ContentValues(mValues), (contentValues, column) -> { - contentValues.remove(column); - return contentValues; + contentValues.remove(column); + return contentValues; }, - INSTANCE_COLUMN_NAMES).value(); + INSTANCE_COLUMN_NAMES) + .value(); - // create a new cursor which doesn't contain the instance columns - String[] cursorColumns = new Collected<>( + // create a new cursor which doesn't contain the instance columns + String[] cursorColumns = + new Collected<>( ArrayList::new, - new Sieved<>(col -> !INSTANCE_COLUMN_NAMES.contains(col), new Seq<>(mCursor.getColumnNames()))) - .value().toArray(new String[0]); - MatrixCursor cursor = new MatrixCursor(cursorColumns); - cursor.addRow( - new Mapped<>( - column -> mCursor.getType(column) == Cursor.FIELD_TYPE_BLOB ? mCursor.getBlob(column) : mCursor.getString(column), - new Mapped<>( - mCursor::getColumnIndex, - new Seq<>(cursorColumns)))); - cursor.moveToFirst(); - return new CursorContentValuesTaskAdapter(valueOf(InstanceAdapter.TASK_ID), cursor, values); - } + new Sieved<>( + col -> !INSTANCE_COLUMN_NAMES.contains(col), + new Seq<>(mCursor.getColumnNames()))) + .value() + .toArray(new String[0]); + MatrixCursor cursor = new MatrixCursor(cursorColumns); + cursor.addRow( + new Mapped<>( + column -> + mCursor.getType(column) == Cursor.FIELD_TYPE_BLOB + ? mCursor.getBlob(column) + : mCursor.getString(column), + new Mapped<>(mCursor::getColumnIndex, new Seq<>(cursorColumns)))); + cursor.moveToFirst(); + return new CursorContentValuesTaskAdapter(valueOf(InstanceAdapter.TASK_ID), cursor, values); + } } 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 4bdffb58..6b8bdf5f 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 @@ -19,121 +19,99 @@ package org.dmfs.provider.tasks.model; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.adapters.FieldAdapter; import org.dmfs.provider.tasks.utils.ContainsValues; import org.dmfs.tasks.contract.TaskContract; - /** * @author Marten Gajda */ -public class CursorContentValuesListAdapter extends AbstractListAdapter -{ - 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; - } - - - @Override - public long id() - { - return mId; - } - - - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor, mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor); - } - - - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - if (mValues == null || !fieldAdapter.isSetIn(mValues)) - { - return false; - } - Object oldValue = fieldAdapter.getFrom(mCursor); - Object newValue = fieldAdapter.getFrom(mValues); - - return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue); - } - - - @Override - public boolean isWriteable() - { - return true; - } - - - @Override - public boolean hasUpdates() - { - return mValues != null && mValues.size() > 0 && !new ContainsValues(mValues).satisfiedBy(mCursor); - } - - - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); +public class CursorContentValuesListAdapter extends AbstractListAdapter { + 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; + } + + @Override + public long id() { + return mId; + } + + @Override + public T valueOf(FieldAdapter fieldAdapter) { + return fieldAdapter.getFrom(mCursor, mValues); + } + + @Override + public T oldValueOf(FieldAdapter fieldAdapter) { + return fieldAdapter.getFrom(mCursor); + } + + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) { + if (mValues == null || !fieldAdapter.isSetIn(mValues)) { + return false; } - - - @Override - public void unset(FieldAdapter fieldAdapter) throws IllegalStateException - { - fieldAdapter.removeFrom(mValues); + Object oldValue = fieldAdapter.getFrom(mCursor); + Object newValue = fieldAdapter.getFrom(mValues); + + return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue); + } + + @Override + public boolean isWriteable() { + return true; + } + + @Override + public boolean hasUpdates() { + return mValues != null + && mValues.size() > 0 + && !new ContainsValues(mValues).satisfiedBy(mCursor); + } + + @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; } - - @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); + + // 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)); + } } - - @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)); - } - } - - 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 0ed20dfc..1c42fa8c 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 @@ -19,151 +19,120 @@ package org.dmfs.provider.tasks.model; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.adapters.FieldAdapter; import org.dmfs.provider.tasks.utils.ContainsValues; import org.dmfs.tasks.contract.TaskContract; - /** - * 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)}. + * 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; - - - public CursorContentValuesTaskAdapter(Cursor cursor, ContentValues values) - { - if (cursor == null && !_ID.existsIn(values)) - { - mId = -1L; - } - else - { - mId = _ID.getFrom(cursor); - } - mCursor = cursor; - mValues = values; +public class CursorContentValuesTaskAdapter extends AbstractTaskAdapter { + 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); } - - - public CursorContentValuesTaskAdapter(long id, Cursor cursor, ContentValues values) - { - mId = id; - mCursor = cursor; - mValues = values; + 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); } - - - @Override - public long id() - { - return mId; + return fieldAdapter.getFrom(mCursor, mValues); + } + + @Override + public T oldValueOf(FieldAdapter fieldAdapter) { + return fieldAdapter.getFrom(mCursor); + } + + @Override + public boolean isUpdated(FieldAdapter fieldAdapter) { + if (mValues == null || !fieldAdapter.isSetIn(mValues)) { + return false; } - - - @Override - public T valueOf(FieldAdapter fieldAdapter) - { - if (mValues == null) - { - return fieldAdapter.getFrom(mCursor); - } - return fieldAdapter.getFrom(mCursor, mValues); + Object oldValue = fieldAdapter.existsIn(mCursor) ? fieldAdapter.getFrom(mCursor) : null; + Object newValue = fieldAdapter.getFrom(mValues); + // we need to special case RRULE, because RecurrenceRule doesn't support `equals` + if (fieldAdapter != TaskAdapter.RRULE) { + return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue); + } else { + // in case of RRULE we compare the String values. + return oldValue == null && newValue != null + || oldValue != null + && (newValue == null || !oldValue.toString().equals(newValue.toString())); } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor); + } + + @Override + public boolean isWriteable() { + return mValues != null; + } + + @Override + public boolean hasUpdates() { + return mValues != null + && mValues.size() > 0 + && !new ContainsValues(mValues).satisfiedBy(mCursor); + } + + @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 boolean isUpdated(FieldAdapter fieldAdapter) - { - if (mValues == null || !fieldAdapter.isSetIn(mValues)) - { - return false; - } - Object oldValue = fieldAdapter.existsIn(mCursor) ? fieldAdapter.getFrom(mCursor) : null; - Object newValue = fieldAdapter.getFrom(mValues); - // we need to special case RRULE, because RecurrenceRule doesn't support `equals` - if (fieldAdapter != TaskAdapter.RRULE) - { - return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue); - } - else - { - // in case of RRULE we compare the String values. - return oldValue == null && newValue != null || oldValue != null && (newValue == null || !oldValue.toString().equals(newValue.toString())); - } - } - - - @Override - public boolean isWriteable() - { - return mValues != null; - } - + @Override + public TaskAdapter duplicate() { + ContentValues newValues = new ContentValues(mValues); - @Override - public boolean hasUpdates() - { - return mValues != null && mValues.size() > 0 && !new ContainsValues(mValues).satisfiedBy(mCursor); + // 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)); + } } - - @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 b3d6c7e5..892b8d02 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 @@ -20,132 +20,117 @@ 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. + * 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. - */ - 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. - */ - 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. - */ - 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. - */ - 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. - */ - 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. - */ - boolean isWriteable(); - - /** - * Returns whether any value has been modified. - * - * @return true if there are modified values, false otherwise. - */ - 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. - */ - 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. - */ - 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. - */ - 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. - */ - 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. - */ - 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. - */ - EntityAdapter duplicate(); +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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + boolean isWriteable(); + + /** + * Returns whether any value has been modified. + * + * @return true if there are modified values, false otherwise. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + EntityAdapter duplicate(); } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/InstanceAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/InstanceAdapter.java index b2c9de3c..c30c0c02 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/InstanceAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/InstanceAdapter.java @@ -16,9 +16,12 @@ package org.dmfs.provider.tasks.model; +import static java.util.Arrays.asList; + import android.content.ContentValues; import android.database.Cursor; - +import java.util.Collection; +import java.util.HashSet; import org.dmfs.provider.tasks.model.adapters.DateTimeFieldAdapter; import org.dmfs.provider.tasks.model.adapters.IntegerFieldAdapter; import org.dmfs.provider.tasks.model.adapters.LongFieldAdapter; @@ -27,83 +30,68 @@ import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.contract.TaskContract.Tasks; -import java.util.Collection; -import java.util.HashSet; - -import static java.util.Arrays.asList; - - /** - * Adapter to read instance values from primitive data sets like {@link Cursor}s or {@link ContentValues}s. + * Adapter to read instance values from primitive data sets like {@link Cursor}s or {@link + * ContentValues}s. * * @author Marten Gajda */ -public interface InstanceAdapter extends EntityAdapter -{ - - Collection INSTANCE_COLUMN_NAMES = new HashSet<>(asList( - TaskContract.Instances.INSTANCE_START, - TaskContract.Instances.INSTANCE_START_SORTING, - TaskContract.Instances.INSTANCE_DUE, - TaskContract.Instances.INSTANCE_DUE_SORTING, - TaskContract.Instances.INSTANCE_DURATION, - TaskContract.Instances.INSTANCE_ORIGINAL_TIME, - TaskContract.Instances.TASK_ID, - TaskContract.Instances.DISTANCE_FROM_CURRENT, - "_id:1")); - - /** - * Adapter for the row id of a task instance. - */ - LongFieldAdapter _ID = new LongFieldAdapter(Instances._ID); - - /** - * Adapter for the due date of a task instance. - */ - DateTimeFieldAdapter INSTANCE_DUE = new DateTimeFieldAdapter<>(Instances.INSTANCE_DUE, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the start date of a task instance. - */ - DateTimeFieldAdapter INSTANCE_START = new DateTimeFieldAdapter<>(Instances.INSTANCE_START, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the start sorting of a task instance. - */ - LongFieldAdapter INSTANCE_START_SORTING = new LongFieldAdapter<>(Instances.INSTANCE_START_SORTING); - - /** - * Adapter for the due sorting of a task instance. - */ - LongFieldAdapter INSTANCE_DUE_SORTING = new LongFieldAdapter<>(Instances.INSTANCE_DUE_SORTING); - - /** - * Adapter for the original time of a task instance. - */ - DateTimeFieldAdapter INSTANCE_ORIGINAL_TIME = new DateTimeFieldAdapter<>(Instances.INSTANCE_ORIGINAL_TIME, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the distance of a task instance from the current instance. - */ - IntegerFieldAdapter DISTANCE_FROM_CURRENT = new IntegerFieldAdapter<>(Instances.DISTANCE_FROM_CURRENT); - - /** - * Adapter for the title of a task instance. - */ - StringFieldAdapter TITLE = new StringFieldAdapter<>(Tasks.TITLE); - - /** - * Adapter for the row id of the task. - */ - LongFieldAdapter TASK_ID = new LongFieldAdapter(Instances.TASK_ID); - - @Override - InstanceAdapter duplicate(); - - /** - * Returns a {@link TaskAdapter} for the task component of the instanced view. - * - * @return - */ - TaskAdapter taskAdapter(); +public interface InstanceAdapter extends EntityAdapter { + + Collection INSTANCE_COLUMN_NAMES = + new HashSet<>( + asList( + TaskContract.Instances.INSTANCE_START, + TaskContract.Instances.INSTANCE_START_SORTING, + TaskContract.Instances.INSTANCE_DUE, + TaskContract.Instances.INSTANCE_DUE_SORTING, + TaskContract.Instances.INSTANCE_DURATION, + TaskContract.Instances.INSTANCE_ORIGINAL_TIME, + TaskContract.Instances.TASK_ID, + TaskContract.Instances.DISTANCE_FROM_CURRENT, + "_id:1")); + + /** Adapter for the row id of a task instance. */ + LongFieldAdapter _ID = new LongFieldAdapter(Instances._ID); + + /** Adapter for the due date of a task instance. */ + DateTimeFieldAdapter INSTANCE_DUE = + new DateTimeFieldAdapter<>(Instances.INSTANCE_DUE, Tasks.TZ, Tasks.IS_ALLDAY); + + /** Adapter for the start date of a task instance. */ + DateTimeFieldAdapter INSTANCE_START = + new DateTimeFieldAdapter<>(Instances.INSTANCE_START, Tasks.TZ, Tasks.IS_ALLDAY); + + /** Adapter for the start sorting of a task instance. */ + LongFieldAdapter INSTANCE_START_SORTING = + new LongFieldAdapter<>(Instances.INSTANCE_START_SORTING); + + /** Adapter for the due sorting of a task instance. */ + LongFieldAdapter INSTANCE_DUE_SORTING = + new LongFieldAdapter<>(Instances.INSTANCE_DUE_SORTING); + + /** Adapter for the original time of a task instance. */ + DateTimeFieldAdapter INSTANCE_ORIGINAL_TIME = + new DateTimeFieldAdapter<>(Instances.INSTANCE_ORIGINAL_TIME, Tasks.TZ, Tasks.IS_ALLDAY); + + /** Adapter for the distance of a task instance from the current instance. */ + IntegerFieldAdapter DISTANCE_FROM_CURRENT = + new IntegerFieldAdapter<>(Instances.DISTANCE_FROM_CURRENT); + + /** Adapter for the title of a task instance. */ + StringFieldAdapter TITLE = new StringFieldAdapter<>(Tasks.TITLE); + + /** Adapter for the row id of the task. */ + LongFieldAdapter TASK_ID = + new LongFieldAdapter(Instances.TASK_ID); + + @Override + InstanceAdapter duplicate(); + + /** + * Returns a {@link TaskAdapter} for the task component of the instanced view. + * + * @return + */ + TaskAdapter taskAdapter(); } 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 d72a6738..27959e0b 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 @@ -18,65 +18,52 @@ package org.dmfs.provider.tasks.model; import android.content.ContentValues; import android.database.Cursor; - 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 org.dmfs.tasks.contract.TaskContract.TaskLists; - /** - * Adapter to read list values from primitive data sets like {@link Cursor}s or {@link ContentValues}s. + * 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. - */ - LongFieldAdapter _ID = new LongFieldAdapter(TaskLists._ID); +public interface ListAdapter extends EntityAdapter { + /** Adapter for the row id of a task list. */ + LongFieldAdapter _ID = new LongFieldAdapter(TaskLists._ID); - /** - * Adapter for the _sync_id of a list. - */ - StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskLists._SYNC_ID); + /** Adapter for the _sync_id of a list. */ + StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskLists._SYNC_ID); - /** - * Adapter for the sync version of a list. - */ - StringFieldAdapter SYNC_VERSION = new StringFieldAdapter(TaskLists.SYNC_VERSION); + /** Adapter for the sync version of a list. */ + StringFieldAdapter SYNC_VERSION = + new StringFieldAdapter(TaskLists.SYNC_VERSION); - /** - * Adapter for the account name of a list. - */ - StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(TaskLists.ACCOUNT_NAME); + /** Adapter for the account name of a list. */ + StringFieldAdapter ACCOUNT_NAME = + new StringFieldAdapter(TaskLists.ACCOUNT_NAME); - /** - * Adapter for the account type of a list. - */ - StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(TaskLists.ACCOUNT_TYPE); + /** Adapter for the account type of a list. */ + StringFieldAdapter ACCOUNT_TYPE = + new StringFieldAdapter(TaskLists.ACCOUNT_TYPE); - /** - * Adapter for the owner of a list. - */ - StringFieldAdapter OWNER = new StringFieldAdapter(TaskLists.OWNER); + /** Adapter for the owner of a list. */ + StringFieldAdapter OWNER = new StringFieldAdapter(TaskLists.OWNER); - /** - * Adapter for the name of a list. - */ - StringFieldAdapter LIST_NAME = new StringFieldAdapter(TaskLists.LIST_NAME); + /** Adapter for the name of a list. */ + StringFieldAdapter LIST_NAME = + new StringFieldAdapter(TaskLists.LIST_NAME); - /** - * Adapter for the color of a list. - */ - IntegerFieldAdapter LIST_COLOR = new IntegerFieldAdapter(TaskLists.LIST_COLOR); + /** Adapter for the color of a list. */ + 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 - 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 + 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 4668cce3..31ce3304 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 @@ -18,7 +18,6 @@ package org.dmfs.provider.tasks.model; import android.content.ContentValues; import android.database.Cursor; - import org.dmfs.provider.tasks.model.adapters.BinaryFieldAdapter; import org.dmfs.provider.tasks.model.adapters.BooleanFieldAdapter; import org.dmfs.provider.tasks.model.adapters.DateTimeFieldAdapter; @@ -33,330 +32,257 @@ import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.contract.TaskContract.Tasks; - /** - * Adapter to read task values from primitive data sets like {@link Cursor}s or {@link ContentValues}s. + * 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. - */ - LongFieldAdapter _ID = new LongFieldAdapter(Tasks._ID); - - /** - * Adapter for the version of a task. - */ - LongFieldAdapter VERSION = new LongFieldAdapter<>(Tasks.VERSION); - - /** - * Adapter for the task row id of as instance. - */ - LongFieldAdapter INSTANCE_TASK_ID = new LongFieldAdapter(Instances.TASK_ID); - - /** - * Adapter for the row id of the list of a task. - */ - LongFieldAdapter LIST_ID = new LongFieldAdapter(Tasks.LIST_ID); - - /** - * Adapter for the owner of the list of a task. - */ - StringFieldAdapter LIST_OWNER = new StringFieldAdapter(Tasks.LIST_OWNER); - - /** - * Adapter for the row id of original instance of a task. - */ - LongFieldAdapter ORIGINAL_INSTANCE_ID = new LongFieldAdapter(Tasks.ORIGINAL_INSTANCE_ID); - - /** - * Adapter for the sync_id of original instance of a task. - */ - StringFieldAdapter ORIGINAL_INSTANCE_SYNC_ID = new StringFieldAdapter(Tasks.ORIGINAL_INSTANCE_SYNC_ID); - - /** - * Adapter for the original instance all day flag of a task. - */ - BooleanFieldAdapter ORIGINAL_INSTANCE_ALLDAY = new BooleanFieldAdapter(Tasks.ORIGINAL_INSTANCE_ALLDAY); - - /** - * Adapter for the parent_id of a task. - */ - LongFieldAdapter PARENT_ID = new LongFieldAdapter(Tasks.PARENT_ID); - - /** - * Adapter for the all day flag of a task. - */ - BooleanFieldAdapter IS_ALLDAY = new BooleanFieldAdapter(Tasks.IS_ALLDAY); - - /** - * Adapter for the percent complete value of a task. - */ - IntegerFieldAdapter PERCENT_COMPLETE = new IntegerFieldAdapter(Tasks.PERCENT_COMPLETE); - - /** - * Adapter for the status of a task. - */ - IntegerFieldAdapter STATUS = new IntegerFieldAdapter(Tasks.STATUS); - - /** - * Adapter for the priority value of a task. - */ - IntegerFieldAdapter PRIORITY = new IntegerFieldAdapter(Tasks.PRIORITY); - - /** - * Adapter for the classification value of a task. - */ - IntegerFieldAdapter CLASSIFICATION = new IntegerFieldAdapter(Tasks.CLASSIFICATION); - - /** - * Adapter for the list name of a task. - */ - StringFieldAdapter LIST_NAME = new StringFieldAdapter(Tasks.LIST_NAME); - - /** - * Adapter for the account name of a task. - */ - StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(Tasks.ACCOUNT_NAME); - - /** - * Adapter for the account type of a task. - */ - StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(Tasks.ACCOUNT_TYPE); - - /** - * Adapter for the title of a task. - */ - StringFieldAdapter TITLE = new StringFieldAdapter(Tasks.TITLE); - - /** - * Adapter for the location of a task. - */ - StringFieldAdapter LOCATION = new StringFieldAdapter(Tasks.LOCATION); - - /** - * Adapter for the description of a task. - */ - StringFieldAdapter DESCRIPTION = new StringFieldAdapter(Tasks.DESCRIPTION); - - /** - * Adapter for the start date of a task. - */ - DateTimeFieldAdapter DTSTART = new DateTimeFieldAdapter(Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the original date of a task. - */ - DateTimeFieldAdapter ORIGINAL_INSTANCE_TIME = new DateTimeFieldAdapter(Tasks.ORIGINAL_INSTANCE_TIME, Tasks.TZ, - Tasks.ORIGINAL_INSTANCE_ALLDAY); - - /** - * Adapter for the raw start date timestamp of a task. - */ - LongFieldAdapter DTSTART_RAW = new LongFieldAdapter(Tasks.DTSTART); - - /** - * Adapter for the due date of a task. - */ - DateTimeFieldAdapter DUE = new DateTimeFieldAdapter(Tasks.DUE, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the raw due date timestamp of a task. - */ - LongFieldAdapter DUE_RAW = new LongFieldAdapter(Tasks.DUE); - - /** - * Adapter for the start date of a task. - */ - DurationFieldAdapter DURATION = new DurationFieldAdapter(Tasks.DURATION); - - /** - * Adapter for the dirty flag of a task. - */ - BooleanFieldAdapter _DIRTY = new BooleanFieldAdapter(Tasks._DIRTY); - - /** - * Adapter for the deleted flag of a task. - */ - BooleanFieldAdapter _DELETED = new BooleanFieldAdapter(Tasks._DELETED); - - /** - * Adapter for the completed date of a task. - */ - DateTimeFieldAdapter COMPLETED = new DateTimeFieldAdapter(Tasks.COMPLETED, null, null); - - /** - * Adapter for the created date of a task. - */ - DateTimeFieldAdapter CREATED = new DateTimeFieldAdapter(Tasks.CREATED, null, null); - - /** - * Adapter for the last modified date of a task. - */ - DateTimeFieldAdapter LAST_MODIFIED = new DateTimeFieldAdapter(Tasks.LAST_MODIFIED, null, null); - - /** - * Adapter for the URL of a task. - */ - UrlFieldAdapter URL = new UrlFieldAdapter(TaskContract.Tasks.URL); - - /** - * Adapter for the UID of a task. - */ - StringFieldAdapter _UID = new StringFieldAdapter(TaskContract.Tasks._UID); - - /** - * Adapter for the raw time zone of a task. - */ - StringFieldAdapter TIMEZONE_RAW = new StringFieldAdapter(TaskContract.Tasks.TZ); - - /** - * Adapter for the Color of the task. - */ - IntegerFieldAdapter LIST_COLOR = new IntegerFieldAdapter(TaskContract.Tasks.LIST_COLOR); - - /** - * Adapter for the access level of the task list. - */ - IntegerFieldAdapter LIST_ACCESS_LEVEL = new IntegerFieldAdapter(TaskContract.Tasks.LIST_ACCESS_LEVEL); - - /** - * Adapter for the visibility setting of the task list. - */ - BooleanFieldAdapter LIST_VISIBLE = new BooleanFieldAdapter(TaskContract.Tasks.VISIBLE); - - /** - * Adpater for the ID of the task. - */ - IntegerFieldAdapter TASK_ID = new IntegerFieldAdapter(TaskContract.Tasks._ID); - - /** - * Adapter for the IS_CLOSED flag of a task. - */ - BooleanFieldAdapter IS_CLOSED = new BooleanFieldAdapter(TaskContract.Tasks.IS_CLOSED); - - /** - * Adapter for the IS_NEW flag of a task. - */ - BooleanFieldAdapter IS_NEW = new BooleanFieldAdapter(TaskContract.Tasks.IS_NEW); - - /** - * Adapter for the PINNED flag of a task. - */ - BooleanFieldAdapter PINNED = new BooleanFieldAdapter(TaskContract.Tasks.PINNED); - - /** - * Adapter for the HAS_ALARMS flag of a task. - */ - BooleanFieldAdapter HAS_ALARMS = new BooleanFieldAdapter(TaskContract.Tasks.HAS_ALARMS); - - /** - * Adapter for the HAS_PROPERTIES flag of a task. - */ - BooleanFieldAdapter HAS_PROPERTIES = new BooleanFieldAdapter(TaskContract.Tasks.HAS_PROPERTIES); - - /** - * Adapter for the RRULE of a task. - */ - RRuleFieldAdapter RRULE = new RRuleFieldAdapter(TaskContract.Tasks.RRULE); - - /** - * Adapter for the RDATE of a task. - */ - DateTimeIterableFieldAdapter RDATE = new DateTimeIterableFieldAdapter(TaskContract.Tasks.RDATE, - TaskContract.Tasks.TZ); - - /** - * Adapter for the EXDATE of a task. - */ - DateTimeIterableFieldAdapter EXDATE = new DateTimeIterableFieldAdapter(TaskContract.Tasks.EXDATE, - TaskContract.Tasks.TZ); - - /** - * Adapter for the SYNC1 field of a task. - */ - BinaryFieldAdapter SYNC1 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC1); - - /** - * Adapter for the SYNC2 field of a task. - */ - BinaryFieldAdapter SYNC2 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC2); - - /** - * Adapter for the SYNC3 field of a task. - */ - BinaryFieldAdapter SYNC3 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC3); - - /** - * Adapter for the SYNC4 field of a task. - */ - BinaryFieldAdapter SYNC4 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC4); - - /** - * Adapter for the SYNC5 field of a task. - */ - BinaryFieldAdapter SYNC5 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC5); - - /** - * Adapter for the SYNC6 field of a task. - */ - BinaryFieldAdapter SYNC6 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC6); - - /** - * Adapter for the SYNC7 field of a task. - */ - BinaryFieldAdapter SYNC7 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC7); - - /** - * Adapter for the SYNC8 field of a task. - */ - BinaryFieldAdapter SYNC8 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC8); - - /** - * Adapter for the SYNC_VERSION field of a task. - */ - BinaryFieldAdapter SYNC_VERSION = new BinaryFieldAdapter(TaskContract.Tasks.SYNC_VERSION); - - /** - * Adapter for the SYNC_ID field of a task. - */ - StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskContract.Tasks._SYNC_ID); - - /** - * Adapter for the due date of a task instance. - */ - DateTimeFieldAdapter INSTANCE_DUE = new DateTimeFieldAdapter(Instances.INSTANCE_DUE, Tasks.TZ, - Tasks.IS_ALLDAY); - - /** - * Adapter for the start date of a task instance. - */ - 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. - */ - 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. - */ - 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 - TaskAdapter duplicate(); +public interface TaskAdapter extends EntityAdapter { + /** Adapter for the row id of a task. */ + LongFieldAdapter _ID = new LongFieldAdapter(Tasks._ID); + + /** Adapter for the version of a task. */ + LongFieldAdapter VERSION = new LongFieldAdapter<>(Tasks.VERSION); + + /** Adapter for the task row id of as instance. */ + LongFieldAdapter INSTANCE_TASK_ID = + new LongFieldAdapter(Instances.TASK_ID); + + /** Adapter for the row id of the list of a task. */ + LongFieldAdapter LIST_ID = new LongFieldAdapter(Tasks.LIST_ID); + + /** Adapter for the owner of the list of a task. */ + StringFieldAdapter LIST_OWNER = + new StringFieldAdapter(Tasks.LIST_OWNER); + + /** Adapter for the row id of original instance of a task. */ + LongFieldAdapter ORIGINAL_INSTANCE_ID = + new LongFieldAdapter(Tasks.ORIGINAL_INSTANCE_ID); + + /** Adapter for the sync_id of original instance of a task. */ + StringFieldAdapter ORIGINAL_INSTANCE_SYNC_ID = + new StringFieldAdapter(Tasks.ORIGINAL_INSTANCE_SYNC_ID); + + /** Adapter for the original instance all day flag of a task. */ + BooleanFieldAdapter ORIGINAL_INSTANCE_ALLDAY = + new BooleanFieldAdapter(Tasks.ORIGINAL_INSTANCE_ALLDAY); + + /** Adapter for the parent_id of a task. */ + LongFieldAdapter PARENT_ID = new LongFieldAdapter(Tasks.PARENT_ID); + + /** Adapter for the all day flag of a task. */ + BooleanFieldAdapter IS_ALLDAY = + new BooleanFieldAdapter(Tasks.IS_ALLDAY); + + /** Adapter for the percent complete value of a task. */ + IntegerFieldAdapter PERCENT_COMPLETE = + new IntegerFieldAdapter(Tasks.PERCENT_COMPLETE); + + /** Adapter for the status of a task. */ + IntegerFieldAdapter STATUS = new IntegerFieldAdapter(Tasks.STATUS); + + /** Adapter for the priority value of a task. */ + IntegerFieldAdapter PRIORITY = new IntegerFieldAdapter(Tasks.PRIORITY); + + /** Adapter for the classification value of a task. */ + IntegerFieldAdapter CLASSIFICATION = + new IntegerFieldAdapter(Tasks.CLASSIFICATION); + + /** Adapter for the list name of a task. */ + StringFieldAdapter LIST_NAME = new StringFieldAdapter(Tasks.LIST_NAME); + + /** Adapter for the account name of a task. */ + StringFieldAdapter ACCOUNT_NAME = + new StringFieldAdapter(Tasks.ACCOUNT_NAME); + + /** Adapter for the account type of a task. */ + StringFieldAdapter ACCOUNT_TYPE = + new StringFieldAdapter(Tasks.ACCOUNT_TYPE); + + /** Adapter for the title of a task. */ + StringFieldAdapter TITLE = new StringFieldAdapter(Tasks.TITLE); + + /** Adapter for the location of a task. */ + StringFieldAdapter LOCATION = new StringFieldAdapter(Tasks.LOCATION); + + /** Adapter for the description of a task. */ + StringFieldAdapter DESCRIPTION = + new StringFieldAdapter(Tasks.DESCRIPTION); + + /** Adapter for the start date of a task. */ + DateTimeFieldAdapter DTSTART = + new DateTimeFieldAdapter(Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY); + + /** Adapter for the original date of a task. */ + DateTimeFieldAdapter ORIGINAL_INSTANCE_TIME = + new DateTimeFieldAdapter( + Tasks.ORIGINAL_INSTANCE_TIME, Tasks.TZ, Tasks.ORIGINAL_INSTANCE_ALLDAY); + + /** Adapter for the raw start date timestamp of a task. */ + LongFieldAdapter DTSTART_RAW = new LongFieldAdapter(Tasks.DTSTART); + + /** Adapter for the due date of a task. */ + DateTimeFieldAdapter DUE = + new DateTimeFieldAdapter(Tasks.DUE, Tasks.TZ, Tasks.IS_ALLDAY); + + /** Adapter for the raw due date timestamp of a task. */ + LongFieldAdapter DUE_RAW = new LongFieldAdapter(Tasks.DUE); + + /** Adapter for the start date of a task. */ + DurationFieldAdapter DURATION = + new DurationFieldAdapter(Tasks.DURATION); + + /** Adapter for the dirty flag of a task. */ + BooleanFieldAdapter _DIRTY = new BooleanFieldAdapter(Tasks._DIRTY); + + /** Adapter for the deleted flag of a task. */ + BooleanFieldAdapter _DELETED = new BooleanFieldAdapter(Tasks._DELETED); + + /** Adapter for the completed date of a task. */ + DateTimeFieldAdapter COMPLETED = + new DateTimeFieldAdapter(Tasks.COMPLETED, null, null); + + /** Adapter for the created date of a task. */ + DateTimeFieldAdapter CREATED = + new DateTimeFieldAdapter(Tasks.CREATED, null, null); + + /** Adapter for the last modified date of a task. */ + DateTimeFieldAdapter LAST_MODIFIED = + new DateTimeFieldAdapter(Tasks.LAST_MODIFIED, null, null); + + /** Adapter for the URL of a task. */ + UrlFieldAdapter URL = new UrlFieldAdapter(TaskContract.Tasks.URL); + + /** Adapter for the UID of a task. */ + StringFieldAdapter _UID = + new StringFieldAdapter(TaskContract.Tasks._UID); + + /** Adapter for the raw time zone of a task. */ + StringFieldAdapter TIMEZONE_RAW = + new StringFieldAdapter(TaskContract.Tasks.TZ); + + /** Adapter for the Color of the task. */ + IntegerFieldAdapter LIST_COLOR = + new IntegerFieldAdapter(TaskContract.Tasks.LIST_COLOR); + + /** Adapter for the access level of the task list. */ + IntegerFieldAdapter LIST_ACCESS_LEVEL = + new IntegerFieldAdapter(TaskContract.Tasks.LIST_ACCESS_LEVEL); + + /** Adapter for the visibility setting of the task list. */ + BooleanFieldAdapter LIST_VISIBLE = + new BooleanFieldAdapter(TaskContract.Tasks.VISIBLE); + + /** Adpater for the ID of the task. */ + IntegerFieldAdapter TASK_ID = + new IntegerFieldAdapter(TaskContract.Tasks._ID); + + /** Adapter for the IS_CLOSED flag of a task. */ + BooleanFieldAdapter IS_CLOSED = + new BooleanFieldAdapter(TaskContract.Tasks.IS_CLOSED); + + /** Adapter for the IS_NEW flag of a task. */ + BooleanFieldAdapter IS_NEW = + new BooleanFieldAdapter(TaskContract.Tasks.IS_NEW); + + /** Adapter for the PINNED flag of a task. */ + BooleanFieldAdapter PINNED = + new BooleanFieldAdapter(TaskContract.Tasks.PINNED); + + /** Adapter for the HAS_ALARMS flag of a task. */ + BooleanFieldAdapter HAS_ALARMS = + new BooleanFieldAdapter(TaskContract.Tasks.HAS_ALARMS); + + /** Adapter for the HAS_PROPERTIES flag of a task. */ + BooleanFieldAdapter HAS_PROPERTIES = + new BooleanFieldAdapter(TaskContract.Tasks.HAS_PROPERTIES); + + /** Adapter for the RRULE of a task. */ + RRuleFieldAdapter RRULE = + new RRuleFieldAdapter(TaskContract.Tasks.RRULE); + + /** Adapter for the RDATE of a task. */ + DateTimeIterableFieldAdapter RDATE = + new DateTimeIterableFieldAdapter( + TaskContract.Tasks.RDATE, TaskContract.Tasks.TZ); + + /** Adapter for the EXDATE of a task. */ + DateTimeIterableFieldAdapter EXDATE = + new DateTimeIterableFieldAdapter( + TaskContract.Tasks.EXDATE, TaskContract.Tasks.TZ); + + /** Adapter for the SYNC1 field of a task. */ + BinaryFieldAdapter SYNC1 = + new BinaryFieldAdapter(TaskContract.Tasks.SYNC1); + + /** Adapter for the SYNC2 field of a task. */ + BinaryFieldAdapter SYNC2 = + new BinaryFieldAdapter(TaskContract.Tasks.SYNC2); + + /** Adapter for the SYNC3 field of a task. */ + BinaryFieldAdapter SYNC3 = + new BinaryFieldAdapter(TaskContract.Tasks.SYNC3); + + /** Adapter for the SYNC4 field of a task. */ + BinaryFieldAdapter SYNC4 = + new BinaryFieldAdapter(TaskContract.Tasks.SYNC4); + + /** Adapter for the SYNC5 field of a task. */ + BinaryFieldAdapter SYNC5 = + new BinaryFieldAdapter(TaskContract.Tasks.SYNC5); + + /** Adapter for the SYNC6 field of a task. */ + BinaryFieldAdapter SYNC6 = + new BinaryFieldAdapter(TaskContract.Tasks.SYNC6); + + /** Adapter for the SYNC7 field of a task. */ + BinaryFieldAdapter SYNC7 = + new BinaryFieldAdapter(TaskContract.Tasks.SYNC7); + + /** Adapter for the SYNC8 field of a task. */ + BinaryFieldAdapter SYNC8 = + new BinaryFieldAdapter(TaskContract.Tasks.SYNC8); + + /** Adapter for the SYNC_VERSION field of a task. */ + BinaryFieldAdapter SYNC_VERSION = + new BinaryFieldAdapter(TaskContract.Tasks.SYNC_VERSION); + + /** Adapter for the SYNC_ID field of a task. */ + StringFieldAdapter SYNC_ID = + new StringFieldAdapter(TaskContract.Tasks._SYNC_ID); + + /** Adapter for the due date of a task instance. */ + DateTimeFieldAdapter INSTANCE_DUE = + new DateTimeFieldAdapter(Instances.INSTANCE_DUE, Tasks.TZ, Tasks.IS_ALLDAY); + + /** Adapter for the start date of a task instance. */ + 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. + */ + 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. + */ + 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 + 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 0d6c24df..3caaeb03 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 @@ -19,77 +19,54 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; import android.database.Cursor; - /** * Knows how to load and store a binary value from a {@link Cursor} or {@link ContentValues}. * - * @param - * The type of the entity the field belongs to. - * + * @param 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; +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"); } - - - @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); + 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."); } - - - @Override - public void setIn(ContentValues values, byte[] value) - { - if (value != null) - { - values.put(mFieldName, value); - } - else - { - values.putNull(mFieldName); - } + 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 ef7e9483..55a9bf6a 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 @@ -19,76 +19,58 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; 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). + * Knows how to load and store a {@link Boolean} value from a {@link Cursor} or {@link + * ContentValues}. + * + *

Implementation detail: * - * @param - * The type of the entity the field belongs to. + *

The values are loaded and stored as 0 (for false) and 1 + * (for true). * + * @param 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; +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"); } - - - @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); + 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/DateTimeFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java index 5c9159be..959438f6 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 @@ -18,226 +18,182 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; import android.database.Cursor; - -import org.dmfs.rfc5545.DateTime; - import java.util.TimeZone; - +import org.dmfs.rfc5545.DateTime; /** - * 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: + * 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
  • + *
  • 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. * - * @param - * The type of the entity the field belongs to. + *

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. * + * @param 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; +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"); } - - - @Override - String fieldName() - { - return mTimestampField; + 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; } + String timezone = mTzField == null ? null : values.getAsString(mTzField); + DateTime value = + new DateTime(timezone == null ? null : TimeZone.getTimeZone(timezone), timestamp); + // cache mAlldayField locally + String allDayField = mAllDayField; - @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; - } - String timezone = mTzField == null ? null : values.getAsString(mTzField); - DateTime value = new DateTime(timezone == null ? null : TimeZone.getTimeZone(timezone), timestamp); + // set the allday flag appropriately + Integer allDayInt = allDayField == null ? null : values.getAsInteger(allDayField); - // 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; + 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); + @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 (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; - } + if (cursor.isNull(tsIdx)) { + // if the time stamp is null we return null + return null; + } - Long timestamp = cursor.getLong(tsIdx); + Long timestamp = cursor.getLong(tsIdx); - String timezone = mTzField == null ? null : cursor.getString(tzIdx); - DateTime value = new DateTime(timezone == null ? null : TimeZone.getTimeZone(timezone), timestamp); + String timezone = mTzField == null ? null : cursor.getString(tzIdx); + DateTime value = + new DateTime(timezone == null ? null : TimeZone.getTimeZone(timezone), timestamp); - // set the allday flag appropriately - Integer allDayInt = adIdx < 0 ? null : cursor.getInt(adIdx); + // 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; + 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."); + } + } - @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."); - } - } - - DateTime value = new DateTime(timeZoneId == null ? null : TimeZone.getTimeZone(timeZoneId), timestamp); - - if (allDay != 0) - { - value = value.toAllDay(); - } - return value; + 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."); + } } + DateTime value = + new DateTime(timeZoneId == null ? null : TimeZone.getTimeZone(timeZoneId), timestamp); - @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); - } + 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/DateTimeIterableFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapter.java index 3f1ebbc3..2a468dae 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapter.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapter.java @@ -19,180 +19,147 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; import android.database.Cursor; import android.text.TextUtils; - +import java.util.TimeZone; import org.dmfs.iterables.EmptyIterable; import org.dmfs.iterables.Split; import org.dmfs.iterables.decorators.DelegatingIterable; import org.dmfs.jems.iterable.decorators.Mapped; import org.dmfs.rfc5545.DateTime; -import java.util.TimeZone; - - /** - * Knows how to load and store {@link Iterable}s of {@link DateTime} values from a {@link Cursor} or {@link ContentValues}. - * - * @param - * The type of the entity the field belongs to. + * Knows how to load and store {@link Iterable}s of {@link DateTime} values from a {@link Cursor} or + * {@link ContentValues}. * + * @param The type of the entity the field belongs to. * @author Marten Gajda */ -public final class DateTimeIterableFieldAdapter extends SimpleFieldAdapter, EntityType> -{ - private final String mDateTimeListFieldName; - private final String mTimeZoneFieldName; - - - /** - * Constructor for a new {@link DateTimeIterableFieldAdapter}. - * - * @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 DateTimeIterableFieldAdapter(String datetimeListFieldName, String timezoneFieldName) - { - if (datetimeListFieldName == null) - { - throw new IllegalArgumentException("datetimeListFieldName must not be null"); - } - mDateTimeListFieldName = datetimeListFieldName; - mTimeZoneFieldName = timezoneFieldName; +public final class DateTimeIterableFieldAdapter + extends SimpleFieldAdapter, EntityType> { + private final String mDateTimeListFieldName; + private final String mTimeZoneFieldName; + + /** + * Constructor for a new {@link DateTimeIterableFieldAdapter}. + * + * @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 DateTimeIterableFieldAdapter(String datetimeListFieldName, String timezoneFieldName) { + if (datetimeListFieldName == null) { + throw new IllegalArgumentException("datetimeListFieldName must not be null"); } - - - @Override - String fieldName() - { - return mDateTimeListFieldName; + mDateTimeListFieldName = datetimeListFieldName; + mTimeZoneFieldName = timezoneFieldName; + } + + @Override + String fieldName() { + return mDateTimeListFieldName; + } + + @Override + public Iterable getFrom(ContentValues values) { + String datetimeList = values.getAsString(mDateTimeListFieldName); + if (datetimeList == null) { + // no list, return an empty Iterable + return EmptyIterable.instance(); } + // 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); - @Override - public Iterable getFrom(ContentValues values) - { - String datetimeList = values.getAsString(mDateTimeListFieldName); - if (datetimeList == null) - { - // no list, return an empty Iterable - return EmptyIterable.instance(); - } + return new DateTimeList(timeZone, datetimeList); + } - // 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); + @Override + public Iterable getFrom(Cursor cursor) { + int tdLIdx = cursor.getColumnIndex(mDateTimeListFieldName); + int tzIdx = mTimeZoneFieldName == null ? -1 : cursor.getColumnIndex(mTimeZoneFieldName); - return new DateTimeList(timeZone, datetimeList); + if (tdLIdx < 0 || (mTimeZoneFieldName != null && tzIdx < 0)) { + throw new IllegalArgumentException("At least one column is missing in cursor."); } - - @Override - public Iterable 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 an empty Iterable - return EmptyIterable.instance(); - } - - 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); - - return new DateTimeList(timeZone, datetimeList); + if (cursor.isNull(tdLIdx)) { + // if the time stamp list is null we return an empty Iterable + return EmptyIterable.instance(); } - - @Override - public Iterable getFrom(Cursor cursor, ContentValues values) - { - int tsIdx; - int tzIdx; - String datetimeList; - String timeZoneId = null; - - if (values != null && values.containsKey(mDateTimeListFieldName)) - { - if (values.getAsString(mDateTimeListFieldName) == null) - { - // the date times are null, so we return null - return EmptyIterable.instance(); - } - 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 an empty Iterable. - return EmptyIterable.instance(); - } - 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); - - return new DateTimeList(timeZone, datetimeList); + 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); + + return new DateTimeList(timeZone, datetimeList); + } + + @Override + public Iterable getFrom(Cursor cursor, ContentValues values) { + int tsIdx; + int tzIdx; + String datetimeList; + String timeZoneId = null; + + if (values != null && values.containsKey(mDateTimeListFieldName)) { + if (values.getAsString(mDateTimeListFieldName) == null) { + // the date times are null, so we return null + return EmptyIterable.instance(); + } + 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 an empty Iterable. + return EmptyIterable.instance(); + } + datetimeList = cursor.getString(tsIdx); + } else { + throw new IllegalArgumentException("Missing date time list column."); } - - @Override - public void setIn(ContentValues values, Iterable value) - { - if (value != null) - { - String stringValue = TextUtils.join(",", new Mapped<>(dt -> dt.isFloating() ? dt : dt.shiftTimeZone(DateTime.UTC), value)); - values.put(mDateTimeListFieldName, stringValue.isEmpty() ? null : stringValue); - } - else - { - values.put(mDateTimeListFieldName, (String) null); - } + 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."); + } } - - private final class DateTimeList extends DelegatingIterable - { - - public DateTimeList(TimeZone timeZone, String dateTimeList) - { - super(new Mapped<>( - datetime -> !datetime.isFloating() && timeZone != null ? datetime.shiftTimeZone(timeZone) : datetime, - new Mapped( - charSequence -> DateTime.parse(timeZone, charSequence.toString()), - new Split(dateTimeList, ',')))); - } + // create a new TimeZone for the given time zone string + TimeZone timeZone = timeZoneId == null ? null : TimeZone.getTimeZone(timeZoneId); + + return new DateTimeList(timeZone, datetimeList); + } + + @Override + public void setIn(ContentValues values, Iterable value) { + if (value != null) { + String stringValue = + TextUtils.join( + ",", + new Mapped<>(dt -> dt.isFloating() ? dt : dt.shiftTimeZone(DateTime.UTC), value)); + values.put(mDateTimeListFieldName, stringValue.isEmpty() ? null : stringValue); + } else { + values.put(mDateTimeListFieldName, (String) null); + } + } + + private final class DateTimeList extends DelegatingIterable { + + public DateTimeList(TimeZone timeZone, String dateTimeList) { + super( + new Mapped<>( + datetime -> + !datetime.isFloating() && timeZone != null + ? datetime.shiftTimeZone(timeZone) + : datetime, + new Mapped( + charSequence -> DateTime.parse(timeZone, charSequence.toString()), + new Split(dateTimeList, ',')))); } + } } 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 5a5f8eb8..b288c265 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 @@ -18,89 +18,67 @@ package org.dmfs.provider.tasks.model.adapters; 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}. - * - * @param - * The type of the entity the field belongs to. + * Knows how to load and store {@link Duration} values from a {@link Cursor} or {@link + * ContentValues}. * + * @param 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; +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"); } - - - @Override - String fieldName() - { - return mFieldName; + 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(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."); } - - @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)); + 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); - } + @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 e9fe287f..5d07ec68 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 @@ -19,130 +19,104 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; import android.database.Cursor; - /** - * Knows how to load and store a specific field from or to {@link ContentValues} or from {@link Cursor}s. - * - * @param - * The type of the value this adapter stores. - * @param - * The type of the entity the field belongs to. + * Knows how to load and store a specific field from or to {@link ContentValues} or from {@link + * Cursor}s. * + * @param The type of the value this adapter stores. + * @param 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 - */ - 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 - */ - 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. - */ - 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 - */ - 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. - */ - 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 - */ - 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. - */ - 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. - */ - 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. - */ - 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. - */ - 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. - */ - void copyValue(ContentValues source, ContentValues dest); - +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 + */ + 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 + */ + 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. + */ + 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 + */ + 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. + */ + 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 + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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 28b8a014..00214f77 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 @@ -19,77 +19,54 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; import android.database.Cursor; - /** * Knows how to load and store a {@link Float} value from a {@link Cursor} or {@link ContentValues}. * - * @param - * The type of the entity the field belongs to. - * + * @param 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; +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"); } - - - @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); + 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."); } - - - @Override - public void setIn(ContentValues values, Float value) - { - if (value != null) - { - values.put(mFieldName, value); - } - else - { - values.putNull(mFieldName); - } + 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 933c5e80..b9f4a1ec 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 @@ -19,78 +19,55 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; import android.database.Cursor; - /** * Knows how to load and store an {@link Integer} from a {@link Cursor} or {@link ContentValues}. * - * @param - * The type of the entity the field belongs to. - * + * @param 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; +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"); } - - - @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); + 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."); } - - - @Override - public void setIn(ContentValues values, Integer value) - { - if (value != null) - { - values.put(mFieldName, value); - } - else - { - values.putNull(mFieldName); - } + 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 517ca233..6686f397 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 @@ -19,76 +19,54 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; import android.database.Cursor; - /** * Knows how to load and store a {@link Long} value from a {@link Cursor} or {@link ContentValues}. * - * @param - * The type of the entity the field belongs to. - * + * @param 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; +public final class LongFieldAdapter extends SimpleFieldAdapter { + /** 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 201b075f..b6760462 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 @@ -18,105 +18,76 @@ package org.dmfs.provider.tasks.model.adapters; 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}. - * - * @param - * The type of the entity the field belongs to. + * Knows how to load and store a {@link RecurrenceRule} from a {@link Cursor} or {@link + * ContentValues}. * + * @param 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; +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"); } - - - @Override - String fieldName() - { - return mFieldName; + mFieldName = fieldName; + } + + @Override + String fieldName() { + return mFieldName; + } + + @Override + public RecurrenceRule getFrom(ContentValues values) { + String rrule = values.getAsString(mFieldName); + if (rrule == null) { + return null; } - - - @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); - } + 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 RecurrenceRule getFrom(Cursor cursor) { + int columnIdx = cursor.getColumnIndex(mFieldName); + if (columnIdx < 0) { + throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); } - - - @Override - public void setIn(ContentValues values, RecurrenceRule value) - { - if (value != null) - { - values.put(mFieldName, value.toString()); - } - else - { - values.putNull(mFieldName); - } + 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 2752ba96..3a9929d2 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 @@ -19,82 +19,62 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; import android.database.Cursor; - /** - * An abstract {@link FieldAdapter} that implements a couple of methods as used by most simple FieldAdapters. - * - * @param - * The Type of the field this adapter handles. - * @param - * The type of the entity the field belongs to. + * An abstract {@link FieldAdapter} that implements a couple of methods as used by most simple + * FieldAdapters. * + * @param The Type of the field this adapter handles. + * @param 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(); - - - @Override - public boolean existsIn(ContentValues values) - { - return values.get(fieldName()) != null; - } - - - @Override - public boolean isSetIn(ContentValues values) - { - return values.containsKey(fieldName()); - } - - - @Override - public boolean existsIn(Cursor cursor) - { - int columnIdx = cursor.getColumnIndex(fieldName()); - return columnIdx >= 0 && !cursor.isNull(columnIdx); - } - - - @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 void removeFrom(ContentValues values) - { - values.remove(fieldName()); - } - - - @Override - public void copyValue(Cursor cursor, ContentValues values) - { - setIn(values, getFrom(cursor)); - } - - - @Override - public void copyValue(ContentValues oldValues, ContentValues newValues) - { - setIn(newValues, getFrom(oldValues)); - } - +public abstract class SimpleFieldAdapter + implements FieldAdapter { + + /** + * 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 isSetIn(ContentValues values) { + return values.containsKey(fieldName()); + } + + @Override + public boolean existsIn(Cursor cursor) { + int columnIdx = cursor.getColumnIndex(fieldName()); + return columnIdx >= 0 && !cursor.isNull(columnIdx); + } + + @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 void removeFrom(ContentValues values) { + values.remove(fieldName()); + } + + @Override + public void copyValue(Cursor cursor, ContentValues values) { + setIn(values, getFrom(cursor)); + } + + @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 4c5311af..52f8ab0f 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 @@ -19,77 +19,56 @@ package org.dmfs.provider.tasks.model.adapters; import android.content.ContentValues; import android.database.Cursor; - /** - * Knows how to load and store a {@link String} value from a {@link Cursor} or {@link ContentValues}. - * - * @param - * The type of the entity the field belongs to. + * Knows how to load and store a {@link String} value from a {@link Cursor} or {@link + * ContentValues}. * + * @param 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; +public final class StringFieldAdapter extends SimpleFieldAdapter { + /** 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 53496b8d..60b687ac 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 @@ -18,78 +18,57 @@ package org.dmfs.provider.tasks.model.adapters; 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}. * - * @param - * The type of the entity the field belongs to. - * + * @param 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; +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"); } - - - @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)); + 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); - } + @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/EntityProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/EntityProcessor.java index 8ae6323f..812e28a3 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,19 +17,15 @@ package org.dmfs.provider.tasks.processors; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.model.EntityAdapter; - /** * @author Marten Gajda */ -public interface EntityProcessor> -{ - T insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - - T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); +public interface EntityProcessor> { + T insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); + T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); + void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/Logging.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/Logging.java index 87f7379f..910186a3 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/Logging.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/Logging.java @@ -18,50 +18,39 @@ package org.dmfs.provider.tasks.processors; import android.database.sqlite.SQLiteDatabase; import android.util.Log; - import org.dmfs.provider.tasks.model.EntityAdapter; - /** * @author Marten Gajda */ -public final class Logging> implements EntityProcessor -{ - public static final String TAG = "Logging EntityProcessor"; - private final EntityProcessor mDelegate; - - - public Logging(EntityProcessor delegate) - { - mDelegate = delegate; - } - - - @Override - public T insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) - { - Log.d(TAG, "before insert"); - T result = mDelegate.insert(db, entityAdapter, isSyncAdapter); - Log.d(TAG, "after insert on " + entityAdapter.id()); - return result; - } - - - @Override - public T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) - { - Log.d(TAG, "before update of " + entityAdapter.id()); - T result = mDelegate.update(db, entityAdapter, isSyncAdapter); - Log.d(TAG, "after update of " + entityAdapter.id()); - return result; - } - - - @Override - public void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) - { - Log.d(TAG, "before delete of " + entityAdapter.id()); - mDelegate.delete(db, entityAdapter, isSyncAdapter); - Log.d(TAG, "after delete of " + entityAdapter.id()); - } +public final class Logging> implements EntityProcessor { + public static final String TAG = "Logging EntityProcessor"; + private final EntityProcessor mDelegate; + + public Logging(EntityProcessor delegate) { + mDelegate = delegate; + } + + @Override + public T insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) { + Log.d(TAG, "before insert"); + T result = mDelegate.insert(db, entityAdapter, isSyncAdapter); + Log.d(TAG, "after insert on " + entityAdapter.id()); + return result; + } + + @Override + public T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) { + Log.d(TAG, "before update of " + entityAdapter.id()); + T result = mDelegate.update(db, entityAdapter, isSyncAdapter); + Log.d(TAG, "after update of " + entityAdapter.id()); + return result; + } + + @Override + public void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) { + Log.d(TAG, "before delete of " + entityAdapter.id()); + mDelegate.delete(db, entityAdapter, isSyncAdapter); + Log.d(TAG, "after delete of " + entityAdapter.id()); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/NoOpProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/NoOpProcessor.java index d86f026a..72cfecf8 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/NoOpProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/NoOpProcessor.java @@ -17,34 +17,26 @@ package org.dmfs.provider.tasks.processors; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.model.EntityAdapter; - /** * A simple No-Op {@link EntityProcessor}. * * @author Marten Gajda */ -public final class NoOpProcessor> implements EntityProcessor -{ - @Override - public T insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) - { - return entityAdapter; - } - - - @Override - public T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) - { - return entityAdapter; - } - - - @Override - public void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) - { - // do nothing - } +public final class NoOpProcessor> implements EntityProcessor { + @Override + public T insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) { + return entityAdapter; + } + + @Override + public T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) { + return entityAdapter; + } + + @Override + public void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter) { + // do nothing + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java index 8c98fd04..4e9291dd 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java @@ -16,10 +16,13 @@ package org.dmfs.provider.tasks.processors.instances; +import static java.util.Arrays.asList; + import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - +import java.util.HashSet; +import java.util.TimeZone; import org.dmfs.iterables.SingletonIterable; import org.dmfs.iterables.decorators.Sieved; import org.dmfs.jems.iterable.composite.Joined; @@ -45,293 +48,292 @@ import org.dmfs.rfc5545.recurrenceset.RecurrenceSet; import org.dmfs.rfc5545.recurrenceset.RecurrenceSetIterator; import org.dmfs.tasks.contract.TaskContract; -import java.util.HashSet; -import java.util.TimeZone; - -import static java.util.Arrays.asList; - - /** - * An instance {@link EntityProcessor} detaches completed instances at the start of a recurring task. + * An instance {@link EntityProcessor} detaches completed instances at the start of a recurring + * task. * * @author Marten Gajda */ -public final class Detaching implements EntityProcessor -{ - - private final EntityProcessor mDelegate; - private final EntityProcessor mTaskDelegate; - - - public Detaching(EntityProcessor delegate, EntityProcessor taskDelegate) - { - mDelegate = delegate; - mTaskDelegate = taskDelegate; - } - - - @Override - public InstanceAdapter insert(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) - { - // just delegate for now - // if we ever support inserting instances, we'll have to make sure that inserting a completed instance results in a detached task - return mDelegate.insert(db, entityAdapter, isSyncAdapter); - } - - - /** - * Detach the given instance if all of the following conditions are met - *

- * - The instance is a recurrence instance (INSTANCE_ORIGINAL_TIME != null) - * - and the task has been closed (IS_CLOSED != 0) - * - and the instance is the first non-closed instance (DISTANCE_FROM_CURRENT==0). - *

- */ - @Override - public InstanceAdapter update(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) - { - if (entityAdapter.valueOf(InstanceAdapter.DISTANCE_FROM_CURRENT) != 0 // not the first open task - - // not closed, note we can't use IS_CLOSED at this point because its not updated yet - || (!new HashSet<>(asList(TaskContract.Tasks.STATUS_COMPLETED, TaskContract.Tasks.STATUS_CANCELLED)).contains( - entityAdapter.valueOf(new IntegerFieldAdapter<>(TaskContract.Tasks.STATUS)))) - - // not recurring - || entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME) == null) - { - // not a detachable instance - return mDelegate.update(db, entityAdapter, isSyncAdapter); - } - // update instance accordingly and detach it - return detachAll(db, mDelegate.update(db, entityAdapter, isSyncAdapter)); +public final class Detaching implements EntityProcessor { + + private final EntityProcessor mDelegate; + private final EntityProcessor mTaskDelegate; + + public Detaching( + EntityProcessor delegate, EntityProcessor taskDelegate) { + mDelegate = delegate; + mTaskDelegate = taskDelegate; + } + + @Override + public InstanceAdapter insert( + SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) { + // just delegate for now + // if we ever support inserting instances, we'll have to make sure that inserting a completed + // instance results in a detached task + return mDelegate.insert(db, entityAdapter, isSyncAdapter); + } + + /** + * Detach the given instance if all of the following conditions are met + * + *

- The instance is a recurrence instance (INSTANCE_ORIGINAL_TIME != null) - and the task has + * been closed (IS_CLOSED != 0) - and the instance is the first non-closed instance + * (DISTANCE_FROM_CURRENT==0). + * + *

+ */ + @Override + public InstanceAdapter update( + SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) { + if (entityAdapter.valueOf(InstanceAdapter.DISTANCE_FROM_CURRENT) != 0 // not the first open task + + // not closed, note we can't use IS_CLOSED at this point because its not updated yet + || (!new HashSet<>( + asList(TaskContract.Tasks.STATUS_COMPLETED, TaskContract.Tasks.STATUS_CANCELLED)) + .contains(entityAdapter.valueOf(new IntegerFieldAdapter<>(TaskContract.Tasks.STATUS)))) + + // not recurring + || entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME) == null) { + // not a detachable instance + return mDelegate.update(db, entityAdapter, isSyncAdapter); } - - - @Override - public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) - { - // just delegate - mDelegate.delete(db, entityAdapter, isSyncAdapter); + // update instance accordingly and detach it + return detachAll(db, mDelegate.update(db, entityAdapter, isSyncAdapter)); + } + + @Override + public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) { + // just delegate + mDelegate.delete(db, entityAdapter, isSyncAdapter); + } + + /** + * Detach all closed instances preceding the given one. + * + *

TODO: this method needs some refactoring + */ + private InstanceAdapter detachAll(SQLiteDatabase db, InstanceAdapter entityAdapter) { + // keep some values for later + long masterId = + new FirstPresent<>( + new NullSafe<>( + entityAdapter.valueOf( + new LongFieldAdapter<>(TaskContract.Instances.ORIGINAL_INSTANCE_ID))), + new NullSafe<>( + entityAdapter.valueOf(new LongFieldAdapter<>(TaskContract.Instances.TASK_ID)))) + .value(); + DateTime instanceOriginalTime = entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME); + + // detach instances which are completed + try (Cursor instances = + db.query( + TaskDatabaseHelper.Tables.INSTANCE_VIEW, + null, + String.format( + "%s < 0 and %s == ?", + TaskContract.Instances.DISTANCE_FROM_CURRENT, + TaskContract.Instances.ORIGINAL_INSTANCE_ID), + new String[] {String.valueOf(masterId)}, + null, + null, + null)) { + while (instances.moveToNext()) { + detachSingle(db, new CursorContentValuesInstanceAdapter(instances, new ContentValues())); + } } - - /** - * Detach all closed instances preceding the given one. - *

- * TODO: this method needs some refactoring - */ - private InstanceAdapter detachAll(SQLiteDatabase db, InstanceAdapter entityAdapter) - { - // keep some values for later - long masterId = new FirstPresent<>( - new NullSafe<>(entityAdapter.valueOf(new LongFieldAdapter<>(TaskContract.Instances.ORIGINAL_INSTANCE_ID))), - new NullSafe<>(entityAdapter.valueOf(new LongFieldAdapter<>(TaskContract.Instances.TASK_ID)))).value(); - DateTime instanceOriginalTime = entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME); - - // detach instances which are completed - try (Cursor instances = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, - null, - String.format("%s < 0 and %s == ?", TaskContract.Instances.DISTANCE_FROM_CURRENT, TaskContract.Instances.ORIGINAL_INSTANCE_ID), - new String[] { String.valueOf(masterId) }, - null, - null, - null)) - { - while (instances.moveToNext()) - { - detachSingle(db, new CursorContentValuesInstanceAdapter(instances, new ContentValues())); + // move the master to the first incomplete task + try (Cursor task = + db.query( + TaskDatabaseHelper.Tables.TASKS_VIEW, + null, + String.format("%s == ?", TaskContract.Tasks._ID), + new String[] {String.valueOf(masterId)}, + null, + null, + null)) { + if (task.moveToFirst()) { + TaskAdapter masterTask = new CursorContentValuesTaskAdapter(task, new ContentValues()); + DateTime oldStart = + new FirstPresent<>( + new NullSafe<>(masterTask.valueOf(TaskAdapter.DTSTART)), + new NullSafe<>(masterTask.valueOf(TaskAdapter.DUE))) + .value(); + + // assume we have no instances left + boolean noInstances = true; + + // update RRULE, if existent + RecurrenceRule rule = masterTask.valueOf(TaskAdapter.RRULE); + int count = 0; + if (rule != null) { + RecurrenceSet ruleSet = new RecurrenceSet(); + ruleSet.addInstances(new RecurrenceRuleAdapter(rule)); + if (rule.getCount() == null) { + // rule has no count limit, allowing us to exclude exdates + ruleSet.addExceptions( + new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.EXDATE)).value())); + } + RecurrenceSetIterator ruleIterator = + ruleSet.iterator(oldStart.getTimeZone(), oldStart.getTimestamp()); + + // move DTSTART to next RRULE instance which is > instanceOriginalTime + // reduce COUNT by the number of skipped instances, if present + while (count < 1000 && ruleIterator.hasNext()) { + DateTime inst = new DateTime(oldStart.getTimeZone(), ruleIterator.next()); + if (instanceOriginalTime.before(inst)) { + updateStart(masterTask, inst); + noInstances = false; // just found another instance + break; } - } - - // move the master to the first incomplete task - try (Cursor task = db.query(TaskDatabaseHelper.Tables.TASKS_VIEW, - null, - String.format("%s == ?", TaskContract.Tasks._ID), - new String[] { String.valueOf(masterId) }, - null, - null, - null)) - { - if (task.moveToFirst()) - { - TaskAdapter masterTask = new CursorContentValuesTaskAdapter(task, new ContentValues()); - DateTime oldStart = new FirstPresent<>( - new NullSafe<>(masterTask.valueOf(TaskAdapter.DTSTART)), - new NullSafe<>(masterTask.valueOf(TaskAdapter.DUE))).value(); - - // assume we have no instances left - boolean noInstances = true; - - // update RRULE, if existent - RecurrenceRule rule = masterTask.valueOf(TaskAdapter.RRULE); - int count = 0; - if (rule != null) - { - RecurrenceSet ruleSet = new RecurrenceSet(); - ruleSet.addInstances(new RecurrenceRuleAdapter(rule)); - if (rule.getCount() == null) - { - // rule has no count limit, allowing us to exclude exdates - ruleSet.addExceptions(new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.EXDATE)).value())); - } - RecurrenceSetIterator ruleIterator = ruleSet.iterator( - oldStart.getTimeZone(), - oldStart.getTimestamp()); - - // move DTSTART to next RRULE instance which is > instanceOriginalTime - // reduce COUNT by the number of skipped instances, if present - while (count < 1000 && ruleIterator.hasNext()) - { - DateTime inst = new DateTime(oldStart.getTimeZone(), ruleIterator.next()); - if (instanceOriginalTime.before(inst)) - { - updateStart(masterTask, inst); - noInstances = false; // just found another instance - break; - } - count += 1; - } - - if (noInstances) - { - // remove the RRULE but keep a mask for the old start - masterTask.set(TaskAdapter.EXDATE, - new Joined<>(new SingletonIterable<>(oldStart), new Sieved<>(new Not<>(oldStart::equals), masterTask.valueOf(TaskAdapter.EXDATE)))); - masterTask.set(TaskAdapter.RRULE, null); - } - else - { - // adjust COUNT if present - if (rule.getCount() != null) - { - rule.setCount(rule.getCount() - count); - masterTask.set(TaskAdapter.RRULE, rule); - } - } - } - - DateTime newStart = new FirstPresent<>( - new NullSafe<>(masterTask.valueOf(TaskAdapter.DTSTART)), - new NullSafe<>(masterTask.valueOf(TaskAdapter.DUE))).value(); - - // update RDATE and EXDATE - masterTask.set(TaskAdapter.RDATE, new Sieved<>(instanceOriginalTime::before, masterTask.valueOf(TaskAdapter.RDATE))); - masterTask.set(TaskAdapter.EXDATE, - new Sieved<>(new AnyOf<>(instanceOriginalTime::before, newStart::equals), masterTask.valueOf(TaskAdapter.EXDATE))); - - // First check if we still have any RDATE instances left - // TODO: 6 lines for something we should be able to express in one simple expression, we need to straighten lib-recur!! - RecurrenceSet rdateSet = new RecurrenceSet(); - rdateSet.addInstances(new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.RDATE)).value())); - rdateSet.addExceptions(new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.EXDATE)).value())); - RecurrenceSetIterator iterator = rdateSet.iterator(DateTime.UTC, Long.MIN_VALUE); - iterator.fastForward(Long.MIN_VALUE + 1); // skip bogus start - noInstances &= !iterator.hasNext(); - - if (noInstances) - { - // no more instances left, remove the master - mTaskDelegate.delete(db, masterTask, false); - } - else - { - if (masterTask.valueOf(TaskAdapter.RRULE) == null) - { - // we don't have any RRULE, allowing us to adjust DTSTART/DUE to the first RDATE - DateTime start = new DateTime(iterator.next()); - if (masterTask.valueOf(TaskAdapter.IS_ALLDAY)) - { - start = start.toAllDay(); - } - else if (masterTask.valueOf(TaskAdapter.TIMEZONE_RAW) != null) - { - start = start.shiftTimeZone(TimeZone.getTimeZone(masterTask.valueOf(TaskAdapter.TIMEZONE_RAW))); - } - updateStart(masterTask, start); - } - - // we still have instances, update the database - mTaskDelegate.update(db, masterTask, false); - } + count += 1; + } + + if (noInstances) { + // remove the RRULE but keep a mask for the old start + masterTask.set( + TaskAdapter.EXDATE, + new Joined<>( + new SingletonIterable<>(oldStart), + new Sieved<>( + new Not<>(oldStart::equals), masterTask.valueOf(TaskAdapter.EXDATE)))); + masterTask.set(TaskAdapter.RRULE, null); + } else { + // adjust COUNT if present + if (rule.getCount() != null) { + rule.setCount(rule.getCount() - count); + masterTask.set(TaskAdapter.RRULE, rule); } + } } - return entityAdapter; - } - - - private void updateStart(TaskAdapter task, DateTime newStart) - { - // this new instance becomes the new start (or due if we don't have a start) - if (task.valueOf(TaskAdapter.DTSTART) != null) - { - DateTime oldStart = task.valueOf(TaskAdapter.DTSTART); - task.set(TaskAdapter.DTSTART, newStart); - if (task.valueOf(TaskAdapter.DUE) != null) - { - long duration = task.valueOf(TaskAdapter.DUE).getTimestamp() - oldStart.getTimestamp(); - task.set(TaskAdapter.DUE, - newStart.addDuration( - new Duration(1, (int) (duration / (3600 * 24 * 1000)), (int) (duration % (3600 * 24 * 1000)) / 1000))); + DateTime newStart = + new FirstPresent<>( + new NullSafe<>(masterTask.valueOf(TaskAdapter.DTSTART)), + new NullSafe<>(masterTask.valueOf(TaskAdapter.DUE))) + .value(); + + // update RDATE and EXDATE + masterTask.set( + TaskAdapter.RDATE, + new Sieved<>(instanceOriginalTime::before, masterTask.valueOf(TaskAdapter.RDATE))); + masterTask.set( + TaskAdapter.EXDATE, + new Sieved<>( + new AnyOf<>(instanceOriginalTime::before, newStart::equals), + masterTask.valueOf(TaskAdapter.EXDATE))); + + // First check if we still have any RDATE instances left + // TODO: 6 lines for something we should be able to express in one simple expression, we + // need to straighten lib-recur!! + RecurrenceSet rdateSet = new RecurrenceSet(); + rdateSet.addInstances( + new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.RDATE)).value())); + rdateSet.addExceptions( + new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.EXDATE)).value())); + RecurrenceSetIterator iterator = rdateSet.iterator(DateTime.UTC, Long.MIN_VALUE); + iterator.fastForward(Long.MIN_VALUE + 1); // skip bogus start + noInstances &= !iterator.hasNext(); + + if (noInstances) { + // no more instances left, remove the master + mTaskDelegate.delete(db, masterTask, false); + } else { + if (masterTask.valueOf(TaskAdapter.RRULE) == null) { + // we don't have any RRULE, allowing us to adjust DTSTART/DUE to the first RDATE + DateTime start = new DateTime(iterator.next()); + if (masterTask.valueOf(TaskAdapter.IS_ALLDAY)) { + start = start.toAllDay(); + } else if (masterTask.valueOf(TaskAdapter.TIMEZONE_RAW) != null) { + start = + start.shiftTimeZone( + TimeZone.getTimeZone(masterTask.valueOf(TaskAdapter.TIMEZONE_RAW))); } - } - else - { - task.set(TaskAdapter.DUE, newStart); - } + updateStart(masterTask, start); + } + // we still have instances, update the database + mTaskDelegate.update(db, masterTask, false); + } + } } - - /** - * Detach the given instance. - *

- * - clone the override into a new deleted task (set _DELETED == 1) - * - detach the original override by removing the ORIGINAL_INSTANCE_ID, ORIGINAL_INSTANCE_SYNC_ID, ORIGINAL_INSTANCE_START and ORIGINAL_INSTANCE_ALLDAY - * (i.e. all columns which relate this to the original) - * - wipe _SYNC_ID, _UID and all sync columns (make this an unsynced task) - */ - private void detachSingle(SQLiteDatabase db, InstanceAdapter entityAdapter) - { - TaskAdapter original = entityAdapter.taskAdapter(); - TaskAdapter cloneAdapter = original.duplicate(); - - // first prepare the original to resemble the same instance but as a new, detached task - original.set(TaskAdapter.SYNC_ID, null); - original.set(TaskAdapter.SYNC_VERSION, null); - original.set(TaskAdapter.SYNC1, null); - original.set(TaskAdapter.SYNC2, null); - original.set(TaskAdapter.SYNC3, null); - original.set(TaskAdapter.SYNC4, null); - original.set(TaskAdapter.SYNC5, null); - original.set(TaskAdapter.SYNC6, null); - original.set(TaskAdapter.SYNC7, null); - original.set(TaskAdapter.SYNC8, null); - original.set(TaskAdapter._UID, null); - original.set(TaskAdapter._DIRTY, true); - original.set(TaskAdapter.ORIGINAL_INSTANCE_ID, null); - original.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, null); - original.set(TaskAdapter.ORIGINAL_INSTANCE_TIME, null); - original.unset(TaskAdapter.COMPLETED); - original.commit(db); - - // wipe INSTANCE_ORIGINAL_TIME from instances entry - ContentValues noOriginalTime = new ContentValues(); - noOriginalTime.putNull(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); - db.update(TaskDatabaseHelper.Tables.INSTANCES, noOriginalTime, "_ID = ?", new String[] { String.valueOf(entityAdapter.id()) }); - - // reset the clone to be a deleted instance - cloneAdapter.set(TaskAdapter._DELETED, true); - // remove joined field values - cloneAdapter.unset(TaskAdapter.LIST_ACCESS_LEVEL); - cloneAdapter.unset(TaskAdapter.LIST_COLOR); - cloneAdapter.unset(TaskAdapter.LIST_NAME); - cloneAdapter.unset(TaskAdapter.LIST_OWNER); - cloneAdapter.unset(TaskAdapter.LIST_VISIBLE); - cloneAdapter.unset(TaskAdapter.ACCOUNT_NAME); - cloneAdapter.unset(TaskAdapter.ACCOUNT_TYPE); - cloneAdapter.commit(db); - - // note, we don't have to create an instance for the clone because it's deleted + return entityAdapter; + } + + private void updateStart(TaskAdapter task, DateTime newStart) { + // this new instance becomes the new start (or due if we don't have a start) + if (task.valueOf(TaskAdapter.DTSTART) != null) { + DateTime oldStart = task.valueOf(TaskAdapter.DTSTART); + task.set(TaskAdapter.DTSTART, newStart); + if (task.valueOf(TaskAdapter.DUE) != null) { + long duration = task.valueOf(TaskAdapter.DUE).getTimestamp() - oldStart.getTimestamp(); + task.set( + TaskAdapter.DUE, + newStart.addDuration( + new Duration( + 1, + (int) (duration / (3600 * 24 * 1000)), + (int) (duration % (3600 * 24 * 1000)) / 1000))); + } + } else { + task.set(TaskAdapter.DUE, newStart); } + } + + /** + * Detach the given instance. + * + *

- clone the override into a new deleted task (set _DELETED == 1) - detach the original + * override by removing the ORIGINAL_INSTANCE_ID, ORIGINAL_INSTANCE_SYNC_ID, + * ORIGINAL_INSTANCE_START and ORIGINAL_INSTANCE_ALLDAY (i.e. all columns which relate this to the + * original) - wipe _SYNC_ID, _UID and all sync columns (make this an unsynced task) + */ + private void detachSingle(SQLiteDatabase db, InstanceAdapter entityAdapter) { + TaskAdapter original = entityAdapter.taskAdapter(); + TaskAdapter cloneAdapter = original.duplicate(); + + // first prepare the original to resemble the same instance but as a new, detached task + original.set(TaskAdapter.SYNC_ID, null); + original.set(TaskAdapter.SYNC_VERSION, null); + original.set(TaskAdapter.SYNC1, null); + original.set(TaskAdapter.SYNC2, null); + original.set(TaskAdapter.SYNC3, null); + original.set(TaskAdapter.SYNC4, null); + original.set(TaskAdapter.SYNC5, null); + original.set(TaskAdapter.SYNC6, null); + original.set(TaskAdapter.SYNC7, null); + original.set(TaskAdapter.SYNC8, null); + original.set(TaskAdapter._UID, null); + original.set(TaskAdapter._DIRTY, true); + original.set(TaskAdapter.ORIGINAL_INSTANCE_ID, null); + original.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, null); + original.set(TaskAdapter.ORIGINAL_INSTANCE_TIME, null); + original.unset(TaskAdapter.COMPLETED); + original.commit(db); + + // wipe INSTANCE_ORIGINAL_TIME from instances entry + ContentValues noOriginalTime = new ContentValues(); + noOriginalTime.putNull(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); + db.update( + TaskDatabaseHelper.Tables.INSTANCES, + noOriginalTime, + "_ID = ?", + new String[] {String.valueOf(entityAdapter.id())}); + + // reset the clone to be a deleted instance + cloneAdapter.set(TaskAdapter._DELETED, true); + // remove joined field values + cloneAdapter.unset(TaskAdapter.LIST_ACCESS_LEVEL); + cloneAdapter.unset(TaskAdapter.LIST_COLOR); + cloneAdapter.unset(TaskAdapter.LIST_NAME); + cloneAdapter.unset(TaskAdapter.LIST_OWNER); + cloneAdapter.unset(TaskAdapter.LIST_VISIBLE); + cloneAdapter.unset(TaskAdapter.ACCOUNT_NAME); + cloneAdapter.unset(TaskAdapter.ACCOUNT_TYPE); + cloneAdapter.commit(db); + + // note, we don't have to create an instance for the clone because it's deleted + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java index f02ed1da..2335b042 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java @@ -20,7 +20,7 @@ import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; - +import java.util.Locale; import org.dmfs.iterables.decorators.Filtered; import org.dmfs.iterables.elementary.Seq; import org.dmfs.iterators.filters.NoneOf; @@ -40,245 +40,269 @@ import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract; -import java.util.Locale; - - /** - * An instance {@link EntityProcessor} which delegates to the appropriate task {@link EntityProcessor}. + * An instance {@link EntityProcessor} which delegates to the appropriate task {@link + * EntityProcessor}. * * @author Marten Gajda */ -public final class TaskValueDelegate implements EntityProcessor -{ - private final static Iterable> SPECIAL_FIELD_ADAPTERS = new Seq<>( - TaskAdapter.SYNC1, - TaskAdapter.SYNC2, - TaskAdapter.SYNC3, - TaskAdapter.SYNC4, - TaskAdapter.SYNC5, - TaskAdapter.SYNC6, - TaskAdapter.SYNC7, - TaskAdapter.SYNC8, - TaskAdapter.SYNC_ID, - TaskAdapter.SYNC_VERSION, - // unset any list and read-only fields - TaskAdapter.VERSION, - TaskAdapter.ACCOUNT_NAME, - TaskAdapter.ACCOUNT_TYPE, - TaskAdapter.LIST_VISIBLE, - TaskAdapter.LIST_COLOR, - TaskAdapter.LIST_NAME, - TaskAdapter.LIST_ACCESS_LEVEL, - TaskAdapter.LIST_OWNER, - TaskAdapter._DELETED, - TaskAdapter._DIRTY, - TaskAdapter.IS_NEW, - TaskAdapter.IS_CLOSED, - TaskAdapter.HAS_PROPERTIES, - TaskAdapter.HAS_ALARMS, - TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, /* this will be resolved automatically */ - // also unset any recurrence fields - TaskAdapter.RRULE, - TaskAdapter.RDATE, - TaskAdapter.EXDATE, - TaskAdapter.CREATED, - TaskAdapter.LAST_MODIFIED - ); +public final class TaskValueDelegate implements EntityProcessor { + private static final Iterable> SPECIAL_FIELD_ADAPTERS = + new Seq<>( + TaskAdapter.SYNC1, + TaskAdapter.SYNC2, + TaskAdapter.SYNC3, + TaskAdapter.SYNC4, + TaskAdapter.SYNC5, + TaskAdapter.SYNC6, + TaskAdapter.SYNC7, + TaskAdapter.SYNC8, + TaskAdapter.SYNC_ID, + TaskAdapter.SYNC_VERSION, + // unset any list and read-only fields + TaskAdapter.VERSION, + TaskAdapter.ACCOUNT_NAME, + TaskAdapter.ACCOUNT_TYPE, + TaskAdapter.LIST_VISIBLE, + TaskAdapter.LIST_COLOR, + TaskAdapter.LIST_NAME, + TaskAdapter.LIST_ACCESS_LEVEL, + TaskAdapter.LIST_OWNER, + TaskAdapter._DELETED, + TaskAdapter._DIRTY, + TaskAdapter.IS_NEW, + TaskAdapter.IS_CLOSED, + TaskAdapter.HAS_PROPERTIES, + TaskAdapter.HAS_ALARMS, + TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, /* this will be resolved automatically */ + // also unset any recurrence fields + TaskAdapter.RRULE, + TaskAdapter.RDATE, + TaskAdapter.EXDATE, + TaskAdapter.CREATED, + TaskAdapter.LAST_MODIFIED); - private final EntityProcessor mDelegate; + private final EntityProcessor mDelegate; + public TaskValueDelegate(EntityProcessor delegate) { + mDelegate = delegate; + } - public TaskValueDelegate(EntityProcessor delegate) - { - mDelegate = delegate; - } + @Override + public InstanceAdapter insert( + SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) { + TaskAdapter taskAdapter = entityAdapter.taskAdapter(); + Long masterTaskId = null; + if (taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) { + // this is going to be an override to an existing task - make sure we add an RDATE first + masterTaskId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID); + DateTime originalTime = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME); + // get the master and add an rdate + try (Cursor c = + db.query( + TaskDatabaseHelper.Tables.TASKS, + null /* all */, + TaskContract.Tasks._ID + "=" + masterTaskId, + null, + null, + null, + null)) { + if (c.moveToFirst()) { + TaskAdapter masterTaskAdapter = + new CursorContentValuesTaskAdapter(masterTaskId, c, new ContentValues()); + if (masterTaskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) { + throw new IllegalArgumentException("Can't add an instance to an override instance"); + } + DateTime masterDate = + new Backed( + new FirstPresent<>( + new Seq<>( + new NullSafe<>(masterTaskAdapter.valueOf(TaskAdapter.DTSTART)), + new NullSafe<>(masterTaskAdapter.valueOf(TaskAdapter.DUE)))), + () -> null) + .value(); + if (!masterTaskAdapter.isRecurring() && masterDate != null) { + // master is not recurring yet, also add its start as an RDATE + appendDate(masterTaskAdapter, TaskAdapter.RDATE, TaskAdapter.EXDATE, masterDate); + } + // TODO: should we throw if the new master has no DTSTART? + appendDate(masterTaskAdapter, TaskAdapter.RDATE, TaskAdapter.EXDATE, originalTime); + mDelegate.update(db, masterTaskAdapter, false); - - @Override - public InstanceAdapter insert(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) - { - TaskAdapter taskAdapter = entityAdapter.taskAdapter(); - Long masterTaskId = null; - if (taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) - { - // this is going to be an override to an existing task - make sure we add an RDATE first - masterTaskId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID); - DateTime originalTime = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME); - // get the master and add an rdate - try (Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null /* all */, TaskContract.Tasks._ID + "=" + masterTaskId, null, null, null, null)) - { - if (c.moveToFirst()) - { - TaskAdapter masterTaskAdapter = new CursorContentValuesTaskAdapter(masterTaskId, c, new ContentValues()); - if (masterTaskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) - { - throw new IllegalArgumentException("Can't add an instance to an override instance"); - } - DateTime masterDate = new Backed(new FirstPresent<>(new Seq<>( - new NullSafe<>(masterTaskAdapter.valueOf(TaskAdapter.DTSTART)), - new NullSafe<>(masterTaskAdapter.valueOf(TaskAdapter.DUE)))), () -> null).value(); - if (!masterTaskAdapter.isRecurring() && masterDate != null) - { - // master is not recurring yet, also add its start as an RDATE - appendDate(masterTaskAdapter, TaskAdapter.RDATE, TaskAdapter.EXDATE, masterDate); - } - // TODO: should we throw if the new master has no DTSTART? - appendDate(masterTaskAdapter, TaskAdapter.RDATE, TaskAdapter.EXDATE, originalTime); - mDelegate.update(db, masterTaskAdapter, false); - - } - else - { - throw new IllegalArgumentException(String.format(Locale.ENGLISH, "No task with _ID %d found", masterTaskId)); - } - } + } else { + throw new IllegalArgumentException( + String.format(Locale.ENGLISH, "No task with _ID %d found", masterTaskId)); } + } + } - // move on with inserting the instance - TaskAdapter taskResult = mDelegate.insert(db, entityAdapter.taskAdapter(), false); - - if (masterTaskId != null) - { - // we just cloned the master task into a new instance, we need to copy the properties as well - copyProperties(db, masterTaskId, taskResult.id()); - } + // move on with inserting the instance + TaskAdapter taskResult = mDelegate.insert(db, entityAdapter.taskAdapter(), false); - try (Cursor c = db.query(TaskDatabaseHelper.Tables.INSTANCES, new String[] { TaskContract.Instances._ID }, - TaskContract.Instances.TASK_ID + "=" + taskResult.id(), null, null, null, null)) - { - // the cursor should contain exactly one row after this operation - c.moveToFirst(); - return new ContentValuesInstanceAdapter(c.getLong(0), new ContentValues()); - } + if (masterTaskId != null) { + // we just cloned the master task into a new instance, we need to copy the properties as well + copyProperties(db, masterTaskId, taskResult.id()); } + try (Cursor c = + db.query( + TaskDatabaseHelper.Tables.INSTANCES, + new String[] {TaskContract.Instances._ID}, + TaskContract.Instances.TASK_ID + "=" + taskResult.id(), + null, + null, + null, + null)) { + // the cursor should contain exactly one row after this operation + c.moveToFirst(); + return new ContentValuesInstanceAdapter(c.getLong(0), new ContentValues()); + } + } - @Override - public InstanceAdapter update(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) - { - // if this is the master of a recurring task, we create a new instance or update an existing one for this override, otherwise we just delegate - TaskAdapter taskAdapter = entityAdapter.taskAdapter(); - if (taskAdapter.isRecurring()) - { - // clone the task to create an unsynced override - InstanceAdapter newInstanceAdapter = entityAdapter.duplicate(); - TaskAdapter override = newInstanceAdapter.taskAdapter(); - override.set(TaskAdapter.ORIGINAL_INSTANCE_ID, entityAdapter.valueOf(InstanceAdapter.TASK_ID)); - override.set(TaskAdapter.ORIGINAL_INSTANCE_TIME, entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME)); - // unset all fields which have special meaning - for (FieldAdapter specialFieldAdapter : SPECIAL_FIELD_ADAPTERS) - { - override.unset(specialFieldAdapter); - } + @Override + public InstanceAdapter update( + SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) { + // if this is the master of a recurring task, we create a new instance or update an existing one + // for this override, otherwise we just delegate + TaskAdapter taskAdapter = entityAdapter.taskAdapter(); + if (taskAdapter.isRecurring()) { + // clone the task to create an unsynced override + InstanceAdapter newInstanceAdapter = entityAdapter.duplicate(); + TaskAdapter override = newInstanceAdapter.taskAdapter(); + override.set( + TaskAdapter.ORIGINAL_INSTANCE_ID, entityAdapter.valueOf(InstanceAdapter.TASK_ID)); + override.set( + TaskAdapter.ORIGINAL_INSTANCE_TIME, + entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME)); + // unset all fields which have special meaning + for (FieldAdapter specialFieldAdapter : SPECIAL_FIELD_ADAPTERS) { + override.unset(specialFieldAdapter); + } - // make sure we update DTSTART and DUE to match the instance values (unless they are set explicitly) - if (!taskAdapter.isUpdated(TaskAdapter.DTSTART)) - { - // set DTSTART to the instance start - override.set(TaskAdapter.DTSTART, newInstanceAdapter.valueOf(InstanceAdapter.INSTANCE_START)); - } - if (!taskAdapter.isUpdated(TaskAdapter.DUE) && !taskAdapter.isUpdated(TaskAdapter.DURATION)) - { - // set DUE to the effective instance DUE and wipe any duration - override.set(TaskAdapter.DUE, newInstanceAdapter.valueOf(InstanceAdapter.INSTANCE_DUE)); - override.set(TaskAdapter.DURATION, null); - } - // copy original instance allday flag - override.set(TaskAdapter.ORIGINAL_INSTANCE_ALLDAY, taskAdapter.valueOf(TaskAdapter.IS_ALLDAY)); + // make sure we update DTSTART and DUE to match the instance values (unless they are set + // explicitly) + if (!taskAdapter.isUpdated(TaskAdapter.DTSTART)) { + // set DTSTART to the instance start + override.set( + TaskAdapter.DTSTART, newInstanceAdapter.valueOf(InstanceAdapter.INSTANCE_START)); + } + if (!taskAdapter.isUpdated(TaskAdapter.DUE) && !taskAdapter.isUpdated(TaskAdapter.DURATION)) { + // set DUE to the effective instance DUE and wipe any duration + override.set(TaskAdapter.DUE, newInstanceAdapter.valueOf(InstanceAdapter.INSTANCE_DUE)); + override.set(TaskAdapter.DURATION, null); + } + // copy original instance allday flag + override.set( + TaskAdapter.ORIGINAL_INSTANCE_ALLDAY, taskAdapter.valueOf(TaskAdapter.IS_ALLDAY)); - TaskAdapter newTask = mDelegate.insert(db, override, false); + TaskAdapter newTask = mDelegate.insert(db, override, false); - copyProperties(db, taskAdapter.id(), newTask.id()); - } - else - { - // this is a non-recurring task or it's already an override, just delegate the update - mDelegate.update(db, taskAdapter, false); - } - return entityAdapter; + copyProperties(db, taskAdapter.id(), newTask.id()); + } else { + // this is a non-recurring task or it's already an override, just delegate the update + mDelegate.update(db, taskAdapter, false); } + return entityAdapter; + } + @Override + public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) { + // deleted instances are converted to deleted tasks (for non-recurring tasks) or exdates (for + // recurring tasks). + TaskAdapter taskAdapter = entityAdapter.taskAdapter(); - @Override - public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) - { - // deleted instances are converted to deleted tasks (for non-recurring tasks) or exdates (for recurring tasks). - TaskAdapter taskAdapter = entityAdapter.taskAdapter(); - - if (taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) - { - /* this is an override - we have to: - * - mark it deleted - * - add an exclusion to the master task - * - * TODO: if this instance was added by an RDATE, just remove the RDATE - * TODO: if this is the first instance, consider moving the recurrence start instead of adding an exdate - * TODO: if this is the last instance of a finite task, consider just setting a new recurrence end - */ - long masterTaskId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID); - DateTime originalTime = entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME); + if (taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) { + /* this is an override - we have to: + * - mark it deleted + * - add an exclusion to the master task + * + * TODO: if this instance was added by an RDATE, just remove the RDATE + * TODO: if this is the first instance, consider moving the recurrence start instead of adding an exdate + * TODO: if this is the last instance of a finite task, consider just setting a new recurrence end + */ + long masterTaskId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID); + DateTime originalTime = entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME); - // delete the override - mDelegate.delete(db, taskAdapter, false); + // delete the override + mDelegate.delete(db, taskAdapter, false); - // get the master and add an exdate - try (Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null /* all */, TaskContract.Tasks._ID + "=" + masterTaskId, null, null, null, null)) - { - if (c.moveToFirst()) - { - TaskAdapter masterTaskAdapter = new CursorContentValuesTaskAdapter(masterTaskId, c, new ContentValues()); - appendDate(masterTaskAdapter, TaskAdapter.EXDATE, TaskAdapter.RDATE, originalTime); - mDelegate.update(db, masterTaskAdapter, false); - } - } - } - else if (taskAdapter.isRecurring()) - { - // TODO: if this is the first instance, consider moving the recurrence start instead of adding an exdate - // TODO: if this is the last instance of a finite task, consider just setting a new recurrence end - appendDate(taskAdapter, TaskAdapter.EXDATE, TaskAdapter.RDATE, entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME)); - mDelegate.update(db, taskAdapter, false); - } - else - { - // task is non-recurring, delete it as a non-sync-adapter (effectively setting the _deleted flag) - mDelegate.delete(db, taskAdapter, false); + // get the master and add an exdate + try (Cursor c = + db.query( + TaskDatabaseHelper.Tables.TASKS, + null /* all */, + TaskContract.Tasks._ID + "=" + masterTaskId, + null, + null, + null, + null)) { + if (c.moveToFirst()) { + TaskAdapter masterTaskAdapter = + new CursorContentValuesTaskAdapter(masterTaskId, c, new ContentValues()); + appendDate(masterTaskAdapter, TaskAdapter.EXDATE, TaskAdapter.RDATE, originalTime); + mDelegate.update(db, masterTaskAdapter, false); } + } + } else if (taskAdapter.isRecurring()) { + // TODO: if this is the first instance, consider moving the recurrence start instead of adding + // an exdate + // TODO: if this is the last instance of a finite task, consider just setting a new recurrence + // end + appendDate( + taskAdapter, + TaskAdapter.EXDATE, + TaskAdapter.RDATE, + entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME)); + mDelegate.update(db, taskAdapter, false); + } else { + // task is non-recurring, delete it as a non-sync-adapter (effectively setting the _deleted + // flag) + mDelegate.delete(db, taskAdapter, false); } + } + private void appendDate( + TaskAdapter taskAdapter, + FieldAdapter, TaskAdapter> addfieldAdapter, + FieldAdapter, TaskAdapter> removefieldAdapter, + DateTime dateTime) { + taskAdapter.set( + addfieldAdapter, + new Joined<>( + new Filtered<>(taskAdapter.valueOf(addfieldAdapter), new NoneOf<>(dateTime)), + new Seq<>(dateTime))); + taskAdapter.set( + removefieldAdapter, + new Filtered<>(taskAdapter.valueOf(removefieldAdapter), new NoneOf<>(dateTime))); + } - private void appendDate(TaskAdapter taskAdapter, FieldAdapter, TaskAdapter> addfieldAdapter, FieldAdapter, TaskAdapter> removefieldAdapter, DateTime dateTime) - { - taskAdapter.set(addfieldAdapter, new Joined<>(new Filtered<>(taskAdapter.valueOf(addfieldAdapter), new NoneOf<>(dateTime)), new Seq<>(dateTime))); - taskAdapter.set(removefieldAdapter, new Filtered<>(taskAdapter.valueOf(removefieldAdapter), new NoneOf<>(dateTime))); - } - - - /** - * Copy the properties from the give original task to the new task. - * - * @param db - * The {@link SQLiteDatabase} - * @param originalId - * The ID of the task of which to copy the properties - * @param newId - * The ID of the task to copy the properties to. - */ - private void copyProperties(SQLiteDatabase db, long originalId, long newId) - { - // for each property of the original task - try (Cursor c = db.query(TaskDatabaseHelper.Tables.PROPERTIES, null /* all */, - String.format(Locale.ENGLISH, "%s = %d", TaskContract.Properties.TASK_ID, originalId), null, null, null, null)) - { - // load the property and insert it for the new task - ContentValues values = new ContentValues(c.getColumnCount()); - while (c.moveToNext()) - { - values.clear(); - DatabaseUtils.cursorRowToContentValues(c, values); - PropertyHandler ph = PropertyHandlerFactory.get(values.getAsString(TaskContract.Properties.MIMETYPE)); - ph.insert(db, newId, ph.cloneForNewTask(newId, values), false); - } - } + /** + * Copy the properties from the give original task to the new task. + * + * @param db The {@link SQLiteDatabase} + * @param originalId The ID of the task of which to copy the properties + * @param newId The ID of the task to copy the properties to. + */ + private void copyProperties(SQLiteDatabase db, long originalId, long newId) { + // for each property of the original task + try (Cursor c = + db.query( + TaskDatabaseHelper.Tables.PROPERTIES, + null /* all */, + String.format(Locale.ENGLISH, "%s = %d", TaskContract.Properties.TASK_ID, originalId), + null, + null, + null, + null)) { + // load the property and insert it for the new task + ContentValues values = new ContentValues(c.getColumnCount()); + while (c.moveToNext()) { + values.clear(); + DatabaseUtils.cursorRowToContentValues(c, values); + PropertyHandler ph = + PropertyHandlerFactory.get(values.getAsString(TaskContract.Properties.MIMETYPE)); + ph.insert(db, newId, ph.cloneForNewTask(newId, values), false); + } } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Validating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Validating.java index 238e6501..08379cc8 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Validating.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Validating.java @@ -18,7 +18,7 @@ package org.dmfs.provider.tasks.processors.instances; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - +import java.util.Locale; import org.dmfs.iterables.decorators.Sieved; import org.dmfs.iterables.elementary.Seq; import org.dmfs.jems.optional.Optional; @@ -32,155 +32,153 @@ import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract; -import java.util.Locale; - - /** * An {@link EntityProcessor} which validates the instance data. * * @author Marten Gajda */ -public final class Validating implements EntityProcessor -{ - private final static Iterable> INSTANCE_FIELD_ADAPTERS = new Seq<>( - InstanceAdapter._ID, - InstanceAdapter.INSTANCE_START, - InstanceAdapter.INSTANCE_START_SORTING, - InstanceAdapter.INSTANCE_DUE, - InstanceAdapter.INSTANCE_DUE_SORTING, - InstanceAdapter.INSTANCE_ORIGINAL_TIME, - InstanceAdapter.DISTANCE_FROM_CURRENT, - InstanceAdapter.TASK_ID); - - private final static Iterable> RECURRENCE_FIELD_ADAPTERS = new Seq<>( - TaskAdapter.RRULE, - TaskAdapter.RDATE, - TaskAdapter.EXDATE); - - private static final Iterable> ORIGINAL_INSTANCE_FIELD_ADAPTERS = new Seq<>( - TaskAdapter.ORIGINAL_INSTANCE_ID, - TaskAdapter.ORIGINAL_INSTANCE_TIME, - TaskAdapter.ORIGINAL_INSTANCE_ALLDAY, - TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID); - - private final EntityProcessor mDelegate; - - - public Validating(EntityProcessor delegate) - { - mDelegate = delegate; - } - - - @Override - public InstanceAdapter insert(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) - { - validateIsSyncAdapter(isSyncAdapter); - validateValues(entityAdapter); - validateInstanceIsNew(db, entityAdapter); - - return mDelegate.insert(db, entityAdapter, false); +public final class Validating implements EntityProcessor { + private static final Iterable> INSTANCE_FIELD_ADAPTERS = + new Seq<>( + InstanceAdapter._ID, + InstanceAdapter.INSTANCE_START, + InstanceAdapter.INSTANCE_START_SORTING, + InstanceAdapter.INSTANCE_DUE, + InstanceAdapter.INSTANCE_DUE_SORTING, + InstanceAdapter.INSTANCE_ORIGINAL_TIME, + InstanceAdapter.DISTANCE_FROM_CURRENT, + InstanceAdapter.TASK_ID); + + private static final Iterable> RECURRENCE_FIELD_ADAPTERS = + new Seq<>(TaskAdapter.RRULE, TaskAdapter.RDATE, TaskAdapter.EXDATE); + + private static final Iterable> ORIGINAL_INSTANCE_FIELD_ADAPTERS = + new Seq<>( + TaskAdapter.ORIGINAL_INSTANCE_ID, + TaskAdapter.ORIGINAL_INSTANCE_TIME, + TaskAdapter.ORIGINAL_INSTANCE_ALLDAY, + TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID); + + private final EntityProcessor mDelegate; + + public Validating(EntityProcessor delegate) { + mDelegate = delegate; + } + + @Override + public InstanceAdapter insert( + SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) { + validateIsSyncAdapter(isSyncAdapter); + validateValues(entityAdapter); + validateInstanceIsNew(db, entityAdapter); + + return mDelegate.insert(db, entityAdapter, false); + } + + @Override + public InstanceAdapter update( + SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) { + validateIsSyncAdapter(isSyncAdapter); + validateValues(entityAdapter); + validateOriginalInstanceValues(entityAdapter); + return mDelegate.update(db, entityAdapter, false); + } + + @Override + public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) { + validateIsSyncAdapter(isSyncAdapter); + mDelegate.delete(db, entityAdapter, false); + } + + private void validateIsSyncAdapter(boolean isSyncAdapter) { + if (isSyncAdapter) { + throw new UnsupportedOperationException( + "Sync adapters are not expected to write to the instances table."); } - - - @Override - public InstanceAdapter update(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) - { - validateIsSyncAdapter(isSyncAdapter); - validateValues(entityAdapter); - validateOriginalInstanceValues(entityAdapter); - return mDelegate.update(db, entityAdapter, false); - } - - - @Override - public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter) - { - validateIsSyncAdapter(isSyncAdapter); - mDelegate.delete(db, entityAdapter, false); + } + + private void validateInstanceIsNew(SQLiteDatabase db, InstanceAdapter entityAdapter) { + Optional instanceId = + new NullSafe<>(entityAdapter.taskAdapter().valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID)); + Optional instanceTime = + new NullSafe<>(entityAdapter.taskAdapter().valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)); + + // check if ORIGINAL_INSTANCE_ID and ORIGINAL_INSTANCE_TIME are both present/absent at the same + // time + if (instanceId.isPresent() != instanceTime.isPresent()) { + throw new IllegalArgumentException( + String.format( + "%s and %s must either be both absent or both present", + TaskContract.Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Tasks.ORIGINAL_INSTANCE_TIME)); } - - private void validateIsSyncAdapter(boolean isSyncAdapter) - { - if (isSyncAdapter) - { - throw new UnsupportedOperationException("Sync adapters are not expected to write to the instances table."); + if (instanceId.isPresent()) { + String timeStampString = Long.toString(instanceTime.value().getTimestamp()); + // Make sure there is no instance at the given time already + try (Cursor c = + db.query( + TaskDatabaseHelper.Tables.INSTANCE_VIEW, + new String[] {TaskContract.Instances._ID}, + // find any instance which refers to the given original ID and has the same instance + // time + // for recurring tasks this matches the INSTANCE_ORIGINAL_TIME, for non-recurring + // tasks this matches start or due (whichever is present). + String.format( + "(%1$s == ? or %2$s == ?) and (%3$s == ? or %3$s is null and %4$s == ? or %3$s is null and %4$s is null and %5$s == ?) ", + TaskContract.Instances.TASK_ID, + TaskContract.Instances.ORIGINAL_INSTANCE_ID, + TaskContract.Instances.INSTANCE_ORIGINAL_TIME, + TaskContract.Instances.INSTANCE_START, + TaskContract.Instances.INSTANCE_DUE), + new String[] { + instanceId.value().toString(), + instanceId.value().toString(), + timeStampString, + timeStampString, + timeStampString + }, + null, + null, + null)) { + if (c.getCount() > 0) { + throw new IllegalArgumentException( + String.format( + Locale.ENGLISH, + "Instance %s of task %d already exists", + entityAdapter + .taskAdapter() + .valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME) + .toString(), + instanceId.value())); } + } } - - - private void validateInstanceIsNew(SQLiteDatabase db, InstanceAdapter entityAdapter) - { - Optional instanceId = new NullSafe<>(entityAdapter.taskAdapter().valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID)); - Optional instanceTime = new NullSafe<>(entityAdapter.taskAdapter().valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)); - - // check if ORIGINAL_INSTANCE_ID and ORIGINAL_INSTANCE_TIME are both present/absent at the same time - if (instanceId.isPresent() != instanceTime.isPresent()) - { - throw new IllegalArgumentException(String.format("%s and %s must either be both absent or both present", - TaskContract.Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Tasks.ORIGINAL_INSTANCE_TIME)); - } - - if (instanceId.isPresent()) - { - String timeStampString = Long.toString(instanceTime.value().getTimestamp()); - // Make sure there is no instance at the given time already - try (Cursor c = db.query( - TaskDatabaseHelper.Tables.INSTANCE_VIEW, - new String[] { TaskContract.Instances._ID }, - // find any instance which refers to the given original ID and has the same instance time - // for recurring tasks this matches the INSTANCE_ORIGINAL_TIME, for non-recurring tasks this matches start or due (whichever is present). - String.format("(%1$s == ? or %2$s == ?) and (%3$s == ? or %3$s is null and %4$s == ? or %3$s is null and %4$s is null and %5$s == ?) ", - TaskContract.Instances.TASK_ID, - TaskContract.Instances.ORIGINAL_INSTANCE_ID, - TaskContract.Instances.INSTANCE_ORIGINAL_TIME, - TaskContract.Instances.INSTANCE_START, - TaskContract.Instances.INSTANCE_DUE), - new String[] { - instanceId.value().toString(), - instanceId.value().toString(), - timeStampString, - timeStampString, - timeStampString }, - null, - null, - null)) - { - if (c.getCount() > 0) - { - throw new IllegalArgumentException(String.format(Locale.ENGLISH, "Instance %s of task %d already exists", - entityAdapter.taskAdapter().valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME).toString(), instanceId.value())); - } - } - } + } + + private void validateValues(InstanceAdapter instanceAdapter) { + // actually, no instance value can be changed, the instance table only allows for updating task + // values + if (new First<>(new Sieved<>(instanceAdapter::isUpdated, INSTANCE_FIELD_ADAPTERS)) + .isPresent()) { + throw new IllegalArgumentException("Instance columns are read-only."); } - - private void validateValues(InstanceAdapter instanceAdapter) - { - // actually, no instance value can be changed, the instance table only allows for updating task values - if (new First<>(new Sieved<>(instanceAdapter::isUpdated, INSTANCE_FIELD_ADAPTERS)).isPresent()) - { - throw new IllegalArgumentException("Instance columns are read-only."); - } - - TaskAdapter taskAdapter = instanceAdapter.taskAdapter(); - // By definition, single instances don't have a recurrence set on their own, hence changes to the recurrence fields are not allowed. - if (new First<>(new Sieved<>(taskAdapter::isUpdated, RECURRENCE_FIELD_ADAPTERS)).isPresent()) - { - throw new IllegalArgumentException("Recurrence values can not be modified through the instances table."); - } + TaskAdapter taskAdapter = instanceAdapter.taskAdapter(); + // By definition, single instances don't have a recurrence set on their own, hence changes to + // the recurrence fields are not allowed. + if (new First<>(new Sieved<>(taskAdapter::isUpdated, RECURRENCE_FIELD_ADAPTERS)).isPresent()) { + throw new IllegalArgumentException( + "Recurrence values can not be modified through the instances table."); } - - - private void validateOriginalInstanceValues(InstanceAdapter instanceAdapter) - { - TaskAdapter taskAdapter = instanceAdapter.taskAdapter(); - // Updates of ORIGINAL_INSTANCE_* fields are not allowed - if (new First<>(new Sieved<>(taskAdapter::isUpdated, ORIGINAL_INSTANCE_FIELD_ADAPTERS)).isPresent()) - { - throw new IllegalArgumentException("ORIGINAL_INSTANCE_* fields can not be updated through the instances table."); - } + } + + private void validateOriginalInstanceValues(InstanceAdapter instanceAdapter) { + TaskAdapter taskAdapter = instanceAdapter.taskAdapter(); + // Updates of ORIGINAL_INSTANCE_* fields are not allowed + if (new First<>(new Sieved<>(taskAdapter::isUpdated, ORIGINAL_INSTANCE_FIELD_ADAPTERS)) + .isPresent()) { + throw new IllegalArgumentException( + "ORIGINAL_INSTANCE_* fields can not be updated through the instances table."); } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListCommitProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListCommitProcessor.java index 0e333698..c2f3db0a 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListCommitProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListCommitProcessor.java @@ -17,40 +17,32 @@ package org.dmfs.provider.tasks.processors.lists; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.ListAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.tasks.contract.TaskContract; - /** * A processor that performs the actual operations on task lists. * * @author Marten Gajda */ -public final class ListCommitProcessor implements EntityProcessor -{ - - @Override - public ListAdapter insert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) - { - list.commit(db); - return list; - } - - - @Override - public ListAdapter update(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) - { - list.commit(db); - return list; - } - - - @Override - public void delete(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) - { - db.delete(TaskDatabaseHelper.Tables.LISTS, TaskContract.TaskLists._ID + "=" + list.id(), null); - } +public final class ListCommitProcessor implements EntityProcessor { + + @Override + public ListAdapter insert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) { + list.commit(db); + return list; + } + + @Override + public ListAdapter update(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) { + list.commit(db); + return list; + } + + @Override + public void delete(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) { + db.delete(TaskDatabaseHelper.Tables.LISTS, TaskContract.TaskLists._ID + "=" + list.id(), null); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/Validating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/Validating.java index 8b272153..32978adf 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/Validating.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/lists/Validating.java @@ -18,120 +18,92 @@ package org.dmfs.provider.tasks.processors.lists; import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; - import org.dmfs.provider.tasks.model.ListAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; - /** * A processor to validate the values of a task list. * * @author Marten Gajda */ -public final class Validating implements EntityProcessor -{ - private final EntityProcessor mDelegate; +public final class Validating implements EntityProcessor { + private final EntityProcessor mDelegate; + public Validating(EntityProcessor delegate) { + mDelegate = delegate; + } - public Validating(EntityProcessor delegate) - { - mDelegate = delegate; + @Override + public ListAdapter insert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) { + if (!isSyncAdapter) { + throw new UnsupportedOperationException("Caller must be a sync adapter to create task lists"); } - - @Override - public ListAdapter insert(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); - return mDelegate.insert(db, list, isSyncAdapter); + 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"); + } - @Override - public ListAdapter update(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); + return mDelegate.insert(db, list, isSyncAdapter); + } - verifyCommon(list, isSyncAdapter); - return mDelegate.update(db, list, isSyncAdapter); + @Override + public ListAdapter update(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) { + if (list.isUpdated(ListAdapter.ACCOUNT_NAME)) { + throw new IllegalArgumentException("ACCOUNT_NAME is write-once"); } - - @Override - public void delete(SQLiteDatabase db, ListAdapter entityAdapter, boolean isSyncAdapter) - { - if (!isSyncAdapter) - { - throw new UnsupportedOperationException("Caller must be a sync adapter to delete task lists"); - } - mDelegate.delete(db, entityAdapter, isSyncAdapter); + if (list.isUpdated(ListAdapter.ACCOUNT_TYPE)) { + throw new IllegalArgumentException("ACCOUNT_TYPE is write-once"); } + verifyCommon(list, isSyncAdapter); + return mDelegate.update(db, list, isSyncAdapter); + } - /** - * 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"); - } + @Override + public void delete(SQLiteDatabase db, ListAdapter entityAdapter, boolean isSyncAdapter) { + if (!isSyncAdapter) { + throw new UnsupportedOperationException("Caller must be a sync adapter to delete task lists"); + } + mDelegate.delete(db, entityAdapter, isSyncAdapter); + } + + /** + * 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 (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."); - } + 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/AutoCompleting.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java index 65bbe9ad..599c56fc 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java @@ -19,192 +19,187 @@ 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.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract; - /** * 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 Relating}. + * + *

Other then recurrence exceptions no relations are handled by this code. Relation specific + * changes go to {@link Relating}. * * @author Marten Gajda */ -public final class AutoCompleting implements EntityProcessor -{ - private final EntityProcessor mDelegate; +public final class AutoCompleting implements EntityProcessor { + private final EntityProcessor mDelegate; - private static final String[] TASK_ID_PROJECTION = { TaskContract.Tasks._ID }; - private static final String[] TASK_SYNC_ID_PROJECTION = { TaskContract.Tasks._SYNC_ID }; + private static final String[] TASK_ID_PROJECTION = {TaskContract.Tasks._ID}; + private static final String[] TASK_SYNC_ID_PROJECTION = {TaskContract.Tasks._SYNC_ID}; - private static final String SYNC_ID_SELECTION = TaskContract.Tasks._SYNC_ID + "=?"; - private static final String TASK_ID_SELECTION = TaskContract.Tasks._ID + "=?"; + private static final String SYNC_ID_SELECTION = TaskContract.Tasks._SYNC_ID + "=?"; + private static final String TASK_ID_SELECTION = TaskContract.Tasks._ID + "=?"; + public AutoCompleting(EntityProcessor delegate) { + mDelegate = delegate; + } - public AutoCompleting(EntityProcessor delegate) - { - mDelegate = delegate; - } - + @Override + public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + updateFields(db, task, isSyncAdapter); - @Override - public TaskAdapter insert(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, DateTime.now()); - } - - TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter); - - if (isSyncAdapter && result.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, result.id()); - db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID + "=? and " - + TaskContract.Tasks.ORIGINAL_INSTANCE_ID + " is null", new String[] { result.valueOf(TaskAdapter.SYNC_ID) }); - } - return result; + if (!isSyncAdapter) { + // set created date for tasks created on the device + task.set(TaskAdapter.CREATED, DateTime.now()); } - - @Override - public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - updateFields(db, task, isSyncAdapter); - TaskAdapter result = mDelegate.update(db, task, isSyncAdapter); - - if (isSyncAdapter && result.isRecurring() && result.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, result.valueOf(TaskAdapter.SYNC_ID)); - db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + result.id(), null); - } - return result; + TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter); + + if (isSyncAdapter && result.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, result.id()); + db.update( + TaskDatabaseHelper.Tables.TASKS, + values, + TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID + + "=? and " + + TaskContract.Tasks.ORIGINAL_INSTANCE_ID + + " is null", + new String[] {result.valueOf(TaskAdapter.SYNC_ID)}); } - - - @Override - public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) - { - mDelegate.delete(db, entityAdapter, isSyncAdapter); + return result; + } + + @Override + public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + updateFields(db, task, isSyncAdapter); + TaskAdapter result = mDelegate.update(db, task, isSyncAdapter); + + if (isSyncAdapter && result.isRecurring() && result.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, result.valueOf(TaskAdapter.SYNC_ID)); + db.update( + TaskDatabaseHelper.Tables.TASKS, + values, + TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + result.id(), + null); + } + return result; + } + + @Override + public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) { + mDelegate.delete(db, entityAdapter, isSyncAdapter); + } + + private void updateFields(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + if (!isSyncAdapter) { + task.set(TaskAdapter._DIRTY, true); + task.set(TaskAdapter.LAST_MODIFIED, DateTime.now()); + + // set proper STATUS if task has been completed + if (task.valueOf(TaskAdapter.COMPLETED) != null && !task.isUpdated(TaskAdapter.STATUS)) { + task.set(TaskAdapter.STATUS, TaskContract.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); + } + } - private void updateFields(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) + // Find corresponding ORIGINAL_INSTANCE_ID + if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID)) { + String[] syncId = {task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID)}; + try (Cursor cursor = + db.query( + TaskDatabaseHelper.Tables.TASKS, + TASK_ID_PROJECTION, + SYNC_ID_SELECTION, + syncId, + null, + null, + null)) { + if (cursor.moveToNext()) { + Long originalId = cursor.getLong(0); + task.set(TaskAdapter.ORIGINAL_INSTANCE_ID, originalId); + } + } + } else if (task.isUpdated( + TaskAdapter.ORIGINAL_INSTANCE_ID)) // Find corresponding ORIGINAL_INSTANCE_SYNC_ID { - if (!isSyncAdapter) - { - task.set(TaskAdapter._DIRTY, true); - task.set(TaskAdapter.LAST_MODIFIED, DateTime.now()); - - // set proper STATUS if task has been completed - if (task.valueOf(TaskAdapter.COMPLETED) != null && !task.isUpdated(TaskAdapter.STATUS)) - { - task.set(TaskAdapter.STATUS, TaskContract.Tasks.STATUS_COMPLETED); - } + String[] id = {Long.toString(task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID))}; + try (Cursor cursor = + db.query( + TaskDatabaseHelper.Tables.TASKS, + TASK_SYNC_ID_PROJECTION, + TASK_ID_SELECTION, + id, + null, + null, + null)) { + if (cursor.moveToNext()) { + String originalSyncId = cursor.getString(0); + task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, originalSyncId); } + } + } - 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); - } - } + // 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); - // Find corresponding ORIGINAL_INSTANCE_ID - if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID)) - { - String[] syncId = { task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) }; - try (Cursor cursor = db.query(TaskDatabaseHelper.Tables.TASKS, TASK_ID_PROJECTION, SYNC_ID_SELECTION, syncId, null, null, null)) - { - if (cursor.moveToNext()) - { - Long originalId = cursor.getLong(0); - task.set(TaskAdapter.ORIGINAL_INSTANCE_ID, originalId); - } - } - } - else if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID)) // Find corresponding ORIGINAL_INSTANCE_SYNC_ID - { - String[] id = { Long.toString(task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID)) }; - try (Cursor cursor = db.query(TaskDatabaseHelper.Tables.TASKS, TASK_SYNC_ID_PROJECTION, TASK_ID_SELECTION, id, null, null, null)) - { - if (cursor.moveToNext()) - { - String originalSyncId = cursor.getString(0); - task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, originalSyncId); - } - } + if (!isSyncAdapter && percent != null && percent == 100) { + if (!task.isUpdated(TaskAdapter.STATUS)) { + task.set(TaskAdapter.STATUS, TaskContract.Tasks.STATUS_COMPLETED); } - // 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, TaskContract.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); - } - } + 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 = TaskContract.Tasks.STATUS_DEFAULT; - task.set(TaskAdapter.STATUS, status); - } - - task.set(TaskAdapter.IS_NEW, status == TaskContract.Tasks.STATUS_NEEDS_ACTION); - task.set(TaskAdapter.IS_CLOSED, status == TaskContract.Tasks.STATUS_COMPLETED || status == TaskContract.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 - * adapter. - */ - if (status == TaskContract.Tasks.STATUS_COMPLETED && !isSyncAdapter) - { - task.set(TaskAdapter.PERCENT_COMPLETE, 100); - if (!task.isUpdated(TaskAdapter.COMPLETED) || task.valueOf(TaskAdapter.COMPLETED) == null) - { - task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis())); - } - } - else if (!isSyncAdapter) - { - 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 = TaskContract.Tasks.STATUS_DEFAULT; + task.set(TaskAdapter.STATUS, status); + } + + task.set(TaskAdapter.IS_NEW, status == TaskContract.Tasks.STATUS_NEEDS_ACTION); + task.set( + TaskAdapter.IS_CLOSED, + status == TaskContract.Tasks.STATUS_COMPLETED + || status == TaskContract.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 + * adapter. + */ + if (status == TaskContract.Tasks.STATUS_COMPLETED && !isSyncAdapter) { + task.set(TaskAdapter.PERCENT_COMPLETE, 100); + if (!task.isUpdated(TaskAdapter.COMPLETED) || task.valueOf(TaskAdapter.COMPLETED) == null) { + 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/Instantiating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java index 1891ff38..10821068 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java @@ -16,10 +16,12 @@ package org.dmfs.provider.tasks.processors.tasks; +import static org.dmfs.provider.tasks.model.TaskAdapter.IS_CLOSED; + import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - +import java.util.Locale; import org.dmfs.jems.function.elementary.DiffMap; import org.dmfs.jems.iterable.composite.Diff; import org.dmfs.jems.iterable.decorators.Mapped; @@ -41,357 +43,363 @@ import org.dmfs.provider.tasks.utils.Range; import org.dmfs.provider.tasks.utils.RowIterator; import org.dmfs.tasks.contract.TaskContract; -import java.util.Locale; - -import static org.dmfs.provider.tasks.model.TaskAdapter.IS_CLOSED; - - /** * A processor that creates or updates the instance values of a task. * * @author Marten Gajda */ -public final class Instantiating implements EntityProcessor -{ - /** - * Projection we use to read the overrides of a task - */ - private final static String[] OVERRIDE_PROJECTION = { - TaskContract.Tasks._ID, - TaskContract.Tasks.DTSTART, - TaskContract.Tasks.DUE, - TaskContract.Tasks.DURATION, - TaskContract.Tasks.TZ, - TaskContract.Tasks.IS_ALLDAY, - TaskContract.Tasks.IS_CLOSED, - TaskContract.Tasks.ORIGINAL_INSTANCE_TIME, - TaskContract.Tasks.ORIGINAL_INSTANCE_ALLDAY }; - - /** - * 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. - *

- * TODO: get rid of it - */ - private final static BooleanFieldAdapter UPDATE_REQUESTED = new BooleanFieldAdapter( - "org.dmfs.tasks.TaskInstanceProcessor.UPDATE_REQUESTED"); - - // for now we only expand the next upcoming instance - private final static int UPCOMING_INSTANCE_COUNT_LIMIT = 1; - - - /** - * 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); +public final class Instantiating implements EntityProcessor { + /** Projection we use to read the overrides of a task */ + private static final String[] OVERRIDE_PROJECTION = { + TaskContract.Tasks._ID, + TaskContract.Tasks.DTSTART, + TaskContract.Tasks.DUE, + TaskContract.Tasks.DURATION, + TaskContract.Tasks.TZ, + TaskContract.Tasks.IS_ALLDAY, + TaskContract.Tasks.IS_CLOSED, + TaskContract.Tasks.ORIGINAL_INSTANCE_TIME, + TaskContract.Tasks.ORIGINAL_INSTANCE_ALLDAY + }; + + /** + * 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. + * + *

TODO: get rid of it + */ + private static final BooleanFieldAdapter UPDATE_REQUESTED = + new BooleanFieldAdapter("org.dmfs.tasks.TaskInstanceProcessor.UPDATE_REQUESTED"); + + // for now we only expand the next upcoming instance + private static final int UPCOMING_INSTANCE_COUNT_LIMIT = 1; + + /** + * 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); + } + + private final EntityProcessor mDelegate; + + public Instantiating(EntityProcessor delegate) { + mDelegate = delegate; + } + + @Override + public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter); + if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) { + // an override was created, insert a single task + updateOverrideInstance(db, result, result.id()); + } else { + // update the recurring instances, there may already be overrides, so we use the update method + updateMasterInstances(db, result, result.id()); } - - - private final EntityProcessor mDelegate; - - - public Instantiating(EntityProcessor delegate) - { - mDelegate = delegate; + return result; + } + + @Override + public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + // TODO: get rid if this mechanism + boolean updateRequested = + task.isUpdated(UPDATE_REQUESTED) ? task.valueOf(UPDATE_REQUESTED) : false; + task.unset(UPDATE_REQUESTED); + + TaskAdapter result = mDelegate.update(db, task, isSyncAdapter); + + if (!result.isUpdated(TaskAdapter.DTSTART) + && !result.isUpdated(TaskAdapter.DUE) + && !result.isUpdated(TaskAdapter.DURATION) + && !result.isUpdated(TaskAdapter.STATUS) + && !result.isUpdated(TaskAdapter.RDATE) + && !result.isUpdated(TaskAdapter.RRULE) + && !result.isUpdated(TaskAdapter.EXDATE) + && !result.isUpdated(IS_CLOSED) + && !updateRequested) { + // date values didn't change and update not requested -> no need to update the instances table + return result; } - - - @Override - public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter); - if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) - { - // an override was created, insert a single task - updateOverrideInstance(db, result, result.id()); - } - else - { - // update the recurring instances, there may already be overrides, so we use the update method - updateMasterInstances(db, result, result.id()); - } - return result; + if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) == null) { + updateMasterInstances(db, result, result.id()); + } else { + updateOverrideInstance(db, result, result.id()); } - - - @Override - public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - // TODO: get rid if this mechanism - boolean updateRequested = task.isUpdated(UPDATE_REQUESTED) ? task.valueOf(UPDATE_REQUESTED) : false; - task.unset(UPDATE_REQUESTED); - - TaskAdapter result = mDelegate.update(db, task, isSyncAdapter); - - if (!result.isUpdated(TaskAdapter.DTSTART) && !result.isUpdated(TaskAdapter.DUE) && !result.isUpdated(TaskAdapter.DURATION) - && !result.isUpdated(TaskAdapter.STATUS) && !result.isUpdated(TaskAdapter.RDATE) && !result.isUpdated(TaskAdapter.RRULE) && !result.isUpdated( - TaskAdapter.EXDATE) && !result.isUpdated(IS_CLOSED) && !updateRequested) - { - // date values didn't change and update not requested -> no need to update the instances table - return result; - } - if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) == null) - { - updateMasterInstances(db, result, result.id()); + return result; + } + + @Override + public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) { + // Note: there is a database trigger which cleans the instances table automatically when a task + // is deleted + mDelegate.delete(db, entityAdapter, isSyncAdapter); + } + + /** + * Update the instance of an override. + * + *

TODO: take instance overrides into account + * + * @param db an {@link SQLiteDatabase}. + * @param taskAdapter the {@link TaskAdapter} of the task to insert. + * @param id the row id of the new task. + */ + private void updateOverrideInstance(SQLiteDatabase db, TaskAdapter taskAdapter, long id) { + long origId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID); + int count = 0; + if (!taskAdapter.isUpdated(IS_CLOSED)) { + // task status was not updated, we can take the shortcut and only update any existing instance + // values + for (Single values : new InstanceValuesIterable(id, taskAdapter)) { + if (count++ > 1) { + throw new RuntimeException( + "more than one instance returned for task instance which was supposed to have exactly one"); } - else - { - updateOverrideInstance(db, result, result.id()); + ContentValues contentValues = values.value(); + // we don't know the current distance, but it for sure hasn't changed either, so just make + // sure we don't change it + contentValues.remove(TaskContract.Instances.DISTANCE_FROM_CURRENT); + // TASK_ID hasn't changed either + contentValues.remove(TaskContract.Instances.TASK_ID); + + db.update( + TaskDatabaseHelper.Tables.INSTANCES, + contentValues, + String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances.TASK_ID, id), + null); + } + if (count == 0) { + throw new RuntimeException( + "no instance returned for task which was supposed to have exactly one"); + } + } else { + // task status was updated, this might affect other instances, update them all + // ensure the distance from current is set properly for all sibling instances + try (Cursor c = + db.query( + TaskDatabaseHelper.Tables.TASKS, + null, + String.format(Locale.ENGLISH, "(%s = %d)", TaskContract.Tasks._ID, origId), + null, + null, + null, + null)) { + if (c.moveToFirst()) { + TaskAdapter ta = new CursorContentValuesTaskAdapter(c, new ContentValues()); + updateMasterInstances(db, ta, ta.id()); } - return result; + } } - - - @Override - public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) - { - // Note: there is a database trigger which cleans the instances table automatically when a task is deleted - mDelegate.delete(db, entityAdapter, isSyncAdapter); - } - - - /** - * Update the instance of an override. - *

- * TODO: take instance overrides into account - * - * @param db - * an {@link SQLiteDatabase}. - * @param taskAdapter - * the {@link TaskAdapter} of the task to insert. - * @param id - * the row id of the new task. - */ - private void updateOverrideInstance(SQLiteDatabase db, TaskAdapter taskAdapter, long id) - { - long origId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID); - int count = 0; - if (!taskAdapter.isUpdated(IS_CLOSED)) - { - // task status was not updated, we can take the shortcut and only update any existing instance values - for (Single values : new InstanceValuesIterable(id, taskAdapter)) - { - if (count++ > 1) - { - throw new RuntimeException("more than one instance returned for task instance which was supposed to have exactly one"); - } - ContentValues contentValues = values.value(); - // we don't know the current distance, but it for sure hasn't changed either, so just make sure we don't change it - contentValues.remove(TaskContract.Instances.DISTANCE_FROM_CURRENT); - // TASK_ID hasn't changed either - contentValues.remove(TaskContract.Instances.TASK_ID); - - db.update(TaskDatabaseHelper.Tables.INSTANCES, - contentValues, - String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances.TASK_ID, id), - null); - } - if (count == 0) - { - throw new RuntimeException("no instance returned for task which was supposed to have exactly one"); - } - } - else - { - // task status was updated, this might affect other instances, update them all - // ensure the distance from current is set properly for all sibling instances - try (Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null, - String.format(Locale.ENGLISH, "(%s = %d)", TaskContract.Tasks._ID, origId), null, null, null, null)) - { - if (c.moveToFirst()) - { - TaskAdapter ta = new CursorContentValuesTaskAdapter(c, new ContentValues()); - updateMasterInstances(db, ta, ta.id()); - } - } - } - } - - - /** - * Updates the instances of an existing task - * - * @param db - * An {@link SQLiteDatabase}. - * @param taskAdapter - * the {@link TaskAdapter} of the task to update - * @param id - * the row id of the new task - */ - private void updateMasterInstances(SQLiteDatabase db, TaskAdapter taskAdapter, long id) - { - try (Cursor existingInstances = db.query( + } + + /** + * Updates the instances of an existing task + * + * @param db An {@link SQLiteDatabase}. + * @param taskAdapter the {@link TaskAdapter} of the task to update + * @param id the row id of the new task + */ + private void updateMasterInstances(SQLiteDatabase db, TaskAdapter taskAdapter, long id) { + try (Cursor existingInstances = + db.query( TaskDatabaseHelper.Tables.INSTANCE_VIEW, new String[] { - TaskContract.Instances._ID, - TaskContract.InstanceColumns.INSTANCE_ORIGINAL_TIME, - TaskContract.InstanceColumns.INSTANCE_START, - TaskContract.InstanceColumns.INSTANCE_START_SORTING, - TaskContract.InstanceColumns.INSTANCE_DUE, - TaskContract.InstanceColumns.INSTANCE_DUE_SORTING, - TaskContract.InstanceColumns.INSTANCE_DURATION, - TaskContract.InstanceColumns.TASK_ID, - TaskContract.InstanceColumns.DISTANCE_FROM_CURRENT, - TaskContract.Instances.IS_CLOSED }, - String.format(Locale.ENGLISH, "%s = ? or %s = ?", TaskContract.Instances.TASK_ID, TaskContract.Instances.ORIGINAL_INSTANCE_ID), - new String[] { Long.toString(id), Long.toString(id) }, + TaskContract.Instances._ID, + TaskContract.InstanceColumns.INSTANCE_ORIGINAL_TIME, + TaskContract.InstanceColumns.INSTANCE_START, + TaskContract.InstanceColumns.INSTANCE_START_SORTING, + TaskContract.InstanceColumns.INSTANCE_DUE, + TaskContract.InstanceColumns.INSTANCE_DUE_SORTING, + TaskContract.InstanceColumns.INSTANCE_DURATION, + TaskContract.InstanceColumns.TASK_ID, + TaskContract.InstanceColumns.DISTANCE_FROM_CURRENT, + TaskContract.Instances.IS_CLOSED + }, + String.format( + Locale.ENGLISH, + "%s = ? or %s = ?", + TaskContract.Instances.TASK_ID, + TaskContract.Instances.ORIGINAL_INSTANCE_ID), + new String[] {Long.toString(id), Long.toString(id)}, null, null, TaskContract.Instances.INSTANCE_ORIGINAL_TIME); - Cursor overrides = db.query( - TaskDatabaseHelper.Tables.TASKS, - OVERRIDE_PROJECTION, - String.format("%s = ? AND %s != 1", TaskContract.Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Tasks._DELETED), - new String[] { Long.toString(id) }, - null, - null, - TaskContract.Tasks.ORIGINAL_INSTANCE_TIME);) - { - - /* - * The goal of the code below is to update existing instances in place (as opposed to delete and recreate all instances). We do this for two reasons: - * 1) efficiency, in most cases existing instances don't change, deleting and recreating them would be overly expensive - * 2) stable row ids, deleting and recreating instances would change their id and void any existing URIs to them - */ - final int idIdx = existingInstances.getColumnIndex(TaskContract.Instances._ID); - final int startIdx = existingInstances.getColumnIndex(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); - final int taskIdIdx = existingInstances.getColumnIndex(TaskContract.Instances.TASK_ID); - final int isClosedIdx = existingInstances.getColumnIndex(TaskContract.Instances.IS_CLOSED); - final int distanceIdx = existingInstances.getColumnIndex(TaskContract.Instances.DISTANCE_FROM_CURRENT); - - // get an Iterator of all expected instances - // for very long or even infinite series we need to stop iterating at some point. - - Iterable, Optional>> diff = new Diff<>( - new Mapped<>(Single::value, new Limited<>(10000 /* hard limit for infinite rules*/, - new Mapped<>( - new DiffMap<>( - (original, override) -> override, // we have both, a regular instance and an override -> take the override - original -> original, - override -> override // we only have an override :-o, not really valid but tolerated - ), - new Diff<>( - new InstanceValuesIterable(id, taskAdapter), - new Mapped<>( - cursor -> - new OverrideValuesFunction() - .value(new CursorContentValuesTaskAdapter(cursor, new ContentValues())), - () -> new RowIterator(overrides)), - (left, right) -> { - Long leftLong = left.value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); - Long rightLong = right.value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); - // null is always smaller - if (leftLong == null) - { - return rightLong == null ? 0 : -1; - } - if (rightLong == null) - { - return 1; - } - - long ldiff = leftLong - rightLong; - return ldiff < 0 ? -1 : (ldiff > 0 ? 1 : 0); - })))), - new Range(existingInstances.getCount()), - (newInstanceValues, cursorRow) -> - { - existingInstances.moveToPosition(cursorRow); - long ldiff = new Backed<>(new NullSafe<>(newInstanceValues.getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME)), 0L).value() - - existingInstances.getLong(startIdx); - return ldiff < 0 ? -1 : (ldiff > 0 ? 1 : 0); - }); - - int distance = -1; - // sync the instances table with the new instances - for (Pair, Optional> next : diff) - { - if (distance >= UPCOMING_INSTANCE_COUNT_LIMIT - 1) - { - // we already expanded enough instances - if (!next.right().isPresent()) - { - // if no further instances exist, stop here - Long original = next.left().value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); - if (original != null && existingInstances.moveToLast() && existingInstances.getLong(startIdx) < original) - { - break; - } - - // we may have to delete a few future instances - continue; - } - next = new RightSidedPair<>(next.right()); - } - - if (!next.left().isPresent()) - { - // there is no new instance for this old one, remove it - existingInstances.moveToPosition(next.right().value()); - db.delete(TaskDatabaseHelper.Tables.INSTANCES, - String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances._ID, existingInstances.getLong(idIdx)), null); - } - else if (!next.right().isPresent()) - { - // there is no old instance for this new one, add it - ContentValues values = next.left().value(); - if (distance >= 0 || values.getAsLong(TaskContract.Instances.DISTANCE_FROM_CURRENT) >= 0) - { - distance += 1; - } - values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance); - db.insert(TaskDatabaseHelper.Tables.INSTANCES, "", values); - } - else // both sides are present - { - // update this instance - existingInstances.moveToPosition(next.right().value()); - ContentValues values = next.left().value(); - if (distance >= 0 || values.getAsLong(TaskContract.Instances.DISTANCE_FROM_CURRENT) >= 0) - { - // the distance needs to be updated - distance += 1; - values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance); - } - - ContentValues updates = updatedOnly(values, existingInstances); - if (updates.size() > 0) - { - db.update(TaskDatabaseHelper.Tables.INSTANCES, - updates, - String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances._ID, existingInstances.getLong(idIdx)), - null); - } - } + Cursor overrides = + db.query( + TaskDatabaseHelper.Tables.TASKS, + OVERRIDE_PROJECTION, + String.format( + "%s = ? AND %s != 1", + TaskContract.Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Tasks._DELETED), + new String[] {Long.toString(id)}, + null, + null, + TaskContract.Tasks.ORIGINAL_INSTANCE_TIME); ) { + + /* + * The goal of the code below is to update existing instances in place (as opposed to delete and recreate all instances). We do this for two reasons: + * 1) efficiency, in most cases existing instances don't change, deleting and recreating them would be overly expensive + * 2) stable row ids, deleting and recreating instances would change their id and void any existing URIs to them + */ + final int idIdx = existingInstances.getColumnIndex(TaskContract.Instances._ID); + final int startIdx = + existingInstances.getColumnIndex(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); + final int taskIdIdx = existingInstances.getColumnIndex(TaskContract.Instances.TASK_ID); + final int isClosedIdx = existingInstances.getColumnIndex(TaskContract.Instances.IS_CLOSED); + final int distanceIdx = + existingInstances.getColumnIndex(TaskContract.Instances.DISTANCE_FROM_CURRENT); + + // get an Iterator of all expected instances + // for very long or even infinite series we need to stop iterating at some point. + + Iterable, Optional>> diff = + new Diff<>( + new Mapped<>( + Single::value, + new Limited<>( + 10000 /* hard limit for infinite rules*/, + new Mapped<>( + new DiffMap<>( + (original, override) -> + override, // we have both, a regular instance and an override -> + // take the override + original -> original, + override -> + override // we only have an override :-o, not really valid but + // tolerated + ), + new Diff<>( + new InstanceValuesIterable(id, taskAdapter), + new Mapped<>( + cursor -> + new OverrideValuesFunction() + .value( + new CursorContentValuesTaskAdapter( + cursor, new ContentValues())), + () -> new RowIterator(overrides)), + (left, right) -> { + Long leftLong = + left.value() + .getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); + Long rightLong = + right + .value() + .getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); + // null is always smaller + if (leftLong == null) { + return rightLong == null ? 0 : -1; + } + if (rightLong == null) { + return 1; + } + + long ldiff = leftLong - rightLong; + return ldiff < 0 ? -1 : (ldiff > 0 ? 1 : 0); + })))), + new Range(existingInstances.getCount()), + (newInstanceValues, cursorRow) -> { + existingInstances.moveToPosition(cursorRow); + long ldiff = + new Backed<>( + new NullSafe<>( + newInstanceValues.getAsLong( + TaskContract.Instances.INSTANCE_ORIGINAL_TIME)), + 0L) + .value() + - existingInstances.getLong(startIdx); + return ldiff < 0 ? -1 : (ldiff > 0 ? 1 : 0); + }); + + int distance = -1; + // sync the instances table with the new instances + for (Pair, Optional> next : diff) { + if (distance >= UPCOMING_INSTANCE_COUNT_LIMIT - 1) { + // we already expanded enough instances + if (!next.right().isPresent()) { + // if no further instances exist, stop here + Long original = + next.left().value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); + if (original != null + && existingInstances.moveToLast() + && existingInstances.getLong(startIdx) < original) { + break; } - } - } + // we may have to delete a few future instances + continue; + } + next = new RightSidedPair<>(next.right()); + } - private static ContentValues updatedOnly(ContentValues newValues, Cursor oldValues) - { - ContentValues result = new ContentValues(newValues); - for (String key : newValues.keySet()) + if (!next.left().isPresent()) { + // there is no new instance for this old one, remove it + existingInstances.moveToPosition(next.right().value()); + db.delete( + TaskDatabaseHelper.Tables.INSTANCES, + String.format( + Locale.ENGLISH, + "%s = %d", + TaskContract.Instances._ID, + existingInstances.getLong(idIdx)), + null); + } else if (!next.right().isPresent()) { + // there is no old instance for this new one, add it + ContentValues values = next.left().value(); + if (distance >= 0 + || values.getAsLong(TaskContract.Instances.DISTANCE_FROM_CURRENT) >= 0) { + distance += 1; + } + values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance); + db.insert(TaskDatabaseHelper.Tables.INSTANCES, "", values); + } else // both sides are present { - int columnIdx = oldValues.getColumnIndex(key); - if (columnIdx < 0) - { - throw new RuntimeException("Missing column " + key + " in Cursor "); - } - if (oldValues.isNull(columnIdx) && newValues.get(key) == null) - { - result.remove(key); - } - else if (!oldValues.isNull(columnIdx) && newValues.get(key) != null && oldValues.getLong(columnIdx) == newValues.getAsLong(key)) - { - result.remove(key); - } + // update this instance + existingInstances.moveToPosition(next.right().value()); + ContentValues values = next.left().value(); + if (distance >= 0 + || values.getAsLong(TaskContract.Instances.DISTANCE_FROM_CURRENT) >= 0) { + // the distance needs to be updated + distance += 1; + values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance); + } + + ContentValues updates = updatedOnly(values, existingInstances); + if (updates.size() > 0) { + db.update( + TaskDatabaseHelper.Tables.INSTANCES, + updates, + String.format( + Locale.ENGLISH, + "%s = %d", + TaskContract.Instances._ID, + existingInstances.getLong(idIdx)), + null); + } } - return result; + } + } + } + + private static ContentValues updatedOnly(ContentValues newValues, Cursor oldValues) { + ContentValues result = new ContentValues(newValues); + for (String key : newValues.keySet()) { + int columnIdx = oldValues.getColumnIndex(key); + if (columnIdx < 0) { + throw new RuntimeException("Missing column " + key + " in Cursor "); + } + if (oldValues.isNull(columnIdx) && newValues.get(key) == null) { + result.remove(key); + } else if (!oldValues.isNull(columnIdx) + && newValues.get(key) != null + && oldValues.getLong(columnIdx) == newValues.getAsLong(key)) { + result.remove(key); + } } + return result; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Moving.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Moving.java index ea9d9cb1..dea9edc7 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Moving.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Moving.java @@ -19,187 +19,200 @@ 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.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.tasks.contract.TaskContract; - /** - * 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. + * 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 final class Moving implements EntityProcessor -{ - private final EntityProcessor mDelegate; - - - public Moving(EntityProcessor delegate) - { - mDelegate = delegate; +public final class Moving implements EntityProcessor { + private final EntityProcessor mDelegate; + + public Moving(EntityProcessor delegate) { + mDelegate = delegate; + } + + @Override + public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + return mDelegate.insert(db, task, isSyncAdapter); + } + + @Override + public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + if (isSyncAdapter) { + // sync-adapters have to implement the move logic themselves + return mDelegate.update(db, task, isSyncAdapter); } - - @Override - public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - return mDelegate.insert(db, task, isSyncAdapter); + if (!task.isUpdated(TaskAdapter.LIST_ID)) { + // list has not been changed + return mDelegate.update(db, task, isSyncAdapter); } + long oldList = task.oldValueOf(TaskAdapter.LIST_ID); + long newList = task.valueOf(TaskAdapter.LIST_ID); - @Override - public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - if (isSyncAdapter) - { - // sync-adapters have to implement the move logic themselves - return mDelegate.update(db, task, isSyncAdapter); - } - - if (!task.isUpdated(TaskAdapter.LIST_ID)) - { - // list has not been changed - return mDelegate.update(db, task, isSyncAdapter); - } - - long oldList = task.oldValueOf(TaskAdapter.LIST_ID); - long newList = task.valueOf(TaskAdapter.LIST_ID); - - if (oldList == newList) - { - // list has not been changed - return mDelegate.update(db, task, isSyncAdapter); - } + if (oldList == newList) { + // list has not been changed + return mDelegate.update(db, task, isSyncAdapter); + } - 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); + 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(); - } + 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); } - - return mDelegate.update(db, task, isSyncAdapter); + } finally { + c.close(); + } } - - @Override - public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - mDelegate.delete(db, task, isSyncAdapter); + return mDelegate.update(db, task, isSyncAdapter); + } + + @Override + public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + mDelegate.delete(db, task, isSyncAdapter); + } + + 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 + * sync adapter field values. This means that the _ID field of the "deleted" task will not equal the _ID field f the original task. Sync adapters should + * 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(); } - - 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 - * sync adapter field values. This means that the _ID field of the "deleted" task will not equal the _ID field f the original task. Sync adapters should - * 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; + // 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/Originating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Originating.java index 15e82b40..4c389e1e 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Originating.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Originating.java @@ -18,60 +18,51 @@ package org.dmfs.provider.tasks.processors.tasks; import android.content.ContentValues; import android.database.sqlite.SQLiteDatabase; - +import java.util.Locale; import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.tasks.contract.TaskContract; -import java.util.Locale; - - /** - * An {@link EntityProcessor} which updates the {@link TaskContract.Tasks#ORIGINAL_INSTANCE_ID} of any overrides when a master is inserted which has the - * matching {@link TaskContract.Tasks#ORIGINAL_INSTANCE_SYNC_ID}. + * An {@link EntityProcessor} which updates the {@link TaskContract.Tasks#ORIGINAL_INSTANCE_ID} of + * any overrides when a master is inserted which has the matching {@link + * TaskContract.Tasks#ORIGINAL_INSTANCE_SYNC_ID}. * * @author Marten Gajda */ -public final class Originating implements EntityProcessor -{ - private final EntityProcessor mDelegate; - - - public Originating(EntityProcessor delegate) - { - mDelegate = delegate; - } - - - @Override - public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter); - String syncId = result.valueOf(TaskAdapter.SYNC_ID); - if (syncId != null) - { - // A master task with a syncId has been inserted. - // Update original ID of any existing overrides. - ContentValues values = new ContentValues(1); - values.put(TaskContract.Tasks.ORIGINAL_INSTANCE_ID, result.id()); - db.update(TaskDatabaseHelper.Tables.TASKS, values, String.format(Locale.ENGLISH, "%s = ?", TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID), - new String[] { syncId }); - } - return result; - } +public final class Originating implements EntityProcessor { + private final EntityProcessor mDelegate; + public Originating(EntityProcessor delegate) { + mDelegate = delegate; + } - @Override - public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - return mDelegate.update(db, task, isSyncAdapter); + @Override + public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter); + String syncId = result.valueOf(TaskAdapter.SYNC_ID); + if (syncId != null) { + // A master task with a syncId has been inserted. + // Update original ID of any existing overrides. + ContentValues values = new ContentValues(1); + values.put(TaskContract.Tasks.ORIGINAL_INSTANCE_ID, result.id()); + db.update( + TaskDatabaseHelper.Tables.TASKS, + values, + String.format(Locale.ENGLISH, "%s = ?", TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID), + new String[] {syncId}); } + return result; + } + @Override + public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + return mDelegate.update(db, task, isSyncAdapter); + } - @Override - public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - mDelegate.delete(db, task, isSyncAdapter); - } + @Override + public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + mDelegate.delete(db, task, isSyncAdapter); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java index d6417795..ac5c0d80 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java @@ -19,132 +19,143 @@ 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.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.tasks.contract.TaskContract; - /** * A processor that updates relations for new tasks. - *

- * In general there is no guarantee that a related task is already in the database when a task is - * inserted. In such a case we can not set the {@link TaskContract.Property.Relation#RELATED_ID} value. This processor updates the {@link - * TaskContract.Property.Relation#RELATED_ID} when a task is inserted. - *

- * It also updates {@link TaskContract.Property.Relation#RELATED_UID} when a tasks - * is synced the first time and a UID has been set. + * + *

In general there is no guarantee that a related task is already in the database when a task is + * inserted. In such a case we can not set the {@link TaskContract.Property.Relation#RELATED_ID} + * value. This processor updates the {@link TaskContract.Property.Relation#RELATED_ID} when a task + * is inserted. + * + *

It also updates {@link TaskContract.Property.Relation#RELATED_UID} when a tasks is synced the + * first time and a UID has been set. + * *

* * @author Marten Gajda */ -public final class Relating implements EntityProcessor -{ - private final EntityProcessor mDelegate; - - - public Relating(EntityProcessor delegate) - { - mDelegate = delegate; +public final class Relating implements EntityProcessor { + private final EntityProcessor mDelegate; + + public Relating(EntityProcessor delegate) { + mDelegate = delegate; + } + + @Override + public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + TaskAdapter result = mDelegate.insert(db, task, 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 result; } - - @Override - public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - TaskAdapter result = mDelegate.insert(db, task, 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 result; - } - - String uid = result.valueOf(TaskAdapter._UID); - - if (uid != null) - { - ContentValues v = new ContentValues(1); - v.put(TaskContract.Property.Relation.RELATED_ID, result.id()); - - int updates = db.update(TaskDatabaseHelper.Tables.PROPERTIES, v, - TaskContract.Property.Relation.MIMETYPE + "= ? AND " + TaskContract.Property.Relation.RELATED_UID + "=?", new String[] { - TaskContract.Property.Relation.CONTENT_ITEM_TYPE, uid }); - - if (updates > 0) - { - // there were other relations pointing towards this task, update PARENT_IDs if necessary - ContentValues parentIdValues = new ContentValues(1); - parentIdValues.put(TaskContract.Tasks.PARENT_ID, result.id()); - // iterate over all tasks which refer to this as their parent and update their PARENT_ID - try (Cursor c = db.query( - TaskDatabaseHelper.Tables.PROPERTIES, new String[] { TaskContract.Property.Relation.TASK_ID }, - String.format("%s = ? and %s = ? and %s = ?", - TaskContract.Property.Relation.MIMETYPE, - TaskContract.Property.Relation.RELATED_ID, - TaskContract.Property.Relation.RELATED_TYPE), - new String[] { - TaskContract.Property.Relation.CONTENT_ITEM_TYPE, - String.valueOf(result.id()), - String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT) }, - null, null, null)) - { - while (c.moveToNext()) - { - db.update(TaskDatabaseHelper.Tables.TASKS, parentIdValues, TaskContract.Tasks._ID + " = ?", new String[] { c.getString(0) }); - } - } - // TODO, way also may have to do this for all the siblings of these tasks. - } + String uid = result.valueOf(TaskAdapter._UID); + + if (uid != null) { + ContentValues v = new ContentValues(1); + v.put(TaskContract.Property.Relation.RELATED_ID, result.id()); + + int updates = + db.update( + TaskDatabaseHelper.Tables.PROPERTIES, + v, + TaskContract.Property.Relation.MIMETYPE + + "= ? AND " + + TaskContract.Property.Relation.RELATED_UID + + "=?", + new String[] {TaskContract.Property.Relation.CONTENT_ITEM_TYPE, uid}); + + if (updates > 0) { + // there were other relations pointing towards this task, update PARENT_IDs if necessary + ContentValues parentIdValues = new ContentValues(1); + parentIdValues.put(TaskContract.Tasks.PARENT_ID, result.id()); + // iterate over all tasks which refer to this as their parent and update their PARENT_ID + try (Cursor c = + db.query( + TaskDatabaseHelper.Tables.PROPERTIES, + new String[] {TaskContract.Property.Relation.TASK_ID}, + String.format( + "%s = ? and %s = ? and %s = ?", + TaskContract.Property.Relation.MIMETYPE, + TaskContract.Property.Relation.RELATED_ID, + TaskContract.Property.Relation.RELATED_TYPE), + new String[] { + TaskContract.Property.Relation.CONTENT_ITEM_TYPE, + String.valueOf(result.id()), + String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT) + }, + null, + null, + null)) { + while (c.moveToNext()) { + db.update( + TaskDatabaseHelper.Tables.TASKS, + parentIdValues, + TaskContract.Tasks._ID + " = ?", + new String[] {c.getString(0)}); + } } - return result; + // TODO, way also may have to do this for all the siblings of these tasks. + } } - - - @Override - public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - TaskAdapter result = mDelegate.update(db, task, isSyncAdapter); - // A task has been updated and may have received a UID by the sync adapter. Update all by-id references to this task. - // in this case we don't need to update any PARENT_ID because it should already be set. - - if (!isSyncAdapter) - { - // only sync adapters may assign a UID - return result; - } - - String uid = result.valueOf(TaskAdapter._UID); - - if (uid != null) - { - ContentValues v = new ContentValues(1); - v.put(TaskContract.Property.Relation.RELATED_UID, uid); - - db.update(TaskDatabaseHelper.Tables.PROPERTIES, v, - TaskContract.Property.Relation.MIMETYPE + "= ? AND " + TaskContract.Property.Relation.RELATED_ID + "=?", new String[] { - TaskContract.Property.Relation.CONTENT_ITEM_TYPE, Long.toString(result.id()) }); - } - return result; + return result; + } + + @Override + public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + TaskAdapter result = mDelegate.update(db, task, isSyncAdapter); + // A task has been updated and may have received a UID by the sync adapter. Update all by-id + // references to this task. + // in this case we don't need to update any PARENT_ID because it should already be set. + + if (!isSyncAdapter) { + // only sync adapters may assign a UID + return result; } + String uid = result.valueOf(TaskAdapter._UID); + + if (uid != null) { + ContentValues v = new ContentValues(1); + v.put(TaskContract.Property.Relation.RELATED_UID, uid); + + db.update( + TaskDatabaseHelper.Tables.PROPERTIES, + v, + TaskContract.Property.Relation.MIMETYPE + + "= ? AND " + + TaskContract.Property.Relation.RELATED_ID + + "=?", + new String[] { + TaskContract.Property.Relation.CONTENT_ITEM_TYPE, Long.toString(result.id()) + }); + } + return result; + } - @Override - public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - mDelegate.delete(db, task, isSyncAdapter); - - if (!isSyncAdapter) - { - // remove once the deletion is final, which is when the sync adapter removes it - return; - } + @Override + public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + mDelegate.delete(db, task, isSyncAdapter); - db.delete(TaskDatabaseHelper.Tables.PROPERTIES, TaskContract.Property.Relation.MIMETYPE + "= ? AND " + TaskContract.Property.Relation.RELATED_ID + "=?", - new String[] { - TaskContract.Property.Relation.CONTENT_ITEM_TYPE, - Long.toString(task.id()) }); + if (!isSyncAdapter) { + // remove once the deletion is final, which is when the sync adapter removes it + return; } + + db.delete( + TaskDatabaseHelper.Tables.PROPERTIES, + TaskContract.Property.Relation.MIMETYPE + + "= ? AND " + + TaskContract.Property.Relation.RELATED_ID + + "=?", + new String[] {TaskContract.Property.Relation.CONTENT_ITEM_TYPE, Long.toString(task.id())}); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java index a08f9b7a..201a3583 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java @@ -18,102 +18,89 @@ package org.dmfs.provider.tasks.processors.tasks; import android.content.ContentValues; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.tasks.contract.TaskContract; - /** - * An {@link EntityProcessor} which updates a task's parent-child relations when its {@link TaskContract.Tasks#PARENT_ID} is updated. + * An {@link EntityProcessor} which updates a task's parent-child relations when its {@link + * TaskContract.Tasks#PARENT_ID} is updated. * * @author Marten Gajda */ -public final class Reparenting implements EntityProcessor -{ - private final EntityProcessor mDelegate; - - - public Reparenting(EntityProcessor delegate) - { - mDelegate = delegate; +public final class Reparenting implements EntityProcessor { + private final EntityProcessor mDelegate; + + public Reparenting(EntityProcessor delegate) { + mDelegate = delegate; + } + + @Override + public TaskAdapter insert(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) { + TaskAdapter result = mDelegate.insert(db, entityAdapter, isSyncAdapter); + if (entityAdapter.isUpdated(TaskAdapter.PARENT_ID)) { + linkParent(db, result); } - - - @Override - public TaskAdapter insert(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) - { - TaskAdapter result = mDelegate.insert(db, entityAdapter, isSyncAdapter); - if (entityAdapter.isUpdated(TaskAdapter.PARENT_ID)) - { - linkParent(db, result); - } - return result; + return result; + } + + @Override + public TaskAdapter update(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) { + if (entityAdapter.isUpdated(TaskAdapter.PARENT_ID)) { + unlinkParent(db, entityAdapter); + TaskAdapter result = mDelegate.update(db, entityAdapter, isSyncAdapter); + linkParent(db, entityAdapter); + return result; + } else { + return mDelegate.update(db, entityAdapter, isSyncAdapter); } - - - @Override - public TaskAdapter update(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) - { - if (entityAdapter.isUpdated(TaskAdapter.PARENT_ID)) - { - unlinkParent(db, entityAdapter); - TaskAdapter result = mDelegate.update(db, entityAdapter, isSyncAdapter); - linkParent(db, entityAdapter); - return result; - } - else - { - return mDelegate.update(db, entityAdapter, isSyncAdapter); - } + } + + @Override + public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) { + unlinkParent(db, entityAdapter); + mDelegate.delete(db, entityAdapter, isSyncAdapter); + } + + private void unlinkParent(SQLiteDatabase db, TaskAdapter taskAdapter) { + if (taskAdapter.oldValueOf(TaskAdapter.PARENT_ID) != null) { + // delete any parent, child or sibling relation with this task + db.delete( + TaskDatabaseHelper.Tables.PROPERTIES, + String.format( + "%s = ? AND (%s = ? and %s in (?, ?) or %s = ? and %s in (?, ?))", + TaskContract.Property.Relation.MIMETYPE, + TaskContract.Property.Relation.TASK_ID, + TaskContract.Property.Relation.RELATED_TYPE, + TaskContract.Property.Relation.RELATED_ID, + TaskContract.Property.Relation.RELATED_TYPE), + new String[] { + TaskContract.Property.Relation.CONTENT_ITEM_TYPE, + String.valueOf(taskAdapter.valueOf(TaskAdapter._ID)), + String.valueOf(TaskContract.Property.Relation.RELTYPE_SIBLING), + String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT), + String.valueOf(taskAdapter.valueOf(TaskAdapter._ID)), + String.valueOf(TaskContract.Property.Relation.RELTYPE_SIBLING), + String.valueOf(TaskContract.Property.Relation.RELTYPE_CHILD) + }); } - - - @Override - public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) - { - unlinkParent(db, entityAdapter); - mDelegate.delete(db, entityAdapter, isSyncAdapter); - } - - - private void unlinkParent(SQLiteDatabase db, TaskAdapter taskAdapter) - { - if (taskAdapter.oldValueOf(TaskAdapter.PARENT_ID) != null) - { - // delete any parent, child or sibling relation with this task - db.delete(TaskDatabaseHelper.Tables.PROPERTIES, - String.format("%s = ? AND (%s = ? and %s in (?, ?) or %s = ? and %s in (?, ?))", - TaskContract.Property.Relation.MIMETYPE, - TaskContract.Property.Relation.TASK_ID, - TaskContract.Property.Relation.RELATED_TYPE, - TaskContract.Property.Relation.RELATED_ID, - TaskContract.Property.Relation.RELATED_TYPE), - new String[] { - TaskContract.Property.Relation.CONTENT_ITEM_TYPE, - String.valueOf(taskAdapter.valueOf(TaskAdapter._ID)), - String.valueOf(TaskContract.Property.Relation.RELTYPE_SIBLING), - String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT), - String.valueOf(taskAdapter.valueOf(TaskAdapter._ID)), - String.valueOf(TaskContract.Property.Relation.RELTYPE_SIBLING), - String.valueOf(TaskContract.Property.Relation.RELTYPE_CHILD) }); - } + } + + private void linkParent(SQLiteDatabase db, TaskAdapter taskAdapter) { + if (taskAdapter.valueOf(TaskAdapter.PARENT_ID) != null) { + ContentValues values = new ContentValues(); + values.put( + TaskContract.Property.Relation.MIMETYPE, + TaskContract.Property.Relation.CONTENT_ITEM_TYPE); + values.put(TaskContract.Property.Relation.TASK_ID, taskAdapter.id()); + values.put( + TaskContract.Property.Relation.RELATED_TYPE, + TaskContract.Property.Relation.RELTYPE_PARENT); + values.put( + TaskContract.Property.Relation.RELATED_ID, taskAdapter.valueOf(TaskAdapter.PARENT_ID)); + values.put(TaskContract.Property.Relation.RELATED_UID, taskAdapter.valueOf(TaskAdapter._UID)); + db.insert(TaskDatabaseHelper.Tables.PROPERTIES, "", values); } - - - private void linkParent(SQLiteDatabase db, TaskAdapter taskAdapter) - { - if (taskAdapter.valueOf(TaskAdapter.PARENT_ID) != null) - { - ContentValues values = new ContentValues(); - values.put(TaskContract.Property.Relation.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE); - values.put(TaskContract.Property.Relation.TASK_ID, taskAdapter.id()); - values.put(TaskContract.Property.Relation.RELATED_TYPE, TaskContract.Property.Relation.RELTYPE_PARENT); - values.put(TaskContract.Property.Relation.RELATED_ID, taskAdapter.valueOf(TaskAdapter.PARENT_ID)); - values.put(TaskContract.Property.Relation.RELATED_UID, taskAdapter.valueOf(TaskAdapter._UID)); - db.insert(TaskDatabaseHelper.Tables.PROPERTIES, "", values); - } - } - + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Searchable.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Searchable.java index 5b62b8e5..5f489a98 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Searchable.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Searchable.java @@ -17,50 +17,40 @@ 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.EntityProcessor; import org.dmfs.provider.tasks.utils.Profiled; - /** - * An {@link EntityProcessor} to update the fast text search table when inserting or updating a task. + * An {@link EntityProcessor} to update the fast text search table when inserting or updating a + * task. * * @author Marten Gajda */ -public final class Searchable implements EntityProcessor -{ - private final EntityProcessor mDelegate; - - - public Searchable(EntityProcessor delegate) - { - mDelegate = delegate; - } - - - @Override - public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter); - new Profiled("InsertFTS").run(() -> FTSDatabaseHelper.updateTaskFTSEntries(db, task)); - return result; - } - - - @Override - public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - TaskAdapter result = mDelegate.update(db, task, isSyncAdapter); - new Profiled("UpdateFTS").run(() -> FTSDatabaseHelper.updateTaskFTSEntries(db, task)); - return result; - } - - - @Override - public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) - { - new Profiled("DeleteFTS").run(() -> mDelegate.delete(db, entityAdapter, isSyncAdapter)); - } +public final class Searchable implements EntityProcessor { + private final EntityProcessor mDelegate; + + public Searchable(EntityProcessor delegate) { + mDelegate = delegate; + } + + @Override + public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter); + new Profiled("InsertFTS").run(() -> FTSDatabaseHelper.updateTaskFTSEntries(db, task)); + return result; + } + + @Override + public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + TaskAdapter result = mDelegate.update(db, task, isSyncAdapter); + new Profiled("UpdateFTS").run(() -> FTSDatabaseHelper.updateTaskFTSEntries(db, task)); + return result; + } + + @Override + public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) { + new Profiled("DeleteFTS").run(() -> mDelegate.delete(db, entityAdapter, isSyncAdapter)); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskCommitProcessor.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskCommitProcessor.java index 677dd61d..e23b6ba9 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskCommitProcessor.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskCommitProcessor.java @@ -17,51 +17,42 @@ package org.dmfs.provider.tasks.processors.tasks; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.tasks.contract.TaskContract; - /** * A processor that performs the actual operations on tasks. * * @author Marten Gajda */ -public final class TaskCommitProcessor implements EntityProcessor -{ - @Override - public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - task.commit(db); - return task; - } - - - @Override - public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - task.commit(db); - return task; - } - - - @Override - public void delete(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's removed by a sync adapter, in either case we delete it right away - db.delete(TaskDatabaseHelper.Tables.TASKS, TaskContract.TaskColumns._ID + "=" + task.id(), null); - } - else - { - // just set the deleted flag otherwise - task.set(TaskAdapter._DELETED, true); - task.commit(db); - } +public final class TaskCommitProcessor implements EntityProcessor { + @Override + public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + task.commit(db); + return task; + } + + @Override + public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + task.commit(db); + return task; + } + + @Override + public void delete(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's removed by a sync adapter, in either case we delete it right + // away + db.delete( + TaskDatabaseHelper.Tables.TASKS, TaskContract.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/Validating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java index a28a97ea..dd99afa0 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java @@ -18,261 +18,225 @@ package org.dmfs.provider.tasks.processors.tasks; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; - import org.dmfs.provider.tasks.TaskDatabaseHelper; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.provider.tasks.processors.EntityProcessor; import org.dmfs.rfc5545.Duration; import org.dmfs.tasks.contract.TaskContract; - /** * A processor that validates the values of a task. * * @author Marten Gajda */ -public final class Validating implements EntityProcessor -{ - private static final String[] TASKLIST_ID_PROJECTION = { TaskContract.TaskLists._ID }; - private static final String TASKLISTS_ID_SELECTION = TaskContract.TaskLists._ID + "="; - - private final EntityProcessor mDelegate; - - - public Validating(EntityProcessor delegate) - { - mDelegate = delegate; - } +public final class Validating implements EntityProcessor { + private static final String[] TASKLIST_ID_PROJECTION = {TaskContract.TaskLists._ID}; + private static final String TASKLISTS_ID_SELECTION = TaskContract.TaskLists._ID + "="; + private final EntityProcessor mDelegate; - @Override - public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) - { - verifyCommon(task, isSyncAdapter); + public Validating(EntityProcessor delegate) { + mDelegate = delegate; + } - // 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"); - } + @Override + public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) { + verifyCommon(task, isSyncAdapter); - // 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(TaskDatabaseHelper.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(); - } - } - return mDelegate.insert(db, 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"); } - - @Override - public TaskAdapter update(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"); - } - - // only sync adapters are allowed to change the UID of existing tasks - if (!isSyncAdapter && task.isUpdated(TaskAdapter._UID)) - { - throw new IllegalArgumentException("modification of _UID is not allowed to non-sync adapters"); - } - - return mDelegate.update(db, task, isSyncAdapter); + // 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( + TaskDatabaseHelper.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 delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) - { - mDelegate.delete(db, entityAdapter, isSyncAdapter); + return mDelegate.insert(db, task, isSyncAdapter); + } + + @Override + public TaskAdapter update(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"); } + // only sync adapters are allowed to change the UID of existing tasks + if (!isSyncAdapter && task.isUpdated(TaskAdapter._UID)) { + throw new IllegalArgumentException( + "modification of _UID is not allowed to non-sync adapters"); + } - /** - * 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"); - } + return mDelegate.update(db, task, isSyncAdapter); + } + + @Override + public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter) { + mDelegate.delete(db, entityAdapter, isSyncAdapter); + } + + /** + * 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"); + } - if (task.isUpdated(TaskAdapter.VERSION)) - { - throw new IllegalArgumentException("VERSION can not be set manually"); - } + if (task.isUpdated(TaskAdapter.VERSION)) { + throw new IllegalArgumentException("VERSION 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 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"); - } + // 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"); - } + // 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"); - } + // 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 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 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"); - } + // 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_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"); - } + // 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_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"); - } + // 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"); - } + // 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"); - } + 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 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 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"); - } - } + // 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 < TaskContract.Tasks.STATUS_NEEDS_ACTION || status > TaskContract.Tasks.STATUS_CANCELLED)) - { - throw new IllegalArgumentException("invalid STATUS: " + status); - } - } + // validate STATUS + if (task.isUpdated(TaskAdapter.STATUS)) { + Integer status = task.valueOf(TaskAdapter.STATUS); + if (status != null + && (status < TaskContract.Tasks.STATUS_NEEDS_ACTION + || status > TaskContract.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"); - } + // 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"); - } + // 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/instancedata/Dated.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java index c9e606c0..714f5465 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java @@ -17,35 +17,38 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; import android.content.ContentValues; - +import java.util.TimeZone; import org.dmfs.jems.optional.Optional; import org.dmfs.jems.single.Single; import org.dmfs.jems.single.decorators.DelegatingSingle; import org.dmfs.provider.tasks.utils.Zipped; import org.dmfs.rfc5545.DateTime; -import java.util.TimeZone; - - /** * A {@link Single} of date and time {@link ContentValues} of an instance. * * @author Marten Gajda */ -public final class Dated extends DelegatingSingle -{ +public final class Dated extends DelegatingSingle { - public Dated(Optional dateTime, String timeStampColumn, String sortingColumn, Single delegate) - { - super(new Zipped<>( - dateTime, - delegate, - (dateTime1, values) -> - { - // add timestamp and sorting - values.put(timeStampColumn, dateTime1.getTimestamp()); - values.put(sortingColumn, dateTime1.isAllDay() ? dateTime1.getInstance() : dateTime1.shiftTimeZone(TimeZone.getDefault()).getInstance()); - return values; - })); - } + public Dated( + Optional dateTime, + String timeStampColumn, + String sortingColumn, + Single delegate) { + super( + new Zipped<>( + dateTime, + delegate, + (dateTime1, values) -> { + // add timestamp and sorting + values.put(timeStampColumn, dateTime1.getTimestamp()); + values.put( + sortingColumn, + dateTime1.isAllDay() + ? dateTime1.getInstance() + : dateTime1.shiftTimeZone(TimeZone.getDefault()).getInstance()); + return values; + })); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Distant.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Distant.java index fafc5ca6..b2775984 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Distant.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Distant.java @@ -17,27 +17,23 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; import android.content.ContentValues; - import org.dmfs.jems.single.Single; import org.dmfs.jems.single.decorators.DelegatingSingle; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link Single} of the instance distance {@link ContentValues} of an instance. * * @author Marten Gajda */ -public final class Distant extends DelegatingSingle -{ +public final class Distant extends DelegatingSingle { - public Distant(int distance, Single delegate) - { - super(() -> - { - ContentValues values = delegate.value(); - values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance); - return values; + public Distant(int distance, Single delegate) { + super( + () -> { + ContentValues values = delegate.value(); + values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance); + return values; }); - } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java index d39005f6..36c78948 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java @@ -17,23 +17,24 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; import android.content.ContentValues; - import org.dmfs.jems.optional.Optional; import org.dmfs.jems.single.Single; import org.dmfs.jems.single.decorators.DelegatingSingle; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract; - /** * A decorator to a {@link Single} of {@link ContentValues} adding due data. * * @author Marten Gajda */ -public final class DueDated extends DelegatingSingle -{ - public DueDated(Optional due, Single delegate) - { - super(new Dated(due, TaskContract.Instances.INSTANCE_DUE, TaskContract.Instances.INSTANCE_DUE_SORTING, delegate)); - } +public final class DueDated extends DelegatingSingle { + public DueDated(Optional due, Single delegate) { + super( + new Dated( + due, + TaskContract.Instances.INSTANCE_DUE, + TaskContract.Instances.INSTANCE_DUE_SORTING, + delegate)); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java index 0552a877..3defab19 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java @@ -17,43 +17,39 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; import android.content.ContentValues; - import org.dmfs.jems.optional.composite.Zipped; import org.dmfs.jems.optional.elementary.NullSafe; import org.dmfs.jems.single.Single; import org.dmfs.jems.single.combined.Backed; import org.dmfs.tasks.contract.TaskContract; - /** - * A decorator for {@link Single}s of Instance {@link ContentValues} which populates the {@link TaskContract.Instances#INSTANCE_DURATION} field based on the - * already populated {@link TaskContract.Instances#INSTANCE_START} and {@link TaskContract.Instances#INSTANCE_DUE} fields. + * A decorator for {@link Single}s of Instance {@link ContentValues} which populates the {@link + * TaskContract.Instances#INSTANCE_DURATION} field based on the already populated {@link + * TaskContract.Instances#INSTANCE_START} and {@link TaskContract.Instances#INSTANCE_DUE} fields. * * @author Marten Gajda */ -public final class Enduring implements Single -{ - private final Single mDelegate; - - - public Enduring(Single delegate) - { - mDelegate = delegate; - } - - - @Override - public ContentValues value() - { - ContentValues values = mDelegate.value(); - // just store the difference between due and start, if both are present, otherwise store null - values.put(TaskContract.Instances.INSTANCE_DURATION, - new Backed( - new Zipped<>( - new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_START)), - new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_DUE)), - (start, due) -> due - start), - () -> null).value()); - return values; - } +public final class Enduring implements Single { + private final Single mDelegate; + + public Enduring(Single delegate) { + mDelegate = delegate; + } + + @Override + public ContentValues value() { + ContentValues values = mDelegate.value(); + // just store the difference between due and start, if both are present, otherwise store null + values.put( + TaskContract.Instances.INSTANCE_DURATION, + new Backed( + new Zipped<>( + new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_START)), + new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_DUE)), + (start, due) -> due - start), + () -> null) + .value()); + return values; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java index 793ffa1e..c42985c6 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java @@ -17,7 +17,6 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; import android.content.ContentValues; - import org.dmfs.jems.optional.Optional; import org.dmfs.jems.optional.decorators.Mapped; import org.dmfs.jems.optional.elementary.Present; @@ -26,37 +25,31 @@ import org.dmfs.jems.single.Single; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract; - /** - * A decorator for {@link Single}s of Instance {@link ContentValues} which populates the {@link TaskContract.Instances#INSTANCE_ORIGINAL_TIME} field based on - * the given {@link Optional} original start. + * A decorator for {@link Single}s of Instance {@link ContentValues} which populates the {@link + * TaskContract.Instances#INSTANCE_ORIGINAL_TIME} field based on the given {@link Optional} original + * start. * * @author Marten Gajda */ -public final class Overridden implements Single -{ - private final Optional mOriginalTime; - private final Single mDelegate; - - - public Overridden(DateTime originalTime, ContentValues delegate) - { - this(new Present<>(originalTime), () -> delegate); - } - - - public Overridden(Optional originalTime, Single delegate) - { - mOriginalTime = originalTime; - mDelegate = delegate; - } - - - @Override - public ContentValues value() - { - ContentValues values = mDelegate.value(); - new ForEach<>(new Mapped<>(DateTime::getTimestamp, mOriginalTime)).process(time -> values.put(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, time)); - return values; - } +public final class Overridden implements Single { + private final Optional mOriginalTime; + private final Single mDelegate; + + public Overridden(DateTime originalTime, ContentValues delegate) { + this(new Present<>(originalTime), () -> delegate); + } + + public Overridden(Optional originalTime, Single delegate) { + mOriginalTime = originalTime; + mDelegate = delegate; + } + + @Override + public ContentValues value() { + ContentValues values = mDelegate.value(); + new ForEach<>(new Mapped<>(DateTime::getTimestamp, mOriginalTime)) + .process(time -> values.put(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, time)); + return values; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java index 5ecb3205..765bdfe4 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java @@ -17,23 +17,24 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; import android.content.ContentValues; - import org.dmfs.jems.optional.Optional; import org.dmfs.jems.single.Single; import org.dmfs.jems.single.decorators.DelegatingSingle; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract; - /** * A decorator to a {@link Single} of {@link ContentValues} adding start data. * * @author Marten Gajda */ -public final class StartDated extends DelegatingSingle -{ - public StartDated(Optional start, Single delegate) - { - super(new Dated(start, TaskContract.Instances.INSTANCE_START, TaskContract.Instances.INSTANCE_START_SORTING, delegate)); - } +public final class StartDated extends DelegatingSingle { + public StartDated(Optional start, Single delegate) { + super( + new Dated( + start, + TaskContract.Instances.INSTANCE_START, + TaskContract.Instances.INSTANCE_START_SORTING, + delegate)); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java index 65be8e90..a706fb08 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java @@ -17,41 +17,33 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; import android.content.ContentValues; - import org.dmfs.jems.single.Single; import org.dmfs.provider.tasks.model.TaskAdapter; import org.dmfs.tasks.contract.TaskContract; - /** - * A decorator to {@link Single}s of {@link ContentValues} adding a {@link TaskContract.Instances#TASK_ID} to the data. + * A decorator to {@link Single}s of {@link ContentValues} adding a {@link + * TaskContract.Instances#TASK_ID} to the data. * * @author Marten Gajda */ -public final class TaskRelated implements Single -{ - private final long mTaskId; - private final Single mDelegate; - - - public TaskRelated(TaskAdapter taskAdapter, Single delegate) - { - this(taskAdapter.id(), delegate); - } - - - public TaskRelated(long taskId, Single delegate) - { - mTaskId = taskId; - mDelegate = delegate; - } - - - @Override - public ContentValues value() - { - ContentValues values = mDelegate.value(); - values.put(TaskContract.Instances.TASK_ID, mTaskId); - return values; - } +public final class TaskRelated implements Single { + private final long mTaskId; + private final Single mDelegate; + + public TaskRelated(TaskAdapter taskAdapter, Single delegate) { + this(taskAdapter.id(), delegate); + } + + public TaskRelated(long taskId, Single delegate) { + mTaskId = taskId; + mDelegate = delegate; + } + + @Override + public ContentValues value() { + ContentValues values = mDelegate.value(); + values.put(TaskContract.Instances.TASK_ID, mTaskId); + return values; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java index b46d3cc2..e3b1637b 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java @@ -17,30 +17,27 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; import android.content.ContentValues; - import org.dmfs.jems.single.Single; import org.dmfs.tasks.contract.TaskContract; - /** - * A {@link Single} of instance data {@link ContentValues}. It initializes most columns with {@code null} values, except for {@link - * TaskContract.Instances#TASK_ID} which is left out and {@link TaskContract.Instances#DISTANCE_FROM_CURRENT} which is initialized with {@code 0} as well. + * A {@link Single} of instance data {@link ContentValues}. It initializes most columns with {@code + * null} values, except for {@link TaskContract.Instances#TASK_ID} which is left out and {@link + * TaskContract.Instances#DISTANCE_FROM_CURRENT} which is initialized with {@code 0} as well. * * @author Marten Gajda */ -public final class VanillaInstanceData implements Single -{ - @Override - public ContentValues value() - { - ContentValues values = new ContentValues(10); - values.putNull(TaskContract.Instances.INSTANCE_START); - values.putNull(TaskContract.Instances.INSTANCE_START_SORTING); - values.putNull(TaskContract.Instances.INSTANCE_DUE); - values.putNull(TaskContract.Instances.INSTANCE_DUE_SORTING); - values.putNull(TaskContract.Instances.INSTANCE_DURATION); - values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, 0); - values.putNull(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); - return values; - } +public final class VanillaInstanceData implements Single { + @Override + public ContentValues value() { + ContentValues values = new ContentValues(10); + values.putNull(TaskContract.Instances.INSTANCE_START); + values.putNull(TaskContract.Instances.INSTANCE_START_SORTING); + values.putNull(TaskContract.Instances.INSTANCE_DUE); + values.putNull(TaskContract.Instances.INSTANCE_DUE_SORTING); + values.putNull(TaskContract.Instances.INSTANCE_DURATION); + values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, 0); + values.putNull(TaskContract.Instances.INSTANCE_ORIGINAL_TIME); + return values; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/ContainsValues.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/ContainsValues.java index 3f37b2ec..a6e84e62 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/ContainsValues.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/ContainsValues.java @@ -18,55 +18,42 @@ package org.dmfs.provider.tasks.utils; import android.content.ContentValues; import android.database.Cursor; - -import org.dmfs.jems.predicate.Predicate; - import java.util.Arrays; - +import org.dmfs.jems.predicate.Predicate; /** - * A {@link Predicate} which determines whether all values of a ContentValues object are present in a {@link Cursor}. + * A {@link Predicate} which determines whether all values of a ContentValues object are present in + * a {@link Cursor}. * * @author Marten Gajda */ -public final class ContainsValues implements Predicate -{ - private final ContentValues mValues; - - - public ContainsValues(ContentValues values) - { - mValues = values; - } - - - @Override - public boolean satisfiedBy(Cursor testedInstance) - { - for (String key : mValues.keySet()) - { - int columnIdx = testedInstance.getColumnIndex(key); - if (columnIdx < 0) - { - return false; - } - - if (testedInstance.getType(columnIdx) == Cursor.FIELD_TYPE_BLOB) - { - if (!Arrays.equals(mValues.getAsByteArray(key), testedInstance.getBlob(columnIdx))) - { - return false; - } - } - else - { - String stringValue = mValues.getAsString(key); - if (stringValue != null && !stringValue.equals(testedInstance.getString(columnIdx)) || stringValue == null && !testedInstance.isNull(columnIdx)) - { - return false; - } - } +public final class ContainsValues implements Predicate { + private final ContentValues mValues; + + public ContainsValues(ContentValues values) { + mValues = values; + } + + @Override + public boolean satisfiedBy(Cursor testedInstance) { + for (String key : mValues.keySet()) { + int columnIdx = testedInstance.getColumnIndex(key); + if (columnIdx < 0) { + return false; + } + + if (testedInstance.getType(columnIdx) == Cursor.FIELD_TYPE_BLOB) { + if (!Arrays.equals(mValues.getAsByteArray(key), testedInstance.getBlob(columnIdx))) { + return false; + } + } else { + String stringValue = mValues.getAsString(key); + if (stringValue != null && !stringValue.equals(testedInstance.getString(columnIdx)) + || stringValue == null && !testedInstance.isNull(columnIdx)) { + return false; } - return true; + } } + return true; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java index 5ee653d9..5a8defaf 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java @@ -17,7 +17,7 @@ package org.dmfs.provider.tasks.utils; import android.content.ContentValues; - +import java.util.Iterator; import org.dmfs.iterators.SingletonIterator; import org.dmfs.jems.iterable.elementary.Seq; import org.dmfs.jems.iterator.decorators.Mapped; @@ -38,83 +38,98 @@ import org.dmfs.provider.tasks.processors.tasks.instancedata.VanillaInstanceData import org.dmfs.rfc5545.DateTime; import org.dmfs.rfc5545.Duration; -import java.util.Iterator; - - /** * An {@link Iterable} of {@link Single} {@link ContentValues} of the instances of a task. * * @author Marten Gajda */ // TODO: replace Single with Generator -public final class InstanceValuesIterable implements Iterable> -{ - private final long mId; - private final TaskAdapter mTaskAdapter; - - - public InstanceValuesIterable(long id, TaskAdapter taskAdapter) - { - mId = id; - mTaskAdapter = taskAdapter; +public final class InstanceValuesIterable implements Iterable> { + private final long mId; + private final TaskAdapter mTaskAdapter; + + public InstanceValuesIterable(long id, TaskAdapter taskAdapter) { + mId = id; + mTaskAdapter = taskAdapter; + } + + @Override + public Iterator> iterator() { + Optional start = new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DTSTART)); + // effective due is either the actual due, start + duration or absent + Optional effectiveDue = + new FirstPresent<>( + new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DUE)), + new Zipped<>( + start, + new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)), + DateTime::addDuration)); + + Single baseData = + new Distant( + mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0, + new Enduring( + new DueDated( + effectiveDue, + new StartDated(start, new TaskRelated(mId, new VanillaInstanceData()))))); + + if (!mTaskAdapter.isRecurring()) { + return new SingletonIterator<>( + // apply the Overridden decorator only if this task has an ORIGINAL_INSTANCE_TIME + new org.dmfs.provider.tasks.utils.Zipped<>( + new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)), + baseData, + (DateTime time, ContentValues data) -> new Overridden(time, data).value())); } - - @Override - public Iterator> iterator() - { - Optional start = new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DTSTART)); - // effective due is either the actual due, start + duration or absent - Optional effectiveDue = new FirstPresent<>( - new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DUE)), - new Zipped<>(start, new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration)); - - Single baseData = new Distant(mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0, - new Enduring(new DueDated(effectiveDue, new StartDated(start, new TaskRelated(mId, new VanillaInstanceData()))))); - - if (!mTaskAdapter.isRecurring()) - { - return new SingletonIterator<>( - // apply the Overridden decorator only if this task has an ORIGINAL_INSTANCE_TIME - new org.dmfs.provider.tasks.utils.Zipped<>( - new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)), - baseData, - (DateTime time, ContentValues data) -> new Overridden(time, data).value())); - } - - if (start.isPresent()) - { - Optional effectiveDuration = new FirstPresent<>( - new Seq<>( - new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)), - new Zipped<>(start, effectiveDue, - (dtStart, due) -> new Duration(1, 0, (int) ((due.getTimestamp() - dtStart.getTimestamp()) / 1000))))); - - return new Mapped<>(dateTime -> new Distant(mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0, - new Overridden(new Present<>(dateTime), - new Enduring( - new DueDated(new Zipped<>(new Present<>(dateTime), effectiveDuration, this::addDuration), - new StartDated(new Present<>(dateTime), - new TaskRelated(mId, new VanillaInstanceData())))))), - new TaskInstanceIterable(mTaskAdapter).iterator()); - } - - // special treatment for recurring tasks without a DTSTART: - return new Mapped<>(dateTime -> new Distant(mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0, - new Overridden(new Present<>(dateTime), - new DueDated(new Present<>(dateTime), new TaskRelated(mId, new VanillaInstanceData())))), - new TaskInstanceIterable(mTaskAdapter).iterator()); - + if (start.isPresent()) { + Optional effectiveDuration = + new FirstPresent<>( + new Seq<>( + new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)), + new Zipped<>( + start, + effectiveDue, + (dtStart, due) -> + new Duration( + 1, + 0, + (int) ((due.getTimestamp() - dtStart.getTimestamp()) / 1000))))); + + return new Mapped<>( + dateTime -> + new Distant( + mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0, + new Overridden( + new Present<>(dateTime), + new Enduring( + new DueDated( + new Zipped<>( + new Present<>(dateTime), effectiveDuration, this::addDuration), + new StartDated( + new Present<>(dateTime), + new TaskRelated(mId, new VanillaInstanceData())))))), + new TaskInstanceIterable(mTaskAdapter).iterator()); } - - private DateTime addDuration(DateTime dt, Duration dur) - { - if (dt.isAllDay() && dur.getSecondsOfDay() != 0) - { - dur = new Duration(1, dur.getWeeks() * 7 + dur.getDays() + dur.getSecondsOfDay() / (3600 * 24), 0); - } - return dt.addDuration(dur); + // special treatment for recurring tasks without a DTSTART: + return new Mapped<>( + dateTime -> + new Distant( + mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0, + new Overridden( + new Present<>(dateTime), + new DueDated( + new Present<>(dateTime), new TaskRelated(mId, new VanillaInstanceData())))), + new TaskInstanceIterable(mTaskAdapter).iterator()); + } + + private DateTime addDuration(DateTime dt, Duration dur) { + if (dt.isAllDay() && dur.getSecondsOfDay() != 0) { + dur = + new Duration( + 1, dur.getWeeks() * 7 + dur.getDays() + dur.getSecondsOfDay() / (3600 * 24), 0); } - + return dt.addDuration(dur); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Limited.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Limited.java index 43833a1a..d50d2695 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Limited.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Limited.java @@ -18,32 +18,26 @@ package org.dmfs.provider.tasks.utils; import java.util.Iterator; - /** * An {@link Iterable} which limits the number of elements. - *

- * TODO: move to jems + * + *

TODO: move to jems * * @author Marten Gajda * @deprecated */ @Deprecated -public final class Limited implements Iterable -{ - private final int mCount; - private final Iterable mDelegate; - - - public Limited(int count, Iterable delegate) - { - mCount = count; - mDelegate = delegate; - } - - - @Override - public Iterator iterator() - { - return new LimitedIterator<>(mCount, mDelegate.iterator()); - } +public final class Limited implements Iterable { + private final int mCount; + private final Iterable mDelegate; + + public Limited(int count, Iterable delegate) { + mCount = count; + mDelegate = delegate; + } + + @Override + public Iterator iterator() { + return new LimitedIterator<>(mCount, mDelegate.iterator()); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/LimitedIterator.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/LimitedIterator.java index 88bba90d..61584d62 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/LimitedIterator.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/LimitedIterator.java @@ -16,48 +16,37 @@ package org.dmfs.provider.tasks.utils; -import org.dmfs.iterators.AbstractBaseIterator; - import java.util.Iterator; import java.util.NoSuchElementException; - +import org.dmfs.iterators.AbstractBaseIterator; /** - * An {@link Iterator} which limits the number elements. - * TODO: move to jems + * An {@link Iterator} which limits the number elements. TODO: move to jems * * @author Marten Gajda * @deprecated */ @Deprecated -public final class LimitedIterator extends AbstractBaseIterator -{ - private int mCount; - private final Iterator mDelegate; - - - public LimitedIterator(int count, Iterator delegate) - { - mCount = count; - mDelegate = delegate; - } - - - @Override - public boolean hasNext() - { - return mCount > 0 && mDelegate.hasNext(); - } - - - @Override - public T next() - { - if (!hasNext()) - { - throw new NoSuchElementException("No more elements to iterate"); - } - mCount--; - return mDelegate.next(); +public final class LimitedIterator extends AbstractBaseIterator { + private int mCount; + private final Iterator mDelegate; + + public LimitedIterator(int count, Iterator delegate) { + mCount = count; + mDelegate = delegate; + } + + @Override + public boolean hasNext() { + return mCount > 0 && mDelegate.hasNext(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException("No more elements to iterate"); } + mCount--; + return mDelegate.next(); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java index b8984b6f..7dc21f48 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java @@ -17,7 +17,6 @@ package org.dmfs.provider.tasks.utils; import android.content.ContentValues; - import org.dmfs.jems.function.Function; import org.dmfs.jems.optional.Optional; import org.dmfs.jems.optional.adapters.FirstPresent; @@ -34,31 +33,38 @@ import org.dmfs.provider.tasks.processors.tasks.instancedata.TaskRelated; import org.dmfs.provider.tasks.processors.tasks.instancedata.VanillaInstanceData; import org.dmfs.rfc5545.DateTime; - /** * An {@link Iterable} of {@link Single} {@link ContentValues} of the overrides of a task. * * @author Marten Gajda */ -public final class OverrideValuesFunction implements Function> -{ +public final class OverrideValuesFunction implements Function> { - @Override - public Single value(TaskAdapter taskAdapter) - { - Optional start = new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DTSTART)); - // effective due is either the actual due, start + duration or absent - Optional effectiveDue = new FirstPresent<>( - new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DUE)), - new Zipped<>(start, new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration)); + @Override + public Single value(TaskAdapter taskAdapter) { + Optional start = new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DTSTART)); + // effective due is either the actual due, start + duration or absent + Optional effectiveDue = + new FirstPresent<>( + new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DUE)), + new Zipped<>( + start, + new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DURATION)), + DateTime::addDuration)); - Single baseData = new Distant(taskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0, - new Enduring(new DueDated(effectiveDue, new StartDated(start, new TaskRelated(taskAdapter, new VanillaInstanceData()))))); + Single baseData = + new Distant( + taskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0, + new Enduring( + new DueDated( + effectiveDue, + new StartDated( + start, new TaskRelated(taskAdapter, new VanillaInstanceData()))))); - // apply the Overridden decorator only if this task has an ORIGINAL_INSTANCE_TIME - return new org.dmfs.provider.tasks.utils.Zipped<>( - new NullSafe<>(taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)), - baseData, - (DateTime time, ContentValues data) -> new Overridden(time, data).value()); - } + // apply the Overridden decorator only if this task has an ORIGINAL_INSTANCE_TIME + return new org.dmfs.provider.tasks.utils.Zipped<>( + new NullSafe<>(taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)), + baseData, + (DateTime time, ContentValues data) -> new Overridden(time, data).value()); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Profiled.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Profiled.java index a30f3fc6..21fa2daa 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Profiled.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Profiled.java @@ -17,64 +17,63 @@ package org.dmfs.provider.tasks.utils; import android.util.Log; - +import java.util.Locale; import org.dmfs.jems.fragile.Fragile; import org.dmfs.jems.single.Single; -import java.util.Locale; - - /** * A simple class to measure the execution time of a given piece of code. * * @author Marten Gajda */ -public final class Profiled -{ - private final String mSubject; - - - public Profiled(String subject) - { - mSubject = subject; - } - - - public void run(Runnable runnable) - { - long start = System.currentTimeMillis(); - runnable.run(); - Log.d("Profiled", String.format(Locale.ENGLISH, "Time spent in %s: %d milliseconds", mSubject, System.currentTimeMillis() - start)); +public final class Profiled { + private final String mSubject; + + public Profiled(String subject) { + mSubject = subject; + } + + public void run(Runnable runnable) { + long start = System.currentTimeMillis(); + runnable.run(); + Log.d( + "Profiled", + String.format( + Locale.ENGLISH, + "Time spent in %s: %d milliseconds", + mSubject, + System.currentTimeMillis() - start)); + } + + public V run(Single runnable) { + + long start = System.currentTimeMillis(); + try { + return runnable.value(); + } finally { + Log.d( + "Profiled", + String.format( + Locale.ENGLISH, + "Time spent in %s: %d milliseconds", + mSubject, + System.currentTimeMillis() - start)); } - - - public V run(Single runnable) - { - - long start = System.currentTimeMillis(); - try - { - return runnable.value(); - } - finally - { - Log.d("Profiled", String.format(Locale.ENGLISH, "Time spent in %s: %d milliseconds", mSubject, System.currentTimeMillis() - start)); - } + } + + public V run(Fragile runnable) throws E { + + long start = System.currentTimeMillis(); + try { + return runnable.value(); + } finally { + Log.d( + "Profiled", + String.format( + Locale.ENGLISH, + "Time spent in %s: %d milliseconds", + mSubject, + System.currentTimeMillis() - start)); } - - - public V run(Fragile runnable) throws E - { - - long start = System.currentTimeMillis(); - try - { - return runnable.value(); - } - finally - { - Log.d("Profiled", String.format(Locale.ENGLISH, "Time spent in %s: %d milliseconds", mSubject, System.currentTimeMillis() - start)); - } - } - + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java index 731e4243..51137f3b 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java @@ -16,41 +16,32 @@ package org.dmfs.provider.tasks.utils; -import org.dmfs.jems.iterator.generators.IntSequenceGenerator; - import java.util.Iterator; - +import org.dmfs.jems.iterator.generators.IntSequenceGenerator; /** * An {@link Iterable} which iterates a range of numbers. - *

- * TODO: implement in jems + * + *

TODO: implement in jems * * @author Marten Gajda */ @Deprecated -public final class Range implements Iterable -{ - private final int mStart; - private final int mEnd; - - - public Range(int end) - { - this(0, end); - } - - - public Range(int start, int end) - { - mStart = start; - mEnd = end; - } - - - @Override - public Iterator iterator() - { - return new LimitedIterator<>(mEnd - mStart, new IntSequenceGenerator(mStart)); - } +public final class Range implements Iterable { + private final int mStart; + private final int mEnd; + + public Range(int end) { + this(0, end); + } + + public Range(int start, int end) { + mStart = start; + mEnd = end; + } + + @Override + public Iterator iterator() { + return new LimitedIterator<>(mEnd - mStart, new IntSequenceGenerator(mStart)); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/ResourceArray.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/ResourceArray.java index e5b5fab1..15d4deba 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/ResourceArray.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/ResourceArray.java @@ -17,33 +17,25 @@ package org.dmfs.provider.tasks.utils; import android.content.Context; - -import org.dmfs.iterators.elementary.Seq; - import java.util.Iterator; - +import org.dmfs.iterators.elementary.Seq; /** * An {@link Iterable} of a string array resource. * * @author Marten Gajda */ -public final class ResourceArray implements Iterable -{ - private final Context mContext; - private final int mResource; - - - public ResourceArray(Context context, int resource) - { - mContext = context; - mResource = resource; - } - - - @Override - public Iterator iterator() - { - return new Seq<>(mContext.getResources().getStringArray(mResource)); - } +public final class ResourceArray implements Iterable { + private final Context mContext; + private final int mResource; + + public ResourceArray(Context context, int resource) { + mContext = context; + mResource = resource; + } + + @Override + public Iterator iterator() { + return new Seq<>(mContext.getResources().getStringArray(mResource)); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java index e49ac383..3902665f 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java @@ -17,41 +17,30 @@ package org.dmfs.provider.tasks.utils; import android.database.Cursor; - -import org.dmfs.iterators.AbstractBaseIterator; - import java.util.NoSuchElementException; - +import org.dmfs.iterators.AbstractBaseIterator; /** * @author Marten Gajda */ -public final class RowIterator extends AbstractBaseIterator -{ - private final Cursor mCursor; - - - public RowIterator(Cursor cursor) - { - mCursor = cursor; - } - - - @Override - public boolean hasNext() - { - return mCursor.getCount() > 0 && !mCursor.isClosed() && !mCursor.isLast(); - } - - - @Override - public Cursor next() - { - if (!hasNext()) - { - throw new NoSuchElementException("No other rows to iterate."); - } - mCursor.moveToNext(); - return mCursor; +public final class RowIterator extends AbstractBaseIterator { + private final Cursor mCursor; + + public RowIterator(Cursor cursor) { + mCursor = cursor; + } + + @Override + public boolean hasNext() { + return mCursor.getCount() > 0 && !mCursor.isClosed() && !mCursor.isLast(); + } + + @Override + public Cursor next() { + if (!hasNext()) { + throw new NoSuchElementException("No other rows to iterate."); } + mCursor.moveToNext(); + return mCursor; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TableColumns.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TableColumns.java index d96ac735..9ac03a06 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TableColumns.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TableColumns.java @@ -19,43 +19,36 @@ package org.dmfs.provider.tasks.utils; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; - -import org.dmfs.jems.function.Function; - import java.util.LinkedList; import java.util.List; - +import org.dmfs.jems.function.Function; /** * A {@link Function} which returns all column names of a specific table on a given database. * * @author Marten Gajda */ -public final class TableColumns implements Function> -{ - private final String mTableName; - - - public TableColumns(String tableName) - { - mTableName = tableName; - } - - - @Override - public Iterable value(SQLiteDatabase db) - { - try (Cursor cursor = db.rawQuery(String.format("PRAGMA table_info(%s)", DatabaseUtils.sqlEscapeString(mTableName)), null)) - { - int nameIdx = cursor.getColumnIndexOrThrow("name"); - - List result = new LinkedList<>(); - while (cursor.moveToNext()) - { - result.add(cursor.getString(nameIdx)); - } - - return result; - } +public final class TableColumns implements Function> { + private final String mTableName; + + public TableColumns(String tableName) { + mTableName = tableName; + } + + @Override + public Iterable value(SQLiteDatabase db) { + try (Cursor cursor = + db.rawQuery( + String.format("PRAGMA table_info(%s)", DatabaseUtils.sqlEscapeString(mTableName)), + null)) { + int nameIdx = cursor.getColumnIndexOrThrow("name"); + + List result = new LinkedList<>(); + while (cursor.moveToNext()) { + result.add(cursor.getString(nameIdx)); + } + + return result; } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java index b3620b39..5a8bacf6 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java @@ -16,6 +16,8 @@ package org.dmfs.provider.tasks.utils; +import java.util.Iterator; +import java.util.TimeZone; import org.dmfs.jems.optional.elementary.NullSafe; import org.dmfs.jems.single.combined.Backed; import org.dmfs.provider.tasks.model.TaskAdapter; @@ -25,56 +27,51 @@ import org.dmfs.rfc5545.recurrenceset.RecurrenceList; import org.dmfs.rfc5545.recurrenceset.RecurrenceRuleAdapter; import org.dmfs.rfc5545.recurrenceset.RecurrenceSet; -import java.util.Iterator; -import java.util.TimeZone; - - /** * An {@link Iterable} of all the instances of a task. * * @author Marten Gajda */ -public final class TaskInstanceIterable implements Iterable -{ - private final TaskAdapter mTaskAdapter; +public final class TaskInstanceIterable implements Iterable { + private final TaskAdapter mTaskAdapter; + public TaskInstanceIterable(TaskAdapter taskAdapter) { + mTaskAdapter = taskAdapter; + } - public TaskInstanceIterable(TaskAdapter taskAdapter) - { - mTaskAdapter = taskAdapter; - } - - - @Override - public Iterator iterator() - { - DateTime dtstart = new Backed(new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DTSTART)), () -> mTaskAdapter.valueOf(TaskAdapter.DUE)).value(); + @Override + public Iterator iterator() { + DateTime dtstart = + new Backed( + new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DTSTART)), + () -> mTaskAdapter.valueOf(TaskAdapter.DUE)) + .value(); - RecurrenceSet set = new RecurrenceSet(); - RecurrenceRule rule = mTaskAdapter.valueOf(TaskAdapter.RRULE); - if (rule != null) - { - if (rule.getUntil() != null && dtstart.isFloating() != rule.getUntil().isFloating()) - { - // rule UNTIL date mismatches start. This is merely a workaround for existing users. In future we should make sure - // such tasks don't exist - if (dtstart.isFloating()) - { - // make until floating too by making it floating in the current time zone - rule.setUntil(rule.getUntil().shiftTimeZone(TimeZone.getDefault()).swapTimeZone(null)); - } - else - { - // anchor UNTIL in the current time zone - rule.setUntil(new DateTime(null, rule.getUntil().getTimestamp()).swapTimeZone(TimeZone.getDefault())); - } - } - set.addInstances(new RecurrenceRuleAdapter(rule)); + RecurrenceSet set = new RecurrenceSet(); + RecurrenceRule rule = mTaskAdapter.valueOf(TaskAdapter.RRULE); + if (rule != null) { + if (rule.getUntil() != null && dtstart.isFloating() != rule.getUntil().isFloating()) { + // rule UNTIL date mismatches start. This is merely a workaround for existing users. In + // future we should make sure + // such tasks don't exist + if (dtstart.isFloating()) { + // make until floating too by making it floating in the current time zone + rule.setUntil(rule.getUntil().shiftTimeZone(TimeZone.getDefault()).swapTimeZone(null)); + } else { + // anchor UNTIL in the current time zone + rule.setUntil( + new DateTime(null, rule.getUntil().getTimestamp()) + .swapTimeZone(TimeZone.getDefault())); } + } + set.addInstances(new RecurrenceRuleAdapter(rule)); + } - set.addInstances(new RecurrenceList(new Timestamps(mTaskAdapter.valueOf(TaskAdapter.RDATE)).value())); - set.addExceptions(new RecurrenceList(new Timestamps(mTaskAdapter.valueOf(TaskAdapter.EXDATE)).value())); + set.addInstances( + new RecurrenceList(new Timestamps(mTaskAdapter.valueOf(TaskAdapter.RDATE)).value())); + set.addExceptions( + new RecurrenceList(new Timestamps(mTaskAdapter.valueOf(TaskAdapter.EXDATE)).value())); - return new TaskInstanceIterator(dtstart, set); - } + return new TaskInstanceIterator(dtstart, set); + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java index 5b74cfb6..331d5017 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java @@ -16,6 +16,9 @@ package org.dmfs.provider.tasks.utils; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.TimeZone; import org.dmfs.iterators.AbstractBaseIterator; import org.dmfs.jems.optional.decorators.Mapped; import org.dmfs.jems.optional.elementary.NullSafe; @@ -24,55 +27,45 @@ import org.dmfs.rfc5545.DateTime; import org.dmfs.rfc5545.recurrenceset.RecurrenceSet; import org.dmfs.rfc5545.recurrenceset.RecurrenceSetIterator; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.TimeZone; - - /** * An {@link Iterator} of instances as returned by a {@link RecurrenceSetIterator}. - *

- * TODO: this should go to lib-recur + * + *

TODO: this should go to lib-recur * * @author Marten Gajda */ -public final class TaskInstanceIterator extends AbstractBaseIterator -{ - private final DateTime mStart; - private final RecurrenceSetIterator mSetIterator; - private final String mTimezone; - - - TaskInstanceIterator(DateTime start, RecurrenceSet set) - { - this(start, set.iterator(start.getTimeZone(), start.getTimestamp()), - new Backed<>(new Mapped<>(TimeZone::getID, new NullSafe<>(start.getTimeZone())), () -> null).value()); - } +public final class TaskInstanceIterator extends AbstractBaseIterator { + private final DateTime mStart; + private final RecurrenceSetIterator mSetIterator; + private final String mTimezone; + TaskInstanceIterator(DateTime start, RecurrenceSet set) { + this( + start, + set.iterator(start.getTimeZone(), start.getTimestamp()), + new Backed<>(new Mapped<>(TimeZone::getID, new NullSafe<>(start.getTimeZone())), () -> null) + .value()); + } - TaskInstanceIterator(DateTime start, RecurrenceSetIterator setIterator, String timezone) - { - mStart = start; - mSetIterator = setIterator; - mTimezone = timezone; - } - - - @Override - public boolean hasNext() - { - return mSetIterator.hasNext(); - } + TaskInstanceIterator(DateTime start, RecurrenceSetIterator setIterator, String timezone) { + mStart = start; + mSetIterator = setIterator; + mTimezone = timezone; + } + @Override + public boolean hasNext() { + return mSetIterator.hasNext(); + } - @Override - public DateTime next() - { - if (!hasNext()) - { - throw new NoSuchElementException("No more elements to iterate"); - } - DateTime result = new DateTime(mStart.getTimeZone(), mSetIterator.next()); - return mStart.isAllDay() ? result.toAllDay() : mTimezone == null ? result.swapTimeZone(null) : result; + @Override + public DateTime next() { + if (!hasNext()) { + throw new NoSuchElementException("No more elements to iterate"); } + DateTime result = new DateTime(mStart.getTimeZone(), mSetIterator.next()); + return mStart.isAllDay() + ? result.toAllDay() + : mTimezone == null ? result.swapTimeZone(null) : result; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java index 84f0949d..ee5d9ef1 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java @@ -19,37 +19,30 @@ package org.dmfs.provider.tasks.utils; import org.dmfs.jems.single.Single; import org.dmfs.rfc5545.DateTime; - /** - * A {@link Single} of an array of timestamp values of a given {@link Iterable} of {@link DateTime}s. + * A {@link Single} of an array of timestamp values of a given {@link Iterable} of {@link + * DateTime}s. * * @author Marten Gajda */ -public final class Timestamps implements Single -{ - private final Iterable mDateTimes; - - - public Timestamps(Iterable dateTimes) - { - mDateTimes = dateTimes; +public final class Timestamps implements Single { + private final Iterable mDateTimes; + + public Timestamps(Iterable dateTimes) { + mDateTimes = dateTimes; + } + + @Override + public long[] value() { + int count = 0; + for (DateTime ignored : mDateTimes) { + count += 1; } - - - @Override - public long[] value() - { - int count = 0; - for (DateTime ignored : mDateTimes) - { - count += 1; - } - long[] timeStamps = new long[count]; - int i = 0; - for (DateTime dt : mDateTimes) - { - timeStamps[i++] = dt.getTimestamp(); - } - return timeStamps; + long[] timeStamps = new long[count]; + int i = 0; + for (DateTime dt : mDateTimes) { + timeStamps[i++] = dt.getTimestamp(); } + return timeStamps; + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/With.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/With.java index 7ae6b448..a5c0a813 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/With.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/With.java @@ -21,44 +21,33 @@ import org.dmfs.jems.optional.adapters.SinglePresent; import org.dmfs.jems.procedure.Procedure; import org.dmfs.jems.single.Single; - /** * Experiemental Procedure which calls another procedure with a given value. - *

- * TODO move to jems if this works out well + * + *

TODO move to jems if this works out well * * @author Marten Gajda */ @Deprecated -public final class With implements Procedure> -{ - private final Optional mValue; - - - public With(T value) - { - this(() -> value); - } +public final class With implements Procedure> { + private final Optional mValue; + public With(T value) { + this(() -> value); + } - public With(Single value) - { - this(new SinglePresent<>(value)); - } - - - public With(Optional value) - { - mValue = value; - } + public With(Single value) { + this(new SinglePresent<>(value)); + } + public With(Optional value) { + mValue = value; + } - @Override - public void process(Procedure delegate) - { - if (mValue.isPresent()) - { - delegate.process(mValue.value()); - } + @Override + public void process(Procedure delegate) { + if (mValue.isPresent()) { + delegate.process(mValue.value()); } + } } diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java index 80df3f7f..71936d55 100644 --- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java +++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java @@ -23,21 +23,21 @@ import org.dmfs.jems.single.Single; import org.dmfs.jems.single.combined.Backed; import org.dmfs.jems.single.decorators.DelegatingSingle; - /** - * Experimental {@link Single} which applies a {@link BiFunction} based on the presence of an {@link Optional}. - *

- * TODO: maybe a more appropriate name? - *

- * TODO: move to jems + * Experimental {@link Single} which applies a {@link BiFunction} based on the presence of an {@link + * Optional}. + * + *

TODO: maybe a more appropriate name? + * + *

TODO: move to jems * * @author Marten Gajda */ @Deprecated -public final class Zipped extends DelegatingSingle -{ - public Zipped(Optional optionalValue, Single delegate, BiFunction function) - { - super(new Backed(new Mapped<>(from -> function.value(from, delegate.value()), optionalValue), delegate)); - } +public final class Zipped extends DelegatingSingle { + public Zipped(Optional optionalValue, Single delegate, BiFunction function) { + super( + new Backed( + new Mapped<>(from -> function.value(from, delegate.value()), optionalValue), delegate)); + } } diff --git a/opentasks-provider/src/main/res/values-pt-rBR/strings.xml b/opentasks-provider/src/main/res/values-pt-rBR/strings.xml index bdb8414b..cbeb93f8 100644 --- a/opentasks-provider/src/main/res/values-pt-rBR/strings.xml +++ b/opentasks-provider/src/main/res/values-pt-rBR/strings.xml @@ -12,4 +12,4 @@ Permite que um aplicativo escreva tarefas na sua lista de tarefas Permite que um aplicativo escreva tarefas na sua lista de tarefas - \ No newline at end of file + diff --git a/opentasks-provider/src/main/res/values/opentasks_defaults.xml b/opentasks-provider/src/main/res/values/opentasks_defaults.xml index 5135fc4f..5b097630 100644 --- a/opentasks-provider/src/main/res/values/opentasks_defaults.xml +++ b/opentasks-provider/src/main/res/values/opentasks_defaults.xml @@ -5,4 +5,4 @@ foundation.e.tasks - \ No newline at end of file + diff --git a/opentasks-provider/src/main/res/values/opentasks_provider_changed_receivers.xml b/opentasks-provider/src/main/res/values/opentasks_provider_changed_receivers.xml index 344b230b..97e74c57 100644 --- a/opentasks-provider/src/main/res/values/opentasks_provider_changed_receivers.xml +++ b/opentasks-provider/src/main/res/values/opentasks_provider_changed_receivers.xml @@ -3,4 +3,4 @@ org.andstatus.todoagenda - \ No newline at end of file + diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapterTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapterTest.java index 29ff8091..49f83347 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapterTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapterTest.java @@ -16,8 +16,12 @@ package org.dmfs.provider.tasks.model.adapters; -import android.content.ContentValues; +import static org.dmfs.jems.hamcrest.matchers.IterableMatcher.iteratesTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; import org.dmfs.iterables.EmptyIterable; import org.dmfs.iterables.elementary.Seq; import org.dmfs.provider.tasks.model.TaskAdapter; @@ -27,197 +31,198 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.dmfs.jems.hamcrest.matchers.IterableMatcher.iteratesTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class DateTimeIterableFieldAdapterTest -{ - @Test - public void testFieldName() - { - assertThat(new DateTimeIterableFieldAdapter<>("x", "y").fieldName(), is("x")); - } - - - @Test - public void testGetFromCVAllDay1() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109"))); - } - - - @Test - public void testGetFromCVAllDay2() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109,20180110"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109"), DateTime.parse("20180110"))); - } - - - @Test - public void testGetFromCVFloating1() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109T140000"); - values.putNull("y"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109T140000"))); - } - - - @Test - public void testGetFromCVFloating2() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109T140000,20180110T140000"); - values.putNull("y"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109T140000"), DateTime.parse("20180110T140000"))); - } - - - @Test - public void testGetFromCVAbsolute1() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109T140000Z"); - values.put("y", "Europe/Berlin"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("Europe/Berlin", "20180109T150000"))); - } - - - @Test - public void testGetFromCVAbsolute2() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109T140000Z,20180110T140000Z"); - values.put("y", "Europe/Berlin"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("Europe/Berlin", "20180109T150000"), DateTime.parse("Europe/Berlin", "20180110T150000"))); - } - - - @Test - public void testSetInNull() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, null); - assertThat(values.getAsString("x"), nullValue()); - } - - - @Test - public void testSetInEmpty() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, EmptyIterable.instance()); - assertThat(values.getAsString("x"), nullValue()); - } - - - @Test - public void testSetInSingleAllDay() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109"))); - assertThat(values.getAsString("x"), is("20180109")); - } - - - @Test - public void testSetInSingleFloating() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109T150000"))); - assertThat(values.getAsString("x"), is("20180109T150000")); - } - - - @Test - public void testSetInSingleAbsolute() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("Europe/Berlin", "20180109T150000"))); - assertThat(values.getAsString("x"), is("20180109T140000Z")); - } - - - @Test - public void testSetInDoubleAllDay() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109"), DateTime.parse("20180110"))); - assertThat(values.getAsString("x"), is("20180109,20180110")); - } - - - @Test - public void testSetInDoubleFloating() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109T150000"), DateTime.parse("20180110T150000"))); - assertThat(values.getAsString("x"), is("20180109T150000,20180110T150000")); - } - - - @Test - public void testSetInDoubleAbsolute() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("Europe/Berlin", "20180109T150000"), DateTime.parse("Europe/Berlin", "20180110T150000"))); - assertThat(values.getAsString("x"), is("20180109T140000Z,20180110T140000Z")); - } - - - @Test - public void testSetInMultiAllDay() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109"), DateTime.parse("20180110"), DateTime.parse("20180111"))); - assertThat(values.getAsString("x"), is("20180109,20180110,20180111")); - } - - - @Test - public void testSetInMultiFloating() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109T150000"), DateTime.parse("20180110T150000"), DateTime.parse("20180111T150000"))); - assertThat(values.getAsString("x"), is("20180109T150000,20180110T150000,20180111T150000")); - } - - - @Test - public void testSetInMultiAbsolute() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("Europe/Berlin", "20180109T150000"), DateTime.parse("Europe/Berlin", "20180110T150000"), - DateTime.parse("Europe/Berlin", "20180111T150000"))); - assertThat(values.getAsString("x"), is("20180109T140000Z,20180110T140000Z,20180111T140000Z")); - } -} \ No newline at end of file +public class DateTimeIterableFieldAdapterTest { + @Test + public void testFieldName() { + assertThat(new DateTimeIterableFieldAdapter<>("x", "y").fieldName(), is("x")); + } + + @Test + public void testGetFromCVAllDay1() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + values.put("x", "20180109"); + assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109"))); + } + + @Test + public void testGetFromCVAllDay2() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + values.put("x", "20180109,20180110"); + assertThat( + adapter.getFrom(values), + iteratesTo(DateTime.parse("20180109"), DateTime.parse("20180110"))); + } + + @Test + public void testGetFromCVFloating1() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + values.put("x", "20180109T140000"); + values.putNull("y"); + assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109T140000"))); + } + + @Test + public void testGetFromCVFloating2() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + values.put("x", "20180109T140000,20180110T140000"); + values.putNull("y"); + assertThat( + adapter.getFrom(values), + iteratesTo(DateTime.parse("20180109T140000"), DateTime.parse("20180110T140000"))); + } + + @Test + public void testGetFromCVAbsolute1() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + values.put("x", "20180109T140000Z"); + values.put("y", "Europe/Berlin"); + assertThat( + adapter.getFrom(values), iteratesTo(DateTime.parse("Europe/Berlin", "20180109T150000"))); + } + + @Test + public void testGetFromCVAbsolute2() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + values.put("x", "20180109T140000Z,20180110T140000Z"); + values.put("y", "Europe/Berlin"); + assertThat( + adapter.getFrom(values), + iteratesTo( + DateTime.parse("Europe/Berlin", "20180109T150000"), + DateTime.parse("Europe/Berlin", "20180110T150000"))); + } + + @Test + public void testSetInNull() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn(values, null); + assertThat(values.getAsString("x"), nullValue()); + } + + @Test + public void testSetInEmpty() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn(values, EmptyIterable.instance()); + assertThat(values.getAsString("x"), nullValue()); + } + + @Test + public void testSetInSingleAllDay() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn(values, new Seq<>(DateTime.parse("20180109"))); + assertThat(values.getAsString("x"), is("20180109")); + } + + @Test + public void testSetInSingleFloating() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn(values, new Seq<>(DateTime.parse("20180109T150000"))); + assertThat(values.getAsString("x"), is("20180109T150000")); + } + + @Test + public void testSetInSingleAbsolute() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn(values, new Seq<>(DateTime.parse("Europe/Berlin", "20180109T150000"))); + assertThat(values.getAsString("x"), is("20180109T140000Z")); + } + + @Test + public void testSetInDoubleAllDay() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn(values, new Seq<>(DateTime.parse("20180109"), DateTime.parse("20180110"))); + assertThat(values.getAsString("x"), is("20180109,20180110")); + } + + @Test + public void testSetInDoubleFloating() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn( + values, new Seq<>(DateTime.parse("20180109T150000"), DateTime.parse("20180110T150000"))); + assertThat(values.getAsString("x"), is("20180109T150000,20180110T150000")); + } + + @Test + public void testSetInDoubleAbsolute() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn( + values, + new Seq<>( + DateTime.parse("Europe/Berlin", "20180109T150000"), + DateTime.parse("Europe/Berlin", "20180110T150000"))); + assertThat(values.getAsString("x"), is("20180109T140000Z,20180110T140000Z")); + } + + @Test + public void testSetInMultiAllDay() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn( + values, + new Seq<>( + DateTime.parse("20180109"), DateTime.parse("20180110"), DateTime.parse("20180111"))); + assertThat(values.getAsString("x"), is("20180109,20180110,20180111")); + } + + @Test + public void testSetInMultiFloating() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn( + values, + new Seq<>( + DateTime.parse("20180109T150000"), + DateTime.parse("20180110T150000"), + DateTime.parse("20180111T150000"))); + assertThat(values.getAsString("x"), is("20180109T150000,20180110T150000,20180111T150000")); + } + + @Test + public void testSetInMultiAbsolute() { + ContentValues values = new ContentValues(); + FieldAdapter, ?> adapter = + new DateTimeIterableFieldAdapter("x", "y"); + adapter.setIn( + values, + new Seq<>( + DateTime.parse("Europe/Berlin", "20180109T150000"), + DateTime.parse("Europe/Berlin", "20180110T150000"), + DateTime.parse("Europe/Berlin", "20180111T150000"))); + assertThat(values.getAsString("x"), is("20180109T140000Z,20180110T140000Z,20180111T140000Z")); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java index 43bb5b95..4224ff7d 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java @@ -16,9 +16,11 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; -import android.content.ContentValues; -import android.util.Log; +import static org.dmfs.jems.optional.elementary.Absent.absent; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; import org.dmfs.jems.optional.elementary.Present; import org.dmfs.provider.tasks.utils.ContentValuesWithLong; import org.dmfs.rfc5545.DateTime; @@ -27,39 +29,29 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.dmfs.jems.optional.elementary.Absent.absent; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class DatedTest -{ - - @Test - public void testAbsent() - { - ContentValues instanceData = new Dated(absent(), "ts", "sorting", ContentValues::new).value(); - // this shouldn't really add any values and go by the "defaults" - assertThat(instanceData.size(), is(0)); - } - - - @Test - public void testPresent() - { - DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500"); - - ContentValues instanceData = new Dated(new Present<>(start), - "ts", - "sorting", ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong("ts", start.getTimestamp())); - assertThat(instanceData, new ContentValuesWithLong("sorting", start.getInstance())); - assertThat(instanceData.size(), is(2)); - } -} \ No newline at end of file +public class DatedTest { + + @Test + public void testAbsent() { + ContentValues instanceData = new Dated(absent(), "ts", "sorting", ContentValues::new).value(); + // this shouldn't really add any values and go by the "defaults" + assertThat(instanceData.size(), is(0)); + } + + @Test + public void testPresent() { + DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500"); + + ContentValues instanceData = + new Dated(new Present<>(start), "ts", "sorting", ContentValues::new).value(); + + assertThat(instanceData, new ContentValuesWithLong("ts", start.getTimestamp())); + assertThat(instanceData, new ContentValuesWithLong("sorting", start.getInstance())); + assertThat(instanceData.size(), is(2)); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DistantTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DistantTest.java index c8be962d..5a10e21e 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DistantTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DistantTest.java @@ -16,31 +16,27 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; -import android.content.ContentValues; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; import org.dmfs.tasks.contract.TaskContract; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class DistantTest -{ +public class DistantTest { - @Test - public void test() - { - ContentValues instanceData = new Distant(100, ContentValues::new).value(); - assertThat(instanceData.get(TaskContract.Instances.DISTANCE_FROM_CURRENT), is(100)); - assertThat(instanceData.size(), is(1)); - } -} \ No newline at end of file + @Test + public void test() { + ContentValues instanceData = new Distant(100, ContentValues::new).value(); + assertThat(instanceData.get(TaskContract.Instances.DISTANCE_FROM_CURRENT), is(100)); + assertThat(instanceData.size(), is(1)); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java index 25840e9a..1d74a9e4 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java @@ -16,8 +16,13 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; -import android.content.ContentValues; +import static org.dmfs.jems.optional.elementary.Absent.absent; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; +import java.util.TimeZone; import org.dmfs.jems.optional.elementary.Present; import org.dmfs.provider.tasks.utils.ContentValuesWithLong; import org.dmfs.rfc5545.DateTime; @@ -27,58 +32,59 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import java.util.TimeZone; - -import static org.dmfs.jems.optional.elementary.Absent.absent; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class DueDatedTest -{ - - @Test - public void testNone() - { - ContentValues instanceData = new DueDated(absent(), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, nullValue(Long.class))); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE_SORTING, nullValue(Long.class))); - // this doesn't actually add anything, the ContentValues are expected to contain null values. - assertThat(instanceData.size(), is(0)); - } - - - @Test - public void testStartEurope() - { - DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500"); - - ContentValues instanceData = new DueDated(new Present<>(start), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, start.getTimestamp())); - assertThat(instanceData, - new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance())); - assertThat(instanceData.size(), is(2)); - } - - - @Test - public void testStartAmerica() - { - DateTime start = DateTime.parse("America/New_York", "20171208T125500"); - - ContentValues instanceData = new DueDated(new Present<>(start), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, start.getTimestamp())); - assertThat(instanceData, - new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance())); - assertThat(instanceData.size(), is(2)); - } -} \ No newline at end of file +public class DueDatedTest { + + @Test + public void testNone() { + ContentValues instanceData = new DueDated(absent(), ContentValues::new).value(); + + assertThat( + instanceData, + new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, nullValue(Long.class))); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_DUE_SORTING, nullValue(Long.class))); + // this doesn't actually add anything, the ContentValues are expected to contain null values. + assertThat(instanceData.size(), is(0)); + } + + @Test + public void testStartEurope() { + DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500"); + + ContentValues instanceData = new DueDated(new Present<>(start), ContentValues::new).value(); + + assertThat( + instanceData, + new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, start.getTimestamp())); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_DUE_SORTING, + start.shiftTimeZone(TimeZone.getDefault()).getInstance())); + assertThat(instanceData.size(), is(2)); + } + + @Test + public void testStartAmerica() { + DateTime start = DateTime.parse("America/New_York", "20171208T125500"); + + ContentValues instanceData = new DueDated(new Present<>(start), ContentValues::new).value(); + + assertThat( + instanceData, + new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, start.getTimestamp())); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_DUE_SORTING, + start.shiftTimeZone(TimeZone.getDefault()).getInstance())); + assertThat(instanceData.size(), is(2)); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java index 4fbd8f52..95135e91 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java @@ -16,8 +16,12 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; -import android.content.ContentValues; +import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; import org.dmfs.provider.tasks.utils.ContentValuesWithLong; import org.dmfs.tasks.contract.TaskContract; import org.junit.Test; @@ -25,56 +29,54 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class EnduringTest -{ - @Test - public void testNoValue() - { - assertThat(new Enduring(ContentValues::new), hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)))); - assertThat(new Enduring(ContentValues::new).value().size(), is(1)); - } - - - @Test - public void testStartValue() - { - ContentValues values = new ContentValues(1); - values.put(TaskContract.Instances.INSTANCE_START, 10); - assertThat(new Enduring(() -> new ContentValues(values)), - hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)))); - assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(2)); - } - +public class EnduringTest { + @Test + public void testNoValue() { + assertThat( + new Enduring(ContentValues::new), + hasValue( + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)))); + assertThat(new Enduring(ContentValues::new).value().size(), is(1)); + } - @Test - public void testDueValue() - { - ContentValues values = new ContentValues(1); - values.put(TaskContract.Instances.INSTANCE_DUE, 10); - assertThat(new Enduring(() -> new ContentValues(values)), - hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)))); - assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(2)); - } + @Test + public void testStartValue() { + ContentValues values = new ContentValues(1); + values.put(TaskContract.Instances.INSTANCE_START, 10); + assertThat( + new Enduring(() -> new ContentValues(values)), + hasValue( + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)))); + assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(2)); + } + @Test + public void testDueValue() { + ContentValues values = new ContentValues(1); + values.put(TaskContract.Instances.INSTANCE_DUE, 10); + assertThat( + new Enduring(() -> new ContentValues(values)), + hasValue( + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)))); + assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(2)); + } - @Test - public void testStartDueValue() - { - ContentValues values = new ContentValues(2); - values.put(TaskContract.Instances.INSTANCE_START, 1); - values.put(TaskContract.Instances.INSTANCE_DUE, 10); - assertThat(new Enduring(() -> new ContentValues(values)), hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, 9))); - assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(3)); - } -} \ No newline at end of file + @Test + public void testStartDueValue() { + ContentValues values = new ContentValues(2); + values.put(TaskContract.Instances.INSTANCE_START, 1); + values.put(TaskContract.Instances.INSTANCE_DUE, 10); + assertThat( + new Enduring(() -> new ContentValues(values)), + hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, 9))); + assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(3)); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java index e56d8a41..cd8bc809 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java @@ -16,8 +16,12 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; -import android.content.ContentValues; +import static org.dmfs.optional.Absent.absent; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; import org.dmfs.jems.optional.elementary.Present; import org.dmfs.provider.tasks.utils.ContentValuesWithLong; import org.dmfs.rfc5545.DateTime; @@ -27,84 +31,82 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.dmfs.optional.Absent.absent; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class OverriddenTest -{ - @Test - public void testAbsent() - { - ContentValues instanceData = new Overridden(absent(), ContentValues::new).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); - assertThat(instanceData.size(), is(0)); - } - - - @Test - public void testAbsentWithStart() - { - ContentValues values = new ContentValues(); - values.put(TaskContract.Instances.INSTANCE_START, 10); - - ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); - assertThat(instanceData.size(), is(1)); - } - - - @Test - public void testAbsentWithDue() - { - ContentValues values = new ContentValues(); - values.put(TaskContract.Instances.INSTANCE_DUE, 20); - - ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); - assertThat(instanceData.size(), is(1)); - } - - - @Test - public void testAbsentWithStartAndDue() - { - ContentValues values = new ContentValues(); - values.put(TaskContract.Instances.INSTANCE_START, 10); - values.put(TaskContract.Instances.INSTANCE_DUE, 20); - - ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); - assertThat(instanceData.size(), is(2)); - } - - - @Test - public void testPresent() - { - - ContentValues instanceData = new Overridden(new Present<>(new DateTime(40)), ContentValues::new).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 40)); - assertThat(instanceData.size(), is(1)); - } - - - @Test - public void testPresentWithStartAndDue() - { - ContentValues values = new ContentValues(); - values.put(TaskContract.Instances.INSTANCE_START, 10); - values.put(TaskContract.Instances.INSTANCE_DUE, 20); - - ContentValues instanceData = new Overridden(new Present<>(new DateTime(40)), () -> new ContentValues(values)).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 40)); - assertThat(instanceData.size(), is(3)); - } -} \ No newline at end of file +public class OverriddenTest { + @Test + public void testAbsent() { + ContentValues instanceData = new Overridden(absent(), ContentValues::new).value(); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); + assertThat(instanceData.size(), is(0)); + } + + @Test + public void testAbsentWithStart() { + ContentValues values = new ContentValues(); + values.put(TaskContract.Instances.INSTANCE_START, 10); + + ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value(); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); + assertThat(instanceData.size(), is(1)); + } + + @Test + public void testAbsentWithDue() { + ContentValues values = new ContentValues(); + values.put(TaskContract.Instances.INSTANCE_DUE, 20); + + ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value(); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); + assertThat(instanceData.size(), is(1)); + } + + @Test + public void testAbsentWithStartAndDue() { + ContentValues values = new ContentValues(); + values.put(TaskContract.Instances.INSTANCE_START, 10); + values.put(TaskContract.Instances.INSTANCE_DUE, 20); + + ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value(); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); + assertThat(instanceData.size(), is(2)); + } + + @Test + public void testPresent() { + + ContentValues instanceData = + new Overridden(new Present<>(new DateTime(40)), ContentValues::new).value(); + assertThat( + instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 40)); + assertThat(instanceData.size(), is(1)); + } + + @Test + public void testPresentWithStartAndDue() { + ContentValues values = new ContentValues(); + values.put(TaskContract.Instances.INSTANCE_START, 10); + values.put(TaskContract.Instances.INSTANCE_DUE, 20); + + ContentValues instanceData = + new Overridden(new Present<>(new DateTime(40)), () -> new ContentValues(values)).value(); + assertThat( + instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 40)); + assertThat(instanceData.size(), is(3)); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java index 85676d18..16c5d96a 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java @@ -16,8 +16,13 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; -import android.content.ContentValues; +import static org.dmfs.optional.Absent.absent; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; +import java.util.TimeZone; import org.dmfs.jems.optional.elementary.Present; import org.dmfs.provider.tasks.utils.ContentValuesWithLong; import org.dmfs.rfc5545.DateTime; @@ -27,58 +32,59 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import java.util.TimeZone; - -import static org.dmfs.optional.Absent.absent; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class StartDatedTest -{ - - @Test - public void testNone() - { - ContentValues instanceData = new StartDated(absent(), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, nullValue(Long.class))); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START_SORTING, nullValue(Long.class))); - // this doesn't actually add anything, the ContentValues are expected to contain null values. - assertThat(instanceData.size(), is(0)); - } - - - @Test - public void testStartEurope() - { - DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500"); - - ContentValues instanceData = new StartDated(new Present<>(start), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, start.getTimestamp())); - assertThat(instanceData, - new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance())); - assertThat(instanceData.size(), is(2)); - } - - - @Test - public void testStartAmerica() - { - DateTime start = DateTime.parse("America/New_York", "20171208T125500"); - - ContentValues instanceData = new StartDated(new Present<>(start), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, start.getTimestamp())); - assertThat(instanceData, - new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance())); - assertThat(instanceData.size(), is(2)); - } -} \ No newline at end of file +public class StartDatedTest { + + @Test + public void testNone() { + ContentValues instanceData = new StartDated(absent(), ContentValues::new).value(); + + assertThat( + instanceData, + new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, nullValue(Long.class))); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_START_SORTING, nullValue(Long.class))); + // this doesn't actually add anything, the ContentValues are expected to contain null values. + assertThat(instanceData.size(), is(0)); + } + + @Test + public void testStartEurope() { + DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500"); + + ContentValues instanceData = new StartDated(new Present<>(start), ContentValues::new).value(); + + assertThat( + instanceData, + new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, start.getTimestamp())); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_START_SORTING, + start.shiftTimeZone(TimeZone.getDefault()).getInstance())); + assertThat(instanceData.size(), is(2)); + } + + @Test + public void testStartAmerica() { + DateTime start = DateTime.parse("America/New_York", "20171208T125500"); + + ContentValues instanceData = new StartDated(new Present<>(start), ContentValues::new).value(); + + assertThat( + instanceData, + new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, start.getTimestamp())); + assertThat( + instanceData, + new ContentValuesWithLong( + TaskContract.Instances.INSTANCE_START_SORTING, + start.shiftTimeZone(TimeZone.getDefault()).getInstance())); + assertThat(instanceData.size(), is(2)); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java index 760ca346..83d3c5b7 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java @@ -16,8 +16,10 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; -import android.content.ContentValues; +import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; import org.dmfs.provider.tasks.utils.ContentValuesWithLong; import org.dmfs.tasks.contract.TaskContract; import org.junit.Test; @@ -25,20 +27,16 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class TaskRelatedTest -{ - @Test - public void testValue() - { - assertThat(new TaskRelated(123, ContentValues::new), hasValue(new ContentValuesWithLong(TaskContract.Instances.TASK_ID, 123))); - } -} \ No newline at end of file +public class TaskRelatedTest { + @Test + public void testValue() { + assertThat( + new TaskRelated(123, ContentValues::new), + hasValue(new ContentValuesWithLong(TaskContract.Instances.TASK_ID, 123))); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java index f78f8bff..8296ada7 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java @@ -16,38 +16,33 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata; -import android.content.ContentValues; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; import org.dmfs.tasks.contract.TaskContract; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class VanillaInstanceDataTest -{ - @Test - public void testValue() - { - ContentValues values = new VanillaInstanceData().value(); - assertThat(values.get(TaskContract.Instances.INSTANCE_START), nullValue()); - assertThat(values.get(TaskContract.Instances.INSTANCE_START_SORTING), nullValue()); - assertThat(values.get(TaskContract.Instances.INSTANCE_DUE), nullValue()); - assertThat(values.get(TaskContract.Instances.INSTANCE_DUE_SORTING), nullValue()); - assertThat(values.get(TaskContract.Instances.INSTANCE_DURATION), nullValue()); - assertThat(values.get(TaskContract.Instances.DISTANCE_FROM_CURRENT), is(0)); - assertThat(values.get(TaskContract.Instances.INSTANCE_ORIGINAL_TIME), nullValue()); - assertThat(values.size(), is(7)); - } - -} \ No newline at end of file +public class VanillaInstanceDataTest { + @Test + public void testValue() { + ContentValues values = new VanillaInstanceData().value(); + assertThat(values.get(TaskContract.Instances.INSTANCE_START), nullValue()); + assertThat(values.get(TaskContract.Instances.INSTANCE_START_SORTING), nullValue()); + assertThat(values.get(TaskContract.Instances.INSTANCE_DUE), nullValue()); + assertThat(values.get(TaskContract.Instances.INSTANCE_DUE_SORTING), nullValue()); + assertThat(values.get(TaskContract.Instances.INSTANCE_DURATION), nullValue()); + assertThat(values.get(TaskContract.Instances.DISTANCE_FROM_CURRENT), is(0)); + assertThat(values.get(TaskContract.Instances.INSTANCE_ORIGINAL_TIME), nullValue()); + assertThat(values.size(), is(7)); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContainsValuesTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContainsValuesTest.java index 750f4778..3535d652 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContainsValuesTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContainsValuesTest.java @@ -16,79 +16,73 @@ package org.dmfs.provider.tasks.utils; +import static org.dmfs.jems.hamcrest.matchers.predicate.PredicateMatcher.satisfiedBy; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + import android.content.ContentValues; import android.database.MatrixCursor; - import org.dmfs.iterables.elementary.Seq; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.dmfs.jems.hamcrest.matchers.predicate.PredicateMatcher.satisfiedBy; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class ContainsValuesTest -{ - @Test - public void test() - { - ContentValues values = new ContentValues(); - values.put("a", 123); - values.put("b", "stringValue"); - values.put("c", new byte[] { 3, 2, 1 }); - values.putNull("d"); - - MatrixCursor cursor = new MatrixCursor(new String[] { "c", "b", "a", "d", "f" }); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue", 123, null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue", "123", null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2 }, "stringValue", 123, null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValueX", 123, null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue", 1234, null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue", 123, "123", "xyz")); - cursor.addRow(new Seq<>(321, "stringValueX", "1234", "123", "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1, 0 }, "stringValueX", 1234, "123", "xyz")); +public class ContainsValuesTest { + @Test + public void test() { + ContentValues values = new ContentValues(); + values.put("a", 123); + values.put("b", "stringValue"); + values.put("c", new byte[] {3, 2, 1}); + values.putNull("d"); - cursor.moveToFirst(); - assertThat(new ContainsValues(values), is(satisfiedBy(cursor))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(satisfiedBy(cursor))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - } + MatrixCursor cursor = new MatrixCursor(new String[] {"c", "b", "a", "d", "f"}); + cursor.addRow(new Seq<>(new byte[] {3, 2, 1}, "stringValue", 123, null, "xyz")); + cursor.addRow(new Seq<>(new byte[] {3, 2, 1}, "stringValue", "123", null, "xyz")); + cursor.addRow(new Seq<>(new byte[] {3, 2}, "stringValue", 123, null, "xyz")); + cursor.addRow(new Seq<>(new byte[] {3, 2, 1}, "stringValueX", 123, null, "xyz")); + cursor.addRow(new Seq<>(new byte[] {3, 2, 1}, "stringValue", 1234, null, "xyz")); + cursor.addRow(new Seq<>(new byte[] {3, 2, 1}, "stringValue", 123, "123", "xyz")); + cursor.addRow(new Seq<>(321, "stringValueX", "1234", "123", "xyz")); + cursor.addRow(new Seq<>(new byte[] {3, 2, 1, 0}, "stringValueX", 1234, "123", "xyz")); + cursor.moveToFirst(); + assertThat(new ContainsValues(values), is(satisfiedBy(cursor))); + cursor.moveToNext(); + assertThat(new ContainsValues(values), is(satisfiedBy(cursor))); + cursor.moveToNext(); + assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); + cursor.moveToNext(); + assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); + cursor.moveToNext(); + assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); + cursor.moveToNext(); + assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); + cursor.moveToNext(); + assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); + cursor.moveToNext(); + assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); + } - @Test - public void testMissingColumns() - { - ContentValues values = new ContentValues(); - values.put("a", 123); - values.put("b", "stringValue"); - values.put("c", new byte[] { 3, 2, 1 }); - values.putNull("d"); + @Test + public void testMissingColumns() { + ContentValues values = new ContentValues(); + values.put("a", 123); + values.put("b", "stringValue"); + values.put("c", new byte[] {3, 2, 1}); + values.putNull("d"); - MatrixCursor cursor = new MatrixCursor(new String[] { "c", "b" }); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue")); + MatrixCursor cursor = new MatrixCursor(new String[] {"c", "b"}); + cursor.addRow(new Seq<>(new byte[] {3, 2, 1}, "stringValue")); - cursor.moveToFirst(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - } -} \ No newline at end of file + cursor.moveToFirst(); + assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java index 6dafc00b..239ad7d4 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java @@ -16,42 +16,34 @@ package org.dmfs.provider.tasks.utils; -import android.content.ContentValues; +import static org.hamcrest.Matchers.is; +import android.content.ContentValues; import org.hamcrest.FeatureMatcher; import org.hamcrest.Matcher; -import static org.hamcrest.Matchers.is; - - /** * A {@link Matcher} to test if {@link ContentValues} contain a specific Long value. - *

- * TODO: can we convert that into a more generic {@link ContentValues} matcher? It might be useful in other places. - *

- * TODO: also consider moving this to "Test-Bolts" + * + *

TODO: can we convert that into a more generic {@link ContentValues} matcher? It might be + * useful in other places. + * + *

TODO: also consider moving this to "Test-Bolts" */ -public final class ContentValuesWithLong extends FeatureMatcher -{ - private final String mKey; - - - public ContentValuesWithLong(String valueKey, long value) - { - this(valueKey, is(value)); - } - - - public ContentValuesWithLong(String valueKey, Matcher matcher) - { - super(matcher, "Long value " + valueKey, "Long value " + valueKey); - mKey = valueKey; - } - - - @Override - protected Long featureValueOf(ContentValues actual) - { - return actual.getAsLong(mKey); - } +public final class ContentValuesWithLong extends FeatureMatcher { + private final String mKey; + + public ContentValuesWithLong(String valueKey, long value) { + this(valueKey, is(value)); + } + + public ContentValuesWithLong(String valueKey, Matcher matcher) { + super(matcher, "Long value " + valueKey, "Long value " + valueKey); + mKey = valueKey; + } + + @Override + protected Long featureValueOf(ContentValues actual) { + return actual.getAsLong(mKey); + } } diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java index 81eff9c6..6d597f02 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java @@ -16,8 +16,10 @@ package org.dmfs.provider.tasks.utils; -import android.content.ContentValues; +import static org.dmfs.jems.hamcrest.matchers.IterableMatcher.iteratesTo; +import static org.junit.Assert.assertThat; +import android.content.ContentValues; import org.dmfs.iterables.elementary.Seq; import org.dmfs.provider.tasks.model.ContentValuesTaskAdapter; import org.dmfs.provider.tasks.model.TaskAdapter; @@ -28,161 +30,149 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import static org.dmfs.jems.hamcrest.matchers.IterableMatcher.iteratesTo; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class TaskInstanceIterableTest -{ - @Test - public void testAbsolute() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314")); - taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("Europe/Berlin", "20170606T121314"), - DateTime.parse("Europe/Berlin", "20170608T121314"), - DateTime.parse("Europe/Berlin", "20170610T121314"), - DateTime.parse("Europe/Berlin", "20170612T121314"), - DateTime.parse("Europe/Berlin", "20170614T121314"), - DateTime.parse("Europe/Berlin", "20170616T121314"), - DateTime.parse("Europe/Berlin", "20170618T121314"), - DateTime.parse("Europe/Berlin", "20170620T121314"), - DateTime.parse("Europe/Berlin", "20170622T121314"), - DateTime.parse("Europe/Berlin", "20170624T121314") - )); - } - - - @Test - public void testAllDay() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("20170606")); - taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("20170606"), - DateTime.parse("20170608"), - DateTime.parse("20170610"), - DateTime.parse("20170612"), - DateTime.parse("20170614"), - DateTime.parse("20170616"), - DateTime.parse("20170618"), - DateTime.parse("20170620"), - DateTime.parse("20170622"), - DateTime.parse("20170624") - )); - } - - - @Test - public void testFloating() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("20170606T121314")); - taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("20170606T121314"), - DateTime.parse("20170608T121314"), - DateTime.parse("20170610T121314"), - DateTime.parse("20170612T121314"), - DateTime.parse("20170614T121314"), - DateTime.parse("20170616T121314"), - DateTime.parse("20170618T121314"), - DateTime.parse("20170620T121314"), - DateTime.parse("20170622T121314"), - DateTime.parse("20170624T121314") - )); - } - - - @Test - public void testRDate() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314")); - taskAdapter.set(TaskAdapter.RDATE, new Seq<>( - DateTime.parse("Europe/Berlin", "20170606T121314"), - DateTime.parse("Europe/Berlin", "20170608T121314"), - DateTime.parse("Europe/Berlin", "20170610T121314"), - DateTime.parse("Europe/Berlin", "20170612T121314"), - DateTime.parse("Europe/Berlin", "20170614T121314"), - DateTime.parse("Europe/Berlin", "20170616T121314"), - DateTime.parse("Europe/Berlin", "20170618T121314"), - DateTime.parse("Europe/Berlin", "20170620T121314"), - DateTime.parse("Europe/Berlin", "20170622T121314"), - DateTime.parse("Europe/Berlin", "20170624T121314") - )); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("Europe/Berlin", "20170606T121314"), - DateTime.parse("Europe/Berlin", "20170608T121314"), - DateTime.parse("Europe/Berlin", "20170610T121314"), - DateTime.parse("Europe/Berlin", "20170612T121314"), - DateTime.parse("Europe/Berlin", "20170614T121314"), - DateTime.parse("Europe/Berlin", "20170616T121314"), - DateTime.parse("Europe/Berlin", "20170618T121314"), - DateTime.parse("Europe/Berlin", "20170620T121314"), - DateTime.parse("Europe/Berlin", "20170622T121314"), - DateTime.parse("Europe/Berlin", "20170624T121314") - )); - } - - - @Test - public void testRDateAndRRule() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314")); - taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); - taskAdapter.set(TaskAdapter.RDATE, new Seq<>( - DateTime.parse("Europe/Berlin", "20170606T121313"), - DateTime.parse("Europe/Berlin", "20170608T121313"), - DateTime.parse("Europe/Berlin", "20170610T121313"), - DateTime.parse("Europe/Berlin", "20170612T121313"), - DateTime.parse("Europe/Berlin", "20170614T121313"), - DateTime.parse("Europe/Berlin", "20170616T121313"), - DateTime.parse("Europe/Berlin", "20170618T121313"), - DateTime.parse("Europe/Berlin", "20170620T121313"), - DateTime.parse("Europe/Berlin", "20170622T121313"), - DateTime.parse("Europe/Berlin", "20170624T121313") - )); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("Europe/Berlin", "20170606T121313"), - DateTime.parse("Europe/Berlin", "20170606T121314"), - DateTime.parse("Europe/Berlin", "20170608T121313"), - DateTime.parse("Europe/Berlin", "20170608T121314"), - DateTime.parse("Europe/Berlin", "20170610T121313"), - DateTime.parse("Europe/Berlin", "20170610T121314"), - DateTime.parse("Europe/Berlin", "20170612T121313"), - DateTime.parse("Europe/Berlin", "20170612T121314"), - DateTime.parse("Europe/Berlin", "20170614T121313"), - DateTime.parse("Europe/Berlin", "20170614T121314"), - DateTime.parse("Europe/Berlin", "20170616T121313"), - DateTime.parse("Europe/Berlin", "20170616T121314"), - DateTime.parse("Europe/Berlin", "20170618T121313"), - DateTime.parse("Europe/Berlin", "20170618T121314"), - DateTime.parse("Europe/Berlin", "20170620T121313"), - DateTime.parse("Europe/Berlin", "20170620T121314"), - DateTime.parse("Europe/Berlin", "20170622T121313"), - DateTime.parse("Europe/Berlin", "20170622T121314"), - DateTime.parse("Europe/Berlin", "20170624T121313"), - DateTime.parse("Europe/Berlin", "20170624T121314") - )); - } -} \ No newline at end of file +public class TaskInstanceIterableTest { + @Test + public void testAbsolute() throws Exception { + TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); + taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314")); + taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); + + assertThat( + new TaskInstanceIterable(taskAdapter), + iteratesTo( + DateTime.parse("Europe/Berlin", "20170606T121314"), + DateTime.parse("Europe/Berlin", "20170608T121314"), + DateTime.parse("Europe/Berlin", "20170610T121314"), + DateTime.parse("Europe/Berlin", "20170612T121314"), + DateTime.parse("Europe/Berlin", "20170614T121314"), + DateTime.parse("Europe/Berlin", "20170616T121314"), + DateTime.parse("Europe/Berlin", "20170618T121314"), + DateTime.parse("Europe/Berlin", "20170620T121314"), + DateTime.parse("Europe/Berlin", "20170622T121314"), + DateTime.parse("Europe/Berlin", "20170624T121314"))); + } + + @Test + public void testAllDay() throws Exception { + TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); + taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("20170606")); + taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); + + assertThat( + new TaskInstanceIterable(taskAdapter), + iteratesTo( + DateTime.parse("20170606"), + DateTime.parse("20170608"), + DateTime.parse("20170610"), + DateTime.parse("20170612"), + DateTime.parse("20170614"), + DateTime.parse("20170616"), + DateTime.parse("20170618"), + DateTime.parse("20170620"), + DateTime.parse("20170622"), + DateTime.parse("20170624"))); + } + + @Test + public void testFloating() throws Exception { + TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); + taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("20170606T121314")); + taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); + + assertThat( + new TaskInstanceIterable(taskAdapter), + iteratesTo( + DateTime.parse("20170606T121314"), + DateTime.parse("20170608T121314"), + DateTime.parse("20170610T121314"), + DateTime.parse("20170612T121314"), + DateTime.parse("20170614T121314"), + DateTime.parse("20170616T121314"), + DateTime.parse("20170618T121314"), + DateTime.parse("20170620T121314"), + DateTime.parse("20170622T121314"), + DateTime.parse("20170624T121314"))); + } + + @Test + public void testRDate() throws Exception { + TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); + taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314")); + taskAdapter.set( + TaskAdapter.RDATE, + new Seq<>( + DateTime.parse("Europe/Berlin", "20170606T121314"), + DateTime.parse("Europe/Berlin", "20170608T121314"), + DateTime.parse("Europe/Berlin", "20170610T121314"), + DateTime.parse("Europe/Berlin", "20170612T121314"), + DateTime.parse("Europe/Berlin", "20170614T121314"), + DateTime.parse("Europe/Berlin", "20170616T121314"), + DateTime.parse("Europe/Berlin", "20170618T121314"), + DateTime.parse("Europe/Berlin", "20170620T121314"), + DateTime.parse("Europe/Berlin", "20170622T121314"), + DateTime.parse("Europe/Berlin", "20170624T121314"))); + + assertThat( + new TaskInstanceIterable(taskAdapter), + iteratesTo( + DateTime.parse("Europe/Berlin", "20170606T121314"), + DateTime.parse("Europe/Berlin", "20170608T121314"), + DateTime.parse("Europe/Berlin", "20170610T121314"), + DateTime.parse("Europe/Berlin", "20170612T121314"), + DateTime.parse("Europe/Berlin", "20170614T121314"), + DateTime.parse("Europe/Berlin", "20170616T121314"), + DateTime.parse("Europe/Berlin", "20170618T121314"), + DateTime.parse("Europe/Berlin", "20170620T121314"), + DateTime.parse("Europe/Berlin", "20170622T121314"), + DateTime.parse("Europe/Berlin", "20170624T121314"))); + } + + @Test + public void testRDateAndRRule() throws Exception { + TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); + taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314")); + taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); + taskAdapter.set( + TaskAdapter.RDATE, + new Seq<>( + DateTime.parse("Europe/Berlin", "20170606T121313"), + DateTime.parse("Europe/Berlin", "20170608T121313"), + DateTime.parse("Europe/Berlin", "20170610T121313"), + DateTime.parse("Europe/Berlin", "20170612T121313"), + DateTime.parse("Europe/Berlin", "20170614T121313"), + DateTime.parse("Europe/Berlin", "20170616T121313"), + DateTime.parse("Europe/Berlin", "20170618T121313"), + DateTime.parse("Europe/Berlin", "20170620T121313"), + DateTime.parse("Europe/Berlin", "20170622T121313"), + DateTime.parse("Europe/Berlin", "20170624T121313"))); + + assertThat( + new TaskInstanceIterable(taskAdapter), + iteratesTo( + DateTime.parse("Europe/Berlin", "20170606T121313"), + DateTime.parse("Europe/Berlin", "20170606T121314"), + DateTime.parse("Europe/Berlin", "20170608T121313"), + DateTime.parse("Europe/Berlin", "20170608T121314"), + DateTime.parse("Europe/Berlin", "20170610T121313"), + DateTime.parse("Europe/Berlin", "20170610T121314"), + DateTime.parse("Europe/Berlin", "20170612T121313"), + DateTime.parse("Europe/Berlin", "20170612T121314"), + DateTime.parse("Europe/Berlin", "20170614T121313"), + DateTime.parse("Europe/Berlin", "20170614T121314"), + DateTime.parse("Europe/Berlin", "20170616T121313"), + DateTime.parse("Europe/Berlin", "20170616T121314"), + DateTime.parse("Europe/Berlin", "20170618T121313"), + DateTime.parse("Europe/Berlin", "20170618T121314"), + DateTime.parse("Europe/Berlin", "20170620T121313"), + DateTime.parse("Europe/Berlin", "20170620T121314"), + DateTime.parse("Europe/Berlin", "20170622T121313"), + DateTime.parse("Europe/Berlin", "20170622T121314"), + DateTime.parse("Europe/Berlin", "20170624T121313"), + DateTime.parse("Europe/Berlin", "20170624T121314"))); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java index 552f43de..64e02902 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java @@ -16,6 +16,10 @@ package org.dmfs.provider.tasks.utils; +import static org.dmfs.jems.hamcrest.matchers.iterator.IteratorMatcher.iteratorOf; +import static org.junit.Assert.assertThat; + +import java.util.TimeZone; import org.dmfs.rfc5545.DateTime; import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; import org.dmfs.rfc5545.recur.RecurrenceRule; @@ -23,99 +27,78 @@ import org.dmfs.rfc5545.recurrenceset.RecurrenceRuleAdapter; import org.dmfs.rfc5545.recurrenceset.RecurrenceSet; import org.junit.Test; -import java.util.TimeZone; - -import static org.dmfs.jems.hamcrest.matchers.iterator.IteratorMatcher.iteratorOf; -import static org.junit.Assert.assertThat; - - /** * @author Marten Gajda */ -public class TaskInstanceIteratorTest -{ - private final static String TIMEZONE = "Europe/Berlin"; - - - @Test - public void testAbsolute() throws InvalidRecurrenceRuleException - { - RecurrenceSet recurrenceSet = new RecurrenceSet(); - recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3"))); - DateTime start = DateTime.parse(TIMEZONE, "20210201T120000"); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet.iterator(TimeZone.getTimeZone(TIMEZONE), start.getTimestamp()), TIMEZONE), - iteratorOf( - DateTime.parse(TIMEZONE, "20210201T120000"), - DateTime.parse(TIMEZONE, "20210202T120000"), - DateTime.parse(TIMEZONE, "20210203T120000") - ) - ); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet), - iteratorOf( - DateTime.parse(TIMEZONE, "20210201T120000"), - DateTime.parse(TIMEZONE, "20210202T120000"), - DateTime.parse(TIMEZONE, "20210203T120000") - ) - ); - } - - - @Test - public void testFloating() throws InvalidRecurrenceRuleException - { - - RecurrenceSet recurrenceSet = new RecurrenceSet(); - recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3"))); - DateTime start = DateTime.parse("20210201T120000"); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet.iterator(null, start.getTimestamp()), null), - iteratorOf( - DateTime.parse("20210201T120000"), - DateTime.parse("20210202T120000"), - DateTime.parse("20210203T120000") - ) - ); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet), - iteratorOf( - DateTime.parse("20210201T120000"), - DateTime.parse("20210202T120000"), - DateTime.parse("20210203T120000") - ) - ); - } - - - @Test - public void testAllDay() throws InvalidRecurrenceRuleException - { - - RecurrenceSet recurrenceSet = new RecurrenceSet(); - recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3"))); - DateTime start = DateTime.parse("20210201"); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet.iterator(null, start.getTimestamp()), null), - iteratorOf( - DateTime.parse("20210201"), - DateTime.parse("20210202"), - DateTime.parse("20210203") - ) - ); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet), - iteratorOf( - DateTime.parse("20210201"), - DateTime.parse("20210202"), - DateTime.parse("20210203") - ) - ); - } -} \ No newline at end of file +public class TaskInstanceIteratorTest { + private static final String TIMEZONE = "Europe/Berlin"; + + @Test + public void testAbsolute() throws InvalidRecurrenceRuleException { + RecurrenceSet recurrenceSet = new RecurrenceSet(); + recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3"))); + DateTime start = DateTime.parse(TIMEZONE, "20210201T120000"); + + assertThat( + () -> + new TaskInstanceIterator( + start, + recurrenceSet.iterator(TimeZone.getTimeZone(TIMEZONE), start.getTimestamp()), + TIMEZONE), + iteratorOf( + DateTime.parse(TIMEZONE, "20210201T120000"), + DateTime.parse(TIMEZONE, "20210202T120000"), + DateTime.parse(TIMEZONE, "20210203T120000"))); + + assertThat( + () -> new TaskInstanceIterator(start, recurrenceSet), + iteratorOf( + DateTime.parse(TIMEZONE, "20210201T120000"), + DateTime.parse(TIMEZONE, "20210202T120000"), + DateTime.parse(TIMEZONE, "20210203T120000"))); + } + + @Test + public void testFloating() throws InvalidRecurrenceRuleException { + + RecurrenceSet recurrenceSet = new RecurrenceSet(); + recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3"))); + DateTime start = DateTime.parse("20210201T120000"); + + assertThat( + () -> + new TaskInstanceIterator( + start, recurrenceSet.iterator(null, start.getTimestamp()), null), + iteratorOf( + DateTime.parse("20210201T120000"), + DateTime.parse("20210202T120000"), + DateTime.parse("20210203T120000"))); + + assertThat( + () -> new TaskInstanceIterator(start, recurrenceSet), + iteratorOf( + DateTime.parse("20210201T120000"), + DateTime.parse("20210202T120000"), + DateTime.parse("20210203T120000"))); + } + + @Test + public void testAllDay() throws InvalidRecurrenceRuleException { + + RecurrenceSet recurrenceSet = new RecurrenceSet(); + recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3"))); + DateTime start = DateTime.parse("20210201"); + + assertThat( + () -> + new TaskInstanceIterator( + start, recurrenceSet.iterator(null, start.getTimestamp()), null), + iteratorOf( + DateTime.parse("20210201"), DateTime.parse("20210202"), DateTime.parse("20210203"))); + + assertThat( + () -> new TaskInstanceIterator(start, recurrenceSet), + iteratorOf( + DateTime.parse("20210201"), DateTime.parse("20210202"), DateTime.parse("20210203"))); + } +} diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java index f81928c0..7b450c3f 100644 --- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java +++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java @@ -16,11 +16,6 @@ package org.dmfs.provider.tasks.utils; -import org.dmfs.jems.function.BiFunction; -import org.dmfs.jems.optional.elementary.Present; -import org.dmfs.jems.single.elementary.ValueSingle; -import org.junit.Test; - import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue; import static org.dmfs.jems.mockito.doubles.TestDoubles.dummy; import static org.dmfs.jems.mockito.doubles.TestDoubles.failingMock; @@ -29,31 +24,33 @@ import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.doReturn; +import org.dmfs.jems.function.BiFunction; +import org.dmfs.jems.optional.elementary.Present; +import org.dmfs.jems.single.elementary.ValueSingle; +import org.junit.Test; /** * @author Marten Gajda */ -public class ZippedTest -{ - @Test - public void testPresent() - { - Object dummyPresentValue = new Object(); - Object dummySingleValue = new Object(); - Object dummyResult = new Object(); - BiFunction mockFunction = failingMock(BiFunction.class); - doReturn(dummyResult).when(mockFunction).value(dummyPresentValue, dummySingleValue); - assertThat(new Zipped<>(new Present<>(dummyPresentValue), new ValueSingle<>(dummySingleValue), mockFunction), hasValue(sameInstance(dummyResult))); - } - +public class ZippedTest { + @Test + public void testPresent() { + Object dummyPresentValue = new Object(); + Object dummySingleValue = new Object(); + Object dummyResult = new Object(); + BiFunction mockFunction = failingMock(BiFunction.class); + doReturn(dummyResult).when(mockFunction).value(dummyPresentValue, dummySingleValue); + assertThat( + new Zipped<>( + new Present<>(dummyPresentValue), new ValueSingle<>(dummySingleValue), mockFunction), + hasValue(sameInstance(dummyResult))); + } - @Test - public void testAbsent() - { - Object dummyObject = new Object(); - assertThat( - new Zipped(absent(), new ValueSingle<>(dummyObject), dummy(BiFunction.class)), - hasValue(sameInstance(dummyObject)) - ); - } -} \ No newline at end of file + @Test + public void testAbsent() { + Object dummyObject = new Object(); + assertThat( + new Zipped(absent(), new ValueSingle<>(dummyObject), dummy(BiFunction.class)), + hasValue(sameInstance(dummyObject))); + } +} diff --git a/opentasks-theme/build.gradle b/opentasks-theme/build.gradle index d675aad7..81ca149a 100644 --- a/opentasks-theme/build.gradle +++ b/opentasks-theme/build.gradle @@ -26,7 +26,6 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 36 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" } @@ -37,6 +36,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + compileOptions { sourceCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21 diff --git a/opentasks-theme/src/main/AndroidManifest.xml b/opentasks-theme/src/main/AndroidManifest.xml index 7e97eb3a..a214d097 100644 --- a/opentasks-theme/src/main/AndroidManifest.xml +++ b/opentasks-theme/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - \ No newline at end of file + diff --git a/opentasks-theme/src/main/res/values/attrs.xml b/opentasks-theme/src/main/res/values/attrs.xml index cf367a11..d11c0da7 100644 --- a/opentasks-theme/src/main/res/values/attrs.xml +++ b/opentasks-theme/src/main/res/values/attrs.xml @@ -21,4 +21,4 @@ format="color" /> - \ No newline at end of file + diff --git a/opentasks-theme/src/main/res/values/colors.xml b/opentasks-theme/src/main/res/values/colors.xml index b8bd0d66..3070b270 100644 --- a/opentasks-theme/src/main/res/values/colors.xml +++ b/opentasks-theme/src/main/res/values/colors.xml @@ -30,4 +30,4 @@ #08000000 #10ffffff - \ No newline at end of file + diff --git a/opentasks-theme/src/main/res/values/dimens.xml b/opentasks-theme/src/main/res/values/dimens.xml index 301e9bf1..8be8287f 100644 --- a/opentasks-theme/src/main/res/values/dimens.xml +++ b/opentasks-theme/src/main/res/values/dimens.xml @@ -4,4 +4,4 @@ 0dp 2dp 4dp - \ No newline at end of file + diff --git a/opentasks/build.gradle b/opentasks/build.gradle index 21ca4136..16daf85c 100644 --- a/opentasks/build.gradle +++ b/opentasks/build.gradle @@ -9,6 +9,7 @@ tasks.named("preBuild") { android { namespace = "org.dmfs.tasks" compileSdkVersion 36 + defaultConfig { applicationId "foundation.e.tasks" minSdkVersion 21 @@ -16,9 +17,7 @@ android { versionCode 7000 versionName '1.4.2' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables.useSupportLibrary = true - } signingConfigs { @@ -45,20 +44,16 @@ android { ) } } + buildFeatures { buildConfig = true } + packagingOptions { exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' } - productFlavors { - } - - lintOptions { - disable 'MissingTranslation' // TODO - } compileOptions { sourceCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21 diff --git a/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java b/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java index 657575ac..0f78e647 100644 --- a/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java +++ b/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java @@ -16,109 +16,109 @@ package org.dmfs.tasks.utils; -import androidx.test.runner.AndroidJUnit4; import android.text.format.Time; - +import androidx.test.runner.AndroidJUnit4; +import java.util.TimeZone; import org.dmfs.rfc5545.DateTime; import org.dmfs.rfc5545.Duration; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.TimeZone; - - /** * Test for {@link DateFormatter#toTime(DateTime)} method. * * @author Gabor Keszthelyi */ @RunWith(AndroidJUnit4.class) -public class DateTimeToTimeConversionTest -{ - - @Test - public void test_toTime_withVariousDateTimes() - { - assertCorrectlyConverted(DateTime.now()); - - assertCorrectlyConverted(DateTime.now(TimeZone.getTimeZone("UTC+04:00"))); - - assertCorrectlyConverted(DateTime.nowAndHere()); - - assertCorrectlyConverted(new DateTime(1509473781000L)); - - assertCorrectlyConverted(new DateTime(1509473781000L).addDuration(new Duration(1, 1, 0))); - - assertCorrectlyConverted(DateTime.now(TimeZone.getTimeZone("UTC+04:00")).shiftTimeZone(TimeZone.getTimeZone("UTC+05:00"))); - - // Floating, all-day - assertCorrectlyConverted(DateTime.now().toAllDay()); - - // Not DST (March 2017 in Hungary): - assertCorrectlyConverted(new DateTime(TimeZone.getTimeZone("Europe/Budapest"), 2017, 2 - 1, 7, 15, 0, 0)); - assertCorrectlyConverted(new DateTime(2017, 2 - 1, 7, 15, 0, 0).shiftTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); - assertCorrectlyConverted(new DateTime(2017, 2 - 1, 7, 15, 0, 0).swapTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); - - // DST (July 2017 in Hungary): - assertCorrectlyConverted(new DateTime(TimeZone.getTimeZone("Europe/Budapest"), 2017, 7 - 1, 7, 15, 0, 0)); - assertCorrectlyConverted(new DateTime(2017, 7 - 1, 7, 15, 0, 0).shiftTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); - assertCorrectlyConverted(new DateTime(2017, 7 - 1, 7, 15, 0, 0).swapTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); +public class DateTimeToTimeConversionTest { + + @Test + public void test_toTime_withVariousDateTimes() { + assertCorrectlyConverted(DateTime.now()); + + assertCorrectlyConverted(DateTime.now(TimeZone.getTimeZone("UTC+04:00"))); + + assertCorrectlyConverted(DateTime.nowAndHere()); + + assertCorrectlyConverted(new DateTime(1509473781000L)); + + assertCorrectlyConverted(new DateTime(1509473781000L).addDuration(new Duration(1, 1, 0))); + + assertCorrectlyConverted( + DateTime.now(TimeZone.getTimeZone("UTC+04:00")) + .shiftTimeZone(TimeZone.getTimeZone("UTC+05:00"))); + + // Floating, all-day + assertCorrectlyConverted(DateTime.now().toAllDay()); + + // Not DST (March 2017 in Hungary): + assertCorrectlyConverted( + new DateTime(TimeZone.getTimeZone("Europe/Budapest"), 2017, 2 - 1, 7, 15, 0, 0)); + assertCorrectlyConverted( + new DateTime(2017, 2 - 1, 7, 15, 0, 0) + .shiftTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); + assertCorrectlyConverted( + new DateTime(2017, 2 - 1, 7, 15, 0, 0) + .swapTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); + + // DST (July 2017 in Hungary): + assertCorrectlyConverted( + new DateTime(TimeZone.getTimeZone("Europe/Budapest"), 2017, 7 - 1, 7, 15, 0, 0)); + assertCorrectlyConverted( + new DateTime(2017, 7 - 1, 7, 15, 0, 0) + .shiftTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); + assertCorrectlyConverted( + new DateTime(2017, 7 - 1, 7, 15, 0, 0) + .swapTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); + } + + @Test(expected = IllegalArgumentException.class) + public void test_toTime_forFloatingButNotAllDayDateTime_throwsSinceItIsNotSupported() { + new DateFormatter(null).toTime(new DateTime(2017, 7 - 1, 7, 15, 0, 0)); + } + + private void assertCorrectlyConverted(DateTime dateTime) { + Time time = new DateFormatter(null).toTime(dateTime); + if (!isEquivalentDateTimeAndTime(dateTime, time)) { + throw new AssertionError( + String.format("DateTime=%s and Time=%s are not equivalent", dateTime, time)); } - - - @Test(expected = IllegalArgumentException.class) - public void test_toTime_forFloatingButNotAllDayDateTime_throwsSinceItIsNotSupported() - { - new DateFormatter(null).toTime(new DateTime(2017, 7 - 1, 7, 15, 0, 0)); - } - - - private void assertCorrectlyConverted(DateTime dateTime) - { - Time time = new DateFormatter(null).toTime(dateTime); - if (!isEquivalentDateTimeAndTime(dateTime, time)) - { - throw new AssertionError(String.format("DateTime=%s and Time=%s are not equivalent", dateTime, time)); - } - } - - - /** - * Contains the definition/requirement of when a {@link DateTime} and {@link Time} is considered equivalent in this project. - */ - private boolean isEquivalentDateTimeAndTime(DateTime dateTime, Time time) - { - // android.text.Time doesn't seem to store in millis precision, there is a 1000 multiplier used there internally - // when calculating millis, so we can only compare in this precision: - boolean millisMatch = - dateTime.getTimestamp() / 1000 - == - time.toMillis(false) / 1000; - - boolean yearMatch = dateTime.getYear() == time.year; - boolean monthMatch = dateTime.getMonth() == time.month; - boolean dayMatch = dateTime.getDayOfMonth() == time.monthDay; - boolean hourMatch = dateTime.getHours() == time.hour; - boolean minuteMatch = dateTime.getMinutes() == time.minute; - boolean secondsMatch = dateTime.getSeconds() == time.second; - - boolean allDaysMatch = time.allDay == dateTime.isAllDay(); - - boolean timeZoneMatch = - (dateTime.isFloating() && dateTime.isAllDay() && time.timezone.equals("UTC")) - || - // This is the regular case with non-floating DateTime - (dateTime.getTimeZone() != null && time.timezone.equals(dateTime.getTimeZone().getID())); - - return millisMatch - && yearMatch - && monthMatch - && dayMatch - && hourMatch - && minuteMatch - && secondsMatch - && allDaysMatch - && timeZoneMatch; - } - -} \ No newline at end of file + } + + /** + * Contains the definition/requirement of when a {@link DateTime} and {@link Time} is considered + * equivalent in this project. + */ + private boolean isEquivalentDateTimeAndTime(DateTime dateTime, Time time) { + // android.text.Time doesn't seem to store in millis precision, there is a 1000 multiplier used + // there internally + // when calculating millis, so we can only compare in this precision: + boolean millisMatch = dateTime.getTimestamp() / 1000 == time.toMillis(false) / 1000; + + boolean yearMatch = dateTime.getYear() == time.year; + boolean monthMatch = dateTime.getMonth() == time.month; + boolean dayMatch = dateTime.getDayOfMonth() == time.monthDay; + boolean hourMatch = dateTime.getHours() == time.hour; + boolean minuteMatch = dateTime.getMinutes() == time.minute; + boolean secondsMatch = dateTime.getSeconds() == time.second; + + boolean allDaysMatch = time.allDay == dateTime.isAllDay(); + + boolean timeZoneMatch = + (dateTime.isFloating() && dateTime.isAllDay() && time.timezone.equals("UTC")) + || + // This is the regular case with non-floating DateTime + (dateTime.getTimeZone() != null + && time.timezone.equals(dateTime.getTimeZone().getID())); + + return millisMatch + && yearMatch + && monthMatch + && dayMatch + && hourMatch + && minuteMatch + && secondsMatch + && allDaysMatch + && timeZoneMatch; + } +} diff --git a/opentasks/src/main/java/com/jmedeisis/draglinearlayout/DragLinearLayout.java b/opentasks/src/main/java/com/jmedeisis/draglinearlayout/DragLinearLayout.java index 4c161301..7d9dda58 100644 --- a/opentasks/src/main/java/com/jmedeisis/draglinearlayout/DragLinearLayout.java +++ b/opentasks/src/main/java/com/jmedeisis/draglinearlayout/DragLinearLayout.java @@ -36,8 +36,6 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; -import androidx.annotation.NonNull; -import androidx.core.view.MotionEventCompat; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; @@ -49,15 +47,18 @@ import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnPreDrawListener; import android.widget.LinearLayout; import android.widget.ScrollView; - +import androidx.annotation.NonNull; +import androidx.core.view.MotionEventCompat; /** - * A LinearLayout that supports children Views that can be dragged and swapped around. See {@link #addDragView(android.view.View, android.view.View)}, - * {@link #addDragView(android.view.View, android.view.View, int)}, {@link #setViewDraggable(android.view.View, android.view.View)}, and + * A LinearLayout that supports children Views that can be dragged and swapped around. See {@link + * #addDragView(android.view.View, android.view.View)}, {@link #addDragView(android.view.View, + * android.view.View, int)}, {@link #setViewDraggable(android.view.View, android.view.View)}, and * {@link #removeDragView(android.view.View)}. - *

- * Currently, no error-checking is done on standard {@link #addView(android.view.View)} and {@link #removeView(android.view.View)} calls, so avoid using these - * with children previously declared as draggable to prevent memory leaks and/or subtle bugs. Pull requests welcome! + * + *

Currently, no error-checking is done on standard {@link #addView(android.view.View)} and + * {@link #removeView(android.view.View)} calls, so avoid using these with children previously + * declared as draggable to prevent memory leaks and/or subtle bugs. Pull requests welcome! */ /* * This file has been modified by dmfs: @@ -66,915 +67,783 @@ import android.widget.ScrollView; * * remove attribute to set scroll distance, the default is fine for us. */ @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; - - +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 { /** - * 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. + * 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. */ - private final SparseArray draggableChildren; + void onSwap(View firstView, int firstPosition, View secondView, int secondPosition); + } + private OnViewSwapListener swapListener; - private class DraggableChild - { - /** - * If non-null, a reference to an on-going position animation. - */ - private ValueAnimator swapAnimation; + 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; - public void endExistingAnimation() - { - if (null != swapAnimation) - { - swapAnimation.end(); - } - } - + private class DraggableChild { + /** If non-null, a reference to an on-going position animation. */ + private ValueAnimator swapAnimation; - public void cancelExistingAnimation() - { - if (null != swapAnimation) - { - swapAnimation.cancel(); - } - } + public void endExistingAnimation() { + if (null != swapAnimation) { + swapAnimation.end(); + } } - - /** - * 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; - } + public void cancelExistingAnimation() { + if (null != swapAnimation) { + swapAnimation.cancel(); + } } - - - /** - * 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}. - */ -// 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; - - - public DragLinearLayout(Context context) - { - this(context, null); + } + + /** + * 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 DragLinearLayout(Context context, AttributeSet attrs) - { - super(context, attrs); - - setOrientation(LinearLayout.VERTICAL); - - draggableChildren = new SparseArray<>(); - - draggedItem = new DragItem(); - ViewConfiguration vc = ViewConfiguration.get(context); - slop = vc.getScaledTouchSlop(); - - 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); - -// TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DragLinearLayout, 0, 0); -// try -// { -// scrollSensitiveAreaHeight = a.getDimensionPixelSize(R.styleable.DragLinearLayout_scrollSensitiveHeight, -// (int) (DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP * resources.getDisplayMetrics().density + 0.5f)); -// } -// finally -// { -// a.recycle(); -// } - - nominalDistanceScaled = (int) (NOMINAL_DISTANCE * resources.getDisplayMetrics().density + 0.5f); + 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; } - - @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); + public void onDragStart() { + view.setVisibility(View.INVISIBLE); + this.dragging = true; } - - /** - * 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); + public void setTotalOffset(int offset) { + totalDragOffset = offset; + updateTargetTop(); } - - /** - * 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); + public void updateTargetTop() { + targetTopOffset = startTop - view.getTop() + totalDragOffset; } - - /** - * 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."); - } + public void onDragStop() { + this.dragging = false; } - - /** - * 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); - } - } - } - } + public boolean settling() { + return null != settleAnimation; } - - @Override - public void removeAllViews() - { - super.removeAllViews(); - draggableChildren.clear(); + 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; } - - - /** - * 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; + } + + /** + * 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}. */ + // 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; + + public DragLinearLayout(Context context) { + this(context, null); + } + + public DragLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + setOrientation(LinearLayout.VERTICAL); + + draggableChildren = new SparseArray<>(); + + draggedItem = new DragItem(); + ViewConfiguration vc = ViewConfiguration.get(context); + slop = vc.getScaledTouchSlop(); + + 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); + + // TypedArray a = context.getTheme().obtainStyledAttributes(attrs, + // R.styleable.DragLinearLayout, 0, 0); + // try + // { + // scrollSensitiveAreaHeight = + // a.getDimensionPixelSize(R.styleable.DragLinearLayout_scrollSensitiveHeight, + // (int) (DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP * + // resources.getDisplayMetrics().density + 0.5f)); + // } + // finally + // { + // 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."); } - - - /** - * 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; + 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); + } - /** - * See {@link com.jmedeisis.draglinearlayout.DragLinearLayout.OnViewSwapListener}. - */ - public void setOnViewSwapListener(OnViewSwapListener swapListener) - { - this.swapListener = swapListener; + /** 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."); } - - /** - * 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))); + 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."); } - - - /** - * 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 + } + + /** + * 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); + } } - - 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); + } + } + } + + @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); - 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); - } + // complete any existing animations, both for the newly selected child and the previous dragged + // one + draggableChildren.get(position).endExistingAnimation(); - draggedItem.onDragStart(); - requestDisallowInterceptTouchEvent(true); + draggedItem.startDetectingOnPossibleDrag(child, position); + } - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + 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); } - - /** - * 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(); + draggedItem.onDragStart(); + requestDisallowInterceptTouchEvent(true); + + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + + /** 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()); - @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); - } - } + final int shadowAlpha = (int) ((1 - animation.getAnimatedFraction()) * 255); + // if (null != dragTopShadowDrawable) + // dragTopShadowDrawable.setAlpha(shadowAlpha); + // dragBottomShadowDrawable.setAlpha(shadowAlpha); + invalidate(); + } }); - 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; + 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 + } - // swap elements - final int originalPosition = draggedItem.position; - final int switchPosition = isBelow ? belowPosition : abovePosition; + draggedItem.settleAnimation = null; + draggedItem.stopDetecting(); - draggableChildren.get(switchPosition).cancelExistingAnimation(); - final float switchViewStartY = switchView.getY(); + // if (null != dragTopShadowDrawable) + // dragTopShadowDrawable.setAlpha(255); + // dragBottomShadowDrawable.setAlpha(255); - if (null != swapListener) - { - swapListener.onSwap(draggedItem.view, draggedItem.position, switchView, switchPosition); + // 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; + } - if (isBelow) - { - removeViewAt(originalPosition); - removeViewAt(switchPosition - 1); + @Override + public void onAnimationEnd(Animator animation) { + draggableChildren.get(originalPosition).swapAnimation = null; + } + }); + switchAnimator.start(); - addView(belowView, originalPosition); - addView(draggedItem.view, switchPosition); + return true; } - else - { - removeViewAt(switchPosition); - removeViewAt(originalPosition - 1); + }); - addView(draggedItem.view, switchPosition); - addView(aboveView, originalPosition); + 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; } - 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 previousDraggablePosition(int position) - { - int startIndex = draggableChildren.indexOfKey(position); - if (startIndex < 1 || startIndex > draggableChildren.size()) + 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); + // + // if (null != dragTopShadowDrawable) + // { + // dragTopShadowDrawable.setBounds(left, top - dragShadowHeight, right, top); + // dragTopShadowDrawable.draw(canvas); + // } + + canvas.restore(); + } + } + + /* + * 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, + * we #startDrag() and proceed to handle the drag 3) User taps on interactive drag handle / child, e.g. Button. #onInterceptTouchEvent receives DOWN + * DragHandleOnTouchListener (attached to each draggable child) #onTouch receives DOWN #startDetectingDrag is called, draggedItem is now detecting view + * handles touch, so our #onTouchEvent is not called yet #onInterceptTouchEvent receives ACTION_MOVE if dy > touch slop, we assume user wants to drag and + * intercept the event #onTouchEvent receives further ACTION_MOVE events, proceed to handle the drag + * + * For cases 2) and 3), lifting the active pointer at any point in the sequence of events triggers #onTouchEnd and the draggedItem, if detecting, is + * #stopDetecting. + */ + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + switch (MotionEventCompat.getActionMasked(event)) { + case MotionEvent.ACTION_DOWN: { - return -1; + if (draggedItem.detecting) { + return false; // an existing item is (likely) settling + } + downY = (int) MotionEventCompat.getY(event, 0); + activePointerId = MotionEventCompat.getPointerId(event, 0); + break; } - return draggableChildren.keyAt(startIndex - 1); - } - - - private int nextDraggablePosition(int position) - { - int startIndex = draggableChildren.indexOfKey(position); - if (startIndex < -1 || startIndex > draggableChildren.size() - 2) + case MotionEvent.ACTION_MOVE: { - return -1; + 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; } - return draggableChildren.keyAt(startIndex + 1); - } - - - private Runnable dragUpdater; - - - private void handleContainerScroll(final int currentTop) - { - if (null != containerScrollView) + case MotionEvent.ACTION_POINTER_UP: { - 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; - } + final int pointerIndex = MotionEventCompat.getActionIndex(event); + final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex); - 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); + if (pointerId != activePointerId) { + break; // if active pointer, fall through and cancel! + } } - } - + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + { + onTouchEnd(); - /** - * 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); + if (draggedItem.detecting) { + draggedItem.stopDetecting(); + } + break; + } + default: + break; } + return false; + } - @Override - protected void dispatchDraw(@NonNull Canvas canvas) - { - super.dispatchDraw(canvas); - - if (draggedItem.detecting && (draggedItem.dragging || draggedItem.settling())) + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + switch (MotionEventCompat.getActionMasked(event)) { + case MotionEvent.ACTION_DOWN: { - 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); -// -// if (null != dragTopShadowDrawable) -// { -// dragTopShadowDrawable.setBounds(left, top - dragShadowHeight, right, top); -// dragTopShadowDrawable.draw(canvas); -// } - - canvas.restore(); + if (!draggedItem.detecting || draggedItem.settling()) { + return false; + } + startDrag(); + return true; } - } - - - /* - * 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, - * we #startDrag() and proceed to handle the drag 3) User taps on interactive drag handle / child, e.g. Button. #onInterceptTouchEvent receives DOWN - * DragHandleOnTouchListener (attached to each draggable child) #onTouch receives DOWN #startDetectingDrag is called, draggedItem is now detecting view - * handles touch, so our #onTouchEvent is not called yet #onInterceptTouchEvent receives ACTION_MOVE if dy > touch slop, we assume user wants to drag and - * intercept the event #onTouchEvent receives further ACTION_MOVE events, proceed to handle the drag - * - * For cases 2) and 3), lifting the active pointer at any point in the sequence of events triggers #onTouchEnd and the draggedItem, if detecting, is - * #stopDetecting. - */ - - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) - { - switch (MotionEventCompat.getActionMasked(event)) + case MotionEvent.ACTION_MOVE: { - 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; + 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); - return false; - } - - - @Override - public boolean onTouchEvent(@NonNull MotionEvent event) - { - switch (MotionEventCompat.getActionMasked(event)) + if (pointerId != activePointerId) { + break; // if active pointer, fall through and cancel! + } + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { - 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; + onTouchEnd(); + + if (draggedItem.dragging) { + onDragStop(); + } else if (draggedItem.detecting) { + draggedItem.stopDetecting(); + } + return true; } - return false; + default: + break; } + return false; + } + private void onTouchEnd() { + downY = -1; + activePointerId = INVALID_POINTER_ID; + } - private void onTouchEnd() - { - downY = -1; - activePointerId = INVALID_POINTER_ID; - } - - - private class DragHandleOnTouchListener implements OnTouchListener - { - private final View view; - + 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; - } + 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); + private BitmapDrawable getDragDrawable(View view) { + int top = view.getTop(); + int left = view.getLeft(); - BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); + Bitmap bitmap = getBitmapFromView(view); - drawable.setBounds(new Rect(left, top, left + view.getWidth(), top + view.getHeight())); + BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); - return drawable; - } + 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; - } + /** + * @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 abbf6181..a88aa7c8 100644 --- a/opentasks/src/main/java/org/dmfs/android/widgets/ColoredShapeCheckBox.java +++ b/opentasks/src/main/java/org/dmfs/android/widgets/ColoredShapeCheckBox.java @@ -24,161 +24,131 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; -import androidx.appcompat.widget.AppCompatCheckBox; import android.util.AttributeSet; - +import androidx.appcompat.widget.AppCompatCheckBox; 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. + * 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 AppCompatCheckBox -{ - /** - * The initial color in case no other color is set. This color is transparent but the check mark will be dark. - */ - private final static int DEFAULT_COLOR = 0x00ffffff; - - /** - * The shape in the background of the check mark. - */ - private GradientDrawable mBackgroundShape; - - /** - * The check mark used for dark background shapes. - */ - private Drawable mLightCheckmark; - - /** - * The check mark used for light background shapes. - */ - private Drawable mDarkCheckmark; - - /** - * A color state list that defines the background color. - */ - private ColorStateList mColorStateList; - - /** - * The current color. - */ - private int mCurrentColor; - - - public ColoredShapeCheckBox(Context context) - { - super(context); - Resources resources = context.getResources(); - mBackgroundShape = (GradientDrawable) resources.getDrawable(R.drawable.oval_shape); - mLightCheckmark = resources.getDrawable(R.drawable.org_dmfs_colorshape_checkbox_selector_dark); - mDarkCheckmark = resources.getDrawable(R.drawable.org_dmfs_colorshape_checkbox_selector_light); - setColor(DEFAULT_COLOR); - } - - - public ColoredShapeCheckBox(Context context, AttributeSet attrs) - { - super(context, attrs); - loadAttrs(attrs); - } - - - public ColoredShapeCheckBox(Context context, AttributeSet attrs, int defStyle) - { - super(context, attrs, defStyle); - loadAttrs(attrs); - } - - - private void loadAttrs(AttributeSet attrs) - { - TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ColoredShapeCheckBox); - - Resources resources = getResources(); - - Drawable backgroundShape = typedArray.getDrawable(R.styleable.ColoredShapeCheckBox_backgroundShape); - if (backgroundShape instanceof GradientDrawable) - { - mBackgroundShape = (GradientDrawable) backgroundShape; - } - else - { - mBackgroundShape = (GradientDrawable) resources.getDrawable(R.drawable.oval_shape); - } - - Drawable darkCheckmark = typedArray.getDrawable(R.styleable.ColoredShapeCheckBox_darkCheckmark); - if (darkCheckmark != null) - { - mDarkCheckmark = darkCheckmark; - } - else - { - mDarkCheckmark = resources.getDrawable(R.drawable.org_dmfs_colorshape_checkbox_selector_light); - } - - Drawable lightCheckmark = typedArray.getDrawable(R.styleable.ColoredShapeCheckBox_lightCheckmark); - if (lightCheckmark != null) - { - mLightCheckmark = lightCheckmark; - } - else - { - mLightCheckmark = resources.getDrawable(R.drawable.org_dmfs_colorshape_checkbox_selector_dark); - } - setColorStateList(typedArray.getColorStateList(R.styleable.ColoredShapeCheckBox_shapeColor)); - - typedArray.recycle(); +public class ColoredShapeCheckBox extends AppCompatCheckBox { + /** + * The initial color in case no other color is set. This color is transparent but the check mark + * will be dark. + */ + private static final int DEFAULT_COLOR = 0x00ffffff; + + /** The shape in the background of the check mark. */ + private GradientDrawable mBackgroundShape; + + /** The check mark used for dark background shapes. */ + private Drawable mLightCheckmark; + + /** The check mark used for light background shapes. */ + private Drawable mDarkCheckmark; + + /** A color state list that defines the background color. */ + private ColorStateList mColorStateList; + + /** The current color. */ + private int mCurrentColor; + + public ColoredShapeCheckBox(Context context) { + super(context); + Resources resources = context.getResources(); + mBackgroundShape = (GradientDrawable) resources.getDrawable(R.drawable.oval_shape); + mLightCheckmark = resources.getDrawable(R.drawable.org_dmfs_colorshape_checkbox_selector_dark); + mDarkCheckmark = resources.getDrawable(R.drawable.org_dmfs_colorshape_checkbox_selector_light); + setColor(DEFAULT_COLOR); + } + + public ColoredShapeCheckBox(Context context, AttributeSet attrs) { + super(context, attrs); + loadAttrs(attrs); + } + + public ColoredShapeCheckBox(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + loadAttrs(attrs); + } + + private void loadAttrs(AttributeSet attrs) { + TypedArray typedArray = + getContext().obtainStyledAttributes(attrs, R.styleable.ColoredShapeCheckBox); + + Resources resources = getResources(); + + Drawable backgroundShape = + typedArray.getDrawable(R.styleable.ColoredShapeCheckBox_backgroundShape); + if (backgroundShape instanceof GradientDrawable) { + mBackgroundShape = (GradientDrawable) backgroundShape; + } else { + mBackgroundShape = (GradientDrawable) resources.getDrawable(R.drawable.oval_shape); } - - public void setColor(int color) - { - mColorStateList = null; - applyColor(color); + Drawable darkCheckmark = typedArray.getDrawable(R.styleable.ColoredShapeCheckBox_darkCheckmark); + if (darkCheckmark != null) { + mDarkCheckmark = darkCheckmark; + } else { + mDarkCheckmark = + resources.getDrawable(R.drawable.org_dmfs_colorshape_checkbox_selector_light); } - - private void applyColor(int color) - { - mCurrentColor = color; - - // get an approximation for the lightness of the given color - int y = (3 * Color.red(color) + 4 * Color.green(color) + Color.blue(color)) >> 3; - - mBackgroundShape.setColor(color); - - LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { mBackgroundShape, y > 190 ? mDarkCheckmark : mLightCheckmark }); - setButtonDrawable(layerDrawable); + Drawable lightCheckmark = + typedArray.getDrawable(R.styleable.ColoredShapeCheckBox_lightCheckmark); + if (lightCheckmark != null) { + mLightCheckmark = lightCheckmark; + } else { + mLightCheckmark = + resources.getDrawable(R.drawable.org_dmfs_colorshape_checkbox_selector_dark); } - - - public void setColorStateList(ColorStateList colorStateList) - { - mColorStateList = colorStateList; - applyColor(colorStateList == null ? DEFAULT_COLOR : colorStateList.getColorForState(getDrawableState(), DEFAULT_COLOR)); - } - - - public void setColorStateList(int id) - { - setColorStateList(getResources().getColorStateList(id)); - } - - - @Override - protected void drawableStateChanged() - { - super.drawableStateChanged(); - if (mColorStateList != null) - { - int newColor = mColorStateList.getColorForState(getDrawableState(), mCurrentColor); - if (newColor != mCurrentColor) - { - applyColor(newColor); - } - } + setColorStateList(typedArray.getColorStateList(R.styleable.ColoredShapeCheckBox_shapeColor)); + + typedArray.recycle(); + } + + public void setColor(int color) { + mColorStateList = null; + applyColor(color); + } + + private void applyColor(int color) { + mCurrentColor = color; + + // get an approximation for the lightness of the given color + int y = (3 * Color.red(color) + 4 * Color.green(color) + Color.blue(color)) >> 3; + + mBackgroundShape.setColor(color); + + LayerDrawable layerDrawable = + new LayerDrawable( + new Drawable[] {mBackgroundShape, y > 190 ? mDarkCheckmark : mLightCheckmark}); + setButtonDrawable(layerDrawable); + } + + public void setColorStateList(ColorStateList colorStateList) { + mColorStateList = colorStateList; + applyColor( + colorStateList == null + ? DEFAULT_COLOR + : colorStateList.getColorForState(getDrawableState(), DEFAULT_COLOR)); + } + + public void setColorStateList(int id) { + setColorStateList(getResources().getColorStateList(id)); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mColorStateList != null) { + int newColor = mColorStateList.getColorForState(getDrawableState(), mCurrentColor); + if (newColor != mCurrentColor) { + applyColor(newColor); + } } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/AboutActivity.java b/opentasks/src/main/java/org/dmfs/tasks/AboutActivity.java index 11f4c8c9..b4d57242 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/AboutActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/AboutActivity.java @@ -1,46 +1,33 @@ package org.dmfs.tasks; -import android.app.ActionBar; -import android.content.Context; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.Bundle; -import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; -import android.widget.TextView; - -import org.dmfs.tasks.utils.BaseActivity; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; - - +import org.dmfs.tasks.utils.BaseActivity; public class AboutActivity extends BaseActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_about); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - getFragmentManager() - .beginTransaction() - .replace(R.id.about_container, - new AboutFragment()) - .commit(); - String title = getResources().getString(R.string.title_activity_about); - SpannableString spannableString = new SpannableString(title); - spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(getApplicationContext(), R.color.color_default_primary_text)), 0, title.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - getSupportActionBar().setTitle(spannableString); - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + getFragmentManager() + .beginTransaction() + .replace(R.id.about_container, new AboutFragment()) + .commit(); + String title = getResources().getString(R.string.title_activity_about); + SpannableString spannableString = new SpannableString(title); + spannableString.setSpan( + new ForegroundColorSpan( + ContextCompat.getColor(getApplicationContext(), R.color.color_default_primary_text)), + 0, + title.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + getSupportActionBar().setTitle(spannableString); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/AboutFragment.java b/opentasks/src/main/java/org/dmfs/tasks/AboutFragment.java index d9cc3e84..431fbb16 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/AboutFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/AboutFragment.java @@ -2,25 +2,19 @@ package org.dmfs.tasks; import android.os.Bundle; import android.preference.PreferenceFragment; - -import org.dmfs.tasks.BuildConfig; - import androidx.annotation.Nullable; - public class AboutFragment extends PreferenceFragment { - public static final String BUILD_VERSION = "build_version"; - public static final String APP_INFO = "tasks_info"; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - addPreferencesFromResource(R.xml.about_preferences); - findPreference(BUILD_VERSION).setSummary(BuildConfig.VERSION_NAME); - findPreference(APP_INFO).setSummary(getString(R.string.preferences_app_info)); + public static final String BUILD_VERSION = "build_version"; + public static final String APP_INFO = "tasks_info"; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - } + addPreferencesFromResource(R.xml.about_preferences); + findPreference(BUILD_VERSION).setSummary(BuildConfig.VERSION_NAME); + findPreference(APP_INFO).setSummary(getString(R.string.preferences_app_info)); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/AppAppearanceSettingsFragment.java b/opentasks/src/main/java/org/dmfs/tasks/AppAppearanceSettingsFragment.java index e8f30b09..191a68f8 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/AppAppearanceSettingsFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/AppAppearanceSettingsFragment.java @@ -16,46 +16,33 @@ package org.dmfs.tasks; -import android.content.SharedPreferences; +import static java.util.Arrays.asList; + import android.os.Build; import android.os.Bundle; - import androidx.appcompat.app.AppCompatDelegate; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import static java.util.Arrays.asList; - - -/** - * Fragment for the app appearance settings. - */ -public final class AppAppearanceSettingsFragment extends PreferenceFragmentCompat -{ - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) - { - setPreferencesFromResource(R.xml.appearance_preferences, rootKey); - } - - @Override - public boolean onPreferenceTreeClick(Preference preference) - { - if (asList( - getString(R.string.opentasks_pref_appearance_system_theme), - getString(R.string.opentasks_pref_appearance_dark_theme)).contains(preference.getKey())) - { - if (Build.VERSION.SDK_INT >= 29) - { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - } - else - { - getActivity().recreate(); - } - } - return super.onPreferenceTreeClick(preference); +/** Fragment for the app appearance settings. */ +public final class AppAppearanceSettingsFragment extends PreferenceFragmentCompat { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.appearance_preferences, rootKey); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (asList( + getString(R.string.opentasks_pref_appearance_system_theme), + getString(R.string.opentasks_pref_appearance_dark_theme)) + .contains(preference.getKey())) { + if (Build.VERSION.SDK_INT >= 29) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + } else { + getActivity().recreate(); + } } + return super.onPreferenceTreeClick(preference); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/AppNotificationSettingsFragment.java b/opentasks/src/main/java/org/dmfs/tasks/AppNotificationSettingsFragment.java index 22da0afe..7b7d2c50 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/AppNotificationSettingsFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/AppNotificationSettingsFragment.java @@ -17,18 +17,12 @@ package org.dmfs.tasks; import android.os.Bundle; - import androidx.preference.PreferenceFragmentCompat; - -/** - * Fragment for the app notifications settings on Android <8. - */ -public final class AppNotificationSettingsFragment extends PreferenceFragmentCompat -{ - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) - { - setPreferencesFromResource(R.xml.notification_preferences, rootKey); - } +/** Fragment for the app notifications settings on Android <8. */ +public final class AppNotificationSettingsFragment extends PreferenceFragmentCompat { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.notification_preferences, rootKey); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/AppSettingsActivity.java b/opentasks/src/main/java/org/dmfs/tasks/AppSettingsActivity.java index cbf1b49f..7cb04a75 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/AppSettingsActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/AppSettingsActivity.java @@ -23,85 +23,75 @@ import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.MenuItem; - -import org.dmfs.tasks.utils.BaseActivity; - -import java.util.Objects; - import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceManager; - +import java.util.Objects; +import org.dmfs.tasks.utils.BaseActivity; /** * Activity for the general app settings screen. * * @author Gabor Keszthelyi */ -public final class AppSettingsActivity extends BaseActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback -{ - private SharedPreferences mPrefs; - - - @Override - protected void onCreate(Bundle savedInstanceState) - { - mPrefs = PreferenceManager.getDefaultSharedPreferences(this); - - super.onCreate(savedInstanceState); - setContentView(R.layout.opentasks_activity_preferences); - if (savedInstanceState == null) - { - getSupportFragmentManager().beginTransaction() - .replace(R.id.content, new AppSettingsFragment()) - .commit(); - } - setSupportActionBar(findViewById(R.id.toolbar)); - Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back); +public final class AppSettingsActivity extends BaseActivity + implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { + private SharedPreferences mPrefs; + + @Override + protected void onCreate(Bundle savedInstanceState) { + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + + super.onCreate(savedInstanceState); + setContentView(R.layout.opentasks_activity_preferences); + if (savedInstanceState == null) { + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.content, new AppSettingsFragment()) + .commit(); } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - if (item.getItemId() == android.R.id.home) - { - finish(); - return true; - } - return super.onOptionsItemSelected(item); + setSupportActionBar(findViewById(R.id.toolbar)); + Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; } - - - @Override - public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) - { - if (Build.VERSION.SDK_INT >= 26 && "notifications".equalsIgnoreCase(pref.getKey())) - { - // open the system notification settings - Intent intent = new Intent(); - intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); - intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); - startActivity(intent); - return true; - } - getSupportFragmentManager().beginTransaction() - .replace(R.id.content, getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment())) - .addToBackStack("") - .commit(); - return true; + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { + if (Build.VERSION.SDK_INT >= 26 && "notifications".equalsIgnoreCase(pref.getKey())) { + // open the system notification settings + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); + startActivity(intent); + return true; } - - - @Override - public Resources.Theme getTheme() - { - Resources.Theme theme = super.getTheme(); - if (Build.VERSION.SDK_INT < 29) - { - theme.applyStyle(org.dmfs.android.sync.opentasks_theme.R.style.OpenTasks_Theme_Default, true); - } - return theme; + getSupportFragmentManager() + .beginTransaction() + .replace( + R.id.content, + getSupportFragmentManager() + .getFragmentFactory() + .instantiate(getClassLoader(), pref.getFragment())) + .addToBackStack("") + .commit(); + return true; + } + + @Override + public Resources.Theme getTheme() { + Resources.Theme theme = super.getTheme(); + if (Build.VERSION.SDK_INT < 29) { + theme.applyStyle(org.dmfs.android.sync.opentasks_theme.R.style.OpenTasks_Theme_Default, true); } + return theme; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/AppSettingsFragment.java b/opentasks/src/main/java/org/dmfs/tasks/AppSettingsFragment.java index 8a4ea58e..43bb27a9 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/AppSettingsFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/AppSettingsFragment.java @@ -17,18 +17,12 @@ package org.dmfs.tasks; import android.os.Bundle; - import androidx.preference.PreferenceFragmentCompat; - -/** - * Fragment for the general app settings. - */ -public final class AppSettingsFragment extends PreferenceFragmentCompat -{ - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) - { - setPreferencesFromResource(R.xml.app_preferences, rootKey); - } +/** Fragment for the general app settings. */ +public final class AppSettingsFragment extends PreferenceFragmentCompat { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.app_preferences, rootKey); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/EditTaskActivity.java b/opentasks/src/main/java/org/dmfs/tasks/EditTaskActivity.java index d3a0ab81..93693614 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/EditTaskActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/EditTaskActivity.java @@ -17,34 +17,22 @@ package org.dmfs.tasks; import android.content.Intent; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; import android.os.Bundle; - -import androidx.core.content.ContextCompat; -import androidx.core.content.res.ResourcesCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.fragment.app.Fragment; -import androidx.core.app.NavUtils; -import androidx.appcompat.app.ActionBar; - -import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; - +import androidx.appcompat.app.ActionBar; +import androidx.core.app.NavUtils; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import java.util.TimeZone; import org.dmfs.provider.tasks.AuthorityUtil; import org.dmfs.tasks.contract.TaskContract.Tasks; import org.dmfs.tasks.model.ContentSet; import org.dmfs.tasks.utils.BaseActivity; -import java.util.TimeZone; - - /** * Activity to edit a task. * @@ -52,190 +40,156 @@ import java.util.TimeZone; * @author Marten Gajda * @author Tobias Reinsch */ -public class EditTaskActivity extends BaseActivity -{ - private static final String ACTION_NOTE_TO_SELF = "com.google.android.gm.action.AUTO_SEND"; +public class EditTaskActivity extends BaseActivity { + 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 static final 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; - - - - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_task_editor); - - mAuthority = AuthorityUtil.taskAuthority(this); - - // hide up button in action bar - ActionBar actionBar = getSupportActionBar(); - actionBar.setDisplayShowHomeEnabled(false); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setHomeAsUpIndicator(R.drawable.ic_baseline_close_24); - - - Intent intent = getIntent(); - String action = intent.getAction(); - - setActivityTitle(action); - - SpannableString spannableString = new SpannableString(actionBar.getTitle()); - spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(getApplicationContext(), R.color.color_default_primary_text)), 0, actionBar.getTitle().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - actionBar.setTitle(spannableString); - - if (savedInstanceState == null) - { - Bundle arguments = new Bundle(); - - 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(); + public static final String EXTRA_DATA_CONTENT_SET = "org.dmfs.DATA"; - } + public static final String EXTRA_DATA_ACCOUNT_TYPE = "org.dmfs.ACCOUNT_TYPE"; - } + private EditTaskFragment mEditFragment; + private String mAuthority; - @Override - public void onAttachFragment(Fragment fragment) - { - super.onAttachFragment(fragment); - if (fragment instanceof EditTaskFragment) - { - mEditFragment = (EditTaskFragment) fragment; - } - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_task_editor); + mAuthority = AuthorityUtil.taskAuthority(this); - @Override - public boolean onCreateOptionsMenu(Menu menu) - { - getMenuInflater().inflate(R.menu.edit_task_activity_menu, menu); + // hide up button in action bar + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowHomeEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeAsUpIndicator(R.drawable.ic_baseline_close_24); - return true; - } + Intent intent = getIntent(); + String action = intent.getAction(); + + setActivityTitle(action); + SpannableString spannableString = new SpannableString(actionBar.getTitle()); + spannableString.setSpan( + new ForegroundColorSpan( + ContextCompat.getColor(getApplicationContext(), R.color.color_default_primary_text)), + 0, + actionBar.getTitle().length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + actionBar.setTitle(spannableString); - @Override - public void onBackPressed() - { - super.onBackPressed(); + if (savedInstanceState == null) { + Bundle arguments = new Bundle(); - if (mEditFragment != null) - { - mEditFragment.saveAndExit(); + 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)); - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - switch (item.getItemId()) - { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - default: - break; + if (intent.hasExtra(Intent.EXTRA_SUBJECT)) { + sharedContentSet.put(Tasks.DESCRIPTION, intent.getStringExtra(Intent.EXTRA_SUBJECT)); } - return super.onOptionsItemSelected(item); - } + if (intent.hasExtra(Intent.EXTRA_TEXT)) { + String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); + sharedContentSet.put(Tasks.TITLE, extraText); + } - private void setActivityTitle(String action) - { - if (Intent.ACTION_EDIT.equals(action)) - { - setTitle(R.string.activity_edit_task_title); + // 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); + } } - else - { - setTitle(R.string.activity_add_task_title); + 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; + } + } + + @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 e03c35e8..f82be6f4 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java @@ -22,27 +22,26 @@ import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.Cursor; -import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; -import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.LinearLayout; import android.widget.Spinner; -import android.widget.TextView; import android.widget.Toast; - -import org.dmfs.android.bolts.color.elementary.ValueColor; +import androidx.fragment.app.FragmentActivity; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; +import java.util.List; +import java.util.TimeZone; import org.dmfs.android.contentpal.predicates.AllOf; import org.dmfs.android.contentpal.predicates.EqArg; import org.dmfs.android.contentpal.predicates.ReferringTo; @@ -70,24 +69,10 @@ 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.utils.colors.BlendColor; -import org.dmfs.tasks.utils.colors.DarkenedForStatusBar; -import org.dmfs.tasks.utils.colors.Mixed; import org.dmfs.tasks.widget.ListenableScrollView; import org.dmfs.tasks.widget.ListenableScrollView.OnScrollListener; import org.dmfs.tasks.widget.TaskEdit; -import java.util.List; -import java.util.TimeZone; - -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.FragmentActivity; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; - - /** * Fragment to edit task details. * @@ -95,672 +80,594 @@ import androidx.loader.content.Loader; * @author Marten Gajda * @author Tobias Reinsch */ - -public class EditTaskFragment extends SupportFragment implements LoaderManager.LoaderCallbacks, OnModelLoadedListener, OnContentChangeListener, - 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"; - - public static final String KEY_NEW_TASK = "new_event"; - - /** - * 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 - { - int id = 0; - @SuppressWarnings("unused") - int list_name = 1; - int account_type = 2; - @SuppressWarnings("unused") - int account_name = 3; - @SuppressWarnings("unused") - 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, Tasks.RRULE, Tasks.RDATE) - .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, Tasks.ORIGINAL_INSTANCE_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 viewAccountColor; - 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() - { +public class EditTaskFragment extends SupportFragment + implements LoaderManager.LoaderCallbacks, + OnModelLoadedListener, + OnContentChangeListener, + 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"; + + public static final String KEY_NEW_TASK = "new_event"; + + /** Projection into the task list. */ + private static final 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 { + int id = 0; + + @SuppressWarnings("unused") + int list_name = 1; + + int account_type = 2; + + @SuppressWarnings("unused") + int account_name = 3; + + @SuppressWarnings("unused") + 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, + Tasks.RRULE, + Tasks.RDATE) + .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, + Tasks.ORIGINAL_INSTANCE_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 viewAccountColor; + 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(); + 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); + }; + + /** + * 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 onResume() { + super.onResume(); + + viewAccountColor.setBackgroundColor(mListColor); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mAuthority = AuthorityUtil.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); } - - - @Override - public void onResume() - { - super.onResume(); - - viewAccountColor.setBackgroundColor(mListColor); - - } - - - - - - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - mAuthority = AuthorityUtil.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(); + 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); + } } - } - 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) + && !TaskContract.Instances.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); + viewAccountColor = mTaskListBar.findViewById(R.id.viewAccountColor); + mTaskListAdapter = new TasksListCursorSpinnerAdapter(mAppContext); + mListSpinner.setAdapter(mTaskListAdapter); + + mListSpinner.setOnItemSelectedListener(this); + + 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); } - mAppForEdit = !Tasks.getContentUri(mAuthority).equals(mTaskUri) && !TaskContract.Instances.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); - viewAccountColor = mTaskListBar.findViewById(R.id.viewAccountColor); - mTaskListAdapter = new TasksListCursorSpinnerAdapter(mAppContext); - mListSpinner.setAdapter(mTaskListAdapter); - - mListSpinner.setOnItemSelectedListener(this); - - 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) { + // adding a new task is always done on the Tasks table + 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; + } } - else - { - if (savedInstanceState == null) - { - // create empty ContentSet if there was no ContentSet supplied - if (mValues == null) - { - // adding a new task is always done on the Tasks table - 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(); + if (mLastAccountType != null) { + Sources.loadModelAsync(mAppContext, mLastAccountType, this); } - - super.onPause(); + } 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 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); - } + @Override + public void onPause() { + // save values on rotation + if (mEditor != null) { + mEditor.updateValues(); } + super.onPause(); + } - 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()); + @Override + public void onDestroyView() { + super.onDestroyView(); + if (mEditor != null) { + // remove values, to ensure all listeners get released + mEditor.setValues(null); } - - - /** - * Update the view. This doesn't call {@link #updateView()} right away, instead it posts it. - */ - private void postUpdateView() - { - if (mContent != null) - { - mContent.post(mUpdateViewRunnable); - } + if (mContent != null) { + mContent.removeAllViews(); } - - @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(); - } - } + final Spinner listSpinner = (Spinner) mTaskListBar.findViewById(R.id.task_list_spinner); + listSpinner.setOnItemSelectedListener(null); + if (mValues != null) { + mValues.removeOnChangeListener(this, null); } + } - - @Override - public void onSaveInstanceState(Bundle outState) - { - super.onSaveInstanceState(outState); - outState.putParcelable(KEY_VALUES, mValues); + 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); - @Override - public Loader onCreateLoader(int id, Bundle bundle) - { - return new CursorLoader(mAppContext, bundle.getParcelable(LIST_LOADER_URI), TASK_LIST_PROJECTION, bundle.getString(LIST_LOADER_FILTER), null, - null); + if (mEditor != null) { + // remove values, to ensure all listeners get released + mEditor.setValues(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(); - } - } + 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()); + } - private void showNoListMessageAndFinish() - { - Toast.makeText(getContext(), R.string.task_list_selection_empty, Toast.LENGTH_LONG).show(); - FragmentActivity activity = getActivity(); - if (activity != null) - { - activity.finish(); - } + /** 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 onLoaderReset(Loader loader) - { - mTaskListAdapter.changeCursor(null); + @Override + public void onModelLoaded(Model model) { + if (model == null) { + Toast.makeText(getActivity(), "Could not load Model", Toast.LENGTH_LONG).show(); + return; } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - if (item.getItemId() == R.id.editor_action_save) - { - saveAndExit(); - return true; - } - return false; + 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 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); - } - + } + + @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, + 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; } - - 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); + 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(); } + } } + } - - @Override - public void onContentChanged(ContentSet contentSet) - { - // nothing to do + 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) { + if (item.getItemId() == R.id.editor_action_save) { + saveAndExit(); + 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); } + } + 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); - @Override - public void onItemSelected(AdapterView arg0, View arg1, int pos, long itemId) - { - Cursor c = (Cursor) arg0.getItemAtPosition(pos); + getLoaderManager().restartLoader(-2, bundle, this); + } + } - String accountType = c.getString(TASK_LIST_PROJECTION_VALUES.account_type); - mListColor = TaskFieldAdapters.LIST_COLOR.get(c); - updateColor((float) mRootView.getScrollY() / mTaskListBar.getMeasuredHeight()); + @Override + public void onContentChanged(ContentSet contentSet) { + // nothing to do + } - if (mEditor != null) - { - mEditor.updateValues(); - } + @Override + public void onItemSelected(AdapterView arg0, View arg1, int pos, long itemId) { + Cursor c = (Cursor) arg0.getItemAtPosition(pos); - 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); + String accountType = c.getString(TASK_LIST_PROJECTION_VALUES.account_type); + mListColor = TaskFieldAdapters.LIST_COLOR.get(c); + updateColor((float) mRootView.getScrollY() / mTaskListBar.getMeasuredHeight()); - if (mModel == null || !mModel.getAccountType().equals(accountType)) - { - // the model changed, load the new model - Sources.loadModelAsync(mAppContext, accountType, this); - } - else - { - postUpdateView(); - } + 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); - @SuppressLint("NewApi") - private void updateColor(float percentage) - { - if (mColorBar == null) - { - percentage = 1; - } - else - { - percentage = Math.max(0, Math.min(Float.isNaN(percentage) ? 0 : percentage, 1)); - } - int alpha = (int) ((0.5 + 0.5 * percentage) * 255); - viewAccountColor.setBackgroundColor(mListColor); + if (mModel == null || !mModel.getAccountType().equals(accountType)) { + // the model changed, load the new model + Sources.loadModelAsync(mAppContext, accountType, this); + } else { + postUpdateView(); } + } + + @SuppressLint("NewApi") + private void updateColor(float percentage) { + if (mColorBar == null) { + percentage = 1; + } else { + percentage = Math.max(0, Math.min(Float.isNaN(percentage) ? 0 : percentage, 1)); + } + int alpha = (int) ((0.5 + 0.5 * percentage) * 255); + viewAccountColor.setBackgroundColor(mListColor); + } + @Override + public void onNothingSelected(AdapterView arg0) { + // nothing to do here + } - @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 + Activity activity = getActivity(); + boolean isNewTask = mValues.isInsert(); - /** - * Persist the current task (if anything has been edited) and close the editor. - */ - public void saveAndExit() - { - // TODO: put that in a background task - Activity activity = getActivity(); + if (mEditor != null) { + mEditor.updateValues(); + } - boolean isNewTask = mValues.isInsert(); + if (mValues.isInsert() || mValues.isUpdate()) { + if (TextUtils.isEmpty(TaskFieldAdapters.TITLE.get(mValues))) { + // there is no title, try to set one from the description or check list - if (mEditor != null) - { - mEditor.updateValues(); + String description = TaskFieldAdapters.DESCRIPTION.get(mValues); + if (description != null) { + // remove spaces and empty lines + description = description.trim(); } - if (mValues.isInsert() || mValues.isUpdate()) - { - if (TextUtils.isEmpty(TaskFieldAdapters.TITLE.get(mValues))) - { - // there is no title, try to set one from the description or check list - - String description = TaskFieldAdapters.DESCRIPTION.get(mValues); - if (description != null) - { - // remove spaces and empty lines - description = description.trim(); - } - - if (!TextUtils.isEmpty(description)) - { - // we have a description, use it to make up a title - int eol = description.indexOf('\n'); - TaskFieldAdapters.TITLE.set(mValues, description.substring(0, eol > 0 ? eol : Math.min(description.length(), 100))); - } - else - { - // no description, try to find a non-empty checklist item - List checklist = TaskFieldAdapters.CHECKLIST.get(mValues); - if (checklist != null && checklist.size() > 0) - { - for (CheckListItem item : checklist) - { - String trimmedItem = item.text.trim(); - if (!TextUtils.isEmpty(trimmedItem)) - { - TaskFieldAdapters.TITLE.set(mValues, trimmedItem); - break; - } - } - } - } - } - - if (!TextUtils.isEmpty(TaskFieldAdapters.TITLE.get(mValues)) || mValues.isUpdate()) - { - if (mValues.isInsert()) - { - // update recently used lists - RecentlyUsedLists.use(getContext(), mValues.getAsLong(Tasks.LIST_ID)); - } - - mTaskUri = mValues.persist(activity); - - activity.setResult(Activity.RESULT_OK, new Intent().setData(mTaskUri).putExtra(KEY_NEW_TASK, isNewTask)); - Toast.makeText(activity, R.string.activity_edit_task_task_saved, Toast.LENGTH_SHORT).show(); - activity.finish(); - if (isNewTask) - { - // When creating a new task we're dealing with a task URI, for now we start the details view with an instance URI though - // so get the first instance of the new task and open it - new With<>( - new First<>( - new Frozen<>( - new QueryRowSet<>( - new InstancesView<>(mAuthority, activity.getContentResolver().acquireContentProviderClient(mAuthority)), - Id.PROJECTION, - new AllOf<>( - new EqArg<>(TaskContract.Instances.DISTANCE_FROM_CURRENT, 0), - new ReferringTo<>(TaskContract.Instances.TASK_ID, new RowUriReference(mTaskUri))))))) - .process( - snapShot -> - activity.startActivity( - new Intent( - Intent.ACTION_VIEW, - ContentUris.withAppendedId(TaskContract.Instances.getContentUri(mAuthority), - new Id(snapShot.values()).value())) - .putExtra(ViewTaskActivity.EXTRA_COLOR, mListColor))); - } - } - else - { - activity.setResult(Activity.RESULT_CANCELED); - Toast.makeText(activity, R.string.activity_edit_task_empty_task_not_saved, Toast.LENGTH_SHORT).show(); - activity.finish(); + if (!TextUtils.isEmpty(description)) { + // we have a description, use it to make up a title + int eol = description.indexOf('\n'); + TaskFieldAdapters.TITLE.set( + mValues, + description.substring(0, eol > 0 ? eol : Math.min(description.length(), 100))); + } else { + // no description, try to find a non-empty checklist item + List checklist = TaskFieldAdapters.CHECKLIST.get(mValues); + if (checklist != null && checklist.size() > 0) { + for (CheckListItem item : checklist) { + String trimmedItem = item.text.trim(); + if (!TextUtils.isEmpty(trimmedItem)) { + TaskFieldAdapters.TITLE.set(mValues, trimmedItem); + break; + } } + } } - else - { - Log.i(TAG, "nothing to save"); + } + + if (!TextUtils.isEmpty(TaskFieldAdapters.TITLE.get(mValues)) || mValues.isUpdate()) { + if (mValues.isInsert()) { + // update recently used lists + RecentlyUsedLists.use(getContext(), mValues.getAsLong(Tasks.LIST_ID)); } + mTaskUri = mValues.persist(activity); + + activity.setResult( + Activity.RESULT_OK, new Intent().setData(mTaskUri).putExtra(KEY_NEW_TASK, isNewTask)); + Toast.makeText(activity, R.string.activity_edit_task_task_saved, Toast.LENGTH_SHORT).show(); + activity.finish(); + if (isNewTask) { + // When creating a new task we're dealing with a task URI, for now we start the details + // view with an instance URI though + // so get the first instance of the new task and open it + new With<>( + new First<>( + new Frozen<>( + new QueryRowSet<>( + new InstancesView<>( + mAuthority, + activity + .getContentResolver() + .acquireContentProviderClient(mAuthority)), + Id.PROJECTION, + new AllOf<>( + new EqArg<>(TaskContract.Instances.DISTANCE_FROM_CURRENT, 0), + new ReferringTo<>( + TaskContract.Instances.TASK_ID, + new RowUriReference(mTaskUri))))))) + .process( + snapShot -> + activity.startActivity( + new Intent( + Intent.ACTION_VIEW, + ContentUris.withAppendedId( + TaskContract.Instances.getContentUri(mAuthority), + new Id(snapShot.values()).value())) + .putExtra(ViewTaskActivity.EXTRA_COLOR, mListColor))); + } + } else { + activity.setResult(Activity.RESULT_CANCELED); + Toast.makeText( + activity, R.string.activity_edit_task_empty_task_not_saved, Toast.LENGTH_SHORT) + .show(); + activity.finish(); + } + } else { + Log.i(TAG, "nothing to save"); } - + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/EmptyTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/EmptyTaskFragment.java index 4b93c061..bda4b526 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/EmptyTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/EmptyTaskFragment.java @@ -18,61 +18,52 @@ package org.dmfs.tasks; import android.app.Activity; import android.os.Bundle; -import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - +import androidx.fragment.app.Fragment; import org.dmfs.android.bolts.color.Color; import org.dmfs.android.bolts.color.elementary.ValueColor; import org.dmfs.android.retentionmagic.SupportFragment; - /** - * A fragment representing a single Task detail screen with empty content. It is used in {@link TaskListActivity} in - * two-pane mode (on tablets) when no task is selected. + * A fragment representing a single Task detail screen with empty content. It is used in {@link + * TaskListActivity} in two-pane mode (on tablets) when no task is selected. * * @author Gabor Keszthelyi */ -public class EmptyTaskFragment extends SupportFragment -{ - private static final String ARG_COLOR = "color"; +public class EmptyTaskFragment extends SupportFragment { + private static final String ARG_COLOR = "color"; - private Color mColor; + private Color mColor; + /** + * @param color The color that the toolbars should take. (If available provide the actual task + * list color, otherwise the primary color.) + */ + public static Fragment newInstance(Color color) { + EmptyTaskFragment fragment = new EmptyTaskFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_COLOR, color.argb()); + fragment.setArguments(args); + return fragment; + } - /** - * @param color - * The color that the toolbars should take. (If available provide the actual task list color, otherwise the primary color.) - */ - public static Fragment newInstance(Color color) - { - EmptyTaskFragment fragment = new EmptyTaskFragment(); - Bundle args = new Bundle(); - args.putInt(ARG_COLOR, color.argb()); - fragment.setArguments(args); - return fragment; - } + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mColor = new ValueColor(getArguments().getInt(ARG_COLOR)); - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - - mColor = new ValueColor(getArguments().getInt(ARG_COLOR)); - - if (activity instanceof ViewTaskFragment.Callback) - { - ((ViewTaskFragment.Callback) activity).onListColorLoaded(mColor); - } + if (activity instanceof ViewTaskFragment.Callback) { + ((ViewTaskFragment.Callback) activity).onListColorLoaded(mColor); } + } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { - View view = inflater.inflate(R.layout.opentasks_fragment_empty_task, container, false); - return view; - } + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.opentasks_fragment_empty_task, container, false); + return view; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/InputTextDialogFragment.java b/opentasks/src/main/java/org/dmfs/tasks/InputTextDialogFragment.java index 0201a174..8a964543 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/InputTextDialogFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/InputTextDialogFragment.java @@ -22,7 +22,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnKeyListener; import android.os.Bundle; -import androidx.fragment.app.Fragment; import android.view.ContextThemeWrapper; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -35,312 +34,250 @@ import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; - +import androidx.fragment.app.Fragment; 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 + * + *

    TODO: Use the style from the support library * * @author Marten Gajda * @author Tristan Heinig */ -public class InputTextDialogFragment extends SupportDialogFragment implements OnEditorActionListener, OnKeyListener -{ - - protected final static String ARG_TITLE_TEXT = "title_text"; - protected final static String ARG_INITIAL_TEXT = "initial_text"; - protected final static String ARG_HINT_TEXT = "hint_text"; - protected final static String ARG_MESSAGE_TEXT = "message_text"; - - @Parameter(key = ARG_TITLE_TEXT) - protected String mTitle; - @Parameter(key = ARG_HINT_TEXT) - protected String mHint; - @Parameter(key = ARG_MESSAGE_TEXT) - protected String mMessage; - @Parameter(key = ARG_INITIAL_TEXT) - protected String mInitialText; - - protected EditText mEditText; - protected TextView mErrorText; - - - public InputTextDialogFragment() - { - } - - - /** - * Creates a {@link InputTextDialogFragment} with the given title and initial text value. - * - * @param title - * The title of the dialog. - * @param message - * The text of the message field. - * @param hint - * The hint of the input field. - * @param initalText - * The initial text in the input field. - * - * @return A new {@link InputTextDialogFragment}. - */ - public static InputTextDialogFragment newInstance(String title, String hint, String initalText, String message) - { - InputTextDialogFragment fragment = new InputTextDialogFragment(); - Bundle args = new Bundle(); - args.putString(ARG_TITLE_TEXT, title); - args.putString(ARG_MESSAGE_TEXT, message); - args.putString(ARG_INITIAL_TEXT, initalText); - args.putString(ARG_HINT_TEXT, hint); - fragment.setArguments(args); - return fragment; +public class InputTextDialogFragment extends SupportDialogFragment + implements OnEditorActionListener, OnKeyListener { + + protected static final String ARG_TITLE_TEXT = "title_text"; + protected static final String ARG_INITIAL_TEXT = "initial_text"; + protected static final String ARG_HINT_TEXT = "hint_text"; + protected static final String ARG_MESSAGE_TEXT = "message_text"; + + @Parameter(key = ARG_TITLE_TEXT) + protected String mTitle; + + @Parameter(key = ARG_HINT_TEXT) + protected String mHint; + + @Parameter(key = ARG_MESSAGE_TEXT) + protected String mMessage; + + @Parameter(key = ARG_INITIAL_TEXT) + protected String mInitialText; + + protected EditText mEditText; + protected TextView mErrorText; + + public InputTextDialogFragment() {} + + /** + * Creates a {@link InputTextDialogFragment} with the given title and initial text value. + * + * @param title The title of the dialog. + * @param message The text of the message field. + * @param hint The hint of the input field. + * @param initalText The initial text in the input field. + * @return A new {@link InputTextDialogFragment}. + */ + public static InputTextDialogFragment newInstance( + String title, String hint, String initalText, String message) { + InputTextDialogFragment fragment = new InputTextDialogFragment(); + Bundle args = new Bundle(); + args.putString(ARG_TITLE_TEXT, title); + args.putString(ARG_MESSAGE_TEXT, message); + args.putString(ARG_INITIAL_TEXT, initalText); + args.putString(ARG_HINT_TEXT, hint); + fragment.setArguments(args); + return fragment; + } + + /** + * Creates a {@link InputTextDialogFragment} with the given title and initial text value. + * + * @param title The title of the dialog. + * @param hint The hint of the input field. + * @return A new {@link InputTextDialogFragment}. + */ + public static InputTextDialogFragment newInstance(String title, String hint, String initalText) { + return newInstance(title, hint, initalText, null); + } + + /** + * Creates a {@link InputTextDialogFragment} with the given title and initial text value. + * + * @param title The title of the dialog. + * @return A new {@link InputTextDialogFragment}. + */ + public static InputTextDialogFragment newInstance(String title, String hint) { + return newInstance(title, hint, null, null); + } + + /** + * Creates a {@link InputTextDialogFragment} with the given title and initial text value. + * + * @param title The title of the dialog. + * @return A new {@link InputTextDialogFragment}. + */ + public static InputTextDialogFragment newInstance(String title) { + return newInstance(title, null, null, null); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final Context contextThemeWrapperLight = + new ContextThemeWrapper( + getActivity(), androidx.appcompat.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); + mErrorText = (TextView) view.findViewById(R.id.error); + if (savedInstanceState == null && mInitialText != null) { + mEditText.setText(mInitialText); } - - - /** - * Creates a {@link InputTextDialogFragment} with the given title and initial text value. - * - * @param title - * The title of the dialog. - * @param hint - * The hint of the input field. - * - * @return A new {@link InputTextDialogFragment}. - */ - public static InputTextDialogFragment newInstance(String title, String hint, String initalText) - { - return newInstance(title, hint, initalText, null); + if (savedInstanceState == null && mHint != null) { + mEditText.setHint(mHint); } - - - /** - * Creates a {@link InputTextDialogFragment} with the given title and initial text value. - * - * @param title - * The title of the dialog. - * @return A new {@link InputTextDialogFragment}. - */ - public static InputTextDialogFragment newInstance(String title, String hint) - { - return newInstance(title, hint, null, null); + if (mMessage != null) { + TextView mMessageView = (TextView) view.findViewById(android.R.id.message); + mMessageView.setText(mMessage); + mMessageView.setVisibility(View.VISIBLE); } + ((TextView) view.findViewById(android.R.id.title)).setText(mTitle); + mEditText.requestFocus(); + getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE); + mEditText.setOnEditorActionListener(this); - /** - * Creates a {@link InputTextDialogFragment} with the given title and initial text value. - * - * @param title - * The title of the dialog. - * - * @return A new {@link InputTextDialogFragment}. - */ - public static InputTextDialogFragment newInstance(String title) - { - return newInstance(title, null, null, null); - } - + view.findViewById(android.R.id.button1) + .setOnClickListener( + new OnClickListener() { - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { - final Context contextThemeWrapperLight = new ContextThemeWrapper(getActivity(), androidx.appcompat.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); - mErrorText = (TextView) view.findViewById(R.id.error); - if (savedInstanceState == null && mInitialText != null) - { - mEditText.setText(mInitialText); - } - if (savedInstanceState == null && mHint != null) - { - mEditText.setHint(mHint); - } - if (mMessage != null) - { - TextView mMessageView = (TextView) view.findViewById(android.R.id.message); - mMessageView.setText(mMessage); - mMessageView.setVisibility(View.VISIBLE); - } - ((TextView) view.findViewById(android.R.id.title)).setText(mTitle); - - mEditText.requestFocus(); - getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE); - mEditText.setOnEditorActionListener(this); - - view.findViewById(android.R.id.button1).setOnClickListener(new OnClickListener() - { - - @Override - public void onClick(View v) - { + @Override + public void onClick(View v) { handleSave(); - } - }); + } + }); - view.findViewById(android.R.id.button2).setOnClickListener(new OnClickListener() - { + view.findViewById(android.R.id.button2) + .setOnClickListener( + new OnClickListener() { - @Override - public void onClick(View v) - { + @Override + public void onClick(View v) { handleCancel(); - } - }); - - return view; - } - - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) - { - Dialog dialog = super.onCreateDialog(savedInstanceState); - // hides the actual dialog title, we have one already... - dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); - // we want to listen to clicks on back button - dialog.setOnKeyListener(this); - return dialog; - } - - - /* - * When the user clicks the back button, we assume that he wants to cancel the input, so we have to handle the back click event. - */ - @Override - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) - { - // if the user clicks the back button - if ((keyCode == android.view.KeyEvent.KEYCODE_BACK)) - { - // filter only the touch down event - if (event.getAction() != KeyEvent.ACTION_DOWN) - { - // we assume that he don't want to save his input - handleCancel(); - } - else - { - return false; - } - } + } + }); + + return view; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + // hides the actual dialog title, we have one already... + dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); + // we want to listen to clicks on back button + dialog.setOnKeyListener(this); + return dialog; + } + + /* + * When the user clicks the back button, we assume that he wants to cancel the input, so we have to handle the back click event. + */ + @Override + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { + // if the user clicks the back button + if ((keyCode == android.view.KeyEvent.KEYCODE_BACK)) { + // filter only the touch down event + if (event.getAction() != KeyEvent.ACTION_DOWN) { + // we assume that he don't want to save his input + handleCancel(); + } else { return false; + } } - - - /** - * Dismisses the input dialog and calls the listener about the user abort. - */ - protected void handleCancel() - { - Fragment parentFragment = getParentFragment(); - Activity activity = getActivity(); - - if (parentFragment instanceof InputTextListener) - { - ((InputTextListener) parentFragment).onCancelInputDialog(); - } - else if (activity instanceof InputTextListener) - { - ((InputTextListener) activity).onCancelInputDialog(); - - } - InputTextDialogFragment.this.dismiss(); - } - - - @Override - public void onCancel(DialogInterface dialog) - { - super.onCancel(dialog); - handleCancel(); + return false; + } + + /** Dismisses the input dialog and calls the listener about the user abort. */ + protected void handleCancel() { + Fragment parentFragment = getParentFragment(); + Activity activity = getActivity(); + + if (parentFragment instanceof InputTextListener) { + ((InputTextListener) parentFragment).onCancelInputDialog(); + } else if (activity instanceof InputTextListener) { + ((InputTextListener) activity).onCancelInputDialog(); } - - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) - { - if (EditorInfo.IME_ACTION_DONE == actionId) - { - handleSave(); - return true; - } - return false; + InputTextDialogFragment.this.dismiss(); + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + handleCancel(); + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (EditorInfo.IME_ACTION_DONE == actionId) { + handleSave(); + return true; } - - - /** - * Dismisses the input dialog and calls the input listener and forwards the user input. - */ - protected void handleSave() - { - String input = mEditText.getText().toString().trim(); - mEditText.setText(input); - if (validate(input)) - { - Fragment parentFragment = getParentFragment(); - Activity activity = getActivity(); - - if (parentFragment instanceof InputTextListener) - { - ((InputTextListener) parentFragment).onInputTextChanged(input); - } - else if (activity instanceof InputTextListener) - { - ((InputTextListener) activity).onInputTextChanged(input); - - } - InputTextDialogFragment.this.dismiss(); - } + return false; + } + + /** Dismisses the input dialog and calls the input listener and forwards the user input. */ + protected void handleSave() { + String input = mEditText.getText().toString().trim(); + mEditText.setText(input); + if (validate(input)) { + Fragment parentFragment = getParentFragment(); + Activity activity = getActivity(); + + if (parentFragment instanceof InputTextListener) { + ((InputTextListener) parentFragment).onInputTextChanged(input); + } else if (activity instanceof InputTextListener) { + ((InputTextListener) activity).onInputTextChanged(input); + } + InputTextDialogFragment.this.dismiss(); } - - - /** - * Validates the user input and returns true if the input is valid. - * - * @param input - * the text of the {@link EditText} field. - * - * @return true, if there is user input, otherwise false. - */ - protected boolean validate(String input) - { - if (input == null || input.trim().length() < 1) - { - mErrorText.setVisibility(View.VISIBLE); - mErrorText.setText(R.string.task_list_name_dialog_error); - return false; - } - mErrorText.setVisibility(View.INVISIBLE); - return true; + } + + /** + * Validates the user input and returns true if the input is valid. + * + * @param input the text of the {@link EditText} field. + * @return true, if there is user input, otherwise false. + */ + protected boolean validate(String input) { + if (input == null || input.trim().length() < 1) { + mErrorText.setVisibility(View.VISIBLE); + mErrorText.setText(R.string.task_list_name_dialog_error); + return false; } - - + mErrorText.setVisibility(View.INVISIBLE); + return true; + } + + /** + * Interface to listen to InputTextDialog events. + * + * @author Tristan Heinig + */ + public interface InputTextListener { /** - * Interface to listen to InputTextDialog events. + * Is Called, when the user wants to save his input. * - * @author Tristan Heinig + * @param inputText the user input. */ - public interface InputTextListener - { - /** - * Is Called, when the user wants to save his input. - * - * @param inputText - * the user input. - */ - void onInputTextChanged(String inputText); - - /** - * Is Called, when the user want to cancel the input. - */ - void onCancelInputDialog(); - } + void onInputTextChanged(String inputText); + /** Is Called, when the user want to cancel the input. */ + void onCancelInputDialog(); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/JobIds.java b/opentasks/src/main/java/org/dmfs/tasks/JobIds.java index ca94dbd5..af88891b 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/JobIds.java +++ b/opentasks/src/main/java/org/dmfs/tasks/JobIds.java @@ -18,19 +18,18 @@ package org.dmfs.tasks; import androidx.core.app.JobIntentService; - /** - * Apparently there is some sort of contraint on Job IDs used with {@link JobIntentService}s or Jobs in general. - *

    - * To avoid ID collisions this interface hosts all Job IDs we're using. + * Apparently there is some sort of contraint on Job IDs used with {@link JobIntentService}s or Jobs + * in general. + * + *

    To avoid ID collisions this interface hosts all Job IDs we're using. * * @author Marten Gajda */ -public interface JobIds -{ - // base number ("task" in hex) - int BASE = 0x7461736b; +public interface JobIds { + // base number ("task" in hex) + int BASE = 0x7461736b; - int NOTIFICATION_SERVICE = BASE + 1; - int NOTIFICATION_ACTION_SERVICE = BASE + 2; + int NOTIFICATION_SERVICE = BASE + 1; + int NOTIFICATION_ACTION_SERVICE = BASE + 2; } diff --git a/opentasks/src/main/java/org/dmfs/tasks/ManageListActivity.java b/opentasks/src/main/java/org/dmfs/tasks/ManageListActivity.java index 1396a474..c171f810 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ManageListActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ManageListActivity.java @@ -25,13 +25,12 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import androidx.appcompat.app.AlertDialog; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager.LayoutParams; import android.widget.TextView; import android.widget.Toast; - +import androidx.appcompat.app.AlertDialog; import org.dmfs.android.colorpicker.ColorPickerDialogFragment; import org.dmfs.android.colorpicker.palettes.ArrayPalette; import org.dmfs.android.colorpicker.palettes.ColorFactory; @@ -47,345 +46,367 @@ import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.contract.TaskContract.TaskLists; import org.dmfs.tasks.utils.BaseActivity; - /** - * Activity to create and edit local task lists. This activity provides an interface to edit the name and the color of a local list. + * 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 BaseActivity implements OnClickListener, InputTextListener, android.content.DialogInterface.OnClickListener, ColorPickerDialogFragment.ColorDialogResultListener -{ - /** - * Account, that is assigned to the task list. - */ - public static final String EXTRA_ACCOUNT = "dmfs_extra_account"; - /** - * Intent filter category to handle local lists only. - */ - public static final String CATEGORY_LOCAL = "org.dmfs.intent.category.LOCAL"; - private static final int NO_COLOR = -1; - - private static final int[] MATERIAL_COLORS_PRIMARY = new int[] { - -1499549, -769226, -43230, -26624, -16121, -5317, -3285959, -7617718, -11751600, -16738680, -16728876, -16537100, -14575885, -12627531, -10011977, - -6543440 }; - private static final int[] MATERIAL_COLORS_DARK = new int[] { - -5434281, -3790808, -2604267, -1086464, -28928, -415707, -6382300, -11171025, -13730510, -16750244, -16743537, -16615491, -15374912, -14142061, - -12245088, -9823334 }; - private static final Palette[] PALETTES = new Palette[] { - new ArrayPalette("material_primary", "Material Colors", MATERIAL_COLORS_PRIMARY), - new ArrayPalette("material_secondary", "Dark Material Colors", MATERIAL_COLORS_DARK), - new FactoryPalette("red", "Red", new CombinedColorFactory(new ColorShadeFactory(340.0F), ColorFactory.RED), 16), - new FactoryPalette("orange", "Orange", new CombinedColorFactory(new ColorShadeFactory(18.0F), ColorFactory.ORANGE), 16), - new FactoryPalette("yellow", "Yellow", new CombinedColorFactory(new ColorShadeFactory(53.0F), ColorFactory.YELLOW), 16), - new FactoryPalette("green", "Green", new CombinedColorFactory(new ColorShadeFactory(80.0F), ColorFactory.GREEN), 16), - new FactoryPalette("cyan", "Cyan", new CombinedColorFactory(new ColorShadeFactory(150.0F), ColorFactory.CYAN), 16), - new FactoryPalette("blue", "Blue", new CombinedColorFactory(new ColorShadeFactory(210.0F), ColorFactory.BLUE), 16), - new FactoryPalette("purple", "Purple", new CombinedColorFactory(new ColorShadeFactory(265.0F), ColorFactory.PURPLE), 16), - new FactoryPalette("pink", "Pink", new CombinedColorFactory(new ColorShadeFactory(300.0F), ColorFactory.PINK), 16), - new FactoryPalette("grey", "Grey", ColorFactory.GREY, 16), new FactoryPalette("pastel", "Pastel", ColorFactory.PASTEL, 16), - new FactoryPalette("rainbow", "Rainbow", ColorFactory.RAINBOW, 16), - new FactoryPalette("dark_rainbow", "Dark Rainbow", new RainbowColorFactory(0.5F, 0.5F), 16) }; - - @Retain - private int mListColor = NO_COLOR; - private boolean mStateInsert; - private String mAction; - @Retain - private String mListName; - private Uri mTaskListUri; - private Account mAccount; - private TextView mNameView; - private View mColorView; - - @Retain( - classNS = "ManageList", - key = "palette", - permanent = true - ) - private String mPaletteId = null; - - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - Intent intent = getIntent(); - mTaskListUri = intent.getData(); - mAction = intent.getAction(); - mAccount = intent.getParcelableExtra(EXTRA_ACCOUNT); - if (mTaskListUri == null || mAction == null || mAccount == null) - { - setResult(Activity.RESULT_CANCELED); - finish(); - return; - } - setContentView(R.layout.activity_manage_task_list); - - LayoutParams params = getWindow().getAttributes(); - params.height = LayoutParams.WRAP_CONTENT; - params.width = LayoutParams.WRAP_CONTENT; - getWindow().setAttributes(params); - - findViewById(R.id.color_setting).setOnClickListener(this); - findViewById(R.id.name_setting).setOnClickListener(this); - mNameView = (TextView) findViewById(R.id.list_name); - mColorView = findViewById(R.id.list_color); - - if (Intent.ACTION_EDIT.equals(mAction)) - { - initEditing(savedInstanceState); - return; - } - - if (Intent.ACTION_INSERT.equals(mAction)) - { - initInsert(savedInstanceState); - } - +public class ManageListActivity extends BaseActivity + implements OnClickListener, + InputTextListener, + android.content.DialogInterface.OnClickListener, + ColorPickerDialogFragment.ColorDialogResultListener { + /** Account, that is assigned to the task list. */ + public static final String EXTRA_ACCOUNT = "dmfs_extra_account"; + + /** Intent filter category to handle local lists only. */ + public static final String CATEGORY_LOCAL = "org.dmfs.intent.category.LOCAL"; + + private static final int NO_COLOR = -1; + + private static final int[] MATERIAL_COLORS_PRIMARY = + new int[] { + -1499549, -769226, -43230, -26624, -16121, -5317, -3285959, -7617718, -11751600, -16738680, + -16728876, -16537100, -14575885, -12627531, -10011977, -6543440 + }; + private static final int[] MATERIAL_COLORS_DARK = + new int[] { + -5434281, -3790808, -2604267, -1086464, -28928, -415707, -6382300, -11171025, -13730510, + -16750244, -16743537, -16615491, -15374912, -14142061, -12245088, -9823334 + }; + private static final Palette[] PALETTES = + new Palette[] { + new ArrayPalette("material_primary", "Material Colors", MATERIAL_COLORS_PRIMARY), + new ArrayPalette("material_secondary", "Dark Material Colors", MATERIAL_COLORS_DARK), + new FactoryPalette( + "red", + "Red", + new CombinedColorFactory(new ColorShadeFactory(340.0F), ColorFactory.RED), + 16), + new FactoryPalette( + "orange", + "Orange", + new CombinedColorFactory(new ColorShadeFactory(18.0F), ColorFactory.ORANGE), + 16), + new FactoryPalette( + "yellow", + "Yellow", + new CombinedColorFactory(new ColorShadeFactory(53.0F), ColorFactory.YELLOW), + 16), + new FactoryPalette( + "green", + "Green", + new CombinedColorFactory(new ColorShadeFactory(80.0F), ColorFactory.GREEN), + 16), + new FactoryPalette( + "cyan", + "Cyan", + new CombinedColorFactory(new ColorShadeFactory(150.0F), ColorFactory.CYAN), + 16), + new FactoryPalette( + "blue", + "Blue", + new CombinedColorFactory(new ColorShadeFactory(210.0F), ColorFactory.BLUE), + 16), + new FactoryPalette( + "purple", + "Purple", + new CombinedColorFactory(new ColorShadeFactory(265.0F), ColorFactory.PURPLE), + 16), + new FactoryPalette( + "pink", + "Pink", + new CombinedColorFactory(new ColorShadeFactory(300.0F), ColorFactory.PINK), + 16), + new FactoryPalette("grey", "Grey", ColorFactory.GREY, 16), + new FactoryPalette("pastel", "Pastel", ColorFactory.PASTEL, 16), + new FactoryPalette("rainbow", "Rainbow", ColorFactory.RAINBOW, 16), + new FactoryPalette("dark_rainbow", "Dark Rainbow", new RainbowColorFactory(0.5F, 0.5F), 16) + }; + + @Retain private int mListColor = NO_COLOR; + private boolean mStateInsert; + private String mAction; + @Retain private String mListName; + private Uri mTaskListUri; + private Account mAccount; + private TextView mNameView; + private View mColorView; + + @Retain(classNS = "ManageList", key = "palette", permanent = true) + private String mPaletteId = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + mTaskListUri = intent.getData(); + mAction = intent.getAction(); + mAccount = intent.getParcelableExtra(EXTRA_ACCOUNT); + if (mTaskListUri == null || mAction == null || mAccount == null) { + setResult(Activity.RESULT_CANCELED); + finish(); + return; } + setContentView(R.layout.activity_manage_task_list); + LayoutParams params = getWindow().getAttributes(); + params.height = LayoutParams.WRAP_CONTENT; + params.width = LayoutParams.WRAP_CONTENT; + getWindow().setAttributes(params); - /** - * Initializes the user interface for editing tasks. - * - * @param savedInstanceState - * saved activity state from {@link #onCreate(Bundle)} - */ - private void initEditing(Bundle savedInstanceState) - { - mStateInsert = false; - findViewById(android.R.id.button2).setOnClickListener(this); - findViewById(android.R.id.button3).setOnClickListener(this); - setTitle(R.string.activity_edit_list_title); + findViewById(R.id.color_setting).setOnClickListener(this); + findViewById(R.id.name_setting).setOnClickListener(this); + mNameView = (TextView) findViewById(R.id.list_name); + mColorView = findViewById(R.id.list_color); - 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); - if (cursor == null || cursor.getCount() < 1) - { - setResult(Activity.RESULT_CANCELED); - finish(); - return; - } - cursor.moveToNext(); - if (mListName == null) - { - mListName = cursor.getString(cursor.getColumnIndex(TaskContract.TaskLists.LIST_NAME)); - } - if (mListColor == NO_COLOR) - { - mListColor = cursor.getInt(cursor.getColumnIndex(TaskContract.TaskLists.LIST_COLOR)); - } - } - mNameView.setText(mListName); - mColorView.setBackgroundColor(mListColor); + if (Intent.ACTION_EDIT.equals(mAction)) { + initEditing(savedInstanceState); + return; } - - /** - * Initializes the user interface for creating tasks. - * - * @param savedInstanceState - * saved activity state from {@link #onCreate(Bundle)} - */ - private void initInsert(Bundle savedInstanceState) - { - mStateInsert = true; - findViewById(android.R.id.button2).setVisibility(View.GONE); - findViewById(android.R.id.button3).setOnClickListener(this); - setTitle(R.string.activity_add_list_title); - - 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)); - dialog.show(getSupportFragmentManager(), null); - } - if (mListColor == NO_COLOR) - { - RandomPalette palette = new RandomPalette("generate list color", "random colors", 1); - mListColor = palette.colorAt(0); - } - mNameView.setText(mListName); - mColorView.setBackgroundColor(mListColor); - + if (Intent.ACTION_INSERT.equals(mAction)) { + initInsert(savedInstanceState); } - - - @Override - public void onClick(View v) - { - // click on save - if (android.R.id.button3 == v.getId()) - { - if (Intent.ACTION_INSERT.equals(mAction)) - { - createList(); - } - else if (Intent.ACTION_EDIT.equals(mAction)) - { - updateList(); - } - } - // click on delete - else if (android.R.id.button2 == v.getId()) - { - final AlertDialog dialog = new AlertDialog.Builder(this, R.style.customAlertDialog) - .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(); - // changes the color of the delete list button to red - dialog.setOnShowListener(new OnShowListener() - { - @Override - public void onShow(DialogInterface arg0) - { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(getResources().getColor(R.color.holo_red_light)); - } - }); - dialog.show(); - } - else if (R.id.color_setting == v.getId()) - { - ColorPickerDialogFragment d = new ColorPickerDialogFragment(); - d.setPalettes(PALETTES); - d.setTitle(org.dmfs.android.colorpicker.R.string.org_dmfs_colorpicker_pick_a_color); - d.selectPaletteId(this.mPaletteId); - d.show(this.getSupportFragmentManager(), ""); - } - else 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()); - dialog.show(getSupportFragmentManager(), null); - } + } + + /** + * Initializes the user interface for editing tasks. + * + * @param savedInstanceState saved activity state from {@link #onCreate(Bundle)} + */ + private void initEditing(Bundle savedInstanceState) { + mStateInsert = false; + findViewById(android.R.id.button2).setOnClickListener(this); + findViewById(android.R.id.button3).setOnClickListener(this); + setTitle(R.string.activity_edit_list_title); + + 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); + if (cursor == null || cursor.getCount() < 1) { + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } + cursor.moveToNext(); + if (mListName == null) { + mListName = cursor.getString(cursor.getColumnIndex(TaskContract.TaskLists.LIST_NAME)); + } + if (mListColor == NO_COLOR) { + mListColor = cursor.getInt(cursor.getColumnIndex(TaskContract.TaskLists.LIST_COLOR)); + } } - - - /* - * Belongs to the delete task confirm dialog. It is fired when the user clicks on the delete (positive) or cancel (negative) button. - */ - @Override - public void onClick(DialogInterface dialog, int which) - { - if (DialogInterface.BUTTON_POSITIVE == which) - { - deleteList(); - } + mNameView.setText(mListName); + mColorView.setBackgroundColor(mListColor); + } + + /** + * Initializes the user interface for creating tasks. + * + * @param savedInstanceState saved activity state from {@link #onCreate(Bundle)} + */ + private void initInsert(Bundle savedInstanceState) { + mStateInsert = true; + findViewById(android.R.id.button2).setVisibility(View.GONE); + findViewById(android.R.id.button3).setOnClickListener(this); + setTitle(R.string.activity_add_list_title); + + 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)); + dialog.show(getSupportFragmentManager(), null); } - - - /** - * Creates a list out of the given user input and insert it to the database. After that the activity will be closed with RESULT_OK. - */ - private void createList() - { - ContentValues values = new ContentValues(); - values.put(TaskLists.LIST_NAME, mNameView.getText().toString()); - values.put(TaskLists.LIST_COLOR, mListColor | 0xff000000); - values.put(TaskLists.VISIBLE, 1); - 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); - setResult(Activity.RESULT_OK); - finish(); + if (mListColor == NO_COLOR) { + RandomPalette palette = new RandomPalette("generate list color", "random colors", 1); + mListColor = palette.colorAt(0); } - - - /** - * Takes the given user input and update a particular task list of the database. After that the activity will be closed with RESULT_OK, if the update was - * successful, otherwise with RESULT_CANCELED. - */ - private void updateList() - { - ContentValues values = new ContentValues(); - values.put(TaskLists.LIST_NAME, mNameView.getText().toString()); - values.put(TaskLists.LIST_COLOR, mListColor | 0xff000000); - values.put(TaskLists.VISIBLE, 1); - 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); - if (count > 0) - { - setResult(Activity.RESULT_OK); - finish(); - return; - } - setResult(Activity.RESULT_CANCELED); - finish(); + mNameView.setText(mListName); + mColorView.setBackgroundColor(mListColor); + } + + @Override + public void onClick(View v) { + // click on save + if (android.R.id.button3 == v.getId()) { + if (Intent.ACTION_INSERT.equals(mAction)) { + createList(); + } else if (Intent.ACTION_EDIT.equals(mAction)) { + updateList(); + } } - - - /** - * Removes a particular task list from the database. After that the activity will be closed with RESULT_OK, if the remove was successful, otherwise with - * RESULT_CANCELED. - */ - 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(), + // click on delete + else if (android.R.id.button2 == v.getId()) { + final AlertDialog dialog = + new AlertDialog.Builder(this, R.style.customAlertDialog) + .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(); + // changes the color of the delete list button to red + dialog.setOnShowListener( + new OnShowListener() { + @Override + public void onShow(DialogInterface arg0) { + dialog + .getButton(AlertDialog.BUTTON_POSITIVE) + .setTextColor(getResources().getColor(R.color.holo_red_light)); + } + }); + dialog.show(); + } else if (R.id.color_setting == v.getId()) { + ColorPickerDialogFragment d = new ColorPickerDialogFragment(); + d.setPalettes(PALETTES); + d.setTitle(org.dmfs.android.colorpicker.R.string.org_dmfs_colorpicker_pick_a_color); + d.selectPaletteId(this.mPaletteId); + d.show(this.getSupportFragmentManager(), ""); + } else 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()); + dialog.show(getSupportFragmentManager(), null); + } + } + + /* + * Belongs to the delete task confirm dialog. It is fired when the user clicks on the delete (positive) or cancel (negative) button. + */ + @Override + public void onClick(DialogInterface dialog, int which) { + if (DialogInterface.BUTTON_POSITIVE == which) { + deleteList(); + } + } + + /** + * Creates a list out of the given user input and insert it to the database. After that the + * activity will be closed with RESULT_OK. + */ + private void createList() { + ContentValues values = new ContentValues(); + values.put(TaskLists.LIST_NAME, mNameView.getText().toString()); + values.put(TaskLists.LIST_COLOR, mListColor | 0xff000000); + values.put(TaskLists.VISIBLE, 1); + 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); + setResult(Activity.RESULT_OK); + finish(); + } + + /** + * Takes the given user input and update a particular task list of the database. After that the + * activity will be closed with RESULT_OK, if the update was successful, otherwise with + * RESULT_CANCELED. + */ + private void updateList() { + ContentValues values = new ContentValues(); + values.put(TaskLists.LIST_NAME, mNameView.getText().toString()); + values.put(TaskLists.LIST_COLOR, mListColor | 0xff000000); + values.put(TaskLists.VISIBLE, 1); + 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); - if (count > 0) - { - setResult(Activity.RESULT_OK); - Toast.makeText(this, getString(R.string.task_list_delete_toast, mListName), Toast.LENGTH_LONG).show(); - finish(); - return; - } - setResult(Activity.RESULT_CANCELED); - finish(); + if (count > 0) { + setResult(Activity.RESULT_OK); + finish(); + return; } - - - @Override - public void onInputTextChanged(String inputText) - { - mStateInsert = false; - mNameView.setText(inputText); - mListName = inputText; - } - - - /* - * If the activity is started to create a new task, the user see the InputDialog for editing task list names first. If he cancels the dialog, we finish the - * activity also. - */ - @Override - public void onCancelInputDialog() - { - if (mStateInsert == true) - { - setResult(Activity.RESULT_CANCELED); - finish(); - } + setResult(Activity.RESULT_CANCELED); + finish(); + } + + /** + * Removes a particular task list from the database. After that the activity will be closed with + * RESULT_OK, if the remove was successful, otherwise with RESULT_CANCELED. + */ + 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); + if (count > 0) { + setResult(Activity.RESULT_OK); + Toast.makeText(this, getString(R.string.task_list_delete_toast, mListName), Toast.LENGTH_LONG) + .show(); + finish(); + return; } - - - @Override - public void onColorChanged(int i, String paletteId, String s1, String s2) - { - this.mPaletteId = paletteId; - mListColor = i; - mColorView.setBackgroundColor(mListColor); + setResult(Activity.RESULT_CANCELED); + finish(); + } + + @Override + public void onInputTextChanged(String inputText) { + mStateInsert = false; + mNameView.setText(inputText); + mListName = inputText; + } + + /* + * If the activity is started to create a new task, the user see the InputDialog for editing task list names first. If he cancels the dialog, we finish the + * activity also. + */ + @Override + public void onCancelInputDialog() { + if (mStateInsert == true) { + setResult(Activity.RESULT_CANCELED); + finish(); } + } + @Override + public void onColorChanged(int i, String paletteId, String s1, String s2) { + this.mPaletteId = paletteId; + mListColor = i; + mColorView.setBackgroundColor(mListColor); + } - @Override - public void onColorDialogCancelled() - { - - } + @Override + public void onColorDialogCancelled() {} } diff --git a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java index 2e751942..65f955be 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java @@ -41,7 +41,10 @@ import android.widget.EditText; import android.widget.Spinner; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; - +import androidx.core.content.ContextCompat; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; import org.dmfs.android.retentionmagic.SupportDialogFragment; import org.dmfs.android.retentionmagic.annotations.Parameter; import org.dmfs.android.retentionmagic.annotations.Retain; @@ -55,450 +58,380 @@ import org.dmfs.tasks.utils.RecentlyUsedLists; import org.dmfs.tasks.utils.SafeFragmentUiRunnable; import org.dmfs.tasks.utils.TasksListCursorSpinnerAdapter; -import java.util.Objects; - -import androidx.core.content.ContextCompat; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; - - /** - * 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 - * title only, but there is an option to fire up the full editor. + * 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 title only, but there is an option to + * fire up the full editor. * * @author Marten Gajda */ public class QuickAddDialogFragment extends SupportDialogFragment - 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 - { - int id = 0; - @SuppressWarnings("unused") - int list_name = 1; - @SuppressWarnings("unused") - int account_type = 2; - @SuppressWarnings("unused") - int account_name = 3; - @SuppressWarnings("unused") - 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 viewAccountColor; - private Spinner mListSpinner; - - private EditText mEditText; - private View mConfirmation; - private View mContent; - - private TextView mSaveButton; - private View mSaveAndNextButton; - - private TasksListCursorSpinnerAdapter mTaskListAdapter; - - private String mAuthority; - - - /** - * Create a {@link QuickAddDialogFragment} with the given title and initial text value. - * - * @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; + implements OnEditorActionListener, + LoaderManager.LoaderCallbacks, + OnItemSelectedListener, + OnClickListener, + TextWatcher { + + /** The minimal duration for the "Task completed" info to be visible */ + private static final int COMPLETION_DELAY_BASE = 500; // ms + + /** The maximum time to add for the first time the "Task completed" info is shown. */ + private static final int COMPLETION_DELAY_MAX = 1500; // ms + + private static final String ARG_LIST_ID = "list_id"; + private static final 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 static final 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 { + int id = 0; + + @SuppressWarnings("unused") + int list_name = 1; + + @SuppressWarnings("unused") + int account_type = 2; + + @SuppressWarnings("unused") + int account_name = 3; + + @SuppressWarnings("unused") + 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 viewAccountColor; + private Spinner mListSpinner; + + private EditText mEditText; + private View mConfirmation; + private View mContent; + + private TextView mSaveButton; + private View mSaveAndNextButton; + + private TasksListCursorSpinnerAdapter mTaskListAdapter; + + private String mAuthority; + + /** + * Create a {@link QuickAddDialogFragment} with the given title and initial text value. + * + * @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. + * + * @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) { + final Context contextThemeWrapperDark = + new ContextThemeWrapper(getActivity(), androidx.appcompat.R.style.Base_Theme_AppCompat); + + View view = inflater.inflate(R.layout.fragment_quick_add_dialog, container); + + ViewGroup headerContainer = (ViewGroup) view.findViewById(R.id.header_container); + LayoutInflater localInflater = inflater.cloneInContext(contextThemeWrapperDark); + localInflater.inflate(R.layout.fragment_quick_add_dialog_header, headerContainer); + + if (savedInstanceState == null) { + if (mListId >= 0) { + mSelectedListId = mListId; + } } - - /** - * Create a {@link QuickAddDialogFragment} with the given title and initial text value. - * - * @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) - { - final Context contextThemeWrapperDark = new ContextThemeWrapper(getActivity(), androidx.appcompat.R.style.Base_Theme_AppCompat); - - View view = inflater.inflate(R.layout.fragment_quick_add_dialog, container); - - ViewGroup headerContainer = (ViewGroup) view.findViewById(R.id.header_container); - LayoutInflater 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); + viewAccountColor = view.findViewById(R.id.viewAccountColor); + 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 = (TextView) view.findViewById(android.R.id.button1); + mSaveButton.setTextColor(ContextCompat.getColor(requireContext(), R.color.accent)); + mSaveButton.setOnClickListener(this); + mSaveAndNextButton = view.findViewById(android.R.id.button2); + mSaveAndNextButton.setOnClickListener(this); + view.findViewById(android.R.id.edit).setOnClickListener(this); + + mAuthority = AuthorityUtil.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(), + 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; } - - // mColorBackground = view.findViewById(R.id.color_background); - //mColorBackground.setBackgroundColor(mLastColor); - viewAccountColor=view.findViewById(R.id.viewAccountColor); - 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 = (TextView)view.findViewById(android.R.id.button1); - mSaveButton.setTextColor(ContextCompat.getColor(requireContext(), R.color.accent)); - mSaveButton.setOnClickListener(this); - mSaveAndNextButton = view.findViewById(android.R.id.button2); - mSaveAndNextButton.setOnClickListener(this); - view.findViewById(android.R.id.edit).setOnClickListener(this); - - mAuthority = AuthorityUtil.taskAuthority(getActivity()); - - afterTextChanged(mEditText.getEditableText()); - - setListUri(TaskLists.getContentUri(mAuthority), LIST_LOADER_VISIBLE_LISTS_FILTER); - - return view; + cursor.moveToNext(); + } } - - - @Override - public Loader onCreateLoader(int id, Bundle bundle) - { - return new CursorLoader(getActivity(), bundle.getParcelable(LIST_LOADER_URI), TASK_LIST_PROJECTION, bundle.getString(LIST_LOADER_FILTER), null, - null); + } + + @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); - @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(); - } - } + getLoaderManager().restartLoader(-2, bundle, this); } - - - @Override - public void onLoaderReset(Loader loader) - { - mTaskListAdapter.changeCursor(null); + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + Cursor c = (Cursor) parent.getItemAtPosition(position); + mLastColor = TaskFieldAdapters.LIST_COLOR.get(c); + viewAccountColor.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 { + // add a new task on the tasks table + task = new ContentSet(Tasks.getContentUri(mAuthority)); } - - - @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); - viewAccountColor.setBackgroundColor(mLastColor); - - mSelectedListId = id; - - - } - - - @Override - public void onNothingSelected(AdapterView parent) - { + task.put(Tasks.LIST_ID, mListSpinner.getSelectedItemId()); + TaskFieldAdapters.TITLE.set(task, mEditText.getText().toString()); + return task; + } + + @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(); } - - - /** - * 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); + } + + @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); + } + + private void notifyUser(boolean close) { + mContent.animate().alpha(0).setDuration(250).start(); + mConfirmation.setAlpha(0); + mConfirmation.setVisibility(View.VISIBLE); + mConfirmation.animate().alpha(1).setDuration(250).start(); + + 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); } - - - /** - * 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 void delayedDismiss() { + mContent.postDelayed(mDismiss, 1000); + mClosing = true; + } + + @Override + public void onPause() { + super.onPause(); + if (mClosing) { + mContent.removeCallbacks(mDismiss); + dismiss(); } - - - private ContentSet buildContentSet() - { - ContentSet task; - if (mInitialContent != null) - { - task = new ContentSet(mInitialContent); - } - else - { - // add a new task on the tasks table - task = new ContentSet(Tasks.getContentUri(mAuthority)); - } - task.put(Tasks.LIST_ID, mListSpinner.getSelectedItemId()); - TaskFieldAdapters.TITLE.set(task, mEditText.getText().toString()); - return task; - } - - - @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); - } - - - private void notifyUser(boolean close) - { - mContent.animate().alpha(0).setDuration(250).start(); - mConfirmation.setAlpha(0); - mConfirmation.setVisibility(View.VISIBLE); - mConfirmation.animate().alpha(1).setDuration(250).start(); - - 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 {@link Runnable} that closes the dialog. - */ - private final Runnable mDismiss = new SafeFragmentUiRunnable(this, this::dismiss); - - /** - * A {@link Runnable} that resets the editor view. - */ - private final Runnable mReset = new SafeFragmentUiRunnable(this, new Runnable() - { - @Override - public void run() - { - mContent.animate().alpha(1).setDuration(250).start(); - mConfirmation.animate().alpha(0).setDuration(250).start(); - 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); - } - }); + } + + /** A {@link Runnable} that closes the dialog. */ + private final Runnable mDismiss = new SafeFragmentUiRunnable(this, this::dismiss); + + /** A {@link Runnable} that resets the editor view. */ + private final Runnable mReset = + new SafeFragmentUiRunnable( + this, + new Runnable() { + @Override + public void run() { + mContent.animate().alpha(1).setDuration(250).start(); + mConfirmation.animate().alpha(0).setDuration(250).start(); + 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 31ea5cc1..1ccffae3 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java @@ -25,7 +25,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.OperationApplicationException; import android.database.Cursor; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.RemoteException; import android.view.LayoutInflater; @@ -40,559 +39,468 @@ import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.TextView; import android.widget.Toast; - -import org.dmfs.android.bolts.color.colors.AttributeColor; -import org.dmfs.android.widgets.ColoredShapeCheckBox; -import org.dmfs.provider.tasks.AuthorityUtil; -import org.dmfs.tasks.contract.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; - import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; -import androidx.core.graphics.drawable.DrawableCompat; import androidx.cursoradapter.widget.CursorAdapter; import androidx.fragment.app.ListFragment; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; - +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.AuthorityUtil; +import org.dmfs.tasks.contract.TaskContract; +import org.dmfs.tasks.model.Model; +import org.dmfs.tasks.model.Sources; /** - * 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}. - *

    + * 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 -{ - 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() - { - +public class SettingsListFragment extends ListFragment + implements AbsListView.OnItemClickListener, + LoaderManager.LoaderCallbacks, + 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)); } - - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); + 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(), R.style.customAlertDialog) + .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; } - - /** - * 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; + 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(); + } } - - - @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); + } + + /* + * Adds an action to the ActionBar to create local lists. + */ + @Override + public void onCreateOptionsMenu(@NonNull 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 = AuthorityUtil.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 void onResume() - { - super.onResume(); - // create a new dialog, that shows accounts for inserting task lists - mChooseAccountToAddListDialog = new AlertDialog.Builder(getActivity(), R.style.customAlertDialog) - .setTitle(R.string.task_list_settings_dialog_account_title) - .setAdapter(mAccountAdapter, this).create(); + 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); + } - /* - * 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(); - } + 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); + } } - - /* - * Adds an action to the ActionBar to create local lists. - */ @Override - public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) - { - inflater.inflate(R.menu.list_settings_menu, menu); + 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; } - - /* - * 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); + public class CheckableItem { + TextView text1; + TextView text2; + View btnSettings; + ColoredShapeCheckBox coloredCheckBox; } - - /** - * 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(); - } + 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; + } } - - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - mSources = Sources.getInstance(activity); - mContext = activity.getBaseContext(); - mAuthority = AuthorityUtil.taskAuthority(activity); + public void clearHashMap() { + savedPositions.clear(); } - - @Override - public void onPause() - { - super.onPause(); - if (mSaveOnPause) - { - saveListState(); - doneSaveListState(); - } + public HashMap getState() { + return savedPositions; } - @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); + public void onClick(View v) { + Integer position = (Integer) v.getTag(); + Cursor cursor = (Cursor) getItem(position); + if (cursor != null) { + onEditListClick( + new Account(cursor.getString(accountNameColumn), cursor.getString(accountTypeColumn)), + getItemId(position), + cursor.getString(listNameColumn), + cursor.getInt(listColorColumn)); + } } - - - @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"); + } + + /** + * 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; } - - @Override - public void onLoadFinished(Loader arg0, Cursor cursor) - { - mAdapter.swapCursor(cursor); + 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(); } - - - @Override - public void onLoaderReset(Loader arg0) - { - mAdapter.changeCursor(null); - + } + + /** + * 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); } - - /** - * 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) - { - Integer position = (Integer) v.getTag(); - Cursor cursor = (Cursor) getItem(position); - if (cursor != null) - { - onEditListClick(new Account(cursor.getString(accountNameColumn), cursor.getString(accountTypeColumn)), getItemId(position), - cursor.getString(listNameColumn), cursor.getInt(listColorColumn)); - } - } - + try { + mContext.getContentResolver().applyBatch(mAuthority, ops); + } catch (RemoteException e) { + e.printStackTrace(); + return false; + } catch (OperationApplicationException e) { + e.printStackTrace(); + return false; } - - - /** - * 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(); + return true; + } + + public void doneSaveListState() { + ((VisibleListAdapter) getListAdapter()).clearHashMap(); + } + + /** + * This class is used to display a list of accounts. 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(); } + } } - - /** - * 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; + @Override + public int getCount() { + return mAccountList.size(); } - - public void doneSaveListState() - { - ((VisibleListAdapter) getListAdapter()).clearHashMap(); + @Override + public Account getItem(int position) { + return mAccountList.get(position); } - - /** - * This class is used to display a list of accounts. 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; - } - + @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/StaleListBroadcastReceiver.java b/opentasks/src/main/java/org/dmfs/tasks/StaleListBroadcastReceiver.java index 5b728fec..fca87225 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/StaleListBroadcastReceiver.java +++ b/opentasks/src/main/java/org/dmfs/tasks/StaleListBroadcastReceiver.java @@ -16,6 +16,8 @@ package org.dmfs.tasks; +import static java.util.Collections.singletonList; + import android.accounts.Account; import android.accounts.AccountManager; import android.app.Notification; @@ -27,7 +29,7 @@ import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.Build; - +import java.util.ArrayList; import org.dmfs.android.bolts.color.colors.AttributeColor; import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.android.contentpal.RowSnapshot; @@ -45,80 +47,101 @@ import org.dmfs.provider.tasks.AuthorityUtil; import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.utils.ManifestAppName; -import java.util.ArrayList; - -import static java.util.Collections.singletonList; - - /** * @author Marten Gajda */ -public final class StaleListBroadcastReceiver extends BroadcastReceiver -{ - @Override - public void onReceive(Context context, Intent intent) - { - if (Build.VERSION.SDK_INT < 26) - { - // this receiver is Android 8+ only - return; - } - AccountManager accountManager = AccountManager.get(context); - String authority = AuthorityUtil.taskAuthority(context); - String description = String.format("Please give %s access to the following account", new ManifestAppName(context).value()); - // request access to each account we don't know yet individually - for (Intent accountRequestIntent : +public final class StaleListBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (Build.VERSION.SDK_INT < 26) { + // this receiver is Android 8+ only + return; + } + AccountManager accountManager = AccountManager.get(context); + String authority = AuthorityUtil.taskAuthority(context); + String description = + String.format( + "Please give %s access to the following account", new ManifestAppName(context).value()); + // request access to each account we don't know yet individually + for (Intent accountRequestIntent : + new Mapped<>( + account -> + AccountManager.newChooseAccountIntent( + account, + new ArrayList(singletonList(account)), + null, + description, + null, + null, + null), + new Mapped<>( + this::account, new Mapped<>( - account -> AccountManager.newChooseAccountIntent(account, new ArrayList(singletonList(account)), null, - description, null, - null, null), - new Mapped<>( - this::account, - new Mapped<>(RowSnapshot::values, - new QueryRowSet<>( - new TaskListsView(authority, context.getContentResolver().acquireContentProviderClient(authority)), - new MultiProjection<>(TaskContract.TaskLists.ACCOUNT_NAME, TaskContract.TaskLists.ACCOUNT_TYPE), - new Not<>(new AnyOf<>( - new Joined<>( - new Seq<>(new EqArg<>(TaskContract.TaskLists.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE)), - new Mapped<>(AccountEq::new, new Seq<>(accountManager.getAccounts())))))))))) - { - if (Build.VERSION.SDK_INT < 28) - { - context.startActivity(accountRequestIntent); - } - else - { - // on newer Android versions post a notification instead because we can't launch activities from the background anymore - String notificationDescription = String.format("%s needs your permission", new ManifestAppName(context).value()); - NotificationManager nm = context.getSystemService(NotificationManager.class); - if (nm != null) - { - NotificationChannel errorChannel = new NotificationChannel("provider_messages", "Sync Messages", NotificationManager.IMPORTANCE_HIGH); - nm.createNotificationChannel(errorChannel); - Resources.Theme theme = context.getTheme(); - theme.applyStyle(context.getApplicationInfo().theme, true); + RowSnapshot::values, + new QueryRowSet<>( + new TaskListsView( + authority, + context.getContentResolver().acquireContentProviderClient(authority)), + new MultiProjection<>( + TaskContract.TaskLists.ACCOUNT_NAME, + TaskContract.TaskLists.ACCOUNT_TYPE), + new Not<>( + new AnyOf<>( + new Joined<>( + new Seq<>( + new EqArg<>( + TaskContract.TaskLists.ACCOUNT_TYPE, + TaskContract.LOCAL_ACCOUNT_TYPE)), + new Mapped<>( + AccountEq::new, + new Seq<>(accountManager.getAccounts())))))))))) { + if (Build.VERSION.SDK_INT < 28) { + context.startActivity(accountRequestIntent); + } else { + // on newer Android versions post a notification instead because we can't launch activities + // from the background anymore + String notificationDescription = + String.format("%s needs your permission", new ManifestAppName(context).value()); + NotificationManager nm = context.getSystemService(NotificationManager.class); + if (nm != null) { + NotificationChannel errorChannel = + new NotificationChannel( + "provider_messages", "Sync Messages", NotificationManager.IMPORTANCE_HIGH); + nm.createNotificationChannel(errorChannel); + Resources.Theme theme = context.getTheme(); + theme.applyStyle(context.getApplicationInfo().theme, true); - nm.notify("stale_list_broadacast", 0, - new Notification.Builder(context, "provider_messages") - .setContentText(notificationDescription) - .setContentIntent(PendingIntent.getActivity(context, 0, accountRequestIntent, PendingIntent.FLAG_UPDATE_CURRENT)) - .addAction(new Notification.Action.Builder(null, "Grant", - PendingIntent.getActivity(context, 0, accountRequestIntent, PendingIntent.FLAG_UPDATE_CURRENT)).build()) - .setColor(new AttributeColor(theme, androidx.appcompat.R.attr.colorPrimary).argb()) - .setColorized(true) - .setSmallIcon(org.dmfs.tasks.provider.R.drawable.ic_24_opentasks) - .build()); - } - } + nm.notify( + "stale_list_broadacast", + 0, + new Notification.Builder(context, "provider_messages") + .setContentText(notificationDescription) + .setContentIntent( + PendingIntent.getActivity( + context, 0, accountRequestIntent, PendingIntent.FLAG_UPDATE_CURRENT)) + .addAction( + new Notification.Action.Builder( + null, + "Grant", + PendingIntent.getActivity( + context, + 0, + accountRequestIntent, + PendingIntent.FLAG_UPDATE_CURRENT)) + .build()) + .setColor( + new AttributeColor(theme, androidx.appcompat.R.attr.colorPrimary).argb()) + .setColorized(true) + .setSmallIcon(org.dmfs.tasks.provider.R.drawable.ic_24_opentasks) + .build()); } + } } + } - - private Account account(RowDataSnapshot data) - { - return (new Account( - data.data(TaskContract.TaskLists.ACCOUNT_NAME, s -> s).value(), - data.data(TaskContract.TaskLists.ACCOUNT_TYPE, s -> s).value())); - } + private Account account(RowDataSnapshot data) { + return (new Account( + data.data(TaskContract.TaskLists.ACCOUNT_NAME, s -> s).value(), + data.data(TaskContract.TaskLists.ACCOUNT_TYPE, s -> s).value())); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java b/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java index cfa1520d..e39f802f 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java @@ -15,146 +15,142 @@ */ package org.dmfs.tasks; -import android.content.Context; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.Bundle; - -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; - -import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.view.View; import android.widget.Button; - +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; import com.google.android.material.appbar.AppBarLayout; - import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.utils.BaseActivity; - /** - * This extends the {@link FragmentActivity} for displaying the list of synced or visible task-providers. It displays the visible providers when it is created. + * 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 BaseActivity { - 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. - AppBarLayout mAppBarLayout = findViewById(R.id.appbar); - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - 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; - String title = getResources().getString(R.string.title_activity_settings); - SpannableString spannableString = new SpannableString(title); - spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(getApplicationContext(), R.color.color_default_primary_text)), 0, title.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - getSupportActionBar().setTitle(spannableString); - getSupportActionBar().getThemedContext().setTheme(R.style.settingToolbarTheme); - } - - - /** - * 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; - - getSupportActionBar().getThemedContext().setTheme(R.style.settingToolbarTheme); - String title = getResources().getString(R.string.synced_task_lists); - SpannableString spannableString = new SpannableString(title); - spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(getApplicationContext(), R.color.color_default_primary_text)), 0, title.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - getSupportActionBar().setTitle(spannableString); - } - - - /** - * 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. + AppBarLayout mAppBarLayout = findViewById(R.id.appbar); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + 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; + String title = getResources().getString(R.string.title_activity_settings); + SpannableString spannableString = new SpannableString(title); + spannableString.setSpan( + new ForegroundColorSpan( + ContextCompat.getColor(getApplicationContext(), R.color.color_default_primary_text)), + 0, + title.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + getSupportActionBar().setTitle(spannableString); + getSupportActionBar().getThemedContext().setTheme(R.style.settingToolbarTheme); + } + + /** 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; + + getSupportActionBar().getThemedContext().setTheme(R.style.settingToolbarTheme); + String title = getResources().getString(R.string.synced_task_lists); + SpannableString spannableString = new SpannableString(title); + spannableString.setSpan( + new ForegroundColorSpan( + ContextCompat.getColor(getApplicationContext(), R.color.color_default_primary_text)), + 0, + title.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + getSupportActionBar().setTitle(spannableString); + } + + /** + * 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 09ebe4e3..1c89d9f1 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/TaskGroupPagerAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/TaskGroupPagerAdapter.java @@ -21,146 +21,114 @@ import android.content.Context; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapter; - +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.dmfs.tasks.groupings.AbstractGroupingFactory; import org.dmfs.tasks.groupings.TabConfig; import org.dmfs.xmlobjects.pull.XmlObjectPullParserException; import org.xmlpull.v1.XmlPullParserException; -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 */ - -public class TaskGroupPagerAdapter extends FragmentStatePagerAdapter -{ - - @SuppressWarnings("unused") - private static final String TAG = "TaskGroupPager"; - private final Map mGroupingFactories = new HashMap(16); - 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; +public class TaskGroupPagerAdapter extends FragmentStatePagerAdapter { + + @SuppressWarnings("unused") + private static final String TAG = "TaskGroupPager"; + + private final Map mGroupingFactories = + new HashMap(16); + 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 Fragment getItem(int position) - { - int pageId = mTabConfig.getVisibleItem(position).getId(); - AbstractGroupingFactory factory = getGroupingFactoryForId(pageId); - - TaskListFragment fragment = TaskListFragment.newInstance(position); - fragment.setExpandableGroupDescriptor(factory.getExpandableGroupDescriptor()); - fragment.setPageId(pageId); - return fragment; - + } + + @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); + 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; + } } - - - /** - * 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 int getTabIcon(int position) - { - return mTabConfig.getVisibleItem(position).getIcon(); - } - + 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 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 f28e0b0c..f44cf7d0 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java @@ -20,34 +20,37 @@ import android.annotation.SuppressLint; import android.app.SearchManager; import android.content.Context; import android.content.Intent; -import android.content.res.ColorStateList; -import android.graphics.PorterDuff; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; import android.content.SharedPreferences; +import android.content.res.ColorStateList; import android.content.res.Resources; +import android.graphics.PorterDuff; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.provider.Settings; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.Window; -import android.view.WindowManager; import android.widget.EditText; import android.widget.ImageView; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; - +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.SearchView.OnQueryTextListener; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import androidx.core.view.MenuItemCompat; +import androidx.core.view.MenuItemCompat.OnActionExpandListener; +import androidx.core.widget.ImageViewCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.preference.PreferenceManager; +import androidx.viewpager.widget.ViewPager; +import androidx.viewpager.widget.ViewPager.OnPageChangeListener; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; - import org.dmfs.android.bolts.color.Color; import org.dmfs.android.bolts.color.colors.PrimaryColor; import org.dmfs.android.bolts.color.elementary.ValueColor; @@ -66,778 +69,626 @@ import org.dmfs.tasks.model.ContentSet; import org.dmfs.tasks.utils.BaseActivity; import org.dmfs.tasks.utils.ExpandableGroupDescriptor; import org.dmfs.tasks.utils.SearchHistoryHelper; -import org.dmfs.tasks.utils.colors.DarkenedForStatusBar; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.appcompat.widget.SearchView; -import androidx.appcompat.widget.SearchView.OnQueryTextListener; -import androidx.appcompat.widget.Toolbar; -import androidx.core.view.MenuItemCompat; -import androidx.core.view.MenuItemCompat.OnActionExpandListener; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.preference.PreferenceManager; -import androidx.viewpager.widget.ViewPager; -import androidx.viewpager.widget.ViewPager.OnPageChangeListener; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.appcompat.widget.SearchView; -import androidx.appcompat.widget.SearchView.OnQueryTextListener; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.core.view.MenuItemCompat; -import androidx.core.view.MenuItemCompat.OnActionExpandListener; -import androidx.core.widget.ImageViewCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.viewpager.widget.ViewPager; -import androidx.viewpager.widget.ViewPager.OnPageChangeListener; - /** - * An activity representing a list of Tasks. This activity has different presentations for handset and tablet-size devices. On handsets, the activity presents a - * list of items, which when touched, lead to a {@link ViewTaskActivity} representing item details. On tablets, the activity presents the list of items and item - * details side-by-side using two vertical panes. - *

    - * 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. - *

    + * An activity representing a list of Tasks. This activity has different presentations for handset + * and tablet-size devices. On handsets, the activity presents a list of items, which when touched, + * lead to a {@link ViewTaskActivity} representing item details. On tablets, the activity presents + * the list of items and item details side-by-side using two vertical panes. + * + *

    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 - *

    + * + *

    TODO: move the code to persist the expanded groups into a the GroupingDescriptor class * * @author Tobias Reinsch */ -public class TaskListActivity extends BaseActivity 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"; +public class TaskListActivity extends BaseActivity + 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 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"; + /** 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 final static int REQUEST_CODE_NEW_TASK = 2924; - private final static int REQUEST_CODE_PREFS = 2925; + private static final int REQUEST_CODE_NEW_TASK = 2924; + private static final int REQUEST_CODE_PREFS = 2925; - /** - * The time to wait for a new key before updating the search view. - */ - private final static int SEARCH_UPDATE_DELAY = 400; // ms + /** The time to wait for a new key before updating the search view. */ + private static final int SEARCH_UPDATE_DELAY = 400; // ms - private final static String DETAILS_FRAGMENT_TAG = "details_fragment_tag"; + private static final String DETAILS_FRAGMENT_TAG = "details_fragment_tag"; - /** - * Array of {@link ExpandableGroupDescriptor}s. - */ - private AbstractGroupingFactory[] mGroupingFactories; + /** 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; + /** Whether or not the activity is in two-pane mode, i.e. running on a tablet device. */ + private boolean mTwoPane; - @Retain(permanent = true) - private int mCurrentPageId; + private ViewPager mViewPager; + private TaskGroupPagerAdapter mPagerAdapter; - /** - * The last used color for the toolbars. {@link android.graphics.Color#TRANSPARENT} represents the absent value. - * (Used upon start/rotation until the actually selected task with its color is loaded, to avoid flashing up primary color.) - */ - @Retain(permanent = true) - @ColorInt - private int mLastUsedColor = android.graphics.Color.TRANSPARENT; + @Retain(permanent = true) + private int mCurrentPageId; - /** - * The current pager position - **/ - private int mCurrentPagePosition = 0; + /** + * The last used color for the toolbars. {@link android.graphics.Color#TRANSPARENT} represents the + * absent value. (Used upon start/rotation until the actually selected task with its color is + * loaded, to avoid flashing up primary color.) + */ + @Retain(permanent = true) + @ColorInt + private int mLastUsedColor = android.graphics.Color.TRANSPARENT; - private int mPreviousPagePosition = -1; + /** The current pager position */ + private int mCurrentPagePosition = 0; - private String mAuthority; + private int mPreviousPagePosition = -1; - private MenuItem mSearchItem; + private String mAuthority; - private TabLayout mTabs; + private MenuItem mSearchItem; - private final Handler mHandler = new Handler(); + private TabLayout mTabs; - private SearchHistoryHelper mSearchHistoryHelper; + private final Handler mHandler = new Handler(); - private boolean mAutoExpandSearchView = false; + private SearchHistoryHelper mSearchHistoryHelper; - /** - * The Uri of the task to display/highlight in the list view. - **/ - @Retain - private Uri mSelectedTaskUri; + private boolean mAutoExpandSearchView = false; - /** - * The Uri of the task to display/highlight in the list view coming from the widget. - **/ - private Uri mSelectedTaskUriOnLaunch; + /** The Uri of the task to display/highlight in the list view. */ + @Retain private Uri mSelectedTaskUri; - /** - * Indicates to show ViewTaskActivity when rotating to single pane. - **/ - @Retain - private boolean mShouldSwitchToDetail = false; + /** The Uri of the task to display/highlight in the list view coming from the widget. */ + private Uri mSelectedTaskUriOnLaunch; - /** - * Indicates the TaskListFragments to select/highlight the mSelectedTaskUri item - **/ - private boolean mShouldSelectTaskListItem = false; + /** Indicates to show ViewTaskActivity when rotating to single pane. */ + @Retain private boolean mShouldSwitchToDetail = false; - /** - * Indicates a transient state after rotation to redirect to the TaskViewActivtiy - **/ - private boolean mTransientState = false; + /** Indicates the TaskListFragments to select/highlight the mSelectedTaskUri item */ + private boolean mShouldSelectTaskListItem = false; - private AppBarLayout mAppBarLayout; + /** Indicates a transient state after rotation to redirect to the TaskViewActivtiy */ + private boolean mTransientState = false; - private FloatingActionButton mFloatingActionButton; - private SharedPreferences mPrefs; + private AppBarLayout mAppBarLayout; + private FloatingActionButton mFloatingActionButton; + private SharedPreferences mPrefs; - @Override - protected void onCreate(Bundle savedInstanceState) - { - mPrefs = PreferenceManager.getDefaultSharedPreferences(this); - - updateTheme(); - super.onCreate(savedInstanceState); - - if (mLastUsedColor == android.graphics.Color.TRANSPARENT) - { - // no saved color, use the primary color - mLastUsedColor = new PrimaryColor(this).argb(); - } - - // check for single pane activity change - mTwoPane = getResources().getBoolean(R.bool.has_two_panes); - - resolveIntentAction(getIntent()); - - if (mSelectedTaskUri != null) - { - if (!mTwoPane && mShouldSwitchToDetail) - { - Intent viewTaskIntent = new Intent(Intent.ACTION_VIEW); - viewTaskIntent.setData(mSelectedTaskUri); - viewTaskIntent.putExtra(ViewTaskActivity.EXTRA_COLOR, mLastUsedColor); - startActivity(viewTaskIntent); - mShouldSwitchToDetail = false; - mTransientState = true; - } - } - else - { - mShouldSwitchToDetail = false; - } - - setContentView(R.layout.activity_task_list); - mAppBarLayout = (AppBarLayout) findViewById(R.id.appbar); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - mAuthority = AuthorityUtil.taskAuthority(this); - mSearchHistoryHelper = new SearchHistoryHelper(this); - - if (findViewById(R.id.task_detail_container) != null) - { - /* Note: 'savedInstanceState == null' is not used here as would be usual with fragments, because of the case of when rotation means - switching from one-pane mode to two-pane mode on small tablets and the fragment has to added. To cover that case as well, the fragment is always replaced. */ - replaceTaskDetailsFragment( - mSelectedTaskUri == null ? - EmptyTaskFragment.newInstance(new ValueColor(mLastUsedColor)) - : ViewTaskFragment.newInstance(mSelectedTaskUri, new ValueColor(mLastUsedColor))); - } - else - { - // When rotating the screen means switching from two-pane to single-pane mode (on small tablets), remove the obsolete fragment that gets recreated by FragmentManager: - FragmentManager fragmentManager = getSupportFragmentManager(); - Fragment detailFragment = fragmentManager.findFragmentByTag(DETAILS_FRAGMENT_TAG); - if (detailFragment != null) - { - fragmentManager.beginTransaction().remove(detailFragment).commit(); - } - } - - mGroupingFactories = new AbstractGroupingFactory[] { - new ByList(mAuthority, this), new ByDueDate(mAuthority), new ByStartDate(mAuthority), - new ByPriority(mAuthority, this), new ByProgress(mAuthority), new BySearch(mAuthority, mSearchHistoryHelper) }; - - mPagerAdapter = new Unchecked<>(() -> new TaskGroupPagerAdapter(getSupportFragmentManager(), mGroupingFactories, this, R.xml.listview_tabs)).value(); - - mViewPager = (ViewPager) findViewById(R.id.pager); - mViewPager.setAdapter(mPagerAdapter); - - int currentPageIndex = mPagerAdapter.getPagePosition(mCurrentPageId); - - if (currentPageIndex >= 0) - { - mCurrentPagePosition = currentPageIndex; - mViewPager.setCurrentItem(currentPageIndex); - if (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); - setupTabIcons(); - - mTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() - { - @Override - public void onTabSelected(TabLayout.Tab tab) - { - tab.getIcon().setColorFilter(getResources().getColor(R.color.color_default_blue1), PorterDuff.Mode.SRC_IN); - } + @Override + protected void onCreate(Bundle savedInstanceState) { + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + updateTheme(); + super.onCreate(savedInstanceState); - @Override - public void onTabUnselected(TabLayout.Tab tab) - { - tab.getIcon().setColorFilter(getResources().getColor(R.color.dark_gray), PorterDuff.Mode.SRC_IN); - } + if (mLastUsedColor == android.graphics.Color.TRANSPARENT) { + // no saved color, use the primary color + mLastUsedColor = new PrimaryColor(this).argb(); + } + // check for single pane activity change + mTwoPane = getResources().getBoolean(R.bool.has_two_panes); + + resolveIntentAction(getIntent()); + + if (mSelectedTaskUri != null) { + if (!mTwoPane && mShouldSwitchToDetail) { + Intent viewTaskIntent = new Intent(Intent.ACTION_VIEW); + viewTaskIntent.setData(mSelectedTaskUri); + viewTaskIntent.putExtra(ViewTaskActivity.EXTRA_COLOR, mLastUsedColor); + startActivity(viewTaskIntent); + mShouldSwitchToDetail = false; + mTransientState = true; + } + } else { + mShouldSwitchToDetail = false; + } - @Override - public void onTabReselected(TabLayout.Tab tab) - { + setContentView(R.layout.activity_task_list); + mAppBarLayout = (AppBarLayout) findViewById(R.id.appbar); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + mAuthority = AuthorityUtil.taskAuthority(this); + mSearchHistoryHelper = new SearchHistoryHelper(this); + + if (findViewById(R.id.task_detail_container) != null) { + /* Note: 'savedInstanceState == null' is not used here as would be usual with fragments, because of the case of when rotation means + switching from one-pane mode to two-pane mode on small tablets and the fragment has to added. To cover that case as well, the fragment is always replaced. */ + replaceTaskDetailsFragment( + mSelectedTaskUri == null + ? EmptyTaskFragment.newInstance(new ValueColor(mLastUsedColor)) + : ViewTaskFragment.newInstance(mSelectedTaskUri, new ValueColor(mLastUsedColor))); + } else { + // When rotating the screen means switching from two-pane to single-pane mode (on small + // tablets), remove the obsolete fragment that gets recreated by FragmentManager: + FragmentManager fragmentManager = getSupportFragmentManager(); + Fragment detailFragment = fragmentManager.findFragmentByTag(DETAILS_FRAGMENT_TAG); + if (detailFragment != null) { + fragmentManager.beginTransaction().remove(detailFragment).commit(); + } + } - } + mGroupingFactories = + new AbstractGroupingFactory[] { + new ByList(mAuthority, this), new ByDueDate(mAuthority), new ByStartDate(mAuthority), + new ByPriority(mAuthority, this), new ByProgress(mAuthority), + new BySearch(mAuthority, mSearchHistoryHelper) + }; + + mPagerAdapter = + new Unchecked<>( + () -> + new TaskGroupPagerAdapter( + getSupportFragmentManager(), mGroupingFactories, this, R.xml.listview_tabs)) + .value(); + + mViewPager = (ViewPager) findViewById(R.id.pager); + mViewPager.setAdapter(mPagerAdapter); + + int currentPageIndex = mPagerAdapter.getPagePosition(mCurrentPageId); + + if (currentPageIndex >= 0) { + mCurrentPagePosition = currentPageIndex; + mViewPager.setCurrentItem(currentPageIndex); + if (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); + setupTabIcons(); + + mTabs.addOnTabSelectedListener( + new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + tab.getIcon() + .setColorFilter( + getResources().getColor(R.color.color_default_blue1), PorterDuff.Mode.SRC_IN); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + tab.getIcon() + .setColorFilter(getResources().getColor(R.color.dark_gray), PorterDuff.Mode.SRC_IN); + } + + @Override + public void onTabReselected(TabLayout.Tab tab) {} }); - 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); - } - + mViewPager.addOnPageChangeListener( + new OnPageChangeListener() { - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) - { + @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(); } - - - @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(() -> MenuItemCompat.expandActionView(mSearchItem), 50); - } + 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(() -> MenuItemCompat.expandActionView(mSearchItem), 50); } + } }); - mFloatingActionButton = (FloatingActionButton) findViewById(R.id.floating_action_button); - - if (mFloatingActionButton != null) - { - mFloatingActionButton.setOnClickListener(v -> onAddNewTask()); - } - } - + mFloatingActionButton = (FloatingActionButton) findViewById(R.id.floating_action_button); - private void updateTheme() - { - if (Build.VERSION.SDK_INT >= 29) - { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - } - } - - - private void setupTabIcons() - { - for (int i = 0, count = mPagerAdapter.getCount(); i < count; ++i) - { - mTabs.getTabAt(i).setIcon(mPagerAdapter.getTabIcon(i)); - mTabs.getTabAt(i).getIcon().setColorFilter(getResources().getColor(R.color.dark_gray), PorterDuff.Mode.SRC_IN); - } + if (mFloatingActionButton != null) { + mFloatingActionButton.setOnClickListener(v -> onAddNewTask()); } + } - - @Override - protected void onResume() - { - updateTitle(mCurrentPageId); - mTabs.getTabAt(mTabs.getSelectedTabPosition()).getIcon().setColorFilter(getResources().getColor(R.color.color_default_blue1), PorterDuff.Mode.SRC_IN); - super.onResume(); + private void updateTheme() { + if (Build.VERSION.SDK_INT >= 29) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); } - - - @Override - protected void onNewIntent(Intent intent) - { - resolveIntentAction(intent); - super.onNewIntent(intent); + } + + private void setupTabIcons() { + for (int i = 0, count = mPagerAdapter.getCount(); i < count; ++i) { + mTabs.getTabAt(i).setIcon(mPagerAdapter.getTabIcon(i)); + mTabs + .getTabAt(i) + .getIcon() + .setColorFilter(getResources().getColor(R.color.dark_gray), PorterDuff.Mode.SRC_IN); } - - - @Override - protected void onDestroy() - { - super.onDestroy(); - mSearchHistoryHelper.close(); + } + + @Override + protected void onResume() { + updateTitle(mCurrentPageId); + mTabs + .getTabAt(mTabs.getSelectedTabPosition()) + .getIcon() + .setColorFilter( + getResources().getColor(R.color.color_default_blue1), PorterDuff.Mode.SRC_IN); + 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( + @NonNull Uri uri, @NonNull Color taskListColor, boolean forceReload, int pagePosition) { + // only accept selections from the current visible task fragment or the activity itself + if (pagePosition == -1 || pagePosition == mCurrentPagePosition) { + if (mTwoPane) { + if (forceReload) { + mSelectedTaskUri = uri; + mShouldSwitchToDetail = false; + } + replaceTaskDetailsFragment(ViewTaskFragment.newInstance(uri, taskListColor)); + } 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); + detailIntent.putExtra(ViewTaskActivity.EXTRA_COLOR, mLastUsedColor); + startActivity(detailIntent); + mShouldSwitchToDetail = false; + } } - - - /** - * Callback method from {@link TaskListFragment.Callbacks} indicating that the item with the given ID was selected. - */ - @Override - public void onItemSelected(@NonNull Uri uri, @NonNull Color taskListColor, boolean forceReload, int pagePosition) - { - // only accept selections from the current visible task fragment or the activity itself - if (pagePosition == -1 || pagePosition == mCurrentPagePosition) - { - if (mTwoPane) - { - if (forceReload) - { - mSelectedTaskUri = uri; - mShouldSwitchToDetail = false; - } - replaceTaskDetailsFragment(ViewTaskFragment.newInstance(uri, taskListColor)); - } - 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); - detailIntent.putExtra(ViewTaskActivity.EXTRA_COLOR, mLastUsedColor); - startActivity(detailIntent); - mShouldSwitchToDetail = false; - } - } + } + + @Override + public void onItemRemoved(@NonNull Uri taskUri) { + if (taskUri.equals(mSelectedTaskUri)) { + mSelectedTaskUri = null; + if (mTwoPane) { + replaceTaskDetailsFragment(EmptyTaskFragment.newInstance(new ValueColor(mLastUsedColor))); + } } - - - @Override - public void onItemRemoved(@NonNull Uri taskUri) - { - if (taskUri.equals(mSelectedTaskUri)) - { - mSelectedTaskUri = null; - if (mTwoPane) - { - replaceTaskDetailsFragment(EmptyTaskFragment.newInstance(new ValueColor(mLastUsedColor))); - } - } + } + + @Override + public void onAddNewTask() { + Intent editTaskIntent = new Intent(Intent.ACTION_INSERT); + editTaskIntent.setData(Tasks.getContentUri(mAuthority)); + startActivityForResult(editTaskIntent, REQUEST_CODE_NEW_TASK); + } + + @Override + public ExpandableGroupDescriptor getGroupDescriptor(int pageId) { + for (AbstractGroupingFactory factory : mGroupingFactories) { + if (factory.getId() == pageId) { + return factory.getExpandableGroupDescriptor(); + } } - - - @Override - public void onAddNewTask() - { - Intent editTaskIntent = new Intent(Intent.ACTION_INSERT); - editTaskIntent.setData(Tasks.getContentUri(mAuthority)); - startActivityForResult(editTaskIntent, REQUEST_CODE_NEW_TASK); + return null; + } + + private void replaceTaskDetailsFragment(@NonNull Fragment fragment) { + FragmentManager fragmentManager = getSupportFragmentManager(); + // only change state if the state has not been saved yet, otherwise just drop it + if (!fragmentManager.isStateSaved()) { + fragmentManager + .beginTransaction() + .setCustomAnimations(0, R.anim.openttasks_fade_exit, 0, 0) + .replace(R.id.task_detail_container, fragment, DETAILS_FRAGMENT_TAG) + .commit(); } - - - @Override - public ExpandableGroupDescriptor getGroupDescriptor(int pageId) - { - for (AbstractGroupingFactory factory : mGroupingFactories) - { - if (factory.getId() == pageId) - { - return factory.getExpandableGroupDescriptor(); - } - } - return null; + } + + private void updateTitle(int pageId) { + if (pageId == R.id.task_group_by_list) { + getSupportActionBar().setTitle(R.string.task_group_title_list); + } else if (pageId == R.id.task_group_by_start) { + getSupportActionBar().setTitle(R.string.task_group_title_start); + } else if (pageId == R.id.task_group_by_due) { + getSupportActionBar().setTitle(R.string.task_group_title_due); + } else if (pageId == R.id.task_group_by_priority) { + getSupportActionBar().setTitle(R.string.task_group_title_priority); + } else if (pageId == R.id.task_group_by_progress) { + getSupportActionBar().setTitle(R.string.task_group_title_progress); + } else { + getSupportActionBar().setTitle(R.string.task_group_title_default); } + } - - private void replaceTaskDetailsFragment(@NonNull Fragment fragment) - { - FragmentManager fragmentManager = getSupportFragmentManager(); - // only change state if the state has not been saved yet, otherwise just drop it - if (!fragmentManager.isStateSaved()) - { - fragmentManager.beginTransaction() - .setCustomAnimations(0, R.anim.openttasks_fade_exit, 0, 0) - .replace(R.id.task_detail_container, fragment, DETAILS_FRAGMENT_TAG).commit(); - } + private void resolveIntentAction(Intent intent) { + // check which task should be selected + if (intent.getBooleanExtra(EXTRA_DISPLAY_TASK, false)) { + mShouldSwitchToDetail = true; + mSelectedTaskUri = intent.getData(); } - - private void updateTitle(int pageId) - { - if (pageId == R.id.task_group_by_list) { - getSupportActionBar().setTitle(R.string.task_group_title_list); - } - else if (pageId == R.id.task_group_by_start) { - getSupportActionBar().setTitle(R.string.task_group_title_start); - } - else if (pageId == R.id.task_group_by_due) { - getSupportActionBar().setTitle(R.string.task_group_title_due); - } - else if (pageId == R.id.task_group_by_priority) { - getSupportActionBar().setTitle(R.string.task_group_title_priority); - } - else if (pageId == R.id.task_group_by_progress) { - getSupportActionBar().setTitle(R.string.task_group_title_progress); - } - else { - getSupportActionBar().setTitle(R.string.task_group_title_default); - } + if (intent.getBooleanExtra(EXTRA_DISPLAY_TASK, false) + && intent.getBooleanExtra(EXTRA_FORCE_LIST_SELECTION, true) + && mTwoPane) { + mShouldSwitchToDetail = true; + mSelectedTaskUriOnLaunch = intent.getData(); + mShouldSelectTaskListItem = true; + if (mPagerAdapter != null) { + mPagerAdapter.notifyDataSetChanged(); + } + } else { + mSelectedTaskUriOnLaunch = null; + mShouldSelectTaskListItem = false; } - - - private void resolveIntentAction(Intent intent) - { - // check which task should be selected - if (intent.getBooleanExtra(EXTRA_DISPLAY_TASK, false)) - { - mShouldSwitchToDetail = true; - mSelectedTaskUri = intent.getData(); - } - - if (intent.getBooleanExtra(EXTRA_DISPLAY_TASK, false) && intent.getBooleanExtra(EXTRA_FORCE_LIST_SELECTION, true) && mTwoPane) - { - mShouldSwitchToDetail = true; - mSelectedTaskUriOnLaunch = intent.getData(); - mShouldSelectTaskListItem = true; - if (mPagerAdapter != null) - { - mPagerAdapter.notifyDataSetChanged(); - } - } - else - { - mSelectedTaskUriOnLaunch = null; - mShouldSelectTaskListItem = false; - } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == REQUEST_CODE_NEW_TASK + && 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, + !intent.getBooleanExtra(EditTaskFragment.KEY_NEW_TASK, false)); + displayIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, true); + Uri newTaskUri = intent.getData(); + displayIntent.setData(newTaskUri); + onNewIntent(displayIntent); + + /* Icons have to be refreshed here because of some bug in ViewPager-TabLayout which causes them to disappear. + See https://github.com/dmfs/opentasks/issues/643 + and https://stackoverflow.com/questions/42209046/tablayout-icons-disappear-after-viewpager-refresh */ + setupTabIcons(); + return; } - - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) - { - if (requestCode == REQUEST_CODE_NEW_TASK && 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, !intent.getBooleanExtra(EditTaskFragment.KEY_NEW_TASK, false)); - displayIntent.putExtra(TaskListActivity.EXTRA_FORCE_LIST_SELECTION, true); - Uri newTaskUri = intent.getData(); - displayIntent.setData(newTaskUri); - onNewIntent(displayIntent); - - /* Icons have to be refreshed here because of some bug in ViewPager-TabLayout which causes them to disappear. - See https://github.com/dmfs/opentasks/issues/643 - and https://stackoverflow.com/questions/42209046/tablayout-icons-disappear-after-viewpager-refresh */ - setupTabIcons(); - return; - } - if (requestCode == REQUEST_CODE_PREFS) - { - updateTheme(); - if (Build.VERSION.SDK_INT < 29) - { - recreate(); - } - } - super.onActivityResult(requestCode, resultCode, intent); + if (requestCode == REQUEST_CODE_PREFS) { + updateTheme(); + if (Build.VERSION.SDK_INT < 29) { + recreate(); + } } - - - @Override - public void onTaskEditRequested(@NonNull 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); + super.onActivityResult(requestCode, resultCode, intent); + } + + @Override + public void onTaskEditRequested(@NonNull 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); } - - - @Override - public void onTaskDeleted(@NonNull Uri taskUri) - { - if (taskUri.equals(mSelectedTaskUri)) - { // Only the selected task can be deleted on the UI, but just to be safe - mSelectedTaskUri = null; - if (mTwoPane) - { - // empty the detail fragment - replaceTaskDetailsFragment(EmptyTaskFragment.newInstance(new ValueColor(mLastUsedColor))); - } - } - // The loader will take care of reloading the list and the list view will take care of selecting the next element. + startActivity(editTaskIntent); + } + + @Override + public void onTaskDeleted(@NonNull Uri taskUri) { + if (taskUri.equals( + mSelectedTaskUri)) { // Only the selected task can be deleted on the UI, but just to be safe + mSelectedTaskUri = null; + if (mTwoPane) { + // empty the detail fragment + replaceTaskDetailsFragment(EmptyTaskFragment.newInstance(new ValueColor(mLastUsedColor))); + } } - - - @Override - public void onTaskCompleted(@NonNull Uri taskUri) - { - /* TODO We delegate to onTaskDeleted() which was used previously for this event, too. - This causes the removal of details view, but the task is selected again if completed tasks are shown. This causes a flash. */ - onTaskDeleted(taskUri); + // The loader will take care of reloading the list and the list view will take care of selecting + // the next element. + } + + @Override + public void onTaskCompleted(@NonNull Uri taskUri) { + /* TODO We delegate to onTaskDeleted() which was used previously for this event, too. + This causes the removal of details view, but the task is selected again if completed tasks are shown. This causes a flash. */ + onTaskDeleted(taskUri); + } + + @SuppressLint("NewApi") + @Override + public void onListColorLoaded(@NonNull Color color) { + // nothing to do + } + + @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); } - - @SuppressLint("NewApi") - @Override - public void onListColorLoaded(@NonNull Color color) - { - // nothing to do + // 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.opentasks_menu_app_settings) { + startActivityForResult(new Intent(this, AppSettingsActivity.class), REQUEST_CODE_PREFS); + return true; + } else if (item.getItemId() == R.id.opentasks_menu_app_about) { + startActivity(new Intent(this, AboutActivity.class)); + return true; + } else { + return super.onOptionsItemSelected(item); } - - - @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); - } - - // search - setupSearch(menu); - - return true; + } + + private void hideSearchActionView() { + MenuItemCompat.collapseActionView(mSearchItem); + } + + public void setupSearch(Menu menu) { + 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) mSearchItem.getActionView(); + EditText searchText = searchView.findViewById(androidx.appcompat.R.id.search_src_text); + ImageView searchClose = searchView.findViewById(androidx.appcompat.R.id.search_close_btn); + searchClose.setImageResource(R.drawable.ic_close); + ImageView searchIcon = searchView.findViewById(androidx.appcompat.R.id.search_mag_icon); + searchIcon.setImageDrawable(null); + ImageViewCompat.setImageTintList( + searchIcon, + ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_default_primary_text))); + + searchView.findViewById(androidx.appcompat.R.id.search_plate).setBackground(null); + + searchText.setHintTextColor(getResources().getColor(R.color.dark_gray)); + searchText.setTextColor(getResources().getColor(R.color.color_default_primary_text)); + searchText.setBackground(ContextCompat.getDrawable(this, R.drawable.rounded_edittext)); + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + if (null != searchManager) { + searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); } - - @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.opentasks_menu_app_settings) - { - startActivityForResult(new Intent(this, AppSettingsActivity.class), REQUEST_CODE_PREFS); - return true; - } - else if (item.getItemId() == R.id.opentasks_menu_app_about) - { - startActivity(new Intent(this, AboutActivity.class)); + searchView.setQueryHint(getString(R.string.menu_search_hint)); + searchView.setIconified(true); + searchView.setIconifiedByDefault(false); + searchView.setOnQueryTextListener( + new OnQueryTextListener() { + + @Override + public boolean onQueryTextSubmit(String query) { + // persist current search + mSearchHistoryHelper.commitSearch(); + mHandler.post(mSearchUpdater); return true; - } - else - { - return super.onOptionsItemSelected(item); - } - } - - - private void hideSearchActionView() - { - MenuItemCompat.collapseActionView(mSearchItem); - } - - - public void setupSearch(Menu menu) - { - 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; + @Override + public boolean onQueryTextChange(String query) { + if (mCurrentPageId != R.id.task_group_search) { + return true; } - }); - SearchView searchView = (SearchView) mSearchItem.getActionView(); - EditText searchText = searchView.findViewById(androidx.appcompat.R.id.search_src_text); - ImageView searchClose = searchView.findViewById(androidx.appcompat.R.id.search_close_btn); - searchClose.setImageResource(R.drawable.ic_close); - ImageView searchIcon = searchView.findViewById(androidx.appcompat.R.id.search_mag_icon); - searchIcon.setImageDrawable(null); - ImageViewCompat.setImageTintList(searchIcon, ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_default_primary_text))); - - searchView.findViewById(androidx.appcompat.R.id.search_plate).setBackground(null); - - searchText.setHintTextColor(getResources().getColor(R.color.dark_gray)); - searchText.setTextColor(getResources().getColor(R.color.color_default_primary_text)); - searchText.setBackground(ContextCompat.getDrawable(this, R.drawable.rounded_edittext)); - 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.setIconifiedByDefault(false); - 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; + 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(); - } - + if (mAutoExpandSearchView) { + mSearchItem.expandActionView(); } + } - - /** - * Notifies the search fragment of an update. - */ - private final Runnable mSearchUpdater = new Runnable() - { + /** 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(); - } - }; - - - public Uri getSelectedTaskUri() - { - if (mShouldSelectTaskListItem) - { - return mSelectedTaskUriOnLaunch; - } - return null; - } - - - public boolean isInTransientState() - { - return mTransientState; + public void run() { + TaskListFragment fragment = + (TaskListFragment) + mPagerAdapter.instantiateItem(mViewPager, mViewPager.getCurrentItem()); + fragment.notifyDataSetChanged(true); + fragment.expandCurrentSearchGroup(); + } + }; + + public Uri getSelectedTaskUri() { + if (mShouldSelectTaskListItem) { + return mSelectedTaskUriOnLaunch; } - - @Override - public Resources.Theme getTheme() - { - Resources.Theme theme = super.getTheme(); - if (Build.VERSION.SDK_INT < 29) - { - theme.applyStyle( - org.dmfs.android.sync.opentasks_theme.R.style.OpenTasks_Theme_Default, - true); - } - return theme; + return null; + } + + public boolean isInTransientState() { + return mTransientState; + } + + @Override + public Resources.Theme getTheme() { + Resources.Theme theme = super.getTheme(); + if (Build.VERSION.SDK_INT < 29) { + theme.applyStyle(org.dmfs.android.sync.opentasks_theme.R.style.OpenTasks_Theme_Default, true); } + return theme; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java b/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java index 7c28c203..ba8884ae 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java @@ -24,8 +24,6 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; @@ -46,9 +44,10 @@ import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ExpandableListView.OnGroupCollapseListener; import android.widget.ListView; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; import com.google.android.material.snackbar.Snackbar; - import org.dmfs.android.bolts.color.Color; import org.dmfs.android.bolts.color.elementary.ValueColor; import org.dmfs.android.retentionmagic.SupportFragment; @@ -73,965 +72,822 @@ import org.dmfs.tasks.utils.RetainExpandableListView; import org.dmfs.tasks.utils.SafeFragmentUiRunnable; import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper.SearchHistoryColumns; -import androidx.annotation.NonNull; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; - - /** - * 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 + * 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"; + @SuppressWarnings("unused") + private static final String TAG = "org.dmfs.tasks.TaskListFragment"; - private final static String ARG_INSTANCE_ID = "instance_id"; + private static final String ARG_INSTANCE_ID = "instance_id"; - private static final long INTERVAL_LISTVIEW_REDRAW = 60000; + 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"); + /** A filter to hide completed tasks. */ + private static final AbstractFilter COMPLETED_FILTER = new ConstantFilter(Tasks.IS_CLOSED + "=0"); - /** - * The group descriptor to use. - */ - private ExpandableGroupDescriptor mGroupDescriptor; + /** The group descriptor to use. */ + private ExpandableGroupDescriptor mGroupDescriptor; - /** - * The fragment's current callback object, which is notified of list item clicks. - */ - private Callbacks mCallbacks; + /** 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; + @Retain(permanent = true, instanceNSField = "mInstancePosition") + private int mActivatedPositionGroup = 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; + @Retain(permanent = true, instanceNSField = "mInstancePosition") + private int mActivatedPositionChild = ExpandableListView.INVALID_POSITION; - @Parameter(key = ARG_INSTANCE_ID) - private int mInstancePosition; + private RetainExpandableListView mExpandableListView; + private Context mAppContext; + private ExpandableGroupDescriptorAdapter mAdapter; + private Handler mHandler; - private Loader mCursorLoader; - private String mAuthority; + @Retain(permanent = true, instanceNSField = "mInstancePosition") + private long[] mSavedExpandedGroups = null; - private Uri mSelectedTaskUri; + @Retain(permanent = true, instanceNSField = "mInstancePosition") + private boolean mSavedCompletedFilter; - private boolean mTwoPaneLayout; + @Parameter(key = ARG_INSTANCE_ID) + private int mInstancePosition; - /** - * The child position to open when the fragment is displayed. - **/ - private ListPosition mSelectedChildPosition; + private Loader mCursorLoader; + private String mAuthority; - @Retain - private int mPageId = -1; + private Uri mSelectedTaskUri; - private final OnChildClickListener mTaskItemClickListener = new OnChildClickListener() - { + private boolean mTwoPaneLayout; - @Override - public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) - { - selectChildView(parent, groupPosition, childPosition, true); - - 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; - } + /** The child position to open when the fragment is displayed. */ + private ListPosition mSelectedChildPosition; - }; + @Retain private int mPageId = -1; - private final OnGroupCollapseListener mTaskListCollapseListener = new OnGroupCollapseListener() - { + private final OnChildClickListener mTaskItemClickListener = + new OnChildClickListener() { @Override - public void onGroupCollapse(int groupPosition) - { - if (groupPosition == mActivatedPositionGroup) - { - mActivatedPositionChild = ExpandableListView.INVALID_POSITION; - mActivatedPositionGroup = ExpandableListView.INVALID_POSITION; - } + public boolean onChildClick( + ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { + selectChildView(parent, groupPosition, childPosition, true); + 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; } - }; - - - /** - * 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 taskListColor - * the color of the task list (used for toolbars) - * @param forceReload - * Whether to reload the task or not. - */ - void onItemSelected(@NonNull Uri taskUri, @NonNull Color taskListColor, boolean forceReload, int pagePosition); - - /** - * Called when a task has been removed from the list. - *

    - * TODO It's only called when task is deleted by the swipe out, and not when it is completed. - * It should probably be called that time, too. See https://github.com/dmfs/opentasks/issues/641. - * - * @param taskUri - * the content uri of the task that has been removed - */ - void onItemRemoved(@NonNull Uri taskUri); - - void onAddNewTask(); - - ExpandableGroupDescriptor getGroupDescriptor(int position); - } + }; - - /** - * 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 SafeFragmentUiRunnable(this, new Runnable() - { + private final OnGroupCollapseListener mTaskListCollapseListener = + new OnGroupCollapseListener() { @Override - public void run() - { - mExpandableListView.invalidateViews(); - mHandler.postDelayed(mListRedrawRunnable, INTERVAL_LISTVIEW_REDRAW); - } - }); - - - public static TaskListFragment newInstance(int instancePosition) - { - TaskListFragment result = new TaskListFragment(); - Bundle args = new Bundle(); - args.putInt(ARG_INSTANCE_ID, instancePosition); - 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); - - mTwoPaneLayout = activity.getResources().getBoolean(R.bool.has_two_panes); - - mAuthority = AuthorityUtil.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 (mGroupDescriptor == null) - { - loadGroupDescriptor(); - } - - // setup the views - this.prepareReload(); - - // expand lists - if (mSavedExpandedGroups != null) - { - mExpandableListView.expandGroups(mSavedExpandedGroups); - } - - FlingDetector swiper = new FlingDetector(mExpandableListView, mGroupDescriptor.getElementViewDescriptor().getFlingContentViewId()); - 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); - } - - - @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); - } - } - - - @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 SafeFragmentUiRunnable(this, () -> mAdapter.reloadLoadedGroups())); - } - - - @Override - public void onLoaderReset(Loader loader) - { - mAdapter.changeCursor(new MatrixCursor(new String[] { "_id" })); - } - - - @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; - } - - Uri taskUri = ContentUris.withAppendedId(Instances.getContentUri(mAuthority), (long) TaskFieldAdapters.TASK_ID.get(cursor)); - Color taskListColor = new ValueColor(TaskFieldAdapters.LIST_COLOR.get(cursor)); - mCallbacks.onItemSelected(taskUri, taskListColor, force, mInstancePosition); - } - } - - - /** - * prepares the update of the view after the group descriptor was changed - */ - public void prepareReload() - { - mAdapter = new ExpandableGroupDescriptorAdapter(new MatrixCursor(new String[] { "_id" }), getActivity(), getLoaderManager(), mGroupDescriptor); - mExpandableListView.setAdapter(mAdapter); - mExpandableListView.setOnChildClickListener(mTaskItemClickListener); - mExpandableListView.setOnGroupCollapseListener(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); - } - } - - } - - + 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 { /** - * 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. + * Callback for when an item has been selected. * - * @return + * @param taskUri The {@link Uri} of the selected task. + * @param taskListColor the color of the task list (used for toolbars) + * @param forceReload Whether to reload the task or not. */ - private void removeTask(final Uri taskUri, final String taskTitle) - { - new AlertDialog.Builder(getActivity(), R.style.customAlertDialog) - .setTitle(R.string.confirm_delete_title) - .setCancelable(true) - .setNegativeButton(android.R.string.cancel, (dialog, which) -> { - // nothing to do here - }).setPositiveButton(android.R.string.ok, (dialog, 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(); - mCallbacks.onItemRemoved(taskUri); - }).setMessage(getString(R.string.confirm_delete_message_with_title, taskTitle)).create().show(); - } - + void onItemSelected( + @NonNull Uri taskUri, @NonNull Color taskListColor, boolean forceReload, int pagePosition); /** - * Opens the task editor for the selected Task. + * Called when a task has been removed from the list. * - * @param taskUri - * The {@link Uri} of the task. + *

    TODO It's only called when task is deleted by the swipe out, and not when it is completed. + * It should probably be called that time, too. See + * https://github.com/dmfs/opentasks/issues/641. + * + * @param taskUri the content uri of the task that has been removed */ - 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) - { - long instanceId = cursor.getLong(cursor.getColumnIndex(Instances._ID)); - - 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(Instances.getContentUri(mAuthority), instanceId); - - 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); + void onItemRemoved(@NonNull Uri taskUri); + + void onAddNewTask(); + + ExpandableGroupDescriptor getGroupDescriptor(int position); + } + + /** + * 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 SafeFragmentUiRunnable( + this, + new Runnable() { + + @Override + public void run() { + mExpandableListView.invalidateViews(); + mHandler.postDelayed(mListRedrawRunnable, INTERVAL_LISTVIEW_REDRAW); } - } - } - - - /** - * 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 static TaskListFragment newInstance(int instancePosition) { + TaskListFragment result = new TaskListFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_INSTANCE_ID, instancePosition); + result.setArguments(args); + return result; + } - public int getOpenGroupPosition() - { - return mActivatedPositionGroup; - } + /** + * 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); - /** - * 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); - } + mTwoPaneLayout = activity.getResources().getBoolean(R.bool.has_two_panes); + mAuthority = AuthorityUtil.taskAuthority(activity); - public void setListViewScrollbarPositionLeft(boolean left) - { - if (left) - { - mExpandableListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_LEFT); - // expandLV.setScrollBarStyle(style); - } - else - { - mExpandableListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); - } - } - + mAppContext = activity.getBaseContext(); - public void setExpandableGroupDescriptor(ExpandableGroupDescriptor groupDescriptor) - { - mGroupDescriptor = groupDescriptor; + // Activities containing this fragment must implement its callbacks. + if (!(activity instanceof Callbacks)) { + throw new IllegalStateException("Activity must implement fragment's callbacks."); } + mCallbacks = (Callbacks) activity; - /** - * 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); - } + // load accounts early + Sources.loadModelAsync(activity, TaskContract.LOCAL_ACCOUNT_TYPE, this); + } - 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; - } + @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 (mGroupDescriptor == null) { + loadGroupDescriptor(); + } + + // setup the views + this.prepareReload(); + + // expand lists + if (mSavedExpandedGroups != null) { + mExpandableListView.expandGroups(mSavedExpandedGroups); + } + FlingDetector swiper = + new FlingDetector( + mExpandableListView, + mGroupDescriptor.getElementViewDescriptor().getFlingContentViewId()); + swiper.setOnFlingListener(this); - public void setOpenChildPosition(int openChildPosition) - { - mActivatedPositionChild = openChildPosition; + return rootView; + } - } + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + @Override + public void onStart() { + reloadCursor(); + super.onStart(); + } - public void setOpenGroupPosition(int openGroupPosition) - { - mActivatedPositionGroup = openGroupPosition; + @Override + public void onResume() { + super.onResume(); + mExpandableListView.invalidateViews(); + startAutomaticRedraw(); + openSelectedChild(); + if (mTwoPaneLayout) { + setListViewScrollbarPositionLeft(true); + setActivateOnItemClick(true); } + } - - public void notifyDataSetChanged(boolean expandFirst) - { - getLoaderManager().restartLoader(-1, null, this); + @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(); + } - private Runnable setOpenHandler = new SafeFragmentUiRunnable(this, 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 - } - } + @Override + public void onSaveInstanceState(Bundle outState) { + if (!((TaskListActivity) getActivity()).isInTransientState()) { + mSavedExpandedGroups = mExpandableListView.getExpandedGroups(); } + super.onSaveInstanceState(outState); + } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + // create menu + inflater.inflate(R.menu.task_list_fragment_menu, menu); - 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); + // restore menu state + MenuItem item = menu.findItem(R.id.menu_show_completed); + if (item != null) { + item.setChecked(mSavedCompletedFilter); + } + } + + @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 SafeFragmentUiRunnable(this, () -> mAdapter.reloadLoadedGroups())); + } + + @Override + public void onLoaderReset(Loader loader) { + mAdapter.changeCursor(new MatrixCursor(new String[] {"_id"})); + } + + @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; + } + + Uri taskUri = + ContentUris.withAppendedId( + Instances.getContentUri(mAuthority), (long) TaskFieldAdapters.TASK_ID.get(cursor)); + Color taskListColor = new ValueColor(TaskFieldAdapters.LIST_COLOR.get(cursor)); + mCallbacks.onItemSelected(taskUri, taskListColor, force, mInstancePosition); + } + } + + /** prepares the update of the view after the group descriptor was changed */ + public void prepareReload() { + mAdapter = + new ExpandableGroupDescriptorAdapter( + new MatrixCursor(new String[] {"_id"}), + getActivity(), + getLoaderManager(), + mGroupDescriptor); + mExpandableListView.setAdapter(mAdapter); + mExpandableListView.setOnChildClickListener(mTaskItemClickListener); + mExpandableListView.setOnGroupCollapseListener(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(), R.style.customAlertDialog) + .setTitle(R.string.confirm_delete_title) + .setCancelable(true) + .setNegativeButton( + android.R.string.cancel, + (dialog, which) -> { + // nothing to do here + }) + .setPositiveButton( + android.R.string.ok, + (dialog, 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(); + mCallbacks.onItemRemoved(taskUri); + }) + .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. + */ + 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) { + long instanceId = cursor.getLong(cursor.getColumnIndex(Instances._ID)); + + 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(Instances.getContentUri(mAuthority), instanceId); + + 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); + } + + public void setListViewScrollbarPositionLeft(boolean left) { + 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); + } + + private Runnable setOpenHandler = + new SafeFragmentUiRunnable( + this, + new Runnable() { + @Override + public void run() { + selectChildView( + mExpandableListView, mActivatedPositionGroup, mActivatedPositionChild, false); + mExpandableListView.expandGroups(mSavedExpandedGroups); + setActivatedItem(mActivatedPositionGroup, mActivatedPositionChild); } - } - } - - - 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 SafeFragmentUiRunnable(this, () -> - { + }); + + 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 SafeFragmentUiRunnable( + this, + () -> { 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); + 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); - } + }), + 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()); + /** 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(); } - return -1; + } while (listCursor.moveToNext()); } + return -1; + } + private static class SelectChildTaskParams { + int groupPosition; + Uri taskUriToSelect; + Cursor childCursor; - 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; - } + 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; - private static class ListPosition - { - int groupPosition; - int childPosition; - int flatListPosition; - - - ListPosition(int groupPosition, int childPosition) - { - this.groupPosition = groupPosition; - this.childPosition = childPosition; - } + ListPosition(int groupPosition, int childPosition) { + this.groupPosition = groupPosition; + this.childPosition = childPosition; } + } + private class AsyncSelectChildTask extends AsyncTask { - 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]; - @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; + 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/TasksApplication.java b/opentasks/src/main/java/org/dmfs/tasks/TasksApplication.java index ccfc5d2f..7d657bd0 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/TasksApplication.java +++ b/opentasks/src/main/java/org/dmfs/tasks/TasksApplication.java @@ -20,36 +20,31 @@ import android.app.Application; import android.os.Process; import android.util.Log; - /** * The {@link Application} class for the app. * * @author Gabor Keszthelyi */ -public final class TasksApplication extends Application -{ - - private static final String TAG = "TasksApplication"; - - - @Override - public void onCreate() - { - super.onCreate(); - checkAppReplacingState(); - } - - - /* - * Fix for https://github.com/dmfs/opentasks/issues/383 - * with workaround suggested at https://issuetracker.google.com/issues/36972466#comment14 - */ - private void checkAppReplacingState() - { - if (getResources() == null) - { - Log.w(TAG, "App is replacing and getResources() found to be null, killing process. (Workaround for framework bug 36972466"); - Process.killProcess(Process.myPid()); - } +public final class TasksApplication extends Application { + + private static final String TAG = "TasksApplication"; + + @Override + public void onCreate() { + super.onCreate(); + checkAppReplacingState(); + } + + /* + * Fix for https://github.com/dmfs/opentasks/issues/383 + * with workaround suggested at https://issuetracker.google.com/issues/36972466#comment14 + */ + private void checkAppReplacingState() { + if (getResources() == null) { + Log.w( + TAG, + "App is replacing and getResources() found to be null, killing process. (Workaround for framework bug 36972466"); + Process.killProcess(Process.myPid()); } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java index 4862d53e..4a34b95d 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java @@ -23,140 +23,121 @@ import android.os.Handler; import android.view.MenuItem; import android.view.Window; import android.view.WindowManager; - +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; import org.dmfs.android.bolts.color.Color; import org.dmfs.android.bolts.color.colors.PrimaryColor; import org.dmfs.android.bolts.color.elementary.ValueColor; import org.dmfs.tasks.model.ContentSet; import org.dmfs.tasks.utils.BaseActivity; -import org.dmfs.tasks.utils.colors.DarkenedForStatusBar; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; - /** - * An activity representing a single Task detail screen. This activity is only used on handset devices. On tablet-size devices, item details are presented - * side-by-side with a list of items in a {@link TaskListActivity}. - *

    - * This activity is mostly just a 'shell' activity containing nothing more than a {@link ViewTaskFragment}. - *

    + * An activity representing a single Task detail screen. This activity is only used on handset + * devices. On tablet-size devices, item details are presented side-by-side with a list of items in + * a {@link TaskListActivity}. + * + *

    This activity is mostly just a 'shell' activity containing nothing more than a {@link + * ViewTaskFragment}. */ -public class ViewTaskActivity extends BaseActivity implements ViewTaskFragment.Callback -{ - - /** - * The {@link ColorInt} the toolbars should take while loading the task. Optional parameter. - * {@link android.graphics.Color#TRANSPARENT} also means absent. - */ - public static final String EXTRA_COLOR = "color"; - - - @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) - { - int color = getIntent().getIntExtra(EXTRA_COLOR, 0); - ViewTaskFragment fragment = ViewTaskFragment.newInstance( - getIntent().getData(), color != 0 ? new ValueColor(color) : new PrimaryColor(this)); - getSupportFragmentManager().beginTransaction().add(R.id.task_detail_container, fragment).commit(); - } +public class ViewTaskActivity extends BaseActivity implements ViewTaskFragment.Callback { + + /** + * The {@link ColorInt} the toolbars should take while loading the task. Optional parameter. + * {@link android.graphics.Color#TRANSPARENT} also means absent. + */ + public static final String EXTRA_COLOR = "color"; + + @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; } - - @Override - public void onAttachFragment(Fragment fragment) - { - if (fragment instanceof ViewTaskFragment) - { - final ViewTaskFragment detailFragment = (ViewTaskFragment) fragment; - new Handler().post(new Runnable() - { + if (savedInstanceState == null) { + int color = getIntent().getIntExtra(EXTRA_COLOR, 0); + ViewTaskFragment fragment = + ViewTaskFragment.newInstance( + getIntent().getData(), color != 0 ? new ValueColor(color) : new PrimaryColor(this)); + 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); + 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 onTaskEditRequested(@NonNull 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 onTaskDeleted(@NonNull Uri taskUri) - { - // The task we're showing has been deleted, just finish. - finish(); + }); } - - - @Override - public void onTaskCompleted(@NonNull Uri taskUri) - { - // The task we're showing has been completed, just finish. + } + + @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; } - - - @Override - public void onListColorLoaded(@NonNull Color color) - { - Window window = getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - window.setStatusBarColor(color.argb()); + return super.onOptionsItemSelected(item); + } + + @Override + public void onTaskEditRequested(@NonNull 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 onTaskDeleted(@NonNull Uri taskUri) { + // The task we're showing has been deleted, just finish. + finish(); + } + + @Override + public void onTaskCompleted(@NonNull Uri taskUri) { + // The task we're showing has been completed, just finish. + finish(); + } + + @Override + public void onListColorLoaded(@NonNull Color color) { + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(color.argb()); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java index 26a6f341..4c09a7f9 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java @@ -40,12 +40,20 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.ShareActionProvider; +import androidx.appcompat.widget.Toolbar; +import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.app.ActivityCompat; +import androidx.core.view.MenuItemCompat; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; - +import java.util.concurrent.atomic.AtomicReference; import org.dmfs.android.bolts.color.Color; import org.dmfs.android.bolts.color.elementary.ValueColor; import org.dmfs.android.contentpal.Operation; @@ -77,828 +85,713 @@ import org.dmfs.tasks.utils.SafeFragmentUiRunnable; import org.dmfs.tasks.utils.colors.AdjustedForFab; import org.dmfs.tasks.widget.TaskView; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.ShareActionProvider; -import androidx.appcompat.widget.Toolbar; -import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.app.ActivityCompat; -import androidx.core.view.MenuItemCompat; - - /** - * 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. + * 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 -{ - private final static String ARG_URI = "uri"; - private static final String ARG_STARTING_COLOR = "starting_color"; - - /** - * The {@link ContentValueMapper} that knows how to map the values in a cursor to {@link ContentValues}. - */ - - private 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, Tasks.RRULE, Tasks.RDATE) - .addInteger(Tasks.PRIORITY, Tasks.LIST_COLOR, Tasks.TASK_COLOR, Tasks.STATUS, Tasks.CLASSIFICATION, Tasks.PERCENT_COMPLETE, Tasks.IS_ALLDAY, - Tasks.IS_CLOSED, Tasks.PINNED, TaskContract.Instances.IS_RECURRING) - .addLong(Tasks.LIST_ID, Tasks.DTSTART, Tasks.DUE, Tasks.COMPLETED, Tasks._ID, Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Instances.TASK_ID); - - 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; - + implements OnModelLoadedListener, + OnContentChangeListener, + OnMenuItemClickListener, + OnOffsetChangedListener { + private static final String ARG_URI = "uri"; + private static final String ARG_STARTING_COLOR = "starting_color"; + + /** + * The {@link ContentValueMapper} that knows how to map the values in a cursor to {@link + * ContentValues}. + */ + private 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, + Tasks.RRULE, + Tasks.RDATE) + .addInteger( + Tasks.PRIORITY, + Tasks.LIST_COLOR, + Tasks.TASK_COLOR, + Tasks.STATUS, + Tasks.CLASSIFICATION, + Tasks.PERCENT_COMPLETE, + Tasks.IS_ALLDAY, + Tasks.IS_CLOSED, + Tasks.PINNED, + TaskContract.Instances.IS_RECURRING) + .addLong( + Tasks.LIST_ID, + Tasks.DTSTART, + Tasks.DUE, + Tasks.COMPLETED, + Tasks._ID, + Tasks.ORIGINAL_INSTANCE_ID, + TaskContract.Instances.TASK_ID); + + 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; + + public interface Callback { /** - * The actual detail view. We store this direct reference to be able to clear it when the fragment gets detached. + * Called when user pressed 'edit' for the 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. */ - private TaskView mDetailView; + void onTaskEditRequested(@NonNull Uri taskUri, @Nullable ContentSet data); - private int mListColor; - private int mOldStatus = -1; - private boolean mPinned = false; - private boolean mRestored; - private AppBarLayout mAppBar; - private Toolbar mToolBar; - private View mRootView; + /** Called when the task has been deleted by the user. */ + void onTaskDeleted(@NonNull Uri taskUri); - private int mAppBarOffset = 0; - - private FloatingActionButton mFloatingActionButton; + /** Called when the task has been marked completed by the user. */ + void onTaskCompleted(@NonNull Uri taskUri); /** - * A {@link Callback} to the activity. + * Notifies the listener about the list color of the current task. + * + * @param color The color. */ - private Callback mCallback; - - private boolean mShowFloatingActionButton = false; - - private boolean mIsTheTitleContainerVisible = true; - - - public interface Callback - { - /** - * Called when user pressed 'edit' for the 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. - */ - void onTaskEditRequested(@NonNull Uri taskUri, @Nullable ContentSet data); - - /** - * Called when the task has been deleted by the user. - */ - void onTaskDeleted(@NonNull Uri taskUri); - - /** - * Called when the task has been marked completed by the user. - */ - void onTaskCompleted(@NonNull Uri taskUri); - - /** - * Notifies the listener about the list color of the current task. - * - * @param color - * The color. - */ - void onListColorLoaded(@NonNull Color color); + void onListColorLoaded(@NonNull Color color); + } + + /** + * @param taskContentUri the content uri of the task to display + * @param startingColor The color that is used for the toolbars until the actual task color is + * loaded. (If available provide the actual task list color, otherwise the primary color.) + */ + public static ViewTaskFragment newInstance( + @NonNull Uri taskContentUri, @NonNull Color startingColor) { + ViewTaskFragment fragment = new ViewTaskFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_URI, taskContentUri); + args.putInt(ARG_STARTING_COLOR, startingColor.argb()); + fragment.setArguments(args); + return fragment; + } + + @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(); + } - /** - * @param taskContentUri - * the content uri of the task to display - * @param startingColor - * The color that is used for the toolbars until the actual task color is loaded. (If available provide the actual task list color, otherwise the - * primary color.) - */ - public static ViewTaskFragment newInstance(@NonNull Uri taskContentUri, @NonNull Color startingColor) - { - ViewTaskFragment fragment = new ViewTaskFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_URI, taskContentUri); - args.putInt(ARG_STARTING_COLOR, startingColor.argb()); - fragment.setArguments(args); - return fragment; + @Override + public void onDestroyView() { + super.onDestroyView(); + // remove listener + if (mContentSet != null) { + mContentSet.removeOnChangeListener(this, null); } - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - setHasOptionsMenu(true); + if (mTaskUri != null) { + mAppContext.getContentResolver().unregisterContentObserver(mObserver); } - - @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(); + if (mDetailView != null) { + // remove values, to ensure all listeners get released + mDetailView.setValues(null); } - - - @Override - public void onDestroyView() - { - super.onDestroyView(); - // remove listener - if (mContentSet != null) - { - mContentSet.removeOnChangeListener(this, null); - } - - if (mTaskUri != null) - { - mAppContext.getContentResolver().unregisterContentObserver(mObserver); - } - - 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.has_two_panes); + + mRootView = inflater.inflate(R.layout.fragment_task_view_detail, container, false); + mContent = (ViewGroup) mRootView.findViewById(R.id.content); + mDetailView = (TaskView) inflater.inflate(R.layout.task_view, mContent, false); + mContent.addView(mDetailView); + 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(v -> completeTask()); + + // Update the toolbar color until the actual is loaded for the task + + mListColor = new ValueColor(getArguments().getInt(ARG_STARTING_COLOR)).argb(); + updateColor(); + + 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 View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) - { - mShowFloatingActionButton = !getResources().getBoolean(R.bool.has_two_panes); + @Override + public void onPause() { + super.onPause(); + persistTask(); + } - mRootView = inflater.inflate(R.layout.fragment_task_view_detail, container, false); - mContent = (ViewGroup) mRootView.findViewById(R.id.content); - mDetailView = (TaskView) inflater.inflate(R.layout.task_view, mContent, false); - mContent.addView(mDetailView); - 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(v -> completeTask()); - - // Update the toolbar color until the actual is loaded for the task - - mListColor = new ValueColor(getArguments().getInt(ARG_STARTING_COLOR)).argb(); - updateColor(); - - 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); - } + private void persistTask() { + Activity activity = getActivity(); + if (mContentSet != null && activity != null) { + if (mDetailView != null) { + mDetailView.updateValues(); + } - return mRootView; + if (mContentSet.isUpdate()) { + mContentSet.persist(activity); + ActivityCompat.invalidateOptionsMenu(activity); + } } - - - @Override - public void onPause() - { - super.onPause(); - persistTask(); + } + + /* + TODO Refactor, simplify ViewTaskFragment now that it is only for displaying a single task once. + Ticket for this: https://github.com/dmfs/opentasks/issues/628 + + Earlier this Fragment was responsible for displaying no task (empty content) + and also updating itself to show a newly selected one, using this loadUri() method which was public at the time. + After refactorings, the Fragment is now only responsible to load an existing task once, for the task uri that is received in the args. + As a result this class can now be simplified, for example potentially removing all uri == null checks. + */ + + /** + * 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. + */ + private void loadUri(Uri uri) { + showFloatingActionButton(false); + + if (mTaskUri != null) { + /* + * Unregister the observer for any previously shown task first. + */ + mAppContext.getContentResolver().unregisterContentObserver(mObserver); + 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); - } - } + 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 { + /* + * Immediately update the view with the empty task uri, i.e. clear the view. + */ + mContentSet = null; + if (mContent != null) { + mContent.removeAllViews(); + } } - - /* - TODO Refactor, simplify ViewTaskFragment now that it is only for displaying a single task once. - Ticket for this: https://github.com/dmfs/opentasks/issues/628 - - Earlier this Fragment was responsible for displaying no task (empty content) - and also updating itself to show a newly selected one, using this loadUri() method which was public at the time. - After refactorings, the Fragment is now only responsible to load an existing task once, for the task uri that is received in the args. - As a result this class can now be simplified, for example potentially removing all uri == null checks. - */ - - - /** - * 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. - */ - private 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) - { - /* - * 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 - { - /* - * 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)) - { - /* - * getActivity().invalidateOptionsMenu() doesn't work in Android 2.x so use the compat lib - */ - ActivityCompat.invalidateOptionsMenu(getActivity()); - } - - mAppBar.setExpanded(true, false); + 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); - /** - * 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)); - } - } + 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(new SafeFragmentUiRunnable(this, this::updateView)); - } + /** Update the view. This doesn't call {@link #updateView()} right away, instead it posts it. */ + private void postUpdateView() { + if (mContent != null) { + mContent.post(new SafeFragmentUiRunnable(this, this::updateView)); } + } - - @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 onModelLoaded(Model model) { + if (model == null) { + return; } - - @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); - } - } - - for(int i = 0; i < menu.size(); i++){ - Drawable drawable = menu.getItem(i).getIcon(); - if(drawable != null) { - drawable.mutate(); - drawable.setColorFilter(getResources().getColor(android.R.color.white), PorterDuff.Mode.SRC_ATOP); - } - } - } + // 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 boolean onMenuItemClick(MenuItem item) - { - return onOptionsItemSelected(item); + @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); + } + } + + for (int i = 0; i < menu.size(); i++) { + Drawable drawable = menu.getItem(i).getIcon(); + if (drawable != null) { + drawable.mutate(); + drawable.setColorFilter( + getResources().getColor(android.R.color.white), PorterDuff.Mode.SRC_ATOP); + } + } } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - mDetailView.updateValues(); - - int itemId = item.getItemId(); - if (itemId == R.id.edit_task) - { - // open editor for this task - mCallback.onTaskEditRequested(mTaskUri, mContentSet); - return true; - } - else if (itemId == R.id.delete_task) - { - long originalInstanceId = new Backed<>(TaskFieldAdapters.ORIGINAL_INSTANCE_ID.get(mContentSet), () -> - Long.valueOf(TaskFieldAdapters.INSTANCE_TASK_ID.get(mContentSet))).value(); - boolean isRecurring = TaskFieldAdapters.IS_RECURRING_INSTANCE.get(mContentSet); - AtomicReference> operation = new AtomicReference<>( - new BulkDelete<>( - new InstanceTable(mTaskUri.getAuthority()), - new IdIn<>(mTaskUri.getLastPathSegment()))); - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.customAlertDialog) - .setCancelable(true) - .setNegativeButton(android.R.string.cancel, (dialog, which) -> { - // nothing to do here - }) - .setTitle(isRecurring ? R.string.opentasks_task_details_delete_recurring_task : R.string.confirm_delete_title) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - if (mContentSet != null) - { - // TODO: remove the task in a background task - try - { - new BaseTransaction() - .with(new PresentValues<>(new NullSafe<>(operation.get()))) - .commit(getContext().getContentResolver().acquireContentProviderClient(mTaskUri)); - } - catch (RemoteException | OperationApplicationException e) - { - Log.e(ViewTaskFragment.class.getSimpleName(), "Unable to delete task ", e); - } - - mCallback.onTaskDeleted(mTaskUri); - mTaskUri = null; - } - }); - if (isRecurring) - { - builder.setSingleChoiceItems( - new CharSequence[] { - getString(R.string.opentasks_task_details_delete_this_task), - getString(R.string.opentasks_task_details_delete_all_tasks) - }, - 0, - (dialog, which) -> { - switch (which) - { - case 0: - operation.set(new BulkDelete<>( - new InstanceTable(mTaskUri.getAuthority()), - new IdIn<>(mTaskUri.getLastPathSegment()))); - case 1: - operation.set(new BulkDelete<>( - new TasksTable(mTaskUri.getAuthority()), - new AnyOf<>( - new IdIn<>(originalInstanceId), - new EqArg<>(Tasks.ORIGINAL_INSTANCE_ID, originalInstanceId)))); - - } - }); - } - else - { - builder.setMessage(R.string.confirm_delete_message); - } - builder.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); - ActionService.startAction(getActivity(), ActionService.ACTION_UNPIN, mTaskUri); - } - else - { - item.setIcon(R.drawable.ic_pin_off_white_24dp); - ActionService.startAction(getActivity(), ActionService.ACTION_PIN_TASK, mTaskUri); - } - persistTask(); - return true; - } - else if (itemId == R.id.opentasks_send_task) - - { - setSendMenuIntent(); - return false; - } - else - - { - return super.onOptionsItemSelected(item); - } - + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + return onOptionsItemSelected(item); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + mDetailView.updateValues(); + + int itemId = item.getItemId(); + if (itemId == R.id.edit_task) { + // open editor for this task + mCallback.onTaskEditRequested(mTaskUri, mContentSet); + return true; + } else if (itemId == R.id.delete_task) { + long originalInstanceId = + new Backed<>( + TaskFieldAdapters.ORIGINAL_INSTANCE_ID.get(mContentSet), + () -> Long.valueOf(TaskFieldAdapters.INSTANCE_TASK_ID.get(mContentSet))) + .value(); + boolean isRecurring = TaskFieldAdapters.IS_RECURRING_INSTANCE.get(mContentSet); + AtomicReference> operation = + new AtomicReference<>( + new BulkDelete<>( + new InstanceTable(mTaskUri.getAuthority()), + new IdIn<>(mTaskUri.getLastPathSegment()))); + AlertDialog.Builder builder = + new AlertDialog.Builder(getActivity(), R.style.customAlertDialog) + .setCancelable(true) + .setNegativeButton( + android.R.string.cancel, + (dialog, which) -> { + // nothing to do here + }) + .setTitle( + isRecurring + ? R.string.opentasks_task_details_delete_recurring_task + : R.string.confirm_delete_title) + .setPositiveButton( + android.R.string.ok, + (dialog, which) -> { + if (mContentSet != null) { + // TODO: remove the task in a background task + try { + new BaseTransaction() + .with(new PresentValues<>(new NullSafe<>(operation.get()))) + .commit( + getContext() + .getContentResolver() + .acquireContentProviderClient(mTaskUri)); + } catch (RemoteException | OperationApplicationException e) { + Log.e(ViewTaskFragment.class.getSimpleName(), "Unable to delete task ", e); + } + + mCallback.onTaskDeleted(mTaskUri); + mTaskUri = null; + } + }); + if (isRecurring) { + builder.setSingleChoiceItems( + new CharSequence[] { + getString(R.string.opentasks_task_details_delete_this_task), + getString(R.string.opentasks_task_details_delete_all_tasks) + }, + 0, + (dialog, which) -> { + switch (which) { + case 0: + operation.set( + new BulkDelete<>( + new InstanceTable(mTaskUri.getAuthority()), + new IdIn<>(mTaskUri.getLastPathSegment()))); + case 1: + operation.set( + new BulkDelete<>( + new TasksTable(mTaskUri.getAuthority()), + new AnyOf<>( + new IdIn<>(originalInstanceId), + new EqArg<>(Tasks.ORIGINAL_INSTANCE_ID, originalInstanceId)))); + } + }); + } else { + builder.setMessage(R.string.confirm_delete_message); + } + builder.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); + ActionService.startAction(getActivity(), ActionService.ACTION_UNPIN, mTaskUri); + } else { + item.setIcon(R.drawable.ic_pin_off_white_24dp); + ActionService.startAction(getActivity(), ActionService.ACTION_PIN_TASK, mTaskUri); + } + persistTask(); + return true; + } else if (itemId == R.id.opentasks_send_task) { + + setSendMenuIntent(); + return false; + } else { + return super.onOptionsItemSelected(item); } - - - private void setSendMenuIntent() - { - if (mContentSet != null && mModel != null && mToolBar != null && mToolBar.getMenu() != null) - { - MenuItem shareItem = mToolBar.getMenu().findItem(R.id.opentasks_send_task); - if (shareItem != null) - { - ShareActionProvider actionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(shareItem); - Intent shareIntent = new ShareIntentFactory().create(mContentSet, mModel, mAppContext); - actionProvider.setShareIntent(shareIntent); - } - } + } + + private void setSendMenuIntent() { + if (mContentSet != null && mModel != null && mToolBar != null && mToolBar.getMenu() != null) { + MenuItem shareItem = mToolBar.getMenu().findItem(R.id.opentasks_send_task); + if (shareItem != null) { + ShareActionProvider actionProvider = + (ShareActionProvider) MenuItemCompat.getActionProvider(shareItem); + Intent shareIntent = new ShareIntentFactory().create(mContentSet, mModel, mAppContext); + actionProvider.setShareIntent(shareIntent); + } } - - - /** - * 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(); - mCallback.onTaskCompleted(mTaskUri); - if (mShowFloatingActionButton) - { - // hide fab in two pane mode - mFloatingActionButton.hide(); - } + } + + /** 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(); + mCallback.onTaskCompleted(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) - { - mFloatingActionButton.setBackgroundTintList(ColorStateList.valueOf(new AdjustedForFab(mListColor).argb())); - } + } + + @SuppressLint("NewApi") + private void updateColor() { + mAppBar.setBackgroundColor(mListColor); + if (mShowFloatingActionButton && mFloatingActionButton.getVisibility() == View.VISIBLE) { + mFloatingActionButton.setBackgroundTintList( + ColorStateList.valueOf(new AdjustedForFab(mListColor).argb())); } - - - @SuppressLint("NewApi") - @Override - public void onContentLoaded(ContentSet contentSet) - { - if (contentSet.containsKey(Tasks.ACCOUNT_TYPE)) - { - mListColor = TaskFieldAdapters.LIST_COLOR.get(contentSet); - ((Callback) getActivity()).onListColorLoaded(new ValueColor(mListColor)); - - 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(); - } - } + } + + @SuppressLint("NewApi") + @Override + public void onContentLoaded(ContentSet contentSet) { + if (contentSet.containsKey(Tasks.ACCOUNT_TYPE)) { + mListColor = TaskFieldAdapters.LIST_COLOR.get(contentSet); + ((Callback) getActivity()).onListColorLoaded(new ValueColor(mListColor)); + + 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) - { + /** 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 && mTaskUri != null) - { - // reload the task - mContentSet.update(mAppContext, CONTENT_VALUE_MAPPER); - } + public void onChange(boolean selfChange) { + if (mContentSet != null && mTaskUri != null) { + // reload the task + mContentSet.update(mAppContext, CONTENT_VALUE_MAPPER); + } } - }; + }; + @Override + public void onContentChanged(ContentSet contentSet) {} - @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); + } - private boolean hasNewStatus(int newStatus) - { - return (mOldStatus != -1 && mOldStatus != newStatus || mOldStatus == -1 && TaskFieldAdapters.IS_CLOSED.get(mContentSet)); - } + @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); - private boolean pinChanged(boolean newPinned) - { - return !(mPinned == newPinned); + if (mIsTheTitleContainerVisible) { + mAppBar.findViewById(R.id.toolbar_content).setAlpha(1 - percentage); } - - - @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) - { - 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 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); + } + + 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(androidx.appcompat.app.AppCompatActivity activty) { + if (mToolBar == null) { + return; } - - /** - * Set the toolbar of this fragment (if any), as the ActionBar if the given Activity. - * - * @param activty - * an {@link AppCompatActivity}. - */ - public void setupToolbarAsActionbar(androidx.appcompat.app.AppCompatActivity activty) - { - if (mToolBar == null) - { - return; - } - - activty.setSupportActionBar(mToolBar); - 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.show(); - // make sure the FAB has the right color - updateColor(); - } - else - { - p.setAnchorId(View.NO_ID); - mFloatingActionButton.setLayoutParams(p); - mFloatingActionButton.hide(); - } + activty.setSupportActionBar(mToolBar); + 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.show(); + // make sure the FAB has the right color + updateColor(); + } else { + p.setAnchorId(View.NO_ID); + mFloatingActionButton.setLayoutParams(p); + mFloatingActionButton.hide(); } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/CancelDelayedAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/CancelDelayedAction.java index f699272f..26ebf0ed 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/CancelDelayedAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/CancelDelayedAction.java @@ -25,36 +25,35 @@ import android.content.Intent; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.notification.ActionReceiver; - /** * A {@link TaskAction} which cancels the delayed execution of another {@link TaskAction}. * * @author Marten Gajda */ -public final class CancelDelayedAction implements TaskAction -{ - private final String mAction; - - - public CancelDelayedAction(String action) - { - mAction = action; - } - - - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) throws RemoteException, OperationApplicationException - { - ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).cancel( - PendingIntent.getBroadcast( - context, - (int) ContentUris.parseId(taskUri), - new Intent(context, ActionReceiver.class).setAction(mAction).setData(taskUri), - PendingIntent.FLAG_UPDATE_CURRENT)); - } +public final class CancelDelayedAction implements TaskAction { + private final String mAction; + + public CancelDelayedAction(String action) { + mAction = action; + } + + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) + throws RemoteException, OperationApplicationException { + ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)) + .cancel( + PendingIntent.getBroadcast( + context, + (int) ContentUris.parseId(taskUri), + new Intent(context, ActionReceiver.class).setAction(mAction).setData(taskUri), + PendingIntent.FLAG_UPDATE_CURRENT)); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/CancelNotificationAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/CancelNotificationAction.java index f3fdde11..50e613ee 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/CancelNotificationAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/CancelNotificationAction.java @@ -21,37 +21,33 @@ import android.content.ContentUris; import android.content.Context; import android.net.Uri; import androidx.core.app.NotificationManagerCompat; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAction} which cancels a notification. * * @author Marten Gajda */ -public final class CancelNotificationAction implements TaskAction -{ - - private final String mNotificationTag; - - - public CancelNotificationAction() - { - this("tasks"); - } - - - public CancelNotificationAction(String notificationTag) - { - mNotificationTag = notificationTag; - } - - - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) - { - NotificationManagerCompat.from(context).cancel(mNotificationTag, (int) ContentUris.parseId(taskUri)); - } +public final class CancelNotificationAction implements TaskAction { + + private final String mNotificationTag; + + public CancelNotificationAction() { + this("tasks"); + } + + public CancelNotificationAction(String notificationTag) { + mNotificationTag = notificationTag; + } + + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) { + NotificationManagerCompat.from(context) + .cancel(mNotificationTag, (int) ContentUris.parseId(taskUri)); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/CompleteAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/CompleteAction.java index a57de67b..b6f36b17 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/CompleteAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/CompleteAction.java @@ -19,16 +19,13 @@ package org.dmfs.tasks.actions; import org.dmfs.opentaskspal.tasks.StatusData; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAction} which completes a task. * * @author Marten Gajda */ -public final class CompleteAction extends DelegatingTaskAction -{ - public CompleteAction() - { - super(new UpdateAction((snapshot) -> new StatusData<>(TaskContract.Tasks.STATUS_COMPLETED))); - } +public final class CompleteAction extends DelegatingTaskAction { + public CompleteAction() { + super(new UpdateAction((snapshot) -> new StatusData<>(TaskContract.Tasks.STATUS_COMPLETED))); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/Composite.java b/opentasks/src/main/java/org/dmfs/tasks/actions/Composite.java index dd3f8054..11dba77a 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/Composite.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/Composite.java @@ -21,40 +21,35 @@ import android.content.Context; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.iterables.elementary.Seq; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAction} which is composed of other {@link TaskAction}s. * * @author Marten Gajda */ -public final class Composite implements TaskAction -{ - private final Iterable mDelegates; - - - public Composite(TaskAction... delegates) - { - this(new Seq<>(delegates)); - } - - - public Composite(Iterable delegates) - { - mDelegates = delegates; - } - - - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) throws RemoteException, OperationApplicationException - { - for (TaskAction action : mDelegates) - { - action.execute(context, contentProviderClient, rowSnapshot, taskUri); - } +public final class Composite implements TaskAction { + private final Iterable mDelegates; + + public Composite(TaskAction... delegates) { + this(new Seq<>(delegates)); + } + + public Composite(Iterable delegates) { + mDelegates = delegates; + } + + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) + throws RemoteException, OperationApplicationException { + for (TaskAction action : mDelegates) { + action.execute(context, contentProviderClient, rowSnapshot, taskUri); } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/Conditional.java b/opentasks/src/main/java/org/dmfs/tasks/actions/Conditional.java index 11beab49..b9674f9b 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/Conditional.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/Conditional.java @@ -21,36 +21,37 @@ import android.content.Context; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.jems.function.BiFunction; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAction} which is only executed if a test function returns {@code true}. * * @author Marten Gajda */ -public final class Conditional implements TaskAction -{ - private final BiFunction, Boolean> mTestFunction; - private final TaskAction mDelegate; - - - public Conditional(BiFunction, Boolean> testFunction, TaskAction delegate) - { - mTestFunction = testFunction; - mDelegate = delegate; - } - - - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot data, Uri taskUri) throws RemoteException, OperationApplicationException - { - if (mTestFunction.value(context, data)) - { - mDelegate.execute(context, contentProviderClient, data, taskUri); - } +public final class Conditional implements TaskAction { + private final BiFunction, Boolean> + mTestFunction; + private final TaskAction mDelegate; + + public Conditional( + BiFunction, Boolean> + testFunction, + TaskAction delegate) { + mTestFunction = testFunction; + mDelegate = delegate; + } + + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot data, + Uri taskUri) + throws RemoteException, OperationApplicationException { + if (mTestFunction.value(context, data)) { + mDelegate.execute(context, contentProviderClient, data, taskUri); } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/DeferDueAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/DeferDueAction.java index 8d86da00..3ae5fd04 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/DeferDueAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/DeferDueAction.java @@ -25,26 +25,23 @@ import org.dmfs.rfc5545.DateTime; import org.dmfs.rfc5545.Duration; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAction} which defers the due date of a task by a given {@link Duration}. * * @author Marten Gajda */ -public final class DeferDueAction extends DelegatingTaskAction -{ - public DeferDueAction(Duration duration) - { - super(new UpdateAction((data) -> { - Optional start = new TaskDateTime(TaskContract.Tasks.DTSTART, data); - if (start.isPresent()) - { - return new TimeData<>(start.value(), new EffectiveDueDate(data).value().addDuration(duration)); - } - else - { +public final class DeferDueAction extends DelegatingTaskAction { + public DeferDueAction(Duration duration) { + super( + new UpdateAction( + (data) -> { + Optional start = new TaskDateTime(TaskContract.Tasks.DTSTART, data); + if (start.isPresent()) { + return new TimeData<>( + start.value(), new EffectiveDueDate(data).value().addDuration(duration)); + } else { return new DueData<>(new EffectiveDueDate(data).value().addDuration(duration)); - } - })); - } + } + })); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/DelayedAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/DelayedAction.java index 7ffa3859..6a3da291 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/DelayedAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/DelayedAction.java @@ -26,41 +26,39 @@ import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; import androidx.core.app.AlarmManagerCompat; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.notification.ActionReceiver; - /** * A {@link TaskAction} which executes another action after a given time. * * @author Marten Gajda */ -public final class DelayedAction implements TaskAction -{ - private final String mAction; - private final int mDelayMillis; - - - public DelayedAction(String action, int delayMillis) - { - mAction = action; - mDelayMillis = delayMillis; - } +public final class DelayedAction implements TaskAction { + private final String mAction; + private final int mDelayMillis; + public DelayedAction(String action, int delayMillis) { + mAction = action; + mDelayMillis = delayMillis; + } - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) throws RemoteException, OperationApplicationException - { - AlarmManagerCompat.setExactAndAllowWhileIdle( - (AlarmManager) context.getSystemService(Context.ALARM_SERVICE), - AlarmManager.RTC_WAKEUP, - System.currentTimeMillis() + mDelayMillis, - PendingIntent.getBroadcast( - context, - (int) ContentUris.parseId(taskUri), - new Intent(context, ActionReceiver.class).setAction(mAction).setData(taskUri), - PendingIntent.FLAG_UPDATE_CURRENT)); - } + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) + throws RemoteException, OperationApplicationException { + AlarmManagerCompat.setExactAndAllowWhileIdle( + (AlarmManager) context.getSystemService(Context.ALARM_SERVICE), + AlarmManager.RTC_WAKEUP, + System.currentTimeMillis() + mDelayMillis, + PendingIntent.getBroadcast( + context, + (int) ContentUris.parseId(taskUri), + new Intent(context, ActionReceiver.class).setAction(mAction).setData(taskUri), + PendingIntent.FLAG_UPDATE_CURRENT)); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/DelegatingTaskAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/DelegatingTaskAction.java index 733ee0fa..6ec3a3a8 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/DelegatingTaskAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/DelegatingTaskAction.java @@ -21,29 +21,28 @@ import android.content.Context; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.tasks.contract.TaskContract; - /** - * An abstract {@link TaskAction} which only delegates to another {@link TaskAction}. This is meant to easy composition. + * An abstract {@link TaskAction} which only delegates to another {@link TaskAction}. This is meant + * to easy composition. * * @author Marten Gajda */ -public abstract class DelegatingTaskAction implements TaskAction -{ - private final TaskAction mDelegate; - - - protected DelegatingTaskAction(TaskAction delegate) - { - mDelegate = delegate; - } - - - public final void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) throws RemoteException, OperationApplicationException - { - mDelegate.execute(context, contentProviderClient, rowSnapshot, taskUri); - } +public abstract class DelegatingTaskAction implements TaskAction { + private final TaskAction mDelegate; + + protected DelegatingTaskAction(TaskAction delegate) { + mDelegate = delegate; + } + + public final void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) + throws RemoteException, OperationApplicationException { + mDelegate.execute(context, contentProviderClient, rowSnapshot, taskUri); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyAction.java index e5648370..1a5ca2c1 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyAction.java @@ -28,7 +28,9 @@ import android.net.Uri; import android.os.Build; import android.os.RemoteException; import android.text.format.DateUtils; - +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; import org.dmfs.android.bolts.color.colors.AttributeColor; import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.jems.function.Function; @@ -50,183 +52,202 @@ import org.dmfs.tasks.notification.ActionService; import org.dmfs.tasks.notification.signals.Conditional; import org.dmfs.tasks.utils.DateFormatter; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - - /** * Post a notification. - *

    - * TODO: refactor the notification construction, use composition + * + *

    TODO: refactor the notification construction, use composition * * @author Marten Gajda */ -public final class NotifyAction implements TaskAction -{ - private final String GROUP_ALERTS = "org.dmfs.tasks.ALERTS"; - private final String GROUP_PINS = "org.dmfs.tasks.PINS"; - - private final Function, String> mChannelFunction; - private final boolean mRepost; - - - public NotifyAction(Function, String> channelFunction, boolean repost) - { - mChannelFunction = channelFunction; - mRepost = repost; +public final class NotifyAction implements TaskAction { + private final String GROUP_ALERTS = "org.dmfs.tasks.ALERTS"; + private final String GROUP_PINS = "org.dmfs.tasks.PINS"; + + private final Function, String> + mChannelFunction; + private final boolean mRepost; + + public NotifyAction( + Function, String> channelFunction, + boolean repost) { + mChannelFunction = channelFunction; + mRepost = repost; + } + + private static void createChannels(Context context) { + if (Build.VERSION.SDK_INT >= 26) { + NotificationManager nm = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationChannel pinnedChannel = + new NotificationChannel( + ActionService.CHANNEL_PINNED, + context.getString(R.string.opentasks_notification_channel_pinned_tasks), + NotificationManager.IMPORTANCE_DEFAULT); + // pinned Notifications should not get a badge + pinnedChannel.setShowBadge(true); + pinnedChannel.enableLights(false); + pinnedChannel.enableVibration(true); + pinnedChannel.setVibrationPattern(new long[] {0, 100, 100, 100, 0}); + pinnedChannel.setSound(null, null); + nm.createNotificationChannel(pinnedChannel); + + NotificationChannel dueDates = + new NotificationChannel( + ActionService.CHANNEL_DUE_DATES, + context.getString(R.string.opentasks_notification_channel_due_dates), + NotificationManager.IMPORTANCE_HIGH); + dueDates.setShowBadge(true); + dueDates.enableLights(true); + dueDates.enableVibration(true); + nm.createNotificationChannel(dueDates); } - - - private static void createChannels(Context context) - { - if (Build.VERSION.SDK_INT >= 26) - { - NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - NotificationChannel pinnedChannel = new NotificationChannel(ActionService.CHANNEL_PINNED, - context.getString(R.string.opentasks_notification_channel_pinned_tasks), - NotificationManager.IMPORTANCE_DEFAULT); - // pinned Notifications should not get a badge - pinnedChannel.setShowBadge(true); - pinnedChannel.enableLights(false); - pinnedChannel.enableVibration(true); - pinnedChannel.setVibrationPattern(new long[] { 0, 100, 100, 100, 0 }); - pinnedChannel.setSound(null, null); - nm.createNotificationChannel(pinnedChannel); - - NotificationChannel dueDates = new NotificationChannel(ActionService.CHANNEL_DUE_DATES, - context.getString(R.string.opentasks_notification_channel_due_dates), NotificationManager.IMPORTANCE_HIGH); - dueDates.setShowBadge(true); - dueDates.enableLights(true); - dueDates.enableVibration(true); - nm.createNotificationChannel(dueDates); - } + } + + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot data, + Uri taskUri) + throws RemoteException, OperationApplicationException { + // TODO: move to central place, for now we keep it here to be sure we have created the channels + createChannels(context); + + Optional title = new TaskTitle(data); + boolean pin = new TaskPin(data).value(); + int notificationId = (int) ContentUris.parseId(taskUri); + + // build notification + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context, mChannelFunction.value(data)) + .setSmallIcon(pin ? R.drawable.ic_pin_white_24dp : R.drawable.ic_notification) + .setContentTitle(new Backed<>(title, "Untitled Task").value()) + .setContentText(contentText(context, data)); + if (mRepost) { + builder.setTicker(new Backed<>(title, "Untitled Task").value()); } - - - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot data, Uri taskUri) throws RemoteException, OperationApplicationException - { - // TODO: move to central place, for now we keep it here to be sure we have created the channels - createChannels(context); - - Optional title = new TaskTitle(data); - boolean pin = new TaskPin(data).value(); - int notificationId = (int) ContentUris.parseId(taskUri); - - // build notification - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, mChannelFunction.value(data)) - .setSmallIcon(pin ? R.drawable.ic_pin_white_24dp : R.drawable.ic_notification) - .setContentTitle(new Backed<>(title, "Untitled Task").value()) - .setContentText(contentText(context, data)); - if (mRepost) - { - builder.setTicker(new Backed<>(title, "Untitled Task").value()); - } - builder.setAutoCancel(false); - builder.setContentIntent(PendingIntent.getBroadcast(context, notificationId, - new Intent(context, ActionReceiver.class).setAction( - pin ? ActionService.ACTION_OPEN_TASK : ActionService.ACTION_OPEN_TASK_CANCEL_NOTIFICATION) + builder.setAutoCancel(false); + builder.setContentIntent( + PendingIntent.getBroadcast( + context, + notificationId, + new Intent(context, ActionReceiver.class) + .setAction( + pin + ? ActionService.ACTION_OPEN_TASK + : ActionService.ACTION_OPEN_TASK_CANCEL_NOTIFICATION) + .setData(taskUri), + PendingIntent.FLAG_UPDATE_CURRENT)); + + // make sure we un-persist the notification when its cancelled + builder.setDeleteIntent( + PendingIntent.getBroadcast( + context, + notificationId, + new Intent(context, ActionReceiver.class) + .setAction(ActionService.ACTION_REMOVE_NOTIFICATION) + .setData(taskUri), + PendingIntent.FLAG_UPDATE_CURRENT)); + + if (!new TaskIsClosed(data).value()) { + builder.addAction( + new NotificationCompat.Action( + R.drawable.ic_action_complete, + context.getString(R.string.notification_action_complete), + PendingIntent.getBroadcast( + context, + 1, + new Intent(context, ActionReceiver.class) + .setAction(ActionService.ACTION_COMPLETE) + .setData(taskUri), + PendingIntent.FLAG_UPDATE_CURRENT))); + + if (new TaskDateTime(TaskContract.Tasks.DUE, data).isPresent()) { + builder.addAction( + new NotificationCompat.Action( + R.drawable.ic_detail_delay_1d_inverse, + context.getString(R.string.notification_action_delay_1d), + PendingIntent.getBroadcast( + context, + 1, + new Intent(context, ActionReceiver.class) + .setAction(ActionService.ACTION_DEFER_1D) .setData(taskUri), - PendingIntent.FLAG_UPDATE_CURRENT)); - - // make sure we un-persist the notification when its cancelled - builder.setDeleteIntent(PendingIntent.getBroadcast(context, notificationId, - new Intent(context, ActionReceiver.class).setAction(ActionService.ACTION_REMOVE_NOTIFICATION).setData(taskUri), - PendingIntent.FLAG_UPDATE_CURRENT)); - - if (!new TaskIsClosed(data).value()) - { - builder.addAction( - new NotificationCompat.Action( - R.drawable.ic_action_complete, - context.getString(R.string.notification_action_complete), - PendingIntent.getBroadcast( - context, - 1, - new Intent(context, ActionReceiver.class).setAction(ActionService.ACTION_COMPLETE).setData(taskUri), - PendingIntent.FLAG_UPDATE_CURRENT))); - - if (new TaskDateTime(TaskContract.Tasks.DUE, data).isPresent()) - { - builder.addAction(new NotificationCompat.Action(R.drawable.ic_detail_delay_1d_inverse, context.getString(R.string.notification_action_delay_1d), - PendingIntent.getBroadcast( - context, - 1, - new Intent(context, ActionReceiver.class).setAction(ActionService.ACTION_DEFER_1D).setData(taskUri), - PendingIntent.FLAG_UPDATE_CURRENT))); - } - } - - if (pin) - { - builder.addAction(new NotificationCompat.Action( - R.drawable.ic_pin_off_white_24dp, - context.getString(R.string.notification_action_unpin), - PendingIntent.getBroadcast( - context, - 1, - new Intent(context, ActionReceiver.class).setAction(ActionService.ACTION_UNPIN).setData(taskUri), - PendingIntent.FLAG_UPDATE_CURRENT))); - } - builder.setOnlyAlertOnce(!mRepost); - builder.setOngoing(pin); - builder.setShowWhen(false); - builder.setGroup(pin ? GROUP_PINS : GROUP_ALERTS); - builder.setPriority(pin ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_HIGH); - - if (Build.VERSION.SDK_INT < 26) - { - builder.setDefaults(new Conditional(mRepost, context).value()); - } - // TODO: for now we only use the primary app color, later we allow the user to select how to color notifications: default, list, priority - builder.setColor(new AttributeColor(new ContextThemeWrapper(context, - org.dmfs.android.sync.opentasks_theme.R.style.OpenTasks_Theme_Default), - androidx.appcompat.R.attr.colorPrimary).argb()); - //builder.setColor(new EffectiveTaskColor(data).argb()); - NotificationManagerCompat.from(context).notify("tasks", notificationId, builder.build()); + PendingIntent.FLAG_UPDATE_CURRENT))); + } } - - private CharSequence contentText(Context context, RowDataSnapshot data) - { - Optional start = new TaskStart(data); - Optional due = new EffectiveDueDate(data); - if (new TaskCompletionTime(data).isPresent()) - { - // TODO include completed time in notification text - return context.getString(R.string.task_completed); - } - else if (due.isPresent() && (!start.isPresent() || new Sieved<>(DateTime.now()::after, start).isPresent())) - { - return context.getString(R.string.notification_task_due_date, formatTime(context, due.value())); - } - else if (start.isPresent()) - { - return context.getString(R.string.notification_task_start_date, formatTime(context, start.value())); - } - return ""; + if (pin) { + builder.addAction( + new NotificationCompat.Action( + R.drawable.ic_pin_off_white_24dp, + context.getString(R.string.notification_action_unpin), + PendingIntent.getBroadcast( + context, + 1, + new Intent(context, ActionReceiver.class) + .setAction(ActionService.ACTION_UNPIN) + .setData(taskUri), + PendingIntent.FLAG_UPDATE_CURRENT))); } - - - /** - * Returns a string representation for the time, with a relative date and an absolute time - */ - public static String formatTime(Context context, DateTime time) - { - String dateString; - if (time.isAllDay()) - { - dateString = DateUtils.getRelativeTimeSpanString(time.getTimestamp(), DateTime.today().getTimestamp(), DateUtils.DAY_IN_MILLIS).toString(); - } - else - { - dateString = DateUtils.getRelativeTimeSpanString(time.getTimestamp(), DateTime.now().getTimestamp(), DateUtils.DAY_IN_MILLIS).toString(); - } - - // return combined date and time - String timeString = new DateFormatter(context).format(time, DateFormatter.DateFormatContext.NOTIFICATION_VIEW_TIME); - return dateString + ", " + timeString; + builder.setOnlyAlertOnce(!mRepost); + builder.setOngoing(pin); + builder.setShowWhen(false); + builder.setGroup(pin ? GROUP_PINS : GROUP_ALERTS); + builder.setPriority( + pin ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_HIGH); + + if (Build.VERSION.SDK_INT < 26) { + builder.setDefaults(new Conditional(mRepost, context).value()); + } + // TODO: for now we only use the primary app color, later we allow the user to select how to + // color notifications: default, list, priority + builder.setColor( + new AttributeColor( + new ContextThemeWrapper( + context, org.dmfs.android.sync.opentasks_theme.R.style.OpenTasks_Theme_Default), + androidx.appcompat.R.attr.colorPrimary) + .argb()); + // builder.setColor(new EffectiveTaskColor(data).argb()); + NotificationManagerCompat.from(context).notify("tasks", notificationId, builder.build()); + } + + private CharSequence contentText(Context context, RowDataSnapshot data) { + Optional start = new TaskStart(data); + Optional due = new EffectiveDueDate(data); + if (new TaskCompletionTime(data).isPresent()) { + // TODO include completed time in notification text + return context.getString(R.string.task_completed); + } else if (due.isPresent() + && (!start.isPresent() || new Sieved<>(DateTime.now()::after, start).isPresent())) { + return context.getString( + R.string.notification_task_due_date, formatTime(context, due.value())); + } else if (start.isPresent()) { + return context.getString( + R.string.notification_task_start_date, formatTime(context, start.value())); + } + return ""; + } + + /** Returns a string representation for the time, with a relative date and an absolute time */ + public static String formatTime(Context context, DateTime time) { + String dateString; + if (time.isAllDay()) { + dateString = + DateUtils.getRelativeTimeSpanString( + time.getTimestamp(), DateTime.today().getTimestamp(), DateUtils.DAY_IN_MILLIS) + .toString(); + } else { + dateString = + DateUtils.getRelativeTimeSpanString( + time.getTimestamp(), DateTime.now().getTimestamp(), DateUtils.DAY_IN_MILLIS) + .toString(); } + // return combined date and time + String timeString = + new DateFormatter(context) + .format(time, DateFormatter.DateFormatContext.NOTIFICATION_VIEW_TIME); + return dateString + ", " + timeString; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyStickyAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyStickyAction.java index b9e47387..dfdb46fa 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyStickyAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyStickyAction.java @@ -21,22 +21,20 @@ import org.dmfs.jems.function.Function; import org.dmfs.tasks.actions.conditions.NotificationEnabled; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAction} which posts a notification, if enabled, and stores the notification state. * * @author Marten Gajda */ -public final class NotifyStickyAction extends DelegatingTaskAction -{ - - public NotifyStickyAction(Function, String> channelFunction, boolean repost) - { - super(new Conditional( - new NotificationEnabled(), - new Composite( - new NotifyAction(channelFunction, repost), - new PersistNotificationAction()))); - } +public final class NotifyStickyAction extends DelegatingTaskAction { + public NotifyStickyAction( + Function, String> channelFunction, + boolean repost) { + super( + new Conditional( + new NotificationEnabled(), + new Composite( + new NotifyAction(channelFunction, repost), new PersistNotificationAction()))); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/OpenAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/OpenAction.java index 6b75b03f..d858c128 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/OpenAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/OpenAction.java @@ -23,33 +23,34 @@ import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; import androidx.core.app.TaskStackBuilder; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAction} which opens a task. * * @author Marten Gajda */ -public final class OpenAction implements TaskAction -{ - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) throws RemoteException, OperationApplicationException - { - // TODO: check if it's still a good idea to build a custom stack - // Creates an explicit intent for an Activity in your app - Intent resultIntent = new Intent(Intent.ACTION_VIEW); - resultIntent.setData(taskUri); +public final class OpenAction implements TaskAction { + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) + throws RemoteException, OperationApplicationException { + // TODO: check if it's still a good idea to build a custom stack + // Creates an explicit intent for an Activity in your app + Intent resultIntent = new Intent(Intent.ACTION_VIEW); + resultIntent.setData(taskUri); - // The stack builder object will contain an artificial back stack for the - // started Activity. - // This ensures that navigating backward from the Activity leads out of - // your application to the Home screen. - TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); - // Adds the Intent that starts the Activity to the top of the stack - stackBuilder.addNextIntent(resultIntent); - context.startActivities(stackBuilder.getIntents()); - } + // The stack builder object will contain an artificial back stack for the + // started Activity. + // This ensures that navigating backward from the Activity leads out of + // your application to the Home screen. + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + // Adds the Intent that starts the Activity to the top of the stack + stackBuilder.addNextIntent(resultIntent); + context.startActivities(stackBuilder.getIntents()); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/PersistNotificationAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/PersistNotificationAction.java index afb29b10..e12ad146 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/PersistNotificationAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/PersistNotificationAction.java @@ -21,7 +21,6 @@ import android.content.Context; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.opentaskspal.readdata.TaskVersion; import org.dmfs.tasks.actions.utils.NotificationPrefs; @@ -30,35 +29,36 @@ import org.dmfs.tasks.notification.state.RowStateInfo; import org.json.JSONException; import org.json.JSONObject; - /** * A {@link TaskAction} which persist a task notification in the preferences. * * @author Marten Gajda */ -public final class PersistNotificationAction implements TaskAction -{ - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot data, Uri taskUri) throws RemoteException, OperationApplicationException - { - try - { - RowStateInfo rsi = new RowStateInfo(data); - new NotificationPrefs(context).next() - .edit() - .putString( - taskUri.toString(), - new JSONObject() - .put("version", new TaskVersion(data).value()) - .put("started", rsi.started()) - .put("due", rsi.due()) - .put("done", rsi.done()) - .put("ongoing", rsi.pinned()).toString()) - .apply(); - } - catch (JSONException e) - { - throw new RuntimeException("Unable to serialize to JSON", e); - } +public final class PersistNotificationAction implements TaskAction { + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot data, + Uri taskUri) + throws RemoteException, OperationApplicationException { + try { + RowStateInfo rsi = new RowStateInfo(data); + new NotificationPrefs(context) + .next() + .edit() + .putString( + taskUri.toString(), + new JSONObject() + .put("version", new TaskVersion(data).value()) + .put("started", rsi.started()) + .put("due", rsi.due()) + .put("done", rsi.done()) + .put("ongoing", rsi.pinned()) + .toString()) + .apply(); + } catch (JSONException e) { + throw new RuntimeException("Unable to serialize to JSON", e); } + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/PinAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/PinAction.java index b0960b29..82a0f93e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/PinAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/PinAction.java @@ -18,16 +18,13 @@ package org.dmfs.tasks.actions; import org.dmfs.opentaskspal.tasks.PinnedData; - /** * A {@link TaskAction} which pins a task. * * @author Marten Gajda */ -public final class PinAction extends DelegatingTaskAction -{ - public PinAction(boolean pin) - { - super(new UpdateAction((snapshot) -> new PinnedData<>(pin))); - } +public final class PinAction extends DelegatingTaskAction { + public PinAction(boolean pin) { + super(new UpdateAction((snapshot) -> new PinnedData<>(pin))); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/PostUndoAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/PostUndoAction.java index 4c6d6a9f..6d051db5 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/PostUndoAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/PostUndoAction.java @@ -25,7 +25,9 @@ import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; import android.widget.RemoteViews; - +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; import org.dmfs.android.bolts.color.colors.AttributeColor; import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.tasks.R; @@ -34,58 +36,64 @@ import org.dmfs.tasks.notification.ActionReceiver; import org.dmfs.tasks.notification.ActionService; import org.dmfs.tasks.notification.signals.NoSignal; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - - /** * Post a notification. - *

    - * TODO: refactor the notification construction, use composition + * + *

    TODO: refactor the notification construction, use composition * * @author Marten Gajda */ -public final class PostUndoAction implements TaskAction -{ - private final String GROUP_UNDO = "org.dmfs.tasks.UNDO"; - - - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot data, Uri taskUri) throws RemoteException, OperationApplicationException - { - int id = (int) ContentUris.parseId(taskUri); - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, ActionService.CHANNEL_DUE_DATES); - builder.setContentTitle(context.getString(R.string.task_completed)); - builder.setSmallIcon(R.drawable.ic_notification); - builder.setStyle(new NotificationCompat.DecoratedCustomViewStyle()); - builder.setDefaults(new NoSignal().value()); +public final class PostUndoAction implements TaskAction { + private final String GROUP_UNDO = "org.dmfs.tasks.UNDO"; - final RemoteViews undoView = new RemoteViews(context.getPackageName(), R.layout.undo_notification); - undoView.setTextViewText(R.id.description_text, context.getString(R.string.task_completed)); + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot data, + Uri taskUri) + throws RemoteException, OperationApplicationException { + int id = (int) ContentUris.parseId(taskUri); + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context, ActionService.CHANNEL_DUE_DATES); + builder.setContentTitle(context.getString(R.string.task_completed)); + builder.setSmallIcon(R.drawable.ic_notification); + builder.setStyle(new NotificationCompat.DecoratedCustomViewStyle()); + builder.setDefaults(new NoSignal().value()); - undoView.setOnClickPendingIntent( - R.id.status_bar_latest_event_content, - PendingIntent.getBroadcast( - context, - id, - new Intent(context, ActionReceiver.class).setData(taskUri).setAction(ActionService.ACTION_UNDO_COMPLETE), - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE)); - builder.setContent(undoView); + final RemoteViews undoView = + new RemoteViews(context.getPackageName(), R.layout.undo_notification); + undoView.setTextViewText(R.id.description_text, context.getString(R.string.task_completed)); - // When the notification is cleared, we perform the destructive action - builder.setDeleteIntent(PendingIntent.getBroadcast( - context, - id, - new Intent(context, ActionReceiver.class).setData(taskUri).setAction(ActionService.ACTION_FINISH_COMPLETE), - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE)); - builder.setShowWhen(false); - builder.setGroup(GROUP_UNDO); - builder.setColor(new AttributeColor(new ContextThemeWrapper(context, - org.dmfs.android.sync.opentasks_theme.R.style.OpenTasks_Theme_Default), - androidx.appcompat.R.attr.colorPrimary).argb()); + undoView.setOnClickPendingIntent( + R.id.status_bar_latest_event_content, + PendingIntent.getBroadcast( + context, + id, + new Intent(context, ActionReceiver.class) + .setData(taskUri) + .setAction(ActionService.ACTION_UNDO_COMPLETE), + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE)); + builder.setContent(undoView); - NotificationManagerCompat.from(context).notify("tasks.undo", id, builder.build()); - } + // When the notification is cleared, we perform the destructive action + builder.setDeleteIntent( + PendingIntent.getBroadcast( + context, + id, + new Intent(context, ActionReceiver.class) + .setData(taskUri) + .setAction(ActionService.ACTION_FINISH_COMPLETE), + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE)); + builder.setShowWhen(false); + builder.setGroup(GROUP_UNDO); + builder.setColor( + new AttributeColor( + new ContextThemeWrapper( + context, org.dmfs.android.sync.opentasks_theme.R.style.OpenTasks_Theme_Default), + androidx.appcompat.R.attr.colorPrimary) + .argb()); + NotificationManagerCompat.from(context).notify("tasks.undo", id, builder.build()); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/RemoveNotificationAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/RemoveNotificationAction.java index 746888a9..1d638028 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/RemoveNotificationAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/RemoveNotificationAction.java @@ -21,22 +21,23 @@ import android.content.Context; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.tasks.actions.utils.NotificationPrefs; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAction} which removes a notification from the preferences. * * @author Marten Gajda */ -public final class RemoveNotificationAction implements TaskAction -{ - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) throws RemoteException, OperationApplicationException - { - new NotificationPrefs(context).next().edit().remove(taskUri.toString()).apply(); - } +public final class RemoveNotificationAction implements TaskAction { + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) + throws RemoteException, OperationApplicationException { + new NotificationPrefs(context).next().edit().remove(taskUri.toString()).apply(); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/TaskAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/TaskAction.java index c93c3cf6..925d5377 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/TaskAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/TaskAction.java @@ -21,19 +21,23 @@ import android.content.Context; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.tasks.contract.TaskContract; - /** * An action carried out on a specific task. * * @author Marten Gajda */ -public interface TaskAction -{ - // TODO: consider returning an Iterable of Operations or providing a Transaction so multiple changes can be executed in a single transation - // TODO: if we return the operation, also provide a way of executing something in case of a successful execution - void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) throws RemoteException, OperationApplicationException; +public interface TaskAction { + // TODO: consider returning an Iterable of Operations or providing a Transaction so multiple + // changes can be executed in a single transation + // TODO: if we return the operation, also provide a way of executing something in case of a + // successful execution + void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) + throws RemoteException, OperationApplicationException; } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateAction.java index e202546a..39c44b5c 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateAction.java @@ -21,7 +21,6 @@ import android.content.Context; import android.content.OperationApplicationException; import android.net.Uri; import android.os.RemoteException; - import org.dmfs.android.contentpal.RowData; import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.android.contentpal.operations.Update; @@ -31,26 +30,32 @@ import org.dmfs.iterables.elementary.Seq; import org.dmfs.jems.function.Function; import org.dmfs.tasks.contract.TaskContract; - /** * A {@link TaskAction} which updates a task with values based on the original values. * * @author Marten Gajda */ -public final class UpdateAction implements TaskAction -{ - private final Function, RowData> mDataFunction; - - - public UpdateAction(Function, RowData> dataFunction) - { - mDataFunction = dataFunction; - } - - - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) throws RemoteException, OperationApplicationException - { - new BaseTransaction().with(new Seq<>(new Update<>(new RowUriReference<>(taskUri), mDataFunction.value(rowSnapshot)))).commit(contentProviderClient); - } +public final class UpdateAction implements TaskAction { + private final Function, RowData> + mDataFunction; + + public UpdateAction( + Function, RowData> + dataFunction) { + mDataFunction = dataFunction; + } + + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) + throws RemoteException, OperationApplicationException { + new BaseTransaction() + .with( + new Seq<>( + new Update<>(new RowUriReference<>(taskUri), mDataFunction.value(rowSnapshot)))) + .commit(contentProviderClient); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateWidgetsAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateWidgetsAction.java index 6541b080..d12a2cb0 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateWidgetsAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateWidgetsAction.java @@ -21,38 +21,36 @@ import android.content.ComponentName; import android.content.ContentProviderClient; import android.content.Context; import android.net.Uri; - import org.dmfs.android.contentpal.RowDataSnapshot; +import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.homescreen.TaskListWidgetProvider; import org.dmfs.tasks.homescreen.TaskListWidgetProviderLarge; -import org.dmfs.tasks.R; - /** * A {@link TaskAction} that updates the widgets. * * @author Trogel */ -public final class UpdateWidgetsAction implements TaskAction -{ - public UpdateWidgetsAction() - { - } - - - @Override - public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri) - { - AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); - notifyTaskListDataChanged(appWidgetManager, new ComponentName(context, TaskListWidgetProvider.class)); - notifyTaskListDataChanged(appWidgetManager, new ComponentName(context, TaskListWidgetProviderLarge.class)); - } - - - private void notifyTaskListDataChanged(AppWidgetManager appWidgetManager, ComponentName componentName) - { - final int[] appWidgetIds = appWidgetManager.getAppWidgetIds(componentName); - appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.task_list_widget_lv); - } +public final class UpdateWidgetsAction implements TaskAction { + public UpdateWidgetsAction() {} + + @Override + public void execute( + Context context, + ContentProviderClient contentProviderClient, + RowDataSnapshot rowSnapshot, + Uri taskUri) { + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + notifyTaskListDataChanged( + appWidgetManager, new ComponentName(context, TaskListWidgetProvider.class)); + notifyTaskListDataChanged( + appWidgetManager, new ComponentName(context, TaskListWidgetProviderLarge.class)); + } + + private void notifyTaskListDataChanged( + AppWidgetManager appWidgetManager, ComponentName componentName) { + final int[] appWidgetIds = appWidgetManager.getAppWidgetIds(componentName); + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.task_list_widget_lv); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/WipeNotificationAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/WipeNotificationAction.java index 36f5ebfb..304817ca 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/WipeNotificationAction.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/WipeNotificationAction.java @@ -18,16 +18,13 @@ package org.dmfs.tasks.actions; import org.dmfs.iterables.elementary.Seq; - /** * A {@link TaskAction} which removes a notification. * * @author Marten Gajda */ -public final class WipeNotificationAction extends DelegatingTaskAction -{ - public WipeNotificationAction() - { - super(new Composite(new Seq<>(new RemoveNotificationAction(), new CancelNotificationAction()))); - } +public final class WipeNotificationAction extends DelegatingTaskAction { + public WipeNotificationAction() { + super(new Composite(new Seq<>(new RemoveNotificationAction(), new CancelNotificationAction()))); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/conditions/NotificationEnabled.java b/opentasks/src/main/java/org/dmfs/tasks/actions/conditions/NotificationEnabled.java index 35db0ada..e2c3c80e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/conditions/NotificationEnabled.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/conditions/NotificationEnabled.java @@ -20,28 +20,26 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; - import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.jems.function.BiFunction; import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract; - /** * @author Marten Gajda */ -public final class NotificationEnabled implements BiFunction, Boolean> -{ - @Override - public Boolean value(Context context, RowDataSnapshot snapshot) - { - if (Build.VERSION.SDK_INT >= 26) - { - // on Android SDK Level 26+ we leave this decision to Android and always attempt to show the notification - return true; - } - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); - return settings.getBoolean(context.getString(R.string.opentasks_pref_notification_enabled), true); - +public final class NotificationEnabled + implements BiFunction, Boolean> { + @Override + public Boolean value( + Context context, RowDataSnapshot snapshot) { + if (Build.VERSION.SDK_INT >= 26) { + // on Android SDK Level 26+ we leave this decision to Android and always attempt to show the + // notification + return true; } + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); + return settings.getBoolean( + context.getString(R.string.opentasks_pref_notification_enabled), true); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/utils/NotificationPrefs.java b/opentasks/src/main/java/org/dmfs/tasks/actions/utils/NotificationPrefs.java index eb9e24d5..321a7ede 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/actions/utils/NotificationPrefs.java +++ b/opentasks/src/main/java/org/dmfs/tasks/actions/utils/NotificationPrefs.java @@ -18,29 +18,22 @@ package org.dmfs.tasks.actions.utils; import android.content.Context; import android.content.SharedPreferences; - import org.dmfs.jems.generator.Generator; - /** * {@link Generator} of {@link SharedPreferences} instances which contain the active notifications. * * @author Marten Gajda */ -public final class NotificationPrefs implements Generator -{ - private final Context mContext; - - - public NotificationPrefs(Context context) - { - mContext = context; - } +public final class NotificationPrefs implements Generator { + private final Context mContext; + public NotificationPrefs(Context context) { + mContext = context; + } - @Override - public SharedPreferences next() - { - return mContext.getSharedPreferences("org.dmfs.tasks.notifications", Context.MODE_PRIVATE); - } + @Override + public SharedPreferences next() { + return mContext.getSharedPreferences("org.dmfs.tasks.notifications", Context.MODE_PRIVATE); + } } 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 e74c5990..1a3d6710 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/dashclock/DashClockPreferenceActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/dashclock/DashClockPreferenceActivity.java @@ -19,28 +19,23 @@ package org.dmfs.tasks.dashclock; import android.os.Bundle; import android.preference.PreferenceActivity; import android.preference.PreferenceFragment; - import org.dmfs.tasks.R; import org.dmfs.tasks.utils.BaseActivity; - /** - * TODO This doesn't use {@link BaseActivity}. Could be replaced with that plus {@link PreferenceFragment} or - * android.support.v7.preference.PreferenceFragmentCompat. + * TODO This doesn't use {@link BaseActivity}. Could be replaced with that plus {@link + * PreferenceFragment} or android.support.v7.preference.PreferenceFragmentCompat. */ -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 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; - @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 d61f0fc5..3c5f587a 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java +++ b/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java @@ -22,11 +22,10 @@ import android.content.SharedPreferences; import android.database.Cursor; import android.preference.PreferenceManager; import android.text.format.Time; -import android.util.Log; - import com.google.android.apps.dashclock.api.DashClockExtension; import com.google.android.apps.dashclock.api.ExtensionData; - +import java.util.Calendar; +import java.util.TimeZone; import org.dmfs.provider.tasks.AuthorityUtil; import org.dmfs.tasks.EditTaskActivity; import org.dmfs.tasks.R; @@ -38,383 +37,457 @@ import org.dmfs.tasks.model.adapters.TimeFieldAdapter; import org.dmfs.tasks.utils.DateFormatter; import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; -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(AuthorityUtil.taskAuthority(this)).toString() }); - super.onInitialize(isReconnect); - - mDateFormatter = new DateFormatter(this); - } - - - @Override - protected void onUpdateData(int reason) - { - mNow = System.currentTimeMillis(); - mAuthority = AuthorityUtil.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._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); +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(AuthorityUtil.taskAuthority(this)).toString()}); + super.onInitialize(isReconnect); + + mDateFormatter = new DateFormatter(this); + } + + @Override + protected void onUpdateData(int reason) { + mNow = System.currentTimeMillis(); + mAuthority = AuthorityUtil.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; } - } - - - 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) - { + c.moveToFirst(); - // 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(); + boolean isAllDay = allDayTaskCount > 0; - return dueTime == todayUTC; + String description = c.getString(c.getColumnIndex(Tasks.DESCRIPTION)); + if (description != null) { + description = description.replaceAll("\\[\\s?\\]", " ").replaceAll("\\[[xX]\\]", "✓"); } - else - { - return startTime < mNow; - } - + String title = getTaskTitleDisplayString(c, isAllDay); + + // intent + String accountType = c.getString(c.getColumnIndex(Instances.ACCOUNT_TYPE)); + long taskId = c.getLong(c.getColumnIndex(Instances._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); } + } - - protected Intent buildClickIntent(long instanceId, String accountType) - { - Intent clickIntent = new Intent(Intent.ACTION_VIEW); - clickIntent.setData(ContentUris.withAppendedId(Instances.getContentUri(mAuthority), instanceId)); - clickIntent.putExtra(EditTaskActivity.EXTRA_DATA_ACCOUNT_TYPE, accountType); - - return clickIntent; + private void closeCursor(Cursor cursor) { + if (cursor == null || cursor.isClosed()) { + return; } - - - private Cursor loadPinnedTaskCursor() - { - - return getContentResolver().query(Instances.getContentUri(mAuthority), INSTANCE_PROJECTION, INSTANCE_PINNED_SELECTION, null, - Tasks.PRIORITY + " is not null, " + Tasks.PRIORITY + " DESC"); + 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 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 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 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 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) { - 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); + if (c.isNull(c.getColumnIndex(Instances.DUE))) { + return false; } - - - 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); + 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)); - private Cursor loadAllDayTasksStartDueTodayCursor() - { + 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(); + // 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); + return dueTime == todayUTC; + } else { + return startTime < mNow; } + } + + protected Intent buildClickIntent(long instanceId, String accountType) { + Intent clickIntent = new Intent(Intent.ACTION_VIEW); + clickIntent.setData( + ContentUris.withAppendedId(Instances.getContentUri(mAuthority), instanceId)); + 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 d37d2b0f..fbddb5e2 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java @@ -21,89 +21,94 @@ import org.dmfs.tasks.model.adapters.TimeFieldAdapter; import org.dmfs.tasks.utils.ExpandableChildDescriptor; 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; +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 static final 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 static final 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 static final 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; + } - - /** - * 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(); + 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 5f4a8cc7..84537308 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java @@ -16,6 +16,10 @@ package org.dmfs.tasks.groupings; +import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; +import static org.dmfs.tasks.model.TaskFieldAdapters.IS_CLOSED; +import static org.dmfs.tasks.model.TaskFieldAdapters.LIST_COLOR_RAW; + import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; @@ -30,9 +34,16 @@ import android.text.style.ImageSpan; import android.view.View; import android.widget.ImageView; import android.widget.TextView; - +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.collection.SparseArrayCompat; +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; import com.google.android.material.card.MaterialCardView; - +import java.util.List; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.dmfs.android.bolts.color.colors.AttributeColor; import org.dmfs.iterables.decorators.Sieved; import org.dmfs.jems.single.elementary.Reduced; @@ -44,292 +55,263 @@ import org.dmfs.tasks.utils.DateFormatter; import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; import org.dmfs.tasks.utils.ViewDescriptor; -import java.util.List; -import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.collection.SparseArrayCompat; -import androidx.core.content.ContextCompat; -import androidx.preference.PreferenceManager; - -import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; -import static org.dmfs.tasks.model.TaskFieldAdapters.IS_CLOSED; -import static org.dmfs.tasks.model.TaskFieldAdapters.LIST_COLOR_RAW; - - /** * A base implementation of a {@link ViewDescriptor}. It has a number of commonly used methods. * * @author Marten Gajda */ -public abstract class BaseTaskViewDescriptor implements ViewDescriptor -{ - - private final static int[] DRAWABLES = new int[] { R.drawable.ic_outline_check_box_24, R.drawable.ic_outline_check_box_outline_blank_24 }; - private final static Pattern DRAWABLE_PATTERN = Pattern.compile("((?:-\\s*)?\\[[xX]])|((?:-\\s*)?\\[\\s?])"); - /** - * 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 if (isClosed) - { - view.setTextAppearance(view.getContext(), R.style.task_list_due_text_closed); - } - else - { - view.setTextAppearance(view.getContext(), R.style.task_list_due_text); - } - } - else if (view != null) - { - view.setText(""); - if (dueIcon != null) - { - dueIcon.setVisibility(View.GONE); - } - } +public abstract class BaseTaskViewDescriptor implements ViewDescriptor { + + private static final int[] DRAWABLES = + new int[] { + R.drawable.ic_outline_check_box_24, R.drawable.ic_outline_check_box_outline_blank_24 + }; + private static final Pattern DRAWABLE_PATTERN = + Pattern.compile("((?:-\\s*)?\\[[xX]])|((?:-\\s*)?\\[\\s?])"); + + /** 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 if (isClosed) { + view.setTextAppearance(view.getContext(), R.style.task_list_due_text_closed); + } 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) - { + } + + protected void setOverlay(View view, int position, int count) {} + + protected void setDescription(View view, Cursor cursor) { + Context context = view.getContext(); + Resources res = context.getResources(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean isClosed = TaskAdapter.IS_CLOSED.getFrom(cursor); + TextView descriptionView = getView(view, android.R.id.text1); + int maxDescriptionLines = + prefs.getInt( + context.getString(R.string.opentasks_pref_appearance_list_description_lines), + context + .getResources() + .getInteger(R.integer.opentasks_preferences_description_lines_default)); + + List checkList = TaskFieldAdapters.DESCRIPTION_CHECKLIST.get(cursor); + if (maxDescriptionLines > 0 + && checkList.size() > 0 + && !checkList.get(0).checkbox + && !isClosed) { + descriptionView.setVisibility(View.VISIBLE); + descriptionView.setText(withCheckBoxes(descriptionView, checkList.get(0).text)); + descriptionView.setMaxLines(maxDescriptionLines); + } else { + descriptionView.setVisibility(View.GONE); } - - protected void setDescription(View view, Cursor cursor) - { - Context context = view.getContext(); - Resources res = context.getResources(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - boolean isClosed = TaskAdapter.IS_CLOSED.getFrom(cursor); - TextView descriptionView = getView(view, android.R.id.text1); - int maxDescriptionLines = prefs.getInt(context.getString(R.string.opentasks_pref_appearance_list_description_lines), - context.getResources().getInteger(R.integer.opentasks_preferences_description_lines_default)); - - List checkList = TaskFieldAdapters.DESCRIPTION_CHECKLIST.get(cursor); - if (maxDescriptionLines > 0 && checkList.size() > 0 && !checkList.get(0).checkbox && !isClosed) - { - descriptionView.setVisibility(View.VISIBLE); - descriptionView.setText(withCheckBoxes(descriptionView, checkList.get(0).text)); - descriptionView.setMaxLines(maxDescriptionLines); - } - else - { - descriptionView.setVisibility(View.GONE); - } - - boolean showCheckListSummary = prefs.getBoolean( - context.getString(R.string.opentasks_pref_appearance_check_list_summary), - res.getBoolean(R.bool.opentasks_list_check_list_summary_default)); - TextView checkboxItemCountView = getView(view, R.id.checkbox_item_count); - Iterable checkedItems = new Sieved<>(item -> item.checkbox, checkList); - int checkboxItemCount = new Reduced(() -> 0, (count, ignored) -> count + 1, checkedItems).value(); - if (checkboxItemCount == 0 || isClosed || !showCheckListSummary) - { - checkboxItemCountView.setVisibility(View.GONE); - } - else - { - checkboxItemCountView.setVisibility(View.VISIBLE); - int checked = new Reduced(() -> 0, (count, ignored) -> count + 1, - new Sieved<>(item -> item.checked, checkedItems)).value(); - if (checked == 0) - { - checkboxItemCountView.setText( - withCheckBoxes(checkboxItemCountView, - context.getString(R.string.opentasks_checkbox_item_count_none_checked, checkboxItemCount))); - } - else if (checked == checkboxItemCount) - { - checkboxItemCountView.setText( - withCheckBoxes(checkboxItemCountView, - context.getString(R.string.opentasks_checkbox_item_count_all_checked, checkboxItemCount))); - } - else - { - checkboxItemCountView.setText(withCheckBoxes(checkboxItemCountView, - context.getString(R.string.opentasks_checkbox_item_count_partially_checked, checkboxItemCount - checked, checked))); - } - } - - View progressGradient = view.findViewById(R.id.task_progress_background); - if (!isClosed && TaskFieldAdapters.PERCENT_COMPLETE.get(cursor) > 0 - && prefs.getBoolean(context.getString(R.string.opentasks_pref_appearance_progress_gradient), - res.getBoolean(R.bool.opentasks_list_progress_gradient_default))) - { - progressGradient.setVisibility(View.VISIBLE); - progressGradient.setPivotX(0); - progressGradient.setScaleX(TaskFieldAdapters.PERCENT_COMPLETE.get(cursor) / 100f); - } - else - { - progressGradient.setVisibility(View.GONE); - } + boolean showCheckListSummary = + prefs.getBoolean( + context.getString(R.string.opentasks_pref_appearance_check_list_summary), + res.getBoolean(R.bool.opentasks_list_check_list_summary_default)); + TextView checkboxItemCountView = getView(view, R.id.checkbox_item_count); + Iterable checkedItems = new Sieved<>(item -> item.checkbox, checkList); + int checkboxItemCount = + new Reduced(() -> 0, (count, ignored) -> count + 1, checkedItems) + .value(); + if (checkboxItemCount == 0 || isClosed || !showCheckListSummary) { + checkboxItemCountView.setVisibility(View.GONE); + } else { + checkboxItemCountView.setVisibility(View.VISIBLE); + int checked = + new Reduced( + () -> 0, + (count, ignored) -> count + 1, + new Sieved<>(item -> item.checked, checkedItems)) + .value(); + if (checked == 0) { + checkboxItemCountView.setText( + withCheckBoxes( + checkboxItemCountView, + context.getString( + R.string.opentasks_checkbox_item_count_none_checked, checkboxItemCount))); + } else if (checked == checkboxItemCount) { + checkboxItemCountView.setText( + withCheckBoxes( + checkboxItemCountView, + context.getString( + R.string.opentasks_checkbox_item_count_all_checked, checkboxItemCount))); + } else { + checkboxItemCountView.setText( + withCheckBoxes( + checkboxItemCountView, + context.getString( + R.string.opentasks_checkbox_item_count_partially_checked, + checkboxItemCount - checked, + checked))); + } } - - private Spannable withCheckBoxes( - @NonNull TextView view, - @NonNull String s) - { - return withDrawable( - view, - new SpannableString(s), - DRAWABLE_PATTERN, - DRAWABLES); + View progressGradient = view.findViewById(R.id.task_progress_background); + if (!isClosed + && TaskFieldAdapters.PERCENT_COMPLETE.get(cursor) > 0 + && prefs.getBoolean( + context.getString(R.string.opentasks_pref_appearance_progress_gradient), + res.getBoolean(R.bool.opentasks_list_progress_gradient_default))) { + progressGradient.setVisibility(View.VISIBLE); + progressGradient.setPivotX(0); + progressGradient.setScaleX(TaskFieldAdapters.PERCENT_COMPLETE.get(cursor) / 100f); + } else { + progressGradient.setVisibility(View.GONE); } - - - private Spannable withDrawable( - @NonNull TextView view, - @NonNull Spannable s, - @NonNull Pattern pattern, - @DrawableRes int[] drawable) - { - Context context = view.getContext(); - Matcher matcher = pattern.matcher(s.toString()); - while (matcher.find()) - { - int idx = matcher.group(1) == null ? 1 : 0; - Drawable drawable1 = ContextCompat.getDrawable(context, drawable[idx]); - int lineHeight = view.getLineHeight(); - int additionalSpace = (int) ((lineHeight - view.getTextSize()) / 2); - drawable1.setBounds(0, 0, lineHeight + additionalSpace, lineHeight + additionalSpace); - drawable1.setTint(view.getCurrentTextColor()); - s.setSpan(new ImageSpan(drawable1, DynamicDrawableSpan.ALIGN_BOTTOM), matcher.start(), matcher.end(), SPAN_EXCLUSIVE_EXCLUSIVE); - } - - return s; + } + + private Spannable withCheckBoxes(@NonNull TextView view, @NonNull String s) { + return withDrawable(view, new SpannableString(s), DRAWABLE_PATTERN, DRAWABLES); + } + + private Spannable withDrawable( + @NonNull TextView view, + @NonNull Spannable s, + @NonNull Pattern pattern, + @DrawableRes int[] drawable) { + Context context = view.getContext(); + Matcher matcher = pattern.matcher(s.toString()); + while (matcher.find()) { + int idx = matcher.group(1) == null ? 1 : 0; + Drawable drawable1 = ContextCompat.getDrawable(context, drawable[idx]); + int lineHeight = view.getLineHeight(); + int additionalSpace = (int) ((lineHeight - view.getTextSize()) / 2); + drawable1.setBounds(0, 0, lineHeight + additionalSpace, lineHeight + additionalSpace); + drawable1.setTint(view.getCurrentTextColor()); + s.setSpan( + new ImageSpan(drawable1, DynamicDrawableSpan.ALIGN_BOTTOM), + matcher.start(), + matcher.end(), + SPAN_EXCLUSIVE_EXCLUSIVE); } - - protected void setPrio(SharedPreferences prefs, View view, Cursor cursor) - { - // display priority - View prioLabel = getView(view, R.id.priority_label); - prioLabel.setAlpha(IS_CLOSED.get(cursor) ? 0.4f : 1f); - int priority = TaskFieldAdapters.PRIORITY.get(cursor); - if (priority > 0 && - prefs.getBoolean(prioLabel.getContext().getString(R.string.opentasks_pref_appearance_list_show_priority), true)) - { - if (priority < 5) - { - prioLabel.setBackgroundColor(new AttributeColor(prioLabel.getContext(), - org.dmfs.android.sync.opentasks_theme.R.attr.colorHighPriority).argb()); - } - if (priority == 5) - { - prioLabel.setBackgroundColor(new AttributeColor(prioLabel.getContext(), - org.dmfs.android.sync.opentasks_theme.R.attr.colorMediumPriority).argb()); - } - if (priority > 5) - { - prioLabel.setBackgroundColor(new AttributeColor(prioLabel.getContext(), - org.dmfs.android.sync.opentasks_theme.R.attr.colorLowPriority).argb()); - } - prioLabel.setVisibility(View.VISIBLE); - } - else - { - prioLabel.setVisibility(View.GONE); - } + return s; + } + + protected void setPrio(SharedPreferences prefs, View view, Cursor cursor) { + // display priority + View prioLabel = getView(view, R.id.priority_label); + prioLabel.setAlpha(IS_CLOSED.get(cursor) ? 0.4f : 1f); + int priority = TaskFieldAdapters.PRIORITY.get(cursor); + if (priority > 0 + && prefs.getBoolean( + prioLabel.getContext().getString(R.string.opentasks_pref_appearance_list_show_priority), + true)) { + if (priority < 5) { + prioLabel.setBackgroundColor( + new AttributeColor( + prioLabel.getContext(), + org.dmfs.android.sync.opentasks_theme.R.attr.colorHighPriority) + .argb()); + } + if (priority == 5) { + prioLabel.setBackgroundColor( + new AttributeColor( + prioLabel.getContext(), + org.dmfs.android.sync.opentasks_theme.R.attr.colorMediumPriority) + .argb()); + } + if (priority > 5) { + prioLabel.setBackgroundColor( + new AttributeColor( + prioLabel.getContext(), + org.dmfs.android.sync.opentasks_theme.R.attr.colorLowPriority) + .argb()); + } + prioLabel.setVisibility(View.VISIBLE); + } else { + prioLabel.setVisibility(View.GONE); } - - - protected void setColorBar(View view, Cursor cursor) - { - MaterialCardView cardView = getView(view, R.id.flingContentView); - if (cardView != null) - { - boolean isClosed = IS_CLOSED.get(cursor); - cardView.findViewById(R.id.color_label).setBackgroundColor(LIST_COLOR_RAW.get(cursor)); - cardView.findViewById(R.id.card_background).setVisibility(isClosed ? View.VISIBLE : View.GONE); - cardView.findViewById(R.id.color_label).setAlpha(isClosed ? 0.4f : 1f); - cardView.setCardElevation(view.getResources().getDimensionPixelSize( - isClosed ? - org.dmfs.android.sync.opentasks_theme.R.dimen.opentasks_tasklist_card_elevation_closed : - org.dmfs.android.sync.opentasks_theme.R.dimen.opentasks_tasklist_card_elevation)); - ((TextView) cardView.findViewById(android.R.id.title)) - .setTextColor(new AttributeColor(view.getContext(), - isClosed ? - android.R.attr.textColorTertiary : - android.R.attr.textColorPrimary).argb()); - } + } + + protected void setColorBar(View view, Cursor cursor) { + MaterialCardView cardView = getView(view, R.id.flingContentView); + if (cardView != null) { + boolean isClosed = IS_CLOSED.get(cursor); + cardView.findViewById(R.id.color_label).setBackgroundColor(LIST_COLOR_RAW.get(cursor)); + cardView + .findViewById(R.id.card_background) + .setVisibility(isClosed ? View.VISIBLE : View.GONE); + cardView.findViewById(R.id.color_label).setAlpha(isClosed ? 0.4f : 1f); + cardView.setCardElevation( + view.getResources() + .getDimensionPixelSize( + isClosed + ? org.dmfs.android.sync.opentasks_theme.R.dimen + .opentasks_tasklist_card_elevation_closed + : org.dmfs.android.sync.opentasks_theme.R.dimen + .opentasks_tasklist_card_elevation)); + ((TextView) cardView.findViewById(android.R.id.title)) + .setTextColor( + new AttributeColor( + view.getContext(), + isClosed ? android.R.attr.textColorTertiary : android.R.attr.textColorPrimary) + .argb()); } + } - - @SuppressLint("NewApi") - protected void resetFlingView(View view) - { - View flingContentView = getView(view, getFlingContentViewId()); - if (flingContentView == null) - { - flingContentView = view; - } - - if (flingContentView.getTranslationX() != 0) - { - flingContentView.setTranslationX(0); - flingContentView.setAlpha(1); - } + @SuppressLint("NewApi") + protected void resetFlingView(View view) { + View flingContentView = getView(view, getFlingContentViewId()); + if (flingContentView == null) { + flingContentView = view; } - - 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; + if (flingContentView.getTranslationX() != 0) { + flingContentView.setTranslationX(0); + flingContentView.setAlpha(1); } + } + 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 a6ce2893..85195b46 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java @@ -24,9 +24,10 @@ import android.graphics.Paint; import android.view.View; import android.widget.BaseExpandableListAdapter; import android.widget.TextView; - +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; +import java.text.DateFormatSymbols; import org.dmfs.tasks.R; -import org.dmfs.tasks.TaskListActivity; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.groupings.cursorloaders.TimeRangeCursorFactory; import org.dmfs.tasks.groupings.cursorloaders.TimeRangeCursorLoaderFactory; @@ -36,249 +37,241 @@ import org.dmfs.tasks.utils.ExpandableGroupDescriptor; import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; import org.dmfs.tasks.utils.ViewDescriptor; -import java.text.DateFormatSymbols; - -import androidx.core.content.ContextCompat; -import androidx.preference.PreferenceManager; - - /** * Definition of the by-due grouping. + * *

    - *

    - * TODO: refactor! - *

    - *

    - * TODO: refactor! - *

    - *

    - * 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 ... + * + *

    TODO: refactor! + * + *

    TODO: refactor! + * + *

    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 -{ +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() - { + /** 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) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext()); - 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); - } + public void populateView( + View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) { + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(view.getContext()); + 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(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + setDueDate( + getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - setPrio(prefs, view, cursor); + setPrio(prefs, view, cursor); - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); } - @Override - public int getView() - { - return R.layout.task_list_element; + public int getView() { + return R.layout.task_list_element; } - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; + public int getFlingContentViewId() { + return mFlingContentViewId; } - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; + public int getFlingRevealLeftViewId() { + return mFlingRevealLeftViewId; } - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; + public int getFlingRevealRightViewId() { + return mFlingRevealRightViewId; } - }; + }; - /** - * A {@link ViewDescriptor} that knows how to present due date groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { + /** 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())); - title.setTextColor(ContextCompat.getColor(view.getContext(), R.color.color_default_primary_text)); - } - - // 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)); - - } + 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())); + title.setTextColor( + ContextCompat.getColor(view.getContext(), R.color.color_default_primary_text)); + } + + // 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)); + } } - @Override - public int getView() - { - return R.layout.task_list_group_single_line; + 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. - * + * @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 ""; + 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; + public int getFlingContentViewId() { + return -1; } - @Override - public int getFlingRevealLeftViewId() - { - // TODO Auto-generated method stub - return 0; + public int getFlingRevealLeftViewId() { + // TODO Auto-generated method stub + return 0; } - @Override - public int getFlingRevealRightViewId() - { - // TODO Auto-generated method stub - return 0; + 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; - } - + }; + + 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 fc9fd4cf..551fa943 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java @@ -25,10 +25,11 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.BaseExpandableListAdapter; import android.widget.TextView; - +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; +import androidx.preference.PreferenceManager; import org.dmfs.tasks.QuickAddDialogFragment; import org.dmfs.tasks.R; -import org.dmfs.tasks.TaskListActivity; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.contract.TaskContract.TaskLists; import org.dmfs.tasks.groupings.cursorloaders.CursorLoaderFactory; @@ -37,264 +38,225 @@ import org.dmfs.tasks.utils.ExpandableGroupDescriptor; import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; import org.dmfs.tasks.utils.ViewDescriptor; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentActivity; -import androidx.preference.PreferenceManager; - - /** * Definition of the by-list grouping. + * *

    - *

    - * TODO: refactor! - *

    - *

    - * TODO: refactor! - *

    - *

    - * 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 ... + * + *

    TODO: refactor! + * + *

    TODO: refactor! + * + *

    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 */ -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() - { +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) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext()); - 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); - } + public void populateView( + View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) { + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(view.getContext()); + 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(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + setDueDate( + getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - setPrio(prefs, view, cursor); - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); + setPrio(prefs, view, cursor); + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); } - @Override - public int getView() - { - return R.layout.task_list_element; + public int getView() { + return R.layout.task_list_element; } - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; + public int getFlingContentViewId() { + return mFlingContentViewId; } - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; + public int getFlingRevealLeftViewId() { + return mFlingRevealLeftViewId; } - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; + public int getFlingRevealRightViewId() { + return mFlingRevealRightViewId; } - }; + }; - /** - * A {@link ViewDescriptor} that knows how to present list groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { + /** 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())); - title.setTextColor(ContextCompat.getColor(view.getContext(), R.color.color_default_primary_text)); + 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())); + title.setTextColor( + ContextCompat.getColor(view.getContext(), R.color.color_default_primary_text)); + } + + // 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)); + } + + 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) { + // show quick add and hide task count + if (quickAddTask != null) { + quickAddTask.setVisibility(View.VISIBLE); } - - // set list account - TextView text1 = (TextView) view.findViewById(android.R.id.text1); - if (text1 != null) - { - text1.setText(cursor.getString(3)); + if (text2 != null) { + text2.setVisibility(View.GONE); } - - // 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)); + } else { + // hide quick add and show task count + if (quickAddTask != null) { + quickAddTask.setVisibility(View.GONE); } - - 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) - { - // show quick add and hide task count - if (quickAddTask != null) - { - quickAddTask.setVisibility(View.VISIBLE); - } - if (text2 != null) - { - text2.setVisibility(View.GONE); - } - } - else - { - // hide quick add and show task count - if (quickAddTask != null) - { - quickAddTask.setVisibility(View.GONE); - } - if (text2 != null) - { - text2.setVisibility(View.VISIBLE); - } + if (text2 != null) { + text2.setVisibility(View.VISIBLE); } + } } + private final OnClickListener quickAddClickListener = + new OnClickListener() { - private final OnClickListener quickAddClickListener = new OnClickListener() - { - - @Override - public void onClick(View v) - { + @Override + public void onClick(View v) { Long tag = (Long) v.getTag(); - if (tag != null) - { - QuickAddDialogFragment.newInstance(tag) - .show(mActivity.getSupportFragmentManager(), null); + if (tag != null) { + QuickAddDialogFragment.newInstance(tag) + .show(mActivity.getSupportFragmentManager(), null); } - } - }; - + } + }; @Override - public int getView() - { - return R.layout.task_list_group; + 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. - * + * @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); + private String getTitle(Cursor cursor, Context context) { + return cursor.getString(1); } - @Override - public int getFlingContentViewId() - { - return -1; + public int getFlingContentViewId() { + return -1; } - @Override - public int getFlingRevealLeftViewId() - { - return -1; + public int getFlingRevealLeftViewId() { + return -1; } - @Override - public int getFlingRevealRightViewId() - { - return -1; + public int getFlingRevealRightViewId() { + return -1; } - - }; - - private final FragmentActivity mActivity; - - - public ByList(String authority, FragmentActivity activity) - { - super(authority); - mActivity = activity; - } - - - @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; - } - + }; + + private final FragmentActivity mActivity; + + public ByList(String authority, FragmentActivity activity) { + super(authority); + mActivity = activity; + } + + @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 fc228bc6..61d19370 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java @@ -25,11 +25,12 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.BaseExpandableListAdapter; import android.widget.TextView; - +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; +import androidx.preference.PreferenceManager; import org.dmfs.provider.tasks.AuthorityUtil; import org.dmfs.tasks.QuickAddDialogFragment; import org.dmfs.tasks.R; -import org.dmfs.tasks.TaskListActivity; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.contract.TaskContract.Tasks; import org.dmfs.tasks.groupings.cursorloaders.PriorityCursorFactory; @@ -41,247 +42,223 @@ import org.dmfs.tasks.utils.ExpandableGroupDescriptor; import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; import org.dmfs.tasks.utils.ViewDescriptor; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentActivity; -import androidx.preference.PreferenceManager; - - /** * Definition of the by-priority grouping. * * @author Tobias Reinsch */ -public class ByPriority extends AbstractGroupingFactory -{ +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() - { + /** + * 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) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext()); - 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); - } + public void populateView( + View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) { + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(view.getContext()); + 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(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + setDueDate( + getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - setPrio(prefs, view, cursor); + setPrio(prefs, view, cursor); - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); } - @Override - public int getView() - { - return R.layout.task_list_element; + public int getView() { + return R.layout.task_list_element; } - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; + public int getFlingContentViewId() { + return mFlingContentViewId; } - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; + public int getFlingRevealLeftViewId() { + return mFlingRevealLeftViewId; } - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; + public int getFlingRevealRightViewId() { + return mFlingRevealRightViewId; } - }; + }; - /** - * A {@link ViewDescriptor} that knows how to present list groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { + /** 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())); - title.setTextColor(ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text)); + 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())); + title.setTextColor( + ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text)); + } + + // 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)); + } + + 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) { + // show quick add and hide task count + if (quickAddTask != null) { + quickAddTask.setVisibility(View.VISIBLE); } - - // 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)); - } - - 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 (text2 != null) { + text2.setVisibility(View.GONE); } - - if ((flags & FLAG_IS_EXPANDED) != 0) - { - // show quick add and hide task count - if (quickAddTask != null) - { - quickAddTask.setVisibility(View.VISIBLE); - } - if (text2 != null) - { - text2.setVisibility(View.GONE); - } + } else { + // hide quick add and show task count + if (quickAddTask != null) { + quickAddTask.setVisibility(View.GONE); } - else - { - // hide quick add and show task count - if (quickAddTask != null) - { - quickAddTask.setVisibility(View.GONE); - } - if (text2 != null) - { - text2.setVisibility(View.VISIBLE); - } + if (text2 != null) { + text2.setVisibility(View.VISIBLE); } + } } + private final OnClickListener quickAddClickListener = + new OnClickListener() { - private final OnClickListener quickAddClickListener = new OnClickListener() - { - - @Override - public void onClick(View v) - { + @Override + public void onClick(View v) { Integer tag = (Integer) v.getTag(); - if (tag != null) - { - ContentSet content = new ContentSet(Tasks.getContentUri(AuthorityUtil.taskAuthority(v.getContext()))); - TaskFieldAdapters.PRIORITY.set(content, tag); - QuickAddDialogFragment.newInstance(content) - .show(mActivity.getSupportFragmentManager(), null); + if (tag != null) { + ContentSet content = + new ContentSet( + Tasks.getContentUri(AuthorityUtil.taskAuthority(v.getContext()))); + TaskFieldAdapters.PRIORITY.set(content, tag); + QuickAddDialogFragment.newInstance(content) + .show(mActivity.getSupportFragmentManager(), null); } - } - }; - + } + }; @Override - public int getView() - { - return R.layout.task_list_group_single_line; + 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. - * + * @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))); + 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; + public int getFlingContentViewId() { + return -1; } - @Override - public int getFlingRevealLeftViewId() - { - return -1; + public int getFlingRevealLeftViewId() { + return -1; } - @Override - public int getFlingRevealRightViewId() - { - return -1; + public int getFlingRevealRightViewId() { + return -1; } - - }; - - private final FragmentActivity mActivity; - - - public ByPriority(String authority, FragmentActivity activity) - { - super(authority); - mActivity = activity; - } - - - @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; - } - + }; + + private final FragmentActivity mActivity; + + public ByPriority(String authority, FragmentActivity activity) { + super(authority); + mActivity = activity; + } + + @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 54916e92..79a52f4f 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java @@ -24,9 +24,9 @@ import android.graphics.Paint; import android.view.View; import android.widget.BaseExpandableListAdapter; import android.widget.TextView; - +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; import org.dmfs.tasks.R; -import org.dmfs.tasks.TaskListActivity; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.groupings.cursorloaders.ProgressCursorFactory; import org.dmfs.tasks.groupings.cursorloaders.ProgressCursorLoaderFactory; @@ -35,192 +35,178 @@ import org.dmfs.tasks.utils.ExpandableGroupDescriptor; import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; import org.dmfs.tasks.utils.ViewDescriptor; -import androidx.core.content.ContextCompat; -import androidx.preference.PreferenceManager; - - /** * Definition of the by-progress grouping. * * @author Tobias Reinsch */ -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() - { +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) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext()); - 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); - } + public void populateView( + View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) { + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(view.getContext()); + 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(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + setDueDate( + getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - setPrio(prefs, view, cursor); + setPrio(prefs, view, cursor); - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); } - @Override - public int getView() - { - return R.layout.task_list_element; + public int getView() { + return R.layout.task_list_element; } - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; + public int getFlingContentViewId() { + return mFlingContentViewId; } - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; + public int getFlingRevealLeftViewId() { + return mFlingRevealLeftViewId; } - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; + public int getFlingRevealRightViewId() { + return mFlingRevealRightViewId; } - }; + }; - /** - * A {@link ViewDescriptor} that knows how to present list groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { + /** 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())); - title.setTextColor(ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text)); - } - - // 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)); - } + 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())); + title.setTextColor( + ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text)); + } + + // 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)); + } } - @Override - public int getView() - { - return R.layout.task_list_group_single_line; + 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. - * + * @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))); + 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; + public int getFlingContentViewId() { + return -1; } - @Override - public int getFlingRevealLeftViewId() - { - return -1; + public int getFlingRevealLeftViewId() { + return -1; } - @Override - public int getFlingRevealRightViewId() - { - return -1; + 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; - } - + }; + + 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 56820925..e4446ff2 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java @@ -30,9 +30,9 @@ import android.widget.BaseExpandableListAdapter; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; - +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; import org.dmfs.tasks.R; -import org.dmfs.tasks.TaskListActivity; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.contract.TaskContract.Tasks; import org.dmfs.tasks.groupings.cursorloaders.SearchHistoryCursorLoaderFactory; @@ -46,296 +46,265 @@ import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper.SearchHistoryColumns; import org.dmfs.tasks.utils.SearchHistoryHelper; import org.dmfs.tasks.utils.ViewDescriptor; -import androidx.core.content.ContextCompat; -import androidx.preference.PreferenceManager; - - /** * 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() - { +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) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext()); - 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); - } + public void populateView( + View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) { + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(view.getContext()); + 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(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); + setDueDate( + getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed); - setPrio(prefs, view, cursor); + setPrio(prefs, view, cursor); - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); } - @Override - public int getView() - { - return R.layout.task_list_element; + public int getView() { + return R.layout.task_list_element; } - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; + public int getFlingContentViewId() { + return mFlingContentViewId; } - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; + public int getFlingRevealLeftViewId() { + return mFlingRevealLeftViewId; } - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; + public int getFlingRevealRightViewId() { + return mFlingRevealRightViewId; } - }; + }; - /** - * A {@link ViewDescriptor} that knows how to present list groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { + /** 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); - title.setTextColor(ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text)); - - } - // 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)); + 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); + title.setTextColor( + ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text)); + } + // 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)); + } + + 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)); } + } - // 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)); + if ((flags & FLAG_IS_EXPANDED) != 0) { + if (removeSearch != null) { + removeSearch.setVisibility(View.VISIBLE); } - - 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 (text2 != null) { + text2.setVisibility(View.GONE); } - - 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); } - else - { - if (removeSearch != null) - { - removeSearch.setVisibility(View.GONE); - } - if (text2 != null) - { - text2.setVisibility(View.VISIBLE); - } + 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, swapStyle(isHistoric, oldtypeface)); - - // 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); + } + + // 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, swapStyle(isHistoric, oldtypeface)); + + // 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); } - @SuppressLint("WrongConstant") - private int swapStyle(boolean isHistoric, Typeface oldtypeface) - { - return isHistoric ? oldtypeface.getStyle() & ~Typeface.ITALIC : oldtypeface.getStyle() | Typeface.ITALIC; + private int swapStyle(boolean isHistoric, Typeface oldtypeface) { + return isHistoric + ? oldtypeface.getStyle() & ~Typeface.ITALIC + : oldtypeface.getStyle() | Typeface.ITALIC; } + private final OnClickListener removeListener = + new OnClickListener() { - private final OnClickListener removeListener = new OnClickListener() - { - - @Override - public void onClick(View v) - { + @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(); + 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; - } + } + }; + + /** 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; + 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. - * + * @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)); + private String getTitle(Cursor cursor, Context context) { + return cursor.getString( + cursor.getColumnIndex(SearchHistoryDatabaseHelper.SearchHistoryColumns.SEARCH_QUERY)); } - @Override - public int getFlingContentViewId() - { - return -1; + public int getFlingContentViewId() { + return -1; } - @Override - public int getFlingRevealLeftViewId() - { - return -1; + public int getFlingRevealLeftViewId() { + return -1; } - @Override - public int getFlingRevealRightViewId() - { - return -1; + 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; - } + }; + + 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 1b2cd7af..8a8ecefd 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java @@ -26,9 +26,9 @@ import android.view.View; import android.widget.BaseExpandableListAdapter; import android.widget.ImageView; import android.widget.TextView; - +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; import org.dmfs.tasks.R; -import org.dmfs.tasks.TaskListActivity; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.groupings.cursorloaders.TimeRangeCursorFactory; import org.dmfs.tasks.groupings.cursorloaders.TimeRangeStartCursorFactory; @@ -40,245 +40,236 @@ import org.dmfs.tasks.utils.ExpandableGroupDescriptor; import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter; import org.dmfs.tasks.utils.ViewDescriptor; -import androidx.core.content.ContextCompat; -import androidx.preference.PreferenceManager; - - /** * Definition of the by-start date grouping. * * @author Tobias Reinsch */ -public class ByStartDate extends AbstractGroupingFactory -{ +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() - { + /** 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) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext()); - 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); - } + public void populateView( + View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags) { + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(view.getContext()); + 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(getView(view, R.id.task_due_date), 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(""); - } + } + + setDueDate( + getView(view, R.id.task_due_date), + 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(""); } + } - setPrio(prefs, view, cursor); + setPrio(prefs, view, cursor); - setColorBar(view, cursor); - setDescription(view, cursor); - setOverlay(view, cursor.getPosition(), cursor.getCount()); + setColorBar(view, cursor); + setDescription(view, cursor); + setOverlay(view, cursor.getPosition(), cursor.getCount()); } - @Override - public int getView() - { - return R.layout.task_list_element; + public int getView() { + return R.layout.task_list_element; } - @Override - public int getFlingContentViewId() - { - return mFlingContentViewId; + public int getFlingContentViewId() { + return mFlingContentViewId; } - @Override - public int getFlingRevealLeftViewId() - { - return mFlingRevealLeftViewId; + public int getFlingRevealLeftViewId() { + return mFlingRevealLeftViewId; } - @Override - public int getFlingRevealRightViewId() - { - return mFlingRevealRightViewId; + public int getFlingRevealRightViewId() { + return mFlingRevealRightViewId; } - }; + }; - /** - * A {@link ViewDescriptor} that knows how to present start date groups. - */ - public final ViewDescriptor GROUP_VIEW_DESCRIPTOR = new ViewDescriptor() - { + /** 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())); - title.setTextColor(ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text)); - } - - // 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)); - - } + 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())); + title.setTextColor( + ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text)); + } + + // 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)); + } } - @Override - public int getView() - { - return R.layout.task_list_group_single_line; + 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. - * + * @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 ""; + 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; + public int getFlingContentViewId() { + return -1; } - @Override - public int getFlingRevealLeftViewId() - { - return -1; + public int getFlingRevealLeftViewId() { + return -1; } - @Override - public int getFlingRevealRightViewId() - { - return -1; + 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; - } - + }; + + 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 d422e97a..60e3d0d3 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/TabConfig.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/TabConfig.java @@ -19,7 +19,9 @@ package org.dmfs.tasks.groupings; 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; import org.dmfs.android.xmlmagic.AndroidParserContext; import org.dmfs.android.xmlmagic.builder.RecyclingReflectionObjectBuilder; import org.dmfs.android.xmlmagic.builder.ReflectionObjectBuilder; @@ -33,244 +35,192 @@ import org.dmfs.xmlobjects.pull.XmlObjectPullParserException; import org.dmfs.xmlobjects.pull.XmlPath; import org.xmlpull.v1.XmlPullParserException; -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. + * 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"; +public class TabConfig { - @Attribute(name = "title") - private int title; + /** The XML namespace. */ + public static final String NAMESPACE = "http://schema.dmfs.org/tasks"; - @Attribute(name = "icon") - private int icon; + /** The name of the root elements. */ + public static final String TAG = "tabconfig"; - @Attribute(name = "id") - private int id; + /** A Builder that builds a {@link TabConfig} object. */ + public static final IObjectBuilder BUILDER = + new RecyclingReflectionObjectBuilder(TabConfig.class); - @Attribute(name = "visible") - private boolean visible = true; + /** The {@link XmlElementDescriptor} of the tabconfig element. */ + public static final ElementDescriptor DESCRIPTOR = + ElementDescriptor.register(QualifiedName.get(NAMESPACE, TAG), BUILDER); + /** A Builder for {@link Tab} objects. */ + public static final IObjectBuilder TAB_BUILDER = new ReflectionObjectBuilder(Tab.class); - /** - * Get the title of this tab. - * - * @return A string resource id for the title. - */ - public int getTitleId() - { - return title; - } + /** The {@link XmlElementDescriptor} for tab elements. */ + public static final 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 static final String TAG = "tab"; - /** - * Get the icon of the tab. - * - * @return A drawable resource id. - */ - public int getIcon() - { - return icon; - } + @Attribute(name = "title") + private int title; + @Attribute(name = "icon") + private int 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; + @Attribute(name = "id") + private int id; + @Attribute(name = "visible") + private boolean visible = true; /** - * 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. + * Get the title of this tab. * - * @return A {@link TabConfig} instance. - * - * @throws XmlPullParserException - * @throws IOException - * @throws XmlObjectPullParserException + * @return A string resource id for the title. */ - 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; + public int getTitleId() { + return title; } - /** - * Get the {@link Tab} at the specified position. + * Get the icon of the tab. * - * @param position - * The position among all {@link Tab}s. - * - * @return The Tab at the given position. + * @return A drawable resource id. */ - public Tab getItem(int position) - { - return mTabs.get(position); + public int getIcon() { + return icon; } - /** - * Get one of the visible {@link Tab}s by its position. + * Get the id of the tab. * - * @param position - * The position among the visible items. - * - * @return The Tab at the given position. + * @return The id. */ - public Tab getVisibleItem(int position) - { - return mVisible.get(position); + public int getId() { + return id; } - /** - * Get the number of all {@link Tab}s. + * Return the visibility of the tab. * - * @return The number of tabs. + * @return true if the tab is visible, false otherwise. */ - public int size() - { - return mTabs.size(); + public boolean isVisible() { + return visible; } - - - /** - * Get the number of visible {@link Tab}s. - * - * @return The number of visible tabs. - */ - public int visibleSize() - { - return mVisible.size(); + } + + /** 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()); } - - /** - * 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); - } - } + 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 7f10b1c7..c1cea994 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 @@ -20,21 +20,17 @@ import android.content.Context; import android.database.Cursor; import androidx.loader.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); +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); } 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 714bc3ae..f20a1a1d 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 @@ -18,33 +18,27 @@ package org.dmfs.tasks.groupings.cursorloaders; 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(); +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(); } 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 6ba738c2..1203c8c3 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 @@ -22,45 +22,40 @@ import android.net.Uri; import androidx.loader.content.CursorLoader; import androidx.loader.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; - - - /** - * 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); - } - +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; + + /** + * 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); + } } 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 6e1d72ea..4e444a44 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 @@ -20,97 +20,75 @@ import android.content.Context; import android.database.Cursor; import androidx.loader.content.Loader; - /** - * A very simple {@link Loader} that returns the {@link Cursor} from a {@link AbstractCustomCursorFactory}. + * 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; +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; - - @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(); - } + if (isStarted()) { + super.deliverResult(cursor); } - - @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); - } + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { + oldCursor.close(); } - - - @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 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(); + @Override + protected void onReset() { + super.onReset(); - // ensure the cursor is closed before we release it - if (mCursor != null && !mCursor.isClosed()) - { - mCursor.close(); - } + onStopLoading(); - mCursor = null; + // 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 3475eb5c..9ee23c7f 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 @@ -21,26 +21,22 @@ import android.database.Cursor; import android.database.MatrixCursor; import androidx.loader.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. + * 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 class EmptyCursorLoaderFactory extends CustomCursorLoader { + 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 d6c18ea4..8230c3aa 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 @@ -18,58 +18,55 @@ package org.dmfs.tasks.groupings.cursorloaders; 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 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 class PriorityCursorFactory extends AbstractCustomCursorFactory { - 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 PRIORITY_ID = "_id"; + public static final String PRIORITY_MIN_STATUS = "min_status"; + public static final String PRIORITY_MAX_STATUS = "max_status"; + public static final String PRIORITY_TYPE = "type"; + public static final String PRIORITY_TITLE_RES_ID = "res_id"; + public static final int PRIORITY_TYPE_NONE = 0; + public static final int PRIORITY_TYPE_LOW = 3; + public static final int PRIORITY_TYPE_MEDIUM = 2; + public static final int PRIORITY_TYPE_HIGH = 1; - /** - * Initialize the factory with the given projection. - * - * @param projection - * An array of column names. - */ - public PriorityCursorFactory(String[] projection) - { - super(projection); - } + 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); + } - @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 a7ede4be..1f347282 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 @@ -20,27 +20,22 @@ import android.content.Context; import android.database.Cursor; import androidx.loader.content.Loader; - /** - * An {@link AbstractCursorLoaderFactory} that returns {@link CursorLoaderFactory} that know how to load cursors with priority information as values. + * 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; - +public class PriorityCursorLoaderFactory extends AbstractCursorLoaderFactory { - public PriorityCursorLoaderFactory(String[] projection) - { - mProjection = projection; - } + private final String[] mProjection; + 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 86c619b0..bfb83f14 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 @@ -18,61 +18,59 @@ package org.dmfs.tasks.groupings.cursorloaders; 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 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 class ProgressCursorFactory extends AbstractCustomCursorFactory { - 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 PROGRESS_ID = "_id"; + public static final String PROGRESS_MIN_STATUS = "min_status"; + public static final String PROGRESS_MAX_STATUS = "max_status"; + public static final String PROGRESS_TYPE = "type"; + public static final String PROGRESS_TITLE_RES_ID = "res_id"; + public static final int PROGRESS_TYPE_0 = 0; + public static final int PROGRESS_TYPE_40 = 1; + public static final int PROGRESS_TYPE_60 = 2; + public static final int PROGRESS_TYPE_80 = 3; + public static final int PROGRESS_TYPE_100 = 4; - /** - * Initialize the factory with the given projection. - * - * @param projection - * An array of column names. - */ - public ProgressCursorFactory(String[] projection) - { - super(projection); - } + 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); + } - @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 77260111..6c70e665 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 @@ -20,27 +20,22 @@ import android.content.Context; import android.database.Cursor; import androidx.loader.content.Loader; - /** - * An {@link AbstractCursorLoaderFactory} that returns {@link CursorLoaderFactory} that know how to load cursors with progress information as values. + * 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; - +public class ProgressCursorLoaderFactory extends AbstractCursorLoaderFactory { - public ProgressCursorLoaderFactory(String[] projection) - { - mProjection = projection; - } + private final String[] mProjection; + 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 17bdb8b9..6678c222 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 @@ -18,37 +18,30 @@ package org.dmfs.tasks.groupings.cursorloaders; 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; - - - /** - * 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(); - } +public final class SearchHistoryCursorFactory extends AbstractCustomCursorFactory { + + 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; + } + + @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 ea6ac54e..855c44ec 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 @@ -19,44 +19,34 @@ package org.dmfs.tasks.groupings.cursorloaders; import android.content.Context; import android.database.Cursor; import androidx.loader.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. + * 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; - - - public SearchHistoryCursorLoaderFactory(SearchHistoryHelper history) - { - mSeachHistory = history; - } - - - @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(); - } +public class SearchHistoryCursorLoaderFactory extends AbstractCursorLoaderFactory { + + private final SearchHistoryHelper mSeachHistory; + private CustomCursorLoader mLastLoader; + + public SearchHistoryCursorLoaderFactory(SearchHistoryHelper history) { + mSeachHistory = history; + } + + @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(); } + } } 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 be18d36c..111f7610 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 @@ -19,226 +19,210 @@ package org.dmfs.tasks.groupings.cursorloaders; 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 + * + *

    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 class TimeRangeCursorFactory extends AbstractCustomCursorFactory { - public final static String RANGE_ID = "_id"; - public final static String RANGE_TYPE = "type"; + public static final String RANGE_ID = "_id"; + public static final 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 static final int TYPE_END_OF_DAY = 0x01; + public static final int TYPE_END_OF_YESTERDAY = 0x02 | TYPE_END_OF_DAY; + public static final int TYPE_END_OF_TODAY = 0x04 | TYPE_END_OF_DAY; + public static final int TYPE_END_OF_TOMORROW = 0x08 | TYPE_END_OF_DAY; - public final static int TYPE_END_IN_7_DAYS = 0x10 | TYPE_END_OF_DAY; + public static final int TYPE_END_IN_7_DAYS = 0x10 | 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; + /** Not supported yet */ + public static final int TYPE_END_OF_A_WEEK = 0x0100; - 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 static final int TYPE_END_OF_LAST_WEEK = 0x0200 | 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; + /** Not supported yet */ + public static final int TYPE_END_OF_THIS_WEEK = 0x0400 | TYPE_END_OF_A_WEEK; - public final static int TYPE_OVERDUE = 0x20000000; + /** Not supported yet */ + public static final int TYPE_END_OF_NEXT_WEEK = 0x0800 | TYPE_END_OF_A_WEEK; - public final static int TYPE_NO_END = 0x80000000; + public static final int TYPE_END_OF_A_MONTH = 0x010000; + public static final int TYPE_END_OF_LAST_MONTH = 0x020000 | TYPE_END_OF_A_MONTH; + public static final int TYPE_END_OF_THIS_MONTH = 0x040000 | TYPE_END_OF_A_MONTH; + public static final int TYPE_END_OF_NEXT_MONTH = 0x080000 | TYPE_END_OF_A_MONTH; - public final static String RANGE_START = "start"; + public static final int TYPE_END_OF_A_YEAR = 0x01000000; + public static final int TYPE_END_OF_LAST_YEAR = 0x02000000 | TYPE_END_OF_A_YEAR; + public static final int TYPE_END_OF_THIS_YEAR = 0x04000000 | TYPE_END_OF_A_YEAR; + public static final int TYPE_END_OF_NEXT_YEAR = 0x08000000 | TYPE_END_OF_A_YEAR; - public final static String RANGE_END = "end"; + public static final int TYPE_OVERDUE = 0x20000000; - public final static String RANGE_YEAR = "year"; + public static final int TYPE_NO_END = 0x80000000; - public final static String RANGE_MONTH = "month"; + public static final String RANGE_START = "start"; - public final static String RANGE_OPEN_FUTURE = "open_future"; + public static final String RANGE_END = "end"; - public final static String RANGE_OPEN_PAST = "open_past"; + public static final String RANGE_YEAR = "year"; - public final static String RANGE_NULL_ROW = "null_row"; + public static final String RANGE_MONTH = "month"; - public final static String RANGE_START_TZ_OFFSET = "start_tz_offset"; + public static final String RANGE_OPEN_FUTURE = "open_future"; - public final static String RANGE_END_TZ_OFFSET = "end_tz_offset"; + public static final String RANGE_OPEN_PAST = "open_past"; - 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 static final String RANGE_NULL_ROW = "null_row"; - protected final static long MAX_TIME = Long.MAX_VALUE / 2; - protected final static long MIN_TIME = Long.MIN_VALUE / 2; + public static final String RANGE_START_TZ_OFFSET = "start_tz_offset"; - protected final List mProjectionList; + public static final String RANGE_END_TZ_OFFSET = "end_tz_offset"; - protected final Time mTime; - protected final TimeZone mTimezone; + 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 static final long MAX_TIME = Long.MAX_VALUE / 2; + protected static final long MIN_TIME = Long.MIN_VALUE / 2; - public TimeRangeCursorFactory(String[] projection) - { - super(projection); - mProjectionList = Arrays.asList(projection); - mTimezone = TimeZone.getDefault(); - mTime = new Time(mTimezone.getID()); - } + protected final List mProjectionList; + protected final Time mTime; + protected final TimeZone mTimezone; - public Cursor getCursor() - { - mTime.setToNow(); + public TimeRangeCursorFactory(String[] projection) { + super(projection); + mProjectionList = Arrays.asList(projection); + mTimezone = TimeZone.getDefault(); + mTime = new Time(mTimezone.getID()); + } - 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)); - } + long t1 = time.toMillis(false); - 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)); + } - // 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); - 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)); - // tomorrow row - long t3 = time.toMillis(false); - result.addRow(makeRow(4, TYPE_END_OF_TOMORROW, t2, t3)); + time.monthDay += 1; + time.yearDay += 1; + time.normalize(true); - time.monthDay += 5; - time.yearDay += 5; - time.normalize(true); + // tomorrow row + long t3 = time.toMillis(false); + result.addRow(makeRow(4, TYPE_END_OF_TOMORROW, t2, t3)); - // next week row - long t4 = time.toMillis(false); - result.addRow(makeRow(5, TYPE_END_IN_7_DAYS, t3, t4)); + time.monthDay += 5; + time.yearDay += 5; + time.normalize(true); - time.set(1, time.month + 1, time.year); - time.normalize(true); + // next week row + long t4 = time.toMillis(false); + result.addRow(makeRow(5, TYPE_END_IN_7_DAYS, t3, t4)); - // month row - long t5 = time.toMillis(false); - result.addRow(makeRow(6, TYPE_END_OF_A_MONTH, t4, t5)); + time.set(1, time.month + 1, time.year); + time.normalize(true); - 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)); + // month row + long t5 = time.toMillis(false); + result.addRow(makeRow(6, TYPE_END_OF_A_MONTH, t4, t5)); - // open future for future tasks - if (mProjectionList.contains(RANGE_OPEN_FUTURE)) - { - result.addRow(makeRow(8, TYPE_NO_END, t6, MAX_TIME)); - } + 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)); - return result; + // open future for future tasks + if (mProjectionList.contains(RANGE_OPEN_FUTURE)) { + result.addRow(makeRow(8, TYPE_NO_END, t6, MAX_TIME)); } + 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)); + } - 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; + 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; - } + 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 d97ae911..658518e9 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 @@ -18,78 +18,63 @@ package org.dmfs.tasks.groupings.cursorloaders; import android.content.Context; import android.database.Cursor; -import androidx.loader.content.Loader; import android.text.format.Time; - +import androidx.loader.content.Loader; +import java.util.TimeZone; 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. + * 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); - } - +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); + } } 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 53df42f1..96464c2f 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 @@ -20,26 +20,20 @@ import android.content.Context; import android.database.Cursor; import androidx.loader.content.Loader; - /** * A factory that builds {@link TimeRangeCursorLoader}s. * * @author Marten Gajda */ -public class TimeRangeCursorLoaderFactory extends AbstractCursorLoaderFactory -{ - private final String[] mProjection; - - - public TimeRangeCursorLoaderFactory(String[] projection) - { - mProjection = projection; - } +public class TimeRangeCursorLoaderFactory extends AbstractCursorLoaderFactory { + private final String[] mProjection; + 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 aa3e7ebe..a609a45b 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 @@ -20,64 +20,58 @@ import android.database.Cursor; import android.database.MatrixCursor; 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. + * + *

    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 class TimeRangeShortCursorFactory extends TimeRangeCursorFactory { - @Override - public Cursor getCursor() - { - mTime.setToNow(); + public TimeRangeShortCursorFactory(String[] projection) { + super(projection); + } - MatrixCursor result = new MatrixCursor(mProjection); + @Override + public Cursor getCursor() { + mTime.setToNow(); - Time time = new Time(mTimezone.getID()); - time.set(mTime.monthDay + 1, mTime.month, mTime.year); + MatrixCursor result = new MatrixCursor(mProjection); - // today row (including overdue) - long t2 = time.toMillis(false); - result.addRow(makeRow(1, TYPE_END_OF_TODAY, MIN_TIME, t2)); + Time time = new Time(mTimezone.getID()); + time.set(mTime.monthDay + 1, mTime.month, mTime.year); - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + // today row (including overdue) + long t2 = time.toMillis(false); + result.addRow(makeRow(1, TYPE_END_OF_TODAY, MIN_TIME, t2)); - // tomorrow row - long t3 = time.toMillis(false); - result.addRow(makeRow(2, TYPE_END_OF_TOMORROW, t2, t3)); + time.monthDay += 1; + time.yearDay += 1; + time.normalize(true); - time.monthDay += 5; - time.yearDay += 5; - time.normalize(true); + // tomorrow row + long t3 = time.toMillis(false); + result.addRow(makeRow(2, TYPE_END_OF_TOMORROW, t2, t3)); - // next week row - long t4 = time.toMillis(false); - result.addRow(makeRow(3, TYPE_END_IN_7_DAYS, t3, t4)); + time.monthDay += 5; + time.yearDay += 5; + time.normalize(true); - time.monthDay += 1; - time.normalize(true); + // next week row + long t4 = time.toMillis(false); + result.addRow(makeRow(3, TYPE_END_IN_7_DAYS, t3, t4)); - // open future for future tasks (including tasks without dates) - if (mProjectionList.contains(RANGE_OPEN_FUTURE)) - { - result.addRow(makeRow(4, TYPE_NO_END, t4, null)); - } + time.monthDay += 1; + time.normalize(true); - return result; + // 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; + } } 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 8a4263f5..ab82c937 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 @@ -20,76 +20,68 @@ import android.database.Cursor; import android.database.MatrixCursor; 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 class TimeRangeStartCursorFactory extends TimeRangeCursorFactory { - @Override - public Cursor getCursor() - { + public TimeRangeStartCursorFactory(String[] projection) { + super(projection); + } - mTime.setToNow(); + @Override + public Cursor getCursor() { - MatrixCursor result = new MatrixCursor(mProjection); + mTime.setToNow(); - // get time of today 00:00:00 - Time time = new Time(mTime.timezone); - time.set(mTime.monthDay, mTime.month, mTime.year); + MatrixCursor result = new MatrixCursor(mProjection); - // already started row - long t1 = time.toMillis(false); - result.addRow(makeRow(1, TYPE_OVERDUE, MIN_TIME, t1)); + // get time of today 00:00:00 + Time time = new Time(mTime.timezone); + time.set(mTime.monthDay, mTime.month, mTime.year); - time.hour = 0; - time.minute = 0; - time.second = 0; + // already started row + long t1 = time.toMillis(false); + result.addRow(makeRow(1, TYPE_OVERDUE, MIN_TIME, t1)); - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + time.hour = 0; + time.minute = 0; + time.second = 0; - // 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); + // today row + long t2 = time.toMillis(false); + result.addRow(makeRow(2, TYPE_END_OF_TODAY, t1, t2)); - // tomorrow row - long t3 = time.toMillis(false); - result.addRow(makeRow(3, TYPE_END_OF_TOMORROW, t2, t3)); + time.monthDay += 1; + time.yearDay += 1; + time.normalize(true); - time.monthDay += 5; - time.yearDay += 5; - time.normalize(true); + // tomorrow row + long t3 = time.toMillis(false); + result.addRow(makeRow(3, TYPE_END_OF_TOMORROW, t2, t3)); - // next week row - long t4 = time.toMillis(false); - result.addRow(makeRow(4, TYPE_END_IN_7_DAYS, t3, t4)); + time.monthDay += 5; + time.yearDay += 5; + time.normalize(true); - time.monthDay += 1; - time.normalize(true); + // next week row + long t4 = time.toMillis(false); + result.addRow(makeRow(4, TYPE_END_IN_7_DAYS, t3, t4)); - // 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)); - } + time.monthDay += 1; + time.normalize(true); - return result; + // 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; + } } 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 4fc766ec..5524c40f 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 @@ -18,78 +18,63 @@ package org.dmfs.tasks.groupings.cursorloaders; import android.content.Context; import android.database.Cursor; -import androidx.loader.content.Loader; import android.text.format.Time; - +import androidx.loader.content.Loader; +import java.util.TimeZone; 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. + * 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); - } - +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); + } } 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 d302d1a4..4971f342 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 @@ -20,26 +20,20 @@ import android.content.Context; import android.database.Cursor; import androidx.loader.content.Loader; - /** * A factory that builds {@link TimeRangeStartCursorLoader}s. * * @author Tobias Reinsch */ -public class TimeRangeStartCursorLoaderFactory extends AbstractCursorLoaderFactory -{ - private final String[] mProjection; - - - public TimeRangeStartCursorLoaderFactory(String[] projection) - { - mProjection = projection; - } +public class TimeRangeStartCursorLoaderFactory extends AbstractCursorLoaderFactory { + private final String[] mProjection; + 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 e7b5724b..65d31f85 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 @@ -18,28 +18,25 @@ package org.dmfs.tasks.groupings.filters; import java.util.List; - /** * An abstract filter for child cursors in a grouped list. * * @author Marten Gajda */ -public interface AbstractFilter -{ +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. - */ - 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. + */ + 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. - */ - 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. + */ + 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 196bb3b2..f109548e 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 @@ -21,10 +21,8 @@ package org.dmfs.tasks.groupings.filters; * * @author Marten Gajda */ -public final class AndFilter extends BinaryOperationFilter -{ - public AndFilter(AbstractFilter... filters) - { - super("AND", filters); - } +public final class AndFilter extends BinaryOperationFilter { + 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 75b615ea..8c1199d4 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 @@ -18,70 +18,55 @@ package org.dmfs.tasks.groupings.filters; 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; +public class BinaryOperationFilter implements AbstractFilter { + 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 80ce8977..408507cb 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 @@ -19,50 +19,38 @@ package org.dmfs.tasks.groupings.filters; import java.util.Collections; 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; - - - /** - * Creates a ConstantFilter. - * - * @param selection - * The selection string. - * @param selectionArgs - * The positional selection arguments. - */ - public ConstantFilter(String selection, String... selectionArgs) - { - mSelection = selection; - mSelectionArgs = selectionArgs; +public final class ConstantFilter implements AbstractFilter { + 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; + } + + @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 - Collections.addAll(selectionArgs, mSelectionArgs); - } + @Override + public void getSelectionArgs(List selectionArgs) { + if (mSelectionArgs != null) { + // append all arguments, if any + Collections.addAll(selectionArgs, mSelectionArgs); } + } } 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 555d806f..988d2a81 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 @@ -21,10 +21,8 @@ package org.dmfs.tasks.groupings.filters; * * @author Marten Gajda */ -public final class OrFilter extends BinaryOperationFilter -{ - public OrFilter(AbstractFilter... filters) - { - super("OR", filters); - } +public final class OrFilter extends BinaryOperationFilter { + 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 680b7624..18d10e98 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListSelectionFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListSelectionFragment.java @@ -19,17 +19,17 @@ package org.dmfs.tasks.homescreen; import android.app.Activity; import android.database.Cursor; import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.fragment.app.ListFragment; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import android.widget.TextView; - +import androidx.annotation.Nullable; +import androidx.fragment.app.ListFragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; +import java.util.ArrayList; import org.dmfs.provider.tasks.AuthorityUtil; import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract; @@ -37,134 +37,118 @@ import org.dmfs.tasks.contract.TaskContract.TaskLists; 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; - - - @Override - public void onAttach(Activity activity) - { - super.onAttach(activity); - mActivity = activity; - mListener = (OnSelectionListener) activity; - mAuthority = AuthorityUtil.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(v -> - { - if (mListener != null) - { - mListener.onSelection(mTaskListAdapter.getSelectedLists()); - } - +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 static final 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; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = activity; + mListener = (OnSelectionListener) activity; + mAuthority = AuthorityUtil.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( + v -> { + if (mListener != null) { + mListener.onSelection(mTaskListAdapter.getSelectedLists()); + } }); - mButtonCancel.setOnClickListener(v -> - { - if (mListener != null) - { - mListener.onSelectionCancel(); - } - + mButtonCancel.setOnClickListener( + 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); - - } + 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, 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 - { - void onSelection(ArrayList selectedLists); - - void onSelectionCancel(); - } - + 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, + 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 { + void onSelection(ArrayList selectedLists); + + 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 f136c1d4..109ffa24 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java @@ -18,117 +18,88 @@ package org.dmfs.tasks.homescreen; 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 instance ID. - */ - private final int mInstanceId; - - /** - * 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) - { - mInstanceId = 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 getInstanceId() - { - return mInstanceId; - } - - - /** - * Gets the checks if is closed. - * - * @return the checks if is closed - */ - public boolean getIsClosed() - { - return mIsClosed; - } +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 instance ID. */ + private final int mInstanceId; + + /** 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) { + mInstanceId = 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 getInstanceId() { + return mInstanceId; + } + + /** + * 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 a0b11337..0759c3fa 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProvider.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProvider.java @@ -29,7 +29,7 @@ import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.widget.RemoteViews; - +import java.util.ArrayList; import org.dmfs.provider.tasks.AuthorityUtil; import org.dmfs.tasks.EditTaskActivity; import org.dmfs.tasks.R; @@ -40,154 +40,147 @@ import org.dmfs.tasks.model.ContentSet; import org.dmfs.tasks.utils.RecentlyUsedLists; import org.dmfs.tasks.utils.WidgetConfigurationDatabaseHelper; -import java.util.ArrayList; - - /** * The provider for the widget on Android Honeycomb and up. * * @author Arjun Naik * @author Tobias Reinsch */ -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); +public class TaskListWidgetProvider extends AppWidgetProvider { + private static final 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 = AuthorityUtil.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(); } - 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 = AuthorityUtil.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); + } + 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); - } - - + } + + 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[]) + */ + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { /* - * This method is called periodically to update the widget. - * - * @see android.appwidget.AppWidgetProvider#onUpdate(android.content.Context, android.appwidget.AppWidgetManager, int[]) + * Iterate over all the widgets of this type and update them individually. */ - 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. */ - 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); - } + 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. + */ + 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); } + } + @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 b2afd6f7..b8d123d4 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLarge.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetProviderLarge.java @@ -20,37 +20,30 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; - /** * The provider for the large homescreen widget on Android Honeycomb and up. * * @author Tobias Reinsch */ -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); - } +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); + } } 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 d764a2bf..8e980cfb 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetSettingsActivity.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetSettingsActivity.java @@ -20,99 +20,79 @@ import android.appwidget.AppWidgetManager; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; - +import java.util.ArrayList; import org.dmfs.tasks.R; import org.dmfs.tasks.homescreen.TaskListSelectionFragment.OnSelectionListener; import org.dmfs.tasks.utils.BaseActivity; 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 BaseActivity implements OnSelectionListener -{ - private int mAppWidgetId; - private Intent mResultIntent; - - - @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); - - // make the result intent and set the result to canceled - mResultIntent = new Intent(); - mResultIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); - setResult(RESULT_CANCELED, mResultIntent); - } - - getSupportFragmentManager().beginTransaction() - .add(R.id.task_list_selection_container, new TaskListSelectionFragment()) - .commit(); - } - - - @Override - public void onSelection(ArrayList selectedLists) - { - persistSelectedTaskLists(selectedLists); - finishWithResult(true); - +public class TaskListWidgetSettingsActivity extends BaseActivity implements OnSelectionListener { + private int mAppWidgetId; + private Intent mResultIntent; + + @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); + + // make the result intent and set the result to canceled + mResultIntent = new Intent(); + mResultIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); + setResult(RESULT_CANCELED, mResultIntent); } - - @Override - public void onSelectionCancel() - { - finishWithResult(false); - + getSupportFragmentManager() + .beginTransaction() + .add(R.id.task_list_selection_container, new TaskListSelectionFragment()) + .commit(); + } + + @Override + public void onSelection(ArrayList selectedLists) { + persistSelectedTaskLists(selectedLists); + finishWithResult(true); + } + + @Override + public void onSelectionCancel() { + finishWithResult(false); + } + + private void persistSelectedTaskLists(ArrayList lists) { + WidgetConfigurationDatabaseHelper dbHelper = new WidgetConfigurationDatabaseHelper(this); + SQLiteDatabase db = dbHelper.getWritableDatabase(); + + // delete old configuration + WidgetConfigurationDatabaseHelper.deleteConfiguration(db, mAppWidgetId); + + // add new configuration + for (Long taskId : lists) { + WidgetConfigurationDatabaseHelper.insertTaskList(db, mAppWidgetId, taskId); } - - - private void persistSelectedTaskLists(ArrayList lists) - { - WidgetConfigurationDatabaseHelper dbHelper = new WidgetConfigurationDatabaseHelper(this); - SQLiteDatabase db = dbHelper.getWritableDatabase(); - - // delete old configuration - WidgetConfigurationDatabaseHelper.deleteConfiguration(db, mAppWidgetId); - - // add new configuration - for (Long taskId : lists) - { - WidgetConfigurationDatabaseHelper.insertTaskList(db, mAppWidgetId, taskId); - } - db.close(); + 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); + + if (ok) { + setResult(RESULT_OK, intent); + } else { + setResult(RESULT_CANCELED, intent); } - - 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); - } - - 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 68c66289..43487f54 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java @@ -26,11 +26,14 @@ import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; -import androidx.loader.content.CursorLoader; import android.text.format.Time; import android.widget.RemoteViews; import android.widget.RemoteViewsService; - +import androidx.loader.content.CursorLoader; +import java.util.ArrayList; +import java.util.TimeZone; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import org.dmfs.provider.tasks.AuthorityUtil; import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract; @@ -42,416 +45,371 @@ import org.dmfs.tasks.utils.TimeChangeListener; import org.dmfs.tasks.utils.TimeChangeObserver; import org.dmfs.tasks.utils.WidgetConfigurationDatabaseHelper; -import java.util.ArrayList; -import java.util.TimeZone; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - - /** * A service to keep the task list widget updated. - *

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

    + * + *

    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); - } - - +public class TaskListWidgetUpdaterService extends RemoteViewsService { + private static final 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 { /** - * 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. + * The {@link TaskListWidgetItem} array which stores the tasks to be displayed. When the cursor + * loads it is updated. */ - 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 = AuthorityUtil.taskAuthority(context); - } - - - public void reload() - { - mExecutor.execute(mReloadTasks); - } + private TaskListWidgetItem[] mItems = null; + /** The {@link Context} of the {@link Application} to which this widget belongs. */ + private Context mContext; - /** - * Required for the broadcast receiver. - */ - public TaskListViewsFactory() - { - } + /** The app widget id. */ + private int mAppWidgetId = -1; + /** This variable is used to store the current time for reference. */ + private Time mNow; - /* - * (non-Javadoc) - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#onCreate() - */ - @Override - public void onCreate() - { - } + /** The resource from the {@link Application}. */ + private Resources mResources; + /** The due date formatter. */ + private DateFormatter mDueDateFormatter; - /* - * (non-Javadoc) - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#onDestroy() - */ - @Override - public void onDestroy() - { - // no-op - } + /** The executor to reload the tasks. */ + private final Executor mExecutor = Executors.newSingleThreadExecutor(); + private String mAuthority; - /* - * (non-Javadoc) - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#getCount() - */ - @Override - public int getCount() - { - if (mItems == null) - { - return 0; - } - return (mItems.length); - } - + /** + * A status variable that is used in onDataSetChanged to switch between updating the view and + * reloading the the whole dataset + */ + private boolean mDoNotReload; - /* - * (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 && Time.compare(dueDate, mNow) <= 0 || 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.holo_green_light)); - } - } - else - { - row.setTextViewText(android.R.id.text1, null); - } + /** + * 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 = AuthorityUtil.taskAuthority(context); + } - Uri taskUri = ContentUris.withAppendedId(Instances.getContentUri(mAuthority), items[position].getInstanceId()); - Intent i = new Intent(); - i.setData(taskUri); - row.setOnClickFillInIntent(R.id.widget_list_item, i); + public void reload() { + mExecutor.execute(mReloadTasks); + } - return (row); - } + /** Required for the broadcast receiver. */ + public TaskListViewsFactory() {} + /* + * (non-Javadoc) + * + * @see android.widget.RemoteViewsService.RemoteViewsFactory#onCreate() + */ + @Override + public void onCreate() {} - /* - * Don't show any loading views - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#getLoadingView() - */ - @Override - public RemoteViews getLoadingView() - { - return null; - } + /* + * (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); + } - /* - * Only single type of list item. - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#getViewTypeCount() - */ - @Override - public int getViewTypeCount() - { - return 1; + /* + * (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(); } - - - /* - * The position corresponds to the ID. - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#getItemId(int) - */ - @Override - public long getItemId(int position) - { - return position; + 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 && Time.compare(dueDate, mNow) <= 0 + || 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.holo_green_light)); } + } else { + row.setTextViewText(android.R.id.text1, null); + } + + Uri taskUri = + ContentUris.withAppendedId( + Instances.getContentUri(mAuthority), items[position].getInstanceId()); + 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; + } - /* - * - * - * @see android.widget.RemoteViewsService.RemoteViewsFactory#hasStableIds() - */ - @Override - public boolean hasStableIds() - { - return true; - } + /* + * 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; + } - /* - * 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 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(); - } + /* + * @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. + } - /* - * 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.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; + } - - /** - * 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.TASK_ID.get(mTasksCursor), TaskFieldAdapters.TITLE.get(mTasksCursor), - TaskFieldAdapters.DUE.get(mTasksCursor), TaskFieldAdapters.LIST_COLOR.get(mTasksCursor), - TaskFieldAdapters.IS_CLOSED.get(mTasksCursor)); - itemIndex++; + /** 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 + + " )"); + + selection.append(" AND ").append(Instances.DISTANCE_FROM_CURRENT).append(" <=0 "); + 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(" ) "); } - 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 + " )"); - - selection.append(" AND ").append(Instances.DISTANCE_FROM_CURRENT).append(" <=0 "); - 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( + // 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; + 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]; + } - // notify the widget manager about the update - AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext); - if (mAppWidgetId != -1) - { - widgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.task_list_widget_lv); + // 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/linkify/ActionModeLinkify.java b/opentasks/src/main/java/org/dmfs/tasks/linkify/ActionModeLinkify.java index d11fc54f..1eb81ba2 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/linkify/ActionModeLinkify.java +++ b/opentasks/src/main/java/org/dmfs/tasks/linkify/ActionModeLinkify.java @@ -16,6 +16,10 @@ package org.dmfs.tasks.linkify; +import static io.reactivex.Completable.ambArray; +import static org.dmfs.tasks.linkify.ViewObservables.activityTouchEvents; +import static org.dmfs.tasks.linkify.ViewObservables.textChanges; + import android.annotation.SuppressLint; import android.graphics.Rect; import android.net.Uri; @@ -30,147 +34,121 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.TextView; - -import org.dmfs.jems.iterable.elementary.Seq; -import org.dmfs.jems.procedure.composite.ForEach; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import androidx.annotation.NonNull; import io.reactivex.Completable; - -import static io.reactivex.Completable.ambArray; -import static org.dmfs.tasks.linkify.ViewObservables.activityTouchEvents; -import static org.dmfs.tasks.linkify.ViewObservables.textChanges; - +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.dmfs.jems.iterable.elementary.Seq; +import org.dmfs.jems.procedure.composite.ForEach; /** - * Adds clickable links that, on click, present a (floating) action mode to the user (unless running on Android 5). + * Adds clickable links that, on click, present a (floating) action mode to the user (unless running + * on Android 5). */ -public class ActionModeLinkify -{ - private final static String TEL_PATTERN = "(?:\\+\\d{1,5}\\s*)?(?:\\(\\d{1,6}\\)\\s*)?\\d[-, \\.\\/\\d]{4,}\\d"; - private final static String URL_PATTERN = "(?:https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})"; - private final static String MAIL_PATTERN = "(?:[a-zA-Z\\d!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-zA-Z\\d!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z\\d](?:[a-z\\d-]*[a-z\\d])?\\.)+[a-z\\d](?:[a-z\\d-]*[a-z\\d])?|\\[(?:(?:(?:2(?:5[0-5]|[0-4]\\d)|1\\d\\d|\\d?\\d))\\.){3}(?:(?:2(?:5[0-5]|[0-4]\\d)|1\\d\\d|\\d?\\d)|[a-z\\d-]*[a-z\\d]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"; - private final static Pattern LINK_PATTERN = Pattern.compile(String.format("(?:^|\\s+)((%s)|(%s)|(%s))(?:\\s+|$)", TEL_PATTERN, URL_PATTERN, MAIL_PATTERN)); - - - public interface ActionModeListener - { - boolean prepareMenu(TextView view, Uri uri, Menu menu); - - boolean onClick(TextView view, Uri uri, MenuItem item); - } - - - public static void linkify(TextView textView, ActionModeListener listener) - { - CharSequence text = textView.getText(); - Matcher m = LINK_PATTERN.matcher(text); - SpannableString s = new SpannableString(text); - new ForEach<>(new Seq<>(s.getSpans(0, s.length(), ClickableSpan.class))).process(s::removeSpan); - int pos = 0; - while (m.find(pos)) - { - int start = m.start(1); - int end = m.end(1); - pos = end; - Uri uri = null; - if (m.group(2) != null) - { - uri = Uri.parse("tel:" + PhoneNumberUtils.normalizeNumber(m.group(2))); - } - else if (m.group(3) != null) - { - uri = Uri.parse(m.group(3)); - if (uri.getScheme() == null) - { - uri = uri.buildUpon().scheme("https").build(); +public class ActionModeLinkify { + private static final String TEL_PATTERN = + "(?:\\+\\d{1,5}\\s*)?(?:\\(\\d{1,6}\\)\\s*)?\\d[-, \\.\\/\\d]{4,}\\d"; + private static final String URL_PATTERN = + "(?:https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})"; + private static final String MAIL_PATTERN = + "(?:[a-zA-Z\\d!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-zA-Z\\d!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z\\d](?:[a-z\\d-]*[a-z\\d])?\\.)+[a-z\\d](?:[a-z\\d-]*[a-z\\d])?|\\[(?:(?:(?:2(?:5[0-5]|[0-4]\\d)|1\\d\\d|\\d?\\d))\\.){3}(?:(?:2(?:5[0-5]|[0-4]\\d)|1\\d\\d|\\d?\\d)|[a-z\\d-]*[a-z\\d]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"; + private static final Pattern LINK_PATTERN = + Pattern.compile( + String.format( + "(?:^|\\s+)((%s)|(%s)|(%s))(?:\\s+|$)", TEL_PATTERN, URL_PATTERN, MAIL_PATTERN)); + + public interface ActionModeListener { + boolean prepareMenu(TextView view, Uri uri, Menu menu); + + boolean onClick(TextView view, Uri uri, MenuItem item); + } + + public static void linkify(TextView textView, ActionModeListener listener) { + CharSequence text = textView.getText(); + Matcher m = LINK_PATTERN.matcher(text); + SpannableString s = new SpannableString(text); + new ForEach<>(new Seq<>(s.getSpans(0, s.length(), ClickableSpan.class))).process(s::removeSpan); + int pos = 0; + while (m.find(pos)) { + int start = m.start(1); + int end = m.end(1); + pos = end; + Uri uri = null; + if (m.group(2) != null) { + uri = Uri.parse("tel:" + PhoneNumberUtils.normalizeNumber(m.group(2))); + } else if (m.group(3) != null) { + uri = Uri.parse(m.group(3)); + if (uri.getScheme() == null) { + uri = uri.buildUpon().scheme("https").build(); + } + } else if (m.group(4) != null) { + uri = Uri.parse("mailto:" + m.group(4)); + } + Uri finalUri = uri; + s.setSpan( + new ClickableSpan() { + @SuppressLint("CheckResult") + @Override + public void onClick(@NonNull View widget) { + if (Build.VERSION.SDK_INT >= 23) { + Completable closeActionTrigger = + ambArray( + textChanges(textView).firstElement().ignoreElement(), + activityTouchEvents(textView).firstElement().ignoreElement()) + .cache(); + + ActionMode.Callback2 actionMode = + new ActionMode.Callback2() { + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return listener.prepareMenu(textView, finalUri, menu); + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return listener.onClick(textView, finalUri, item); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + closeActionTrigger.subscribe().dispose(); + } + + @Override + public void onGetContentRect(ActionMode mode, View view, Rect outRect) { + Layout layout = textView.getLayout(); + int firstLine = layout.getLineForOffset(start); + int lastLine = layout.getLineForOffset(end); + layout.getLineBounds(firstLine, outRect); + if (firstLine == lastLine) { + outRect.left = (int) layout.getPrimaryHorizontal(start); + outRect.right = (int) layout.getPrimaryHorizontal(end); + } else { + Rect lastLineBounds = new Rect(); + layout.getLineBounds(lastLine, lastLineBounds); + outRect.bottom = lastLineBounds.bottom; + } + } + }; + + ActionMode am = + textView.startActionMode(actionMode, android.view.ActionMode.TYPE_FLOATING); + if (am != null) { + closeActionTrigger.subscribe(am::finish); } + } } - else if (m.group(4) != null) - { - uri = Uri.parse("mailto:" + m.group(4)); - } - Uri finalUri = uri; - s.setSpan(new ClickableSpan() - { - @SuppressLint("CheckResult") - @Override - public void onClick(@NonNull View widget) - { - if (Build.VERSION.SDK_INT >= 23) - { - Completable closeActionTrigger = ambArray( - textChanges(textView).firstElement().ignoreElement(), - activityTouchEvents(textView).firstElement().ignoreElement()) - .cache(); - - ActionMode.Callback2 actionMode = new ActionMode.Callback2() - { - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) - { - return listener.prepareMenu(textView, finalUri, menu); - } - - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) - { - return true; - } - - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) - { - return listener.onClick(textView, finalUri, item); - } - - - @Override - public void onDestroyActionMode(ActionMode mode) - { - closeActionTrigger.subscribe().dispose(); - } - - - @Override - public void onGetContentRect(ActionMode mode, View view, Rect outRect) - { - Layout layout = textView.getLayout(); - int firstLine = layout.getLineForOffset(start); - int lastLine = layout.getLineForOffset(end); - layout.getLineBounds(firstLine, outRect); - if (firstLine == lastLine) - { - outRect.left = (int) layout.getPrimaryHorizontal(start); - outRect.right = (int) layout.getPrimaryHorizontal(end); - } - else - { - Rect lastLineBounds = new Rect(); - layout.getLineBounds(lastLine, lastLineBounds); - outRect.bottom = lastLineBounds.bottom; - } - } - }; - - ActionMode am = textView.startActionMode(actionMode, android.view.ActionMode.TYPE_FLOATING); - if (am != null) - { - closeActionTrigger.subscribe(am::finish); - } - } - } - }, - start, - end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - textView.setText(s); + }, + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } + textView.setText(s); + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/linkify/ViewObservables.java b/opentasks/src/main/java/org/dmfs/tasks/linkify/ViewObservables.java index c6f65c39..50454ade 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/linkify/ViewObservables.java +++ b/opentasks/src/main/java/org/dmfs/tasks/linkify/ViewObservables.java @@ -16,6 +16,8 @@ package org.dmfs.tasks.linkify; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + import android.annotation.SuppressLint; import android.text.Editable; import android.text.TextWatcher; @@ -23,65 +25,58 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import io.reactivex.Observable; import io.reactivex.disposables.Disposables; -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; - - -public final class ViewObservables -{ - public static Observable textChanges(TextView view) - { - return Observable.create(emitter -> { - TextWatcher textWatcher = new TextWatcher() - { +public final class ViewObservables { + public static Observable textChanges(TextView view) { + return Observable.create( + emitter -> { + TextWatcher textWatcher = + new TextWatcher() { @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) - { - // nothing + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // nothing } - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) - { - // nothing + public void onTextChanged(CharSequence s, int start, int before, int count) { + // nothing } - @Override - public void afterTextChanged(Editable s) - { - emitter.onNext(s); + public void afterTextChanged(Editable s) { + emitter.onNext(s); } - }; - emitter.setDisposable(Disposables.fromRunnable(() -> view.removeTextChangedListener(textWatcher))); - view.addTextChangedListener(textWatcher); + }; + emitter.setDisposable( + Disposables.fromRunnable(() -> view.removeTextChangedListener(textWatcher))); + view.addTextChangedListener(textWatcher); }); - } - + } - @SuppressLint("ClickableViewAccessibility") - public static Observable activityTouchEvents(View view) - { - return Observable.create(emitter -> { - // set up a trap to receive touch events outside the ActionMode view. - View touchTrap = new View(view.getContext()); - touchTrap.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); - ViewGroup root = (ViewGroup) view.getRootView(); - root.addView(touchTrap); + @SuppressLint("ClickableViewAccessibility") + public static Observable activityTouchEvents(View view) { + return Observable.create( + emitter -> { + // set up a trap to receive touch events outside the ActionMode view. + View touchTrap = new View(view.getContext()); + touchTrap.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + ViewGroup root = (ViewGroup) view.getRootView(); + root.addView(touchTrap); - emitter.setDisposable(Disposables.fromRunnable(() -> { - touchTrap.setOnTouchListener(null); - root.removeView(touchTrap); - })); + emitter.setDisposable( + Disposables.fromRunnable( + () -> { + touchTrap.setOnTouchListener(null); + root.removeView(touchTrap); + })); - touchTrap.setOnTouchListener((v, event) -> { + touchTrap.setOnTouchListener( + (v, event) -> { emitter.onNext(event); return false; - }); + }); }); - } -} \ No newline at end of file + } +} 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 46b2a09a..d8baac5e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/AbstractArrayChoicesAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/AbstractArrayChoicesAdapter.java @@ -17,94 +17,74 @@ package org.dmfs.tasks.model; 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; +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); + } } - - - @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; + 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); + } } - - - @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 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); - } - + 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 b2443bf9..7bbb20ec 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,52 @@ package org.dmfs.tasks.model; 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. + * 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(); - } - - - /** - * 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; - } +public class ArrayChoicesAdapter extends AbstractArrayChoicesAdapter { + public ArrayChoicesAdapter() { + mChoices = new ArrayList(); + mDrawables = new ArrayList(); + mTitles = new ArrayList(); + mVisibleChoices = new ArrayList(); + } - /** - * 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; - } + /** + * 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; + } } 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 438d322f..a10640b7 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/CheckListItem.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/CheckListItem.java @@ -18,35 +18,25 @@ package org.dmfs.tasks.model; import android.text.TextUtils; - -public final class CheckListItem -{ - public boolean checked; - public String 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); +public final class CheckListItem { + public boolean checked; + public String 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); + } + + public boolean equals(Object o) { + if (!(o instanceof CheckListItem)) { + return false; } - - - public boolean equals(Object o) - { - if (!(o instanceof CheckListItem)) - { - return false; - } - CheckListItem other = (CheckListItem) o; - return TextUtils.equals(text, other.text) && checked == other.checked; - } - + 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 1920d054..64400efc 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java @@ -23,543 +23,433 @@ import android.net.Uri; 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 java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; 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; /** * 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. - *

    + * + *

    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(); - - /** - * Indicates that loading is in process. - */ - private boolean mLoading = false; - - - /** - * Private constructor that is used when creating a ContentSet form a parcel. - */ - private ContentSet() - { +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(); + + /** 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"); } - - /** - * 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); - } - - 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); - } + 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"); } - - @Override - public void onContentLoaded(ContentValues values) - { - mBeforeContentValues = values; - mLoading = false; - notifyLoadedListeners(); + if (other.mBeforeContentValues != null) { + mBeforeContentValues = new ContentValues(other.mBeforeContentValues); } - - /** - * 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; + if (other.mAfterContentValues != null) { + mAfterContentValues = new ContentValues(other.mAfterContentValues); } - - /** - * 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; - mUri = null; - } - else - { - throw new UnsupportedOperationException("Can not load delete a directoy URI: " + mUri); - } - } - else - { - Log.w(TAG, "Trying to delete empty ContentSet"); - } - + 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); } - - - 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 - + } + + @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; - - return mUri; - } - - - public boolean isInsert() - { - return mBeforeContentValues == null && mAfterContentValues != null && mAfterContentValues.size() > 0; + mUri = null; + } else { + throw new UnsupportedOperationException("Can not load delete a directoy URI: " + mUri); + } + } else { + Log.w(TAG, "Trying to delete empty ContentSet"); } + } - - 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 Uri persist(Context context) { + if (mAfterContentValues == null || mAfterContentValues.size() == 0) { + // nothing to do here + return mUri; } - - private ContentValues ensureAfter() - { - ContentValues values = mAfterContentValues; - if (values == null) - { - values = new ContentValues(); - mAfterContentValues = values; - } - return values; + if (isInsert()) { + // update uri with new uri + mUri = context.getContentResolver().insert(mUri, mAfterContentValues); + } else if (isUpdate()) { + context.getContentResolver().update(mUri, mAfterContentValues, null, null); } - - - 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); - notifyUpdateListeners(key); - return; - } - } - // value has changed, update - ensureAfter().put(key, value); - notifyUpdateListeners(key); - } + // 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); + } + + private ContentValues ensureAfter() { + ContentValues values = mAfterContentValues; + if (values == null) { + values = new ContentValues(); + mAfterContentValues = values; } - - - public Integer getAsInteger(String key) - { - final ContentValues after = mAfterContentValues; - if (after != null && after.containsKey(key)) - { - return mAfterContentValues.getAsInteger(key); + 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); + notifyUpdateListeners(key); + return; } - return mBeforeContentValues == null ? null : mBeforeContentValues.getAsInteger(key); + } + // value has changed, update + ensureAfter().put(key, value); + notifyUpdateListeners(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); - notifyUpdateListeners(key); - return; - } - } - ensureAfter().put(key, value); - notifyUpdateListeners(key); - } + public Integer getAsInteger(String key) { + final ContentValues after = mAfterContentValues; + if (after != null && after.containsKey(key)) { + return mAfterContentValues.getAsInteger(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.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); + notifyUpdateListeners(key); + return; } - return mBeforeContentValues == null ? null : mBeforeContentValues.getAsLong(key); + } + ensureAfter().put(key, value); + notifyUpdateListeners(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); - notifyUpdateListeners(key); - return; - } - } - ensureAfter().put(key, value); - notifyUpdateListeners(key); - } + public Long getAsLong(String key) { + final ContentValues after = mAfterContentValues; + if (after != null && after.containsKey(key)) { + return mAfterContentValues.getAsLong(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.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); + notifyUpdateListeners(key); + return; } - return mBeforeContentValues == null ? null : mBeforeContentValues.getAsString(key); + } + ensureAfter().put(key, value); + notifyUpdateListeners(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); - notifyUpdateListeners(key); - return; - } - } - ensureAfter().put(key, value); - notifyUpdateListeners(key); - } + public String getAsString(String key) { + final ContentValues after = mAfterContentValues; + if (after != null && after.containsKey(key)) { + return mAfterContentValues.getAsString(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.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); + notifyUpdateListeners(key); + return; } - 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; + } + ensureAfter().put(key, value); + notifyUpdateListeners(key); } + } - - /** - * 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; + public Float getAsFloat(String key) { + final ContentValues after = mAfterContentValues; + if (after != null && after.containsKey(key)) { + return mAfterContentValues.getAsFloat(key); } - - - /** - * 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; + 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); + } } - - - /** - * 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); - } - else if (mBeforeContentValues != null && mBeforeContentValues.get(key) != null) - { - ensureAfter().putNull(key); - } + --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); + } else if (mBeforeContentValues != null && mBeforeContentValues.get(key) != null) { + ensureAfter().putNull(key); } - - - 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. - listenerSet = Collections.newSetFromMap(new WeakHashMap()); - mOnChangeListeners.put(key, listenerSet); - } - - listenerSet.add(listener); - - if (notify && (mBeforeContentValues != null || mAfterContentValues != null)) - { - listener.onContentLoaded(this); - } + } + + 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. + listenerSet = Collections.newSetFromMap(new WeakHashMap()); + mOnChangeListeners.put(key, listenerSet); } + listenerSet.add(listener); - public void removeOnChangeListener(OnContentChangeListener listener, String key) - { - Set listenerSet = mOnChangeListeners.get(key); - if (listenerSet != null) - { - listenerSet.remove(listener); - } + if (notify && (mBeforeContentValues != null || mAfterContentValues != null)) { + listener.onContentLoaded(this); } + } - - 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); - } - } - } + public void removeOnChangeListener(OnContentChangeListener listener, String key) { + Set listenerSet = mOnChangeListeners.get(key); + if (listenerSet != null) { + listenerSet.remove(listener); } - - - private void notifyLoadedListeners() - { - Set listenerSet = mOnChangeListeners.get(null); - if (listenerSet != null) - { - for (OnContentChangeListener listener : listenerSet) - { - listener.onContentLoaded(this); - } + } + + 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); } + } } - - - @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); - } - - - public void readFromParcel(Parcel source) - { - ClassLoader loader = getClass().getClassLoader(); - mUri = source.readParcelable(loader); - mBeforeContentValues = source.readParcelable(loader); - mAfterContentValues = source.readParcelable(loader); + } + + private void notifyLoadedListeners() { + Set listenerSet = mOnChangeListeners.get(null); + if (listenerSet != null) { + for (OnContentChangeListener listener : listenerSet) { + listener.onContentLoaded(this); + } } - - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() - { - public ContentSet createFromParcel(Parcel in) - { - final ContentSet state = new ContentSet(); - state.readFromParcel(in); - return state; + } + + @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); + } + + public void readFromParcel(Parcel source) { + ClassLoader loader = getClass().getClassLoader(); + mUri = source.readParcelable(loader); + mBeforeContentValues = source.readParcelable(loader); + mAfterContentValues = source.readParcelable(loader); + } + + 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]; + 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 550f5300..ba79d250 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/CursorChoicesAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/CursorChoicesAdapter.java @@ -19,92 +19,70 @@ package org.dmfs.tasks.model; import android.database.Cursor; import android.graphics.drawable.Drawable; - /** * An {@link IChoicesAdapter} implementation that loads all values from a cursor. - *

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

    + * + *

    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; - - - public CursorChoicesAdapter(Cursor cursor) - { - mCursor = cursor; - } - - - @Override - public String getTitle(Object object) - { - // return mCursor.getString(mCursor.getColumnIndex(mTitleColumn)); - return null; - } - - - @Override - public Drawable getDrawable(Object object) - { - return null; - - } - +public class CursorChoicesAdapter implements IChoicesAdapter { - public String getKeyColumn() - { - return mKeyColumn; - } + @SuppressWarnings("unused") + private static final String TAG = "CursorChoicesAdapter"; + private final Cursor mCursor; - public CursorChoicesAdapter setKeyColumn(String keyColumn) - { - mKeyColumn = keyColumn; - return this; - } + @SuppressWarnings("unused") + private String mTitleColumn; + private String mKeyColumn; - public CursorChoicesAdapter setTitleColumn(String column) - { - mTitleColumn = column; - return this; - } + public CursorChoicesAdapter(Cursor cursor) { + mCursor = cursor; + } + @Override + public String getTitle(Object object) { + // return mCursor.getString(mCursor.getColumnIndex(mTitleColumn)); + return null; + } - public Cursor getChoices() - { - return mCursor; - } + @Override + public Drawable getDrawable(Object object) { + return null; + } + public String getKeyColumn() { + return mKeyColumn; + } - @Override - public int getIndex(Object id) - { - return 0; - } + public CursorChoicesAdapter setKeyColumn(String keyColumn) { + mKeyColumn = keyColumn; + return this; + } + public CursorChoicesAdapter setTitleColumn(String column) { + mTitleColumn = column; + return this; + } - @Override - public int getCount() - { - return mCursor.getCount(); - } + public Cursor getChoices() { + return mCursor; + } + @Override + public int getIndex(Object id) { + return 0; + } - @Override - public Object getItem(int position) - { - return null; - } + @Override + public int getCount() { + return mCursor.getCount(); + } + @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 031ab11a..7519df3d 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java @@ -18,166 +18,286 @@ package org.dmfs.tasks.model; import android.content.Context; import android.text.util.Linkify; - import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract.Tasks; import org.dmfs.tasks.model.layout.LayoutDescriptor; - /** * 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 DESCRIPTION_VIEW = new LayoutDescriptor(R.layout.description_field_view); - private final static LayoutDescriptor DESCRIPTION_EDIT = new LayoutDescriptor(R.layout.description_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); - private final static LayoutDescriptor RRULE_EDIT = new LayoutDescriptor(R.layout.opentasks_rrule_field_editor); - - final static LayoutDescriptor LIST_COLOR_VIEW = new LayoutDescriptor(R.layout.list_color_view); - - - public DefaultModel(Context context, String accountType) - { - super(context, accountType); +public class DefaultModel extends Model { + static final LayoutDescriptor TEXT_VIEW = + new LayoutDescriptor(R.layout.text_field_view) + .setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL); + static final LayoutDescriptor TEXT_VIEW_NO_LINKS = + new LayoutDescriptor(R.layout.text_field_view).setOption(LayoutDescriptor.OPTION_LINKIFY, 0); + private static final LayoutDescriptor LOCATION_VIEW = + new LayoutDescriptor(R.layout.opentasks_location_field_view) + .setOption(LayoutDescriptor.OPTION_LINKIFY, 0); + private static final LayoutDescriptor TEXT_EDIT = + new LayoutDescriptor(R.layout.text_field_editor); + private static final LayoutDescriptor TEXT_EDIT_SINGLE_LINE = + new LayoutDescriptor(R.layout.text_field_editor) + .setOption(LayoutDescriptor.OPTION_MULTILINE, false); + private static final LayoutDescriptor DESCRIPTION_VIEW = + new LayoutDescriptor(R.layout.description_field_view); + private static final LayoutDescriptor DESCRIPTION_EDIT = + new LayoutDescriptor(R.layout.description_field_editor); + private static final LayoutDescriptor CHOICES_VIEW = + new LayoutDescriptor(R.layout.choices_field_view); + private static final LayoutDescriptor CHOICES_EDIT = + new LayoutDescriptor(R.layout.choices_field_editor); + private static final LayoutDescriptor PROGRESS_VIEW = + new LayoutDescriptor(R.layout.percentage_field_view); + private static final LayoutDescriptor PROGRESS_EDIT = + new LayoutDescriptor(R.layout.percentage_field_editor); + private static final LayoutDescriptor TIME_VIEW = new LayoutDescriptor(R.layout.time_field_view); + private static final LayoutDescriptor TIME_VIEW_ADD_BUTTON = + new LayoutDescriptor(R.layout.time_field_view) + .setOption(LayoutDescriptor.OPTION_TIME_FIELD_SHOW_ADD_BUTTONS, true); + private static final LayoutDescriptor TIME_EDIT = + new LayoutDescriptor(R.layout.time_field_editor); + + @SuppressWarnings("unused") + private static final LayoutDescriptor BOOLEAN_VIEW = + new LayoutDescriptor(R.layout.boolean_field_view); + + private static final LayoutDescriptor BOOLEAN_EDIT = + new LayoutDescriptor(R.layout.boolean_field_editor); + private static final LayoutDescriptor URL_VIEW = new LayoutDescriptor(R.layout.url_field_view); + private static final LayoutDescriptor URL_EDIT = new LayoutDescriptor(R.layout.url_field_editor); + private static final LayoutDescriptor RRULE_EDIT = + new LayoutDescriptor(R.layout.opentasks_rrule_field_editor); + + static final 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; } - - @Override - public void inflate() - { - if (mInflated) - { - return; - } - - Context context = getContext(); - - /* - * 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_info_24)); - - // 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_location_on_24)); - - // description - addField(new FieldDescriptor(context, R.id.task_field_checklist, R.string.task_description, TaskFieldAdapters.DESCRIPTION_CHECKLIST) - .setViewLayout(DESCRIPTION_VIEW) - .setEditorLayout(DESCRIPTION_EDIT) - .setIcon(R.drawable.ic_notes_24)); - - // 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_timer_24)); - - // due - addField(new FieldDescriptor(context, R.id.task_field_due, R.string.task_due, TaskFieldAdapters.DUE).setEditorLayout(TIME_EDIT).setIcon( - R.drawable.ic_alarm_24)); - - // all day flag - addField(new FieldDescriptor(context, R.id.task_field_all_day, R.string.task_all_day, TaskFieldAdapters.ALLDAY).setEditorLayout(BOOLEAN_EDIT)); - - // rrule - addField(new FieldDescriptor(context, R.id.task_field_rrule, R.string.task_recurrence, TaskFieldAdapters.RRULE) - .setEditorLayout(RRULE_EDIT).setIcon(R.drawable.ic_baseline_repeat_24)); - - 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_flag_24)); - - // 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_progress_done_24)); - - 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_error_24)); - - 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_lock_24)); - - // 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_link_24)); - - // 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_list_24)); - - setAllowRecurrence(false); - setAllowExceptions(false); - - mInflated = true; - } + Context context = getContext(); + + /* + * 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_info_24)); + + // 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_location_on_24)); + + // description + addField( + new FieldDescriptor( + context, + R.id.task_field_checklist, + R.string.task_description, + TaskFieldAdapters.DESCRIPTION_CHECKLIST) + .setViewLayout(DESCRIPTION_VIEW) + .setEditorLayout(DESCRIPTION_EDIT) + .setIcon(R.drawable.ic_notes_24)); + + // 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_timer_24)); + + // due + addField( + new FieldDescriptor(context, R.id.task_field_due, R.string.task_due, TaskFieldAdapters.DUE) + .setEditorLayout(TIME_EDIT) + .setIcon(R.drawable.ic_alarm_24)); + + // all day flag + addField( + new FieldDescriptor( + context, R.id.task_field_all_day, R.string.task_all_day, TaskFieldAdapters.ALLDAY) + .setEditorLayout(BOOLEAN_EDIT)); + + // rrule + addField( + new FieldDescriptor( + context, R.id.task_field_rrule, R.string.task_recurrence, TaskFieldAdapters.RRULE) + .setEditorLayout(RRULE_EDIT) + .setIcon(R.drawable.ic_baseline_repeat_24)); + + 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_flag_24)); + + // 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_progress_done_24)); + + 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_error_24)); + + 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_lock_24)); + + // 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_link_24)); + + // 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_list_24)); + + setAllowRecurrence(false); + setAllowExceptions(false); + + mInflated = true; + } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java b/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java index 10019dbe..5e929e07 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java @@ -18,38 +18,28 @@ package org.dmfs.tasks.model; import androidx.annotation.Nullable; - -/** - * A bloody POJO o_O to store a description/check list item - */ -public final class DescriptionItem -{ - public boolean checkbox; - public boolean checked; - public String text; - - - public DescriptionItem(boolean checkbox, boolean checked, String text) - { - this.checkbox = checkbox; - this.checked = checked; - this.text = text; - } - - - @Override - public boolean equals(@Nullable Object obj) - { - return obj instanceof DescriptionItem - && ((DescriptionItem) obj).checkbox == checkbox - && ((DescriptionItem) obj).checked == checked - && ((DescriptionItem) obj).text.equals(text); - } - - - @Override - public int hashCode() - { - return text.hashCode() * 31 + (checkbox ? 1 : 0) + (checked ? 2 : 0); - } +/** A bloody POJO o_O to store a description/check list item */ +public final class DescriptionItem { + public boolean checkbox; + public boolean checked; + public String text; + + public DescriptionItem(boolean checkbox, boolean checked, String text) { + this.checkbox = checkbox; + this.checked = checked; + this.text = text; + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof DescriptionItem + && ((DescriptionItem) obj).checkbox == checkbox + && ((DescriptionItem) obj).checked == checked + && ((DescriptionItem) obj).text.equals(text); + } + + @Override + public int hashCode() { + return text.hashCode() * 31 + (checkbox ? 1 : 0) + (checked ? 2 : 0); + } } 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 ba531d67..fd01f9b2 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/FieldDescriptor.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/FieldDescriptor.java @@ -19,393 +19,304 @@ 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; - /** * 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 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!"); } - - - public boolean autoAdd() - { - return !mNoAutoAdd; + 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; } - - /** - * Returns the title of this field. - * - * @return The title. - */ - public String getTitle() - { - return mTitle; + 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; } - - /** - * 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; + 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; + } - /** - * 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; + public LayoutOptions getViewLayoutOptions() { + if (mViewLayout == null) { + return null; } + return mViewLayout.getOptions(); + } - /** - * 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; + public LayoutOptions getEditLayoutOptions() { + if (mEditLayout == null) { + return null; } + return mEditLayout.getOptions(); + } - /** - * Returns the hint for this field. - * - * @return The hint. - */ - public String getHint() - { - return mHint; - } - + FieldDescriptor setEditorLayout(LayoutDescriptor layoutDescriptor) { + mEditLayout = layoutDescriptor; + return this; + } - /** - * 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; - } + 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 55344a76..54389d87 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/IChoicesAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/IChoicesAdapter.java @@ -18,62 +18,52 @@ package org.dmfs.tasks.model; 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)}. + * 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 -{ +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. - */ - String getTitle(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. + */ + 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. - */ - Drawable getDrawable(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. + */ + 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. - */ - int getIndex(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. + */ + int getIndex(Object object); - /** - * Get the number of choices. - * - * @return The number of choices. - */ - int getCount(); + /** + * Get the number of choices. + * + * @return The number of choices. + */ + int getCount(); - /** - * Get the choice at the specified position. - * - * @param position - * The position. - * - * @return The choice object. - * - * @throws IndexOutOfBoundsException - * if the position is invalid. - */ - 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. + */ + 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 25f3717e..4b63f896 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/IllegalDataKindException.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/IllegalDataKindException.java @@ -16,17 +16,12 @@ package org.dmfs.tasks.model; -public class IllegalDataKindException extends Exception -{ +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 d6cddaca..7f752c96 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java @@ -18,53 +18,56 @@ package org.dmfs.tasks.model; 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); +public class MinimalModel extends Model { + private static final LayoutDescriptor TEXT_VIEW = + new LayoutDescriptor(R.layout.text_field_view) + .setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL); + private static final LayoutDescriptor TEXT_EDIT_SINGLE_LINE = + new LayoutDescriptor(R.layout.text_field_editor) + .setOption(LayoutDescriptor.OPTION_MULTILINE, false); + private static final LayoutDescriptor TIME_VIEW_ADD_BUTTON = + new LayoutDescriptor(R.layout.time_field_view) + .setOption(LayoutDescriptor.OPTION_TIME_FIELD_SHOW_ADD_BUTTONS, true); + private static final 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; } + Context context = getContext(); - @Override - public void inflate() - { - if (mInflated) - { - return; - } - - 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_alarm_24)); - // 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_alarm_24)); + 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 889b8b02..18271ebf 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/Model.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/Model.java @@ -23,7 +23,10 @@ import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.text.TextUtils; - +import androidx.annotation.IdRes; +import androidx.collection.SparseArrayCompat; +import java.util.ArrayList; +import java.util.List; import org.dmfs.iterables.decorators.Sieved; import org.dmfs.jems.optional.adapters.First; import org.dmfs.jems.single.combined.Backed; @@ -32,258 +35,202 @@ import org.dmfs.provider.tasks.utils.Range; import org.dmfs.tasks.ManageListActivity; import org.dmfs.tasks.contract.TaskContract.TaskLists; -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.IdRes; -import androidx.collection.SparseArrayCompat; - - /** * 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"; - - /** - * 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; - - boolean mInflated = false; - - 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; - - - protected Model(Context context, String accountType) - { - mContext = context; - mAccountType = accountType; - mAuthority = AuthorityUtil.taskAuthority(context); - } - - - public final Context getContext() - { - return mContext; - } - - - 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 addFieldAfter(@IdRes int id, FieldDescriptor descriptor) - { - mFields.add( - new Backed<>( - new First<>( - new Sieved<>(i -> mFields.get(i).getFieldId() == id, - new Range(0, mFields.size()))), mFields::size).value(), - descriptor); - mFieldIndex.put(descriptor.getFieldId(), descriptor); - } - - - 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 boolean getAllowRecurrence() - { - return mAllowRecurrence; - } - - - void setAllowRecurrence(boolean allowRecurrence) - { - mAllowRecurrence = allowRecurrence; - } - - - public boolean getAllowExceptions() - { - return mAllowExceptions; +public abstract class Model { + private static final String INTENT_CATEGORY_PREFIX = "org.dmfs.intent.category."; + private static final String EXTRA_COLOR_HINT = "org.dmfs.COLOR_HINT"; + private static final 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); + + private final Context mContext; + private final String mAuthority; + + boolean mInflated = false; + + 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; + + protected Model(Context context, String accountType) { + mContext = context; + mAccountType = accountType; + mAuthority = AuthorityUtil.taskAuthority(context); + } + + public final Context getContext() { + return mContext; + } + + 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 addFieldAfter(@IdRes int id, FieldDescriptor descriptor) { + mFields.add( + new Backed<>( + new First<>( + new Sieved<>( + i -> mFields.get(i).getFieldId() == id, new Range(0, mFields.size()))), + mFields::size) + .value(), + descriptor); + mFieldIndex.put(descriptor.getFieldId(), descriptor); + } + + 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 boolean getAllowRecurrence() { + return mAllowRecurrence; + } + + void setAllowRecurrence(boolean allowRecurrence) { + mAllowRecurrence = allowRecurrence; + } + + public boolean getAllowExceptions() { + return mAllowExceptions; + } + + void setAllowExceptions(boolean allowExceptions) { + mAllowExceptions = allowExceptions; + } + + public int getIconId() { + return mIconId; + } + + void setIconId(int iconId) { + mIconId = iconId; + } + + 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 boolean hasInsertActivity() { + if (mSupportsInsertListIntent == null) { + ComponentName insertComponent = + getListIntent(mContext, Intent.ACTION_INSERT, null) + .resolveActivity(mContext.getPackageManager()); + mSupportsInsertListIntent = insertComponent != null; } + return mSupportsInsertListIntent; + } - void setAllowExceptions(boolean allowExceptions) - { - mAllowExceptions = allowExceptions; + 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; + } - - public int getIconId() - { - return mIconId; - } - - - void setIconId(int iconId) - { - mIconId = iconId; - } - - - 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 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); + @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 6e18f392..70c493c9 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/ModelInflaterException.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/ModelInflaterException.java @@ -16,23 +16,16 @@ package org.dmfs.tasks.model; -public class ModelInflaterException extends Exception -{ +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 d48bcfbb..7b66ce76 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/OnContentChangeListener.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/OnContentChangeListener.java @@ -21,21 +21,18 @@ package org.dmfs.tasks.model; * * @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. - */ - void onContentChanged(ContentSet contentSet); +public interface OnContentChangeListener { + /** + * Called whenever a specific key in a {@link ContentSet} has changed. + * + * @param contentSet The {@link ContentSet} that contains the changed key. + */ + void onContentChanged(ContentSet contentSet); - /** - * Called whenever the {@link ContentSet} has been (re-)loaded. - * - * @param contentSet - * The {@link ContentSet} that has been reloaded. - */ - void onContentLoaded(ContentSet contentSet); + /** + * Called whenever the {@link ContentSet} has been (re-)loaded. + * + * @param contentSet The {@link ContentSet} that has been reloaded. + */ + 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 22912139..0fcc4e93 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/ResourceArrayChoicesAdapter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/ResourceArrayChoicesAdapter.java @@ -20,75 +20,66 @@ 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 -{ +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)); } - - - /** - * 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(); - } - + 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 e1740ecd..a4cd363e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/Sources.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/Sources.java @@ -28,300 +28,241 @@ import android.content.IntentFilter; import android.content.SyncAdapterType; import android.text.TextUtils; import android.util.Log; - -import org.dmfs.provider.tasks.AuthorityUtil; -import org.dmfs.tasks.contract.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; - +import org.dmfs.provider.tasks.AuthorityUtil; +import org.dmfs.tasks.contract.TaskContract; +import org.dmfs.tasks.utils.AsyncModelLoader; +import org.dmfs.tasks.utils.OnModelLoadedListener; /** * Holds model definitions for all available task sources. * * @author Marten Gajda */ -public final class Sources extends BroadcastReceiver implements OnAccountsUpdateListener -{ - - public final static String TAG = "tasks.model.Sources"; - - /** - * A Singleton instance in order to allow freeing it under memory pressure. - */ - private static Sources sInstance = null; +public final class Sources extends BroadcastReceiver implements OnAccountsUpdateListener { - /** - * Maps account types to their respective task model. - */ - private Map mAccountModelMap = new HashMap(); - - /** - * Our application context. - */ - private final Context mContext; + public static final String TAG = "tasks.model.Sources"; - /** - * The cached account manager. - */ - private final AccountManager mAccountManager; + /** A Singleton instance in order to allow freeing it under memory pressure. */ + private static Sources sInstance = null; - private final String mAuthority; + /** Maps account types to their respective task model. */ + private Map mAccountModelMap = new HashMap(); + /** Our application context. */ + private final Context mContext; - /** - * 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; - } + /** The cached account manager. */ + private final AccountManager mAccountManager; + private final String mAuthority; - /** - * 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; - } + /** + * 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); } - - - /** - * Initialize all model sources. - * - * @param context - */ - private Sources(Context context) - { - mContext = context.getApplicationContext(); - - mAuthority = AuthorityUtil.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(); + 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; } - - - /** - * 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); + } + + /** + * Initialize all model sources. + * + * @param context + */ + private Sources(Context context) { + mContext = context.getApplicationContext(); + + mAuthority = AuthorityUtil.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; } + } - 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); - } + if (model.getIconId() == -1) { + model.setIconId(authenticator.iconId); + } + if (model.getLabelId() == -1) { + model.setLabelId(authenticator.labelId); + } + mAccountModelMap.put(authenticator.type, model); } - - /** - * 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; + 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 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); + } + + /** + * 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; + } } - - - 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; + // 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 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 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 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. + 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. */ - 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. - */ - 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 adbf0325..7cce1569 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java @@ -17,7 +17,6 @@ package org.dmfs.tasks.model; import android.text.format.Time; - import org.dmfs.jems.optional.Optional; import org.dmfs.rfc5545.DateTime; import org.dmfs.rfc5545.recur.RecurrenceRule; @@ -48,184 +47,142 @@ import org.dmfs.tasks.model.constraints.DescriptionConstraint; import org.dmfs.tasks.model.defaults.DefaultAfter; 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. + * 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)); - - /** - * Adapter for the checklist of a task. - */ - public final static DescriptionFieldAdapter DESCRIPTION_CHECKLIST = (DescriptionFieldAdapter) new DescriptionFieldAdapter(Tasks.DESCRIPTION) - .addContraint(new DescriptionConstraint(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