Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stefan-niedermann/nextcloud-deck.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Niedermann <info@niedermann.it>2023-03-01 14:43:53 +0300
committerStefan Niedermann <info@niedermann.it>2023-03-09 11:53:19 +0300
commit3ea462ca9e2ae18ba9d869125da8d8d07f2c7854 (patch)
tree30257c67768325d5972ec499a6eb41e11017ac6d
parentbfab286b0bc6dbfac1211eec64d74b66b2ce1e6d (diff)
refactor: Unidirectional data flow and single point of truth for current state
Signed-off-by: Stefan Niedermann <info@niedermann.it>
-rw-r--r--app/build.gradle3
-rw-r--r--app/src/main/AndroidManifest.xml24
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java156
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java27
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java575
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java79
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java797
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java69
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java54
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java444
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDao.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java18
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_31_32.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/LiveDataHelper.java75
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java32
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java75
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java93
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java20
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java14
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/ConnectivityUtil.java38
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java269
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountViewModel.java33
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java1194
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java303
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java57
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/StackChangeCallback.java71
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java32
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java42
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java107
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountViewModel.java32
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java27
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActivity.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java)101
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsAdapter.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsViewModel.java42
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java88
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java68
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java5
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java52
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java13
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java59
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlViewModel.java48
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardDialogFragment.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java)66
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardListener.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardViewModel.java24
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/LabelsViewModel.java42
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java34
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardActionListener.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java163
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardTabAdapter.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java73
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java55
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java134
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardDialog.java153
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardViewModel.java76
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/SelectCardListener.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java97
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java68
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DeleteAttachmentDialogFragment.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsAdapter.java32
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java78
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java73
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java14
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java84
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeAdapter.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeFragment.java3
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java39
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java28
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java41
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/main/DrawerMenuInflater.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java)85
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java928
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivityNavigationHandler.java120
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java256
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java16
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java37
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java26
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java320
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java36
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java13
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackAdapter.java95
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackViewHolder.java46
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateViewModel.java24
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java44
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/PreferencesViewModel.java39
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java36
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java30
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java32
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackDialogFragment.java30
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackListener.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackDialogFragment.java91
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackListener.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java238
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackViewModel.java87
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java31
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java119
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemeUtils.java32
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDatePickerDialog.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDialogFragment.java16
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedPreferenceCategory.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedSnackbar.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedTimePickerDialog.java32
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/tiles/EditCardTileService.java3
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsActivity.java22
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsAdapter.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsOptionsItemSelectedListener.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsViewModel.java55
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/EmptyContentView.java20
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java3
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/BaseViewModel.java40
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/SyncViewModel.java80
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidget.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetFactory.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetViewModel.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java47
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetFactory.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java18
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidget.java20
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetFactory.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/AutoCompleteAdapter.java74
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/ExecutorServiceProvider.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/OnTextChangedWatcher.java35
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java84
-rw-r--r--app/src/main/res/drawable/selected_check.xml (renamed from app/src/main/res/drawable/selected.xml)4
-rw-r--r--app/src/main/res/layout/activity_about.xml4
-rw-r--r--app/src/main/res/layout/activity_archived.xml4
-rw-r--r--app/src/main/res/layout/activity_attachments.xml2
-rw-r--r--app/src/main/res/layout/activity_edit.xml4
-rw-r--r--app/src/main/res/layout/activity_exception.xml4
-rw-r--r--app/src/main/res/layout/activity_filter_widget.xml4
-rw-r--r--app/src/main/res/layout/activity_import_account.xml2
-rw-r--r--app/src/main/res/layout/activity_main.xml10
-rw-r--r--app/src/main/res/layout/activity_manage_accounts.xml4
-rw-r--r--app/src/main/res/layout/activity_pick_stack.xml17
-rw-r--r--app/src/main/res/layout/activity_push_notification.xml4
-rw-r--r--app/src/main/res/layout/activity_settings.xml20
-rw-r--r--app/src/main/res/layout/activity_upcoming_cards.xml4
-rw-r--r--app/src/main/res/layout/dialog_account_switcher.xml2
-rw-r--r--app/src/main/res/layout/dialog_move_card.xml26
-rw-r--r--app/src/main/res/layout/fragment_about_credits_tab.xml12
-rw-r--r--app/src/main/res/layout/fragment_pick_stack.xml16
-rw-r--r--app/src/main/res/layout/fragment_stack.xml2
-rw-r--r--app/src/main/res/layout/item_account_choose.xml2
-rw-r--r--app/src/main/res/layout/item_board.xml10
-rw-r--r--app/src/main/res/layout/item_card_compact.xml4
-rw-r--r--app/src/main/res/layout/item_card_default.xml2
-rw-r--r--app/src/main/res/layout/item_card_default_only_title.xml2
-rw-r--r--app/src/main/res/layout/item_filter_duetype.xml3
-rw-r--r--app/src/main/res/layout/item_filter_label.xml3
-rw-r--r--app/src/main/res/layout/item_filter_user.xml3
-rw-r--r--app/src/main/res/layout/item_prepare_create_stack.xml31
-rw-r--r--app/src/main/res/layout/item_tip.xml3
-rw-r--r--app/src/main/res/layout/widget_empty_content_view.xml29
-rw-r--r--app/src/main/res/values-night/colors.xml30
-rw-r--r--app/src/main/res/values-v24/styles.xml9
-rw-r--r--app/src/main/res/values/colors.xml58
-rw-r--r--app/src/main/res/values/dimens.xml2
-rw-r--r--app/src/main/res/values/setup.xml5
-rw-r--r--app/src/main/res/values/strings.xml9
-rw-r--r--app/src/main/res/values/styles.xml4
-rw-r--r--app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManagerTest.java47
-rw-r--r--app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapterTest.java4
-rw-r--r--app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDaoTest.java9
-rw-r--r--app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDaoTest.java6
-rw-r--r--app/src/test/resources/robolectric.properties2
-rw-r--r--fastlane/metadata/android/en-US/changelogs/1021009.txt1
-rw-r--r--reactive-livedata/build.gradle28
-rw-r--r--reactive-livedata/src/main/AndroidManifest.xml1
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveData.java216
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataBuilder.java130
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorLiveData.java20
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorObserver.java45
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorLiveData.java28
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorObserver.java62
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceLiveData.java19
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceObserver.java79
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/distinct/DistinctUntilChangedLiveData.java14
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/filter/FilterLiveData.java23
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/flatmap/FlatMapLiveData.java20
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/map/MapLiveData.java21
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/merge/MergeLiveData.java15
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeLiveData.java13
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeObserver.java37
-rw-r--r--reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/tap/TapLiveData.java34
-rw-r--r--reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataTest.java188
-rw-r--r--reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/TestUtil.java45
-rw-r--r--settings.gradle1
222 files changed, 7227 insertions, 4993 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 670758f59..7f28e2b9e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -6,7 +6,7 @@ android {
buildToolsVersion "31.0.0"
defaultConfig {
applicationId "it.niedermann.nextcloud.deck"
- minSdkVersion 23
+ minSdkVersion 24
targetSdkVersion 33
versionCode 1021008
versionName "1.21.8"
@@ -71,6 +71,7 @@ dependencies {
implementation project(path: ':cross-tab-drag-and-drop')
// TabLayoutHelper
implementation project(path: ':tab-layout-helper')
+ implementation project(path: ':reactive-livedata')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e8e3ae6e5..c05e81516 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,8 +29,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
+ android:enableOnBackInvokedCallback="true"
tools:ignore="GoogleAppIndexingWarning"
- tools:targetApi="q">
+ tools:targetApi="tiramisu">
<provider
android:name="androidx.core.content.FileProvider"
@@ -43,7 +44,7 @@
</provider>
<activity
- android:name=".ui.MainActivity"
+ android:name=".ui.main.MainActivity"
android:label="@string/app_name_short"
android:theme="@style/SplashTheme"
android:exported="true">
@@ -67,7 +68,7 @@
<activity
android:name=".ui.manageaccounts.ManageAccountsActivity"
android:label="@string/manage_accounts"
- android:parentActivityName=".ui.MainActivity"
+ android:parentActivityName=".ui.main.MainActivity"
android:windowSoftInputMode="stateHidden" />
<activity
@@ -94,24 +95,19 @@
</activity>
<activity
- android:name=".ui.archivedcards.ArchivedCardsActvitiy"
- android:label="@string/archived_cards"
- android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" />
-
- <activity
- android:name=".ui.archivedboards.ArchivedBoardsActvitiy"
+ android:name=".ui.archivedboards.ArchivedBoardsActivity"
android:label="@string/archived_boards"
- android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" />
+ android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" />
<activity
android:name=".ui.upcomingcards.UpcomingCardsActivity"
android:label="@string/widget_upcoming_title"
- android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" />
+ android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" />
<activity
android:name=".ui.card.EditActivity"
android:label="@string/edit"
- android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" />
+ android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" />
<activity
android:name=".ui.attachments.AttachmentsActivity"
@@ -122,7 +118,7 @@
<activity
android:name=".ui.settings.SettingsActivity"
android:label="@string/simple_settings"
- android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" />
+ android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" />
<activity
android:name=".ui.ImportAccountActivity"
@@ -144,7 +140,7 @@
<activity
android:name=".ui.about.AboutActivity"
android:label="@string/about"
- android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" />
+ android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" />
<activity
android:name=".ui.PushNotificationActivity"
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java b/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java
index 9902e011d..b7711502c 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java
@@ -1,79 +1,46 @@
package it.niedermann.nextcloud.deck;
-import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode;
-import static androidx.lifecycle.Transformations.distinctUntilChanged;
-
import android.app.Application;
-import android.content.Context;
import android.os.StrictMode;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
-import androidx.lifecycle.LiveData;
-import androidx.preference.PreferenceManager;
-
-import com.nextcloud.android.common.ui.util.PlatformThemeUtil;
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
-import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData;
-import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.persistence.PreferencesRepository;
+import it.niedermann.nextcloud.deck.util.CustomAppGlideModule;
public class DeckApplication extends Application {
- public static final long NO_ACCOUNT_ID = -1L;
- public static final long NO_BOARD_ID = -1L;
- public static final long NO_STACK_ID = -1L;
-
- private static String PREF_KEY_THEME;
- private static String PREF_KEY_DEBUGGING;
-
- private static LiveData<Integer> currentAccountColor$;
- private static LiveData<Integer> currentBoardColor$;
+ private final ExecutorService executor = new ThreadPoolExecutor(0, 2, 0L, TimeUnit.SECONDS, new SynchronousQueue<>());
@Override
public void onCreate() {
+ final var repo = new PreferencesRepository(this);
+
if (BuildConfig.DEBUG) {
enableStrictModeLogging();
}
- PREF_KEY_THEME = getString(R.string.pref_key_dark_theme);
- PREF_KEY_DEBUGGING = getString(R.string.pref_key_debugging);
- setAppTheme(getAppThemeSetting(this));
- DeckLog.enablePersistentLogs(isPersistentLoggingEnabled(this));
- final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
- currentAccountColor$ = distinctUntilChanged(new SharedPreferenceIntLiveData(sharedPreferences,
- getString(R.string.shared_preference_last_account_color),
- ContextCompat.getColor(this, R.color.defaultBrand)));
- currentBoardColor$ = distinctUntilChanged(new SharedPreferenceIntLiveData(sharedPreferences,
- getString(R.string.shared_preference_theme_main),
- ContextCompat.getColor(this, R.color.defaultBrand)));
+ repo.getAppThemeSetting().thenAcceptAsync(repo::setAppTheme, executor);
+ repo.isDebugModeEnabled().thenAcceptAsync(DeckLog::enablePersistentLogs, executor);
+
super.onCreate();
}
@Override
public void onLowMemory() {
super.onLowMemory();
+ DeckLog.error("--- Low memory: Clear Glide cache ---");
+ CustomAppGlideModule.clearCache(this);
+ DeckLog.error("--- Low memory: Clear debug log ---");
DeckLog.clearDebugLog();
- DeckLog.error("--- cleared log because of low memory ---");
- }
-
- // ---------
- // Debugging
- // ---------
-
- private static boolean isPersistentLoggingEnabled(@NonNull Context context) {
- final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
- final boolean enabled = sharedPreferences.getBoolean(PREF_KEY_DEBUGGING, false);
- DeckLog.log("--- Read:", PREF_KEY_DEBUGGING, "→", enabled);
- return enabled;
}
- private static void enableStrictModeLogging() {
+ private void enableStrictModeLogging() {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
- // SSO library: setCurrentSingleSignOnAccount works synchronously
- // Deck app: Handling of current account / board / stack works synchronously
.permitDiskReads()
.penaltyLog()
.build());
@@ -82,95 +49,4 @@ public class DeckApplication extends Application {
.penaltyLog()
.build());
}
-
- // -----------------
- // Day / Night theme
- // -----------------
-
- public static void setAppTheme(int setting) {
- setDefaultNightMode(setting);
- }
-
- public static int getAppThemeSetting(@NonNull Context context) {
- final var prefs = PreferenceManager.getDefaultSharedPreferences(context);
- String mode;
- try {
- mode = prefs.getString(PREF_KEY_THEME, context.getString(R.string.pref_value_theme_system_default));
- } catch (ClassCastException e) {
- mode = prefs.getBoolean(PREF_KEY_THEME, false) ? context.getString(R.string.pref_value_theme_dark) : context.getString(R.string.pref_value_theme_light);
- }
- return Integer.parseInt(mode);
- }
-
- public static boolean isDarkTheme(@NonNull Context context) {
- final var darkModeSetting = getAppThemeSetting(context);
- return darkModeSetting == Integer.parseInt(context.getString(R.string.pref_value_theme_system_default))
- ? PlatformThemeUtil.isDarkMode(context)
- : darkModeSetting == Integer.parseInt(context.getString(R.string.pref_value_theme_dark));
- }
-
- // --------------------------------------
- // Current account / board / stack states
- // --------------------------------------
-
- public static void saveCurrentAccount(@NonNull Context context, @NonNull Account account) {
- SingleAccountHelper.setCurrentAccount(context, account.getName());
- final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
- DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_account), "→", account.getId());
- editor.putLong(context.getString(R.string.shared_preference_last_account), account.getId());
- DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_account_color), "→", account.getColor());
- editor.putInt(context.getString(R.string.shared_preference_last_account_color), account.getColor());
- editor.apply();
- }
-
- public static LiveData<Integer> readCurrentAccountColor() {
- return currentAccountColor$;
- }
-
- public static LiveData<Integer> readCurrentBoardColor() {
- return currentBoardColor$;
- }
-
- @ColorInt
- public static int readCurrentAccountColor(@NonNull Context context) {
- final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
- @ColorInt final int accountColor = sharedPreferences.getInt(context.getString(R.string.shared_preference_last_account_color), context.getApplicationContext().getResources().getColor(R.color.defaultBrand));
- DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account_color), "→", accountColor);
- return accountColor;
- }
-
- public static long readCurrentAccountId(@NonNull Context context) {
- final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
- final long accountId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_account), NO_ACCOUNT_ID);
- DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account), "→", accountId);
- return accountId;
- }
-
- public static void saveCurrentBoardId(@NonNull Context context, long accountId, long boardId) {
- final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
- DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId);
- editor.putLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, boardId);
- editor.apply();
- }
-
- public static long readCurrentBoardId(@NonNull Context context, long accountId) {
- final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
- final long boardId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, NO_BOARD_ID);
- DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId);
- return boardId;
- }
-
- public static void saveCurrentStackId(@NonNull Context context, long accountId, long boardId, long stackId) {
- final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
- DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, "→", stackId);
- editor.putLong(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, stackId);
- editor.apply();
- }
-
- public static long readCurrentStackId(@NonNull Context context, long accountId, long boardId) {
- final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
- final long savedStackId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, NO_STACK_ID);
- DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, "→", savedStackId);
- return savedStackId;
- }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java
index aa5733044..2118fc068 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java
@@ -3,16 +3,10 @@ package it.niedermann.nextcloud.deck.api;
import android.content.Context;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.api.NextcloudAPI;
-import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
-import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
-import it.niedermann.nextcloud.deck.DeckLog;
import retrofit2.NextcloudRetrofitApiBuilder;
/**
@@ -27,36 +21,21 @@ public class ApiProvider {
private NextcloudServerAPI nextcloudAPI;
@NonNull
private final Context context;
- @Nullable
- private final String ssoAccountName;
- private SingleSignOnAccount ssoAccount;
+ private final SingleSignOnAccount ssoAccount;
- public ApiProvider(@NonNull Context context, @Nullable String ssoAccountName) {
+ public ApiProvider(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
this.context = context;
- this.ssoAccountName = ssoAccountName;
- setAccount();
+ this.ssoAccount = ssoAccount;
}
public synchronized void initSsoApi(@NonNull final NextcloudAPI.ApiConnectedListener callback) {
- if(this.deckAPI == null) {
+ if (this.deckAPI == null) {
final NextcloudAPI nextcloudAPI = new NextcloudAPI(context, ssoAccount, GsonConfig.getGson(), callback);
this.deckAPI = new NextcloudRetrofitApiBuilder(nextcloudAPI, DECK_API_ENDPOINT).create(DeckAPI.class);
this.nextcloudAPI = new NextcloudRetrofitApiBuilder(nextcloudAPI, NC_API_ENDPOINT).create(NextcloudServerAPI.class);
}
}
- private void setAccount() {
- try {
- if (ssoAccountName == null) {
- this.ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context);
- } else {
- this.ssoAccount = AccountImporter.getSingleSignOnAccount(context, ssoAccountName);
- }
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
- DeckLog.logError(e);
- }
- }
-
public DeckAPI getDeckAPI() {
return deckAPI;
}
@@ -65,18 +44,4 @@ public class ApiProvider {
return nextcloudAPI;
}
- public String getServerUrl(){
- if (ssoAccount == null) {
- setAccount();
- }
- return ssoAccount.url;
- }
-
- public String getApiPath() {
- return DECK_API_ENDPOINT;
- }
-
- public String getApiUrl() {
- return getServerUrl() + getApiPath();
- }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java
index 11fbf8e14..a5d3e3155 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java
@@ -7,8 +7,8 @@ import com.google.gson.JsonSyntaxException;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
public enum HandledServerErrors {
- UNKNOWN(1337, "hopefully won't occurr"),
- LABELS_TITLE_MUST_BE_UNIQUE(400, "title must be unique"),
+ UNKNOWN(1337, "hopefully won't occur"),
+ LABELS_TITLE_MUST_BE_UNIQUE(400, "Title must be unique"),
ATTACHMENTS_FILE_ALREADY_EXISTS(409, "File already exists."),
;
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java
index d7c4e8e8f..a54ad0986 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java
@@ -12,7 +12,7 @@ import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
-import com.nextcloud.android.sso.model.SingleSignOnAccount;
+import com.bumptech.glide.Glide;
import java.io.Serializable;
import java.util.Objects;
@@ -20,7 +20,7 @@ import java.util.Objects;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
import it.niedermann.nextcloud.deck.model.ocs.Version;
-import it.niedermann.nextcloud.deck.ui.accountswitcher.AccountSwitcherDialog;
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
@Entity(indices = {@Index(value = "name", unique = true)})
public class Account implements Serializable {
@@ -218,12 +218,17 @@ public class Account implements Serializable {
}
/**
- * A cache buster parameter is added for duplicate account names on different hosts which shall be fetched from the same {@link SingleSignOnAccount} (e. g. {@link AccountSwitcherDialog})
- *
- * @return an {@link String} to fetch the avatar for this account.
+ * @return The {@link #getAvatarUrl(int, String)} of this {@link Account}
*/
- public String getAvatarUrl(@Px int size) {
- return getUrl() + "/index.php/avatar/" + Uri.encode(getUserName()) + "/" + size;
+ public SingleSignOnUrl getAvatarUrl(@Px int size) {
+ return getAvatarUrl(size, getUserName());
+ }
+
+ /**
+ * @return a {@link SingleSignOnUrl} to fetch the avatar of the given <code>userName</code> from the instance of this {@link Account} via {@link Glide}.
+ */
+ public SingleSignOnUrl getAvatarUrl(@Px int size, @NonNull String userName) {
+ return new SingleSignOnUrl(getName(), getUrl() + "/index.php/avatar/" + Uri.encode(userName) + "/" + size);
}
@Override
@@ -239,9 +244,7 @@ public class Account implements Serializable {
url.equals(account.url) &&
color.equals(account.color) &&
textColor.equals(account.textColor) &&
- serverDeckVersion.equals(account.serverDeckVersion) &&
- Objects.equals(etag, account.etag) &&
- Objects.equals(boardsEtag, account.boardsEtag);
+ serverDeckVersion.equals(account.serverDeckVersion);
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java
index ebe9805ed..d27c35545 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java
@@ -12,6 +12,7 @@ import com.google.gson.annotations.JsonAdapter;
import java.io.Serializable;
import java.time.Instant;
+import java.util.Objects;
import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.DeckLog;
@@ -218,9 +219,9 @@ public class Board extends AbstractRemoteEntity implements Serializable {
if (permissionEdit != board.permissionEdit) return false;
if (permissionManage != board.permissionManage) return false;
if (permissionShare != board.permissionShare) return false;
- if (title != null ? !title.equals(board.title) : board.title != null) return false;
- if (color != null ? !color.equals(board.color) : board.color != null) return false;
- return deletedAt != null ? deletedAt.equals(board.deletedAt) : board.deletedAt == null;
+ if (!Objects.equals(title, board.title)) return false;
+ if (!Objects.equals(color, board.color)) return false;
+ return Objects.equals(deletedAt, board.deletedAt);
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java
index 4f4698a5a..355307ce0 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java
@@ -19,9 +19,9 @@ public class Version implements Comparable<Version> {
private static final Version VERSION_1_3_0 = new Version("1.3.0", 1, 3, 0);
private String originalVersion = "?";
- private int major;
- private int minor;
- private int patch;
+ private final int major;
+ private final int minor;
+ private final int patch;
public Version(String originalVersion, int major, int minor, int patch) {
this(major, minor, patch);
@@ -54,12 +54,8 @@ public class Version implements Comparable<Version> {
return originalVersion;
}
- public void setOriginalVersion(String originalVersion) {
- this.originalVersion = originalVersion;
- }
-
public static Version of(String versionString) {
- int major = 0, minor = 0, micro = 0;
+ int major = 0, minor = 0, patch = 0;
if (versionString != null) {
final String[] split = versionString.split("\\.");
if (split.length > 0) {
@@ -67,12 +63,12 @@ public class Version implements Comparable<Version> {
if (split.length > 1) {
minor = extractNumber(split[1]);
if (split.length > 2) {
- micro = extractNumber(split[2]);
+ patch = extractNumber(split[2]);
}
}
}
}
- return new Version(versionString, major, minor, micro);
+ return new Version(versionString, major, minor, patch);
}
private static int extractNumber(String containsNumbers) {
@@ -187,6 +183,17 @@ public class Version implements Comparable<Version> {
}
/**
+ * The first response structure of the very first call to at least the <code>/boards</code> endpoint of the Deck API can be different compared to all following calls.
+ * This behavior is tracked in an upstream issue and might be resolved in the future with a specific version.
+ *
+ * @return whether or not it is needed to make one request that must be ignored due to a different data structure before performing any other requests.
+ * @see <a href="https://github.com/nextcloud/deck/issues/3229">issue</a>
+ */
+ public boolean firstCallHasDifferentResponseStructure() {
+ return true;
+ }
+
+ /**
* URL to view a card in the web interface has been changed in {@link Version} 1.0.0
*
* @return the id of the string resource which contains the partial URL to open a card in the web UI
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java
new file mode 100644
index 000000000..570b36b13
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java
@@ -0,0 +1,575 @@
+package it.niedermann.nextcloud.deck.persistence;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+import androidx.lifecycle.LiveData;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.api.LastSyncUtil;
+import it.niedermann.nextcloud.deck.api.ResponseCallback;
+import it.niedermann.nextcloud.deck.model.AccessControl;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel;
+import it.niedermann.nextcloud.deck.model.enums.DBStatus;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects;
+import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel;
+import it.niedermann.nextcloud.deck.model.full.FullStack;
+import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
+import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget;
+import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter;
+import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil;
+import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterItem;
+
+/**
+ * Allows basic local access to the {@link DataBaseAdapter} layer but also to some app states which are stored in {@link SharedPreferences}.
+ * <p>
+ * This repository does not know anything about remote synchronization.
+ */
+@SuppressWarnings("WeakerAccess")
+public class BaseRepository {
+
+ @NonNull
+ protected final Context context;
+ @NonNull
+ protected final DataBaseAdapter dataBaseAdapter;
+ @NonNull
+ protected final ExecutorService executor;
+ @NonNull
+ protected final ConnectivityUtil connectivityUtil;
+ @NonNull
+ protected final ReactiveLiveData<Long> currentAccountId$;
+
+ public BaseRepository(@NonNull Context context) {
+ this(context, new ConnectivityUtil(context));
+ }
+
+ protected BaseRepository(@NonNull Context context, @NonNull ConnectivityUtil connectivityUtil) {
+ this(context, connectivityUtil, new DataBaseAdapter(context.getApplicationContext()), Executors.newCachedThreadPool());
+ }
+
+ protected BaseRepository(@NonNull Context context,
+ @NonNull ConnectivityUtil connectivityUtil,
+ @NonNull DataBaseAdapter databaseAdapter,
+ @NonNull ExecutorService executor) {
+ this.context = context.getApplicationContext();
+ this.connectivityUtil = connectivityUtil;
+ this.dataBaseAdapter = databaseAdapter;
+ this.executor = executor;
+ this.currentAccountId$ = new ReactiveLiveData<>(dataBaseAdapter.getCurrentAccountId$()).distinctUntilChanged();
+ LastSyncUtil.init(context.getApplicationContext());
+ }
+
+ public void saveCurrentAccount(@NonNull Account account) {
+ dataBaseAdapter.saveCurrentAccount(account);
+ }
+
+ public LiveData<Long> getCurrentAccountId$() {
+ return this.currentAccountId$;
+ }
+
+ public CompletableFuture<Long> getCurrentAccountId() {
+ return dataBaseAdapter.getCurrentAccountId();
+ }
+
+ public LiveData<Integer> getAccountColor(long accountId) {
+ return dataBaseAdapter.getAccountColor(accountId);
+ }
+
+ @ColorInt
+ public CompletableFuture<Integer> getCurrentAccountColor(long accountId) {
+ return dataBaseAdapter.getCurrentAccountColor(accountId);
+ }
+
+ // -------------
+ // Current board
+ // -------------
+
+ public void saveCurrentBoardId(long accountId, long boardId) {
+ dataBaseAdapter.saveCurrentBoardId(accountId, boardId);
+ }
+
+ public LiveData<Long> getCurrentBoardId$(long accountId) {
+ return dataBaseAdapter.getCurrentBoardId$(accountId);
+ }
+
+ public LiveData<Integer> getBoardColor$(long accountId, long boardId) {
+ return dataBaseAdapter.getBoardColor$(accountId, boardId);
+ }
+
+ public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) {
+ return dataBaseAdapter.getCurrentBoardColor(accountId, boardId);
+ }
+
+ // -------------
+ // Current stack
+ // -------------
+
+ public void saveCurrentStackId(long accountId, long boardId, long stackId) {
+ dataBaseAdapter.saveCurrentStackId(accountId, boardId, stackId);
+ }
+
+ public LiveData<Long> getCurrentStackId$(long accountId, long boardId) {
+ return dataBaseAdapter.getCurrentStackId$(accountId, boardId);
+ }
+
+ // ==================================================================================================================================
+
+ @AnyThread
+ public void createAccount(@NonNull Account account, @NonNull IResponseCallback<Account> callback) {
+ executor.submit(() -> {
+ try {
+ callback.onResponse(dataBaseAdapter.createAccountDirectly(account));
+ } catch (Throwable t) {
+ callback.onError(t);
+ }
+ });
+ }
+
+ @AnyThread
+ public void deleteAccount(long id) {
+ executor.submit(() -> {
+ dataBaseAdapter.saveNeighbourOfAccount(id);
+ dataBaseAdapter.removeCurrentBoardId(id);
+ dataBaseAdapter.deleteAccount(id);
+ LastSyncUtil.resetLastSyncDate(id);
+ });
+ }
+
+ @AnyThread
+ public LiveData<Boolean> hasAccounts() {
+ return dataBaseAdapter.hasAccounts();
+ }
+
+ @UiThread
+ public LiveData<Account> readAccount(long id) {
+ return dataBaseAdapter.readAccount(id);
+ }
+
+ @WorkerThread
+ public Account readAccountDirectly(long id) {
+ return dataBaseAdapter.readAccountDirectly(id);
+ }
+
+ @WorkerThread
+ public Account readAccountDirectly(@Nullable String name) {
+ return dataBaseAdapter.readAccountDirectly(name);
+ }
+
+ @UiThread
+ public LiveData<Account> readAccount(@Nullable String name) {
+ return dataBaseAdapter.readAccount(name);
+ }
+
+ @WorkerThread
+ public Long getBoardLocalIdByAccountAndCardRemoteIdDirectly(long accountId, long cardRemoteId) {
+ return dataBaseAdapter.getBoardLocalIdByAccountAndCardRemoteIdDirectly(accountId, cardRemoteId);
+ }
+
+ @UiThread
+ public LiveData<List<Account>> readAccounts() {
+ return dataBaseAdapter.readAccounts();
+ }
+
+ @WorkerThread
+ public List<Account> readAccountsDirectly() {
+ return dataBaseAdapter.getAllAccountsDirectly();
+ }
+
+ /**
+ * @param localProjectId LocalId of the OcsProject
+ * @return all {@link OcsProjectResource}s of the Project
+ */
+ @AnyThread
+ public LiveData<List<OcsProjectResource>> getResourcesForProject(long localProjectId) {
+ return dataBaseAdapter.getResourcesByLocalProjectId(localProjectId);
+ }
+
+ /**
+ * @param accountId ID of the account
+ * @param archived Decides whether only archived or not-archived boards for the specified account will be returned
+ * @return all archived or non-archived <code>Board</code>s depending on <code>archived</code> parameter
+ */
+ @AnyThread
+ public LiveData<List<Board>> getBoards(long accountId, boolean archived) {
+ return dataBaseAdapter.getBoards(accountId, archived);
+ }
+
+ /**
+ * @param accountId ID of the account
+ * @param archived Decides whether only archived or not-archived boards for the specified account will be returned
+ * @return all archived or non-archived <code>FullBoard</code>s depending on <code>archived</code> parameter
+ */
+ @AnyThread
+ public LiveData<List<FullBoard>> getFullBoards(long accountId, boolean archived) {
+ return dataBaseAdapter.getFullBoards(accountId, archived);
+ }
+
+ /**
+ * Get all non-archived <code>FullBoard</code>s with edit permissions for the specified account.
+ *
+ * @param accountId ID of the account
+ * @return all non-archived <code>Board</code>s with edit permission
+ */
+ @AnyThread
+ public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) {
+ return dataBaseAdapter.getBoardsWithEditPermission(accountId);
+ }
+
+ @AnyThread
+ public LiveData<Boolean> hasArchivedBoards(long accountId) {
+ return dataBaseAdapter.hasArchivedBoards(accountId);
+ }
+
+ public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) {
+ return dataBaseAdapter.getFullBoardById(accountId, localId);
+ }
+
+ public Board getBoardById(Long localId) {
+ return dataBaseAdapter.getBoardByLocalIdDirectly(localId);
+ }
+
+ public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) {
+ return dataBaseAdapter.getFullCommentsForLocalCardId(localCardId);
+ }
+
+ public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
+ return dataBaseAdapter.getStacksForBoard(accountId, localBoardId);
+ }
+
+ public LiveData<FullStack> getStack(long accountId, long localStackId) {
+ return dataBaseAdapter.getStack(accountId, localStackId);
+ }
+
+ public void countCardsInStackDirectly(long accountId, long localStackId, @NonNull IResponseCallback<Integer> callback) {
+ executor.submit(() -> dataBaseAdapter.countCardsInStackDirectly(accountId, localStackId, callback));
+ }
+
+ public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback<Integer> callback) {
+ executor.submit(() -> dataBaseAdapter.countCardsWithLabel(localLabelId, callback));
+ }
+
+ public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) {
+ return dataBaseAdapter.getCardWithProjectsByLocalId(accountId, cardLocalId);
+ }
+
+ public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) {
+ return dataBaseAdapter.getFullCardsForStack(accountId, localStackId, filter);
+ }
+
+ @WorkerThread
+ public Long getBoardLocalIdByLocalCardIdDirectly(long localCardId) {
+ return dataBaseAdapter.getBoardLocalIdByLocalCardIdDirectly(localCardId);
+ }
+
+ @WorkerThread
+ public AccessControl getAccessControlByRemoteIdDirectly(long accountId, Long id) {
+ return dataBaseAdapter.getAccessControlByRemoteIdDirectly(accountId, id);
+ }
+
+ public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long id) {
+ return dataBaseAdapter.getAccessControlByLocalBoardId(accountId, id);
+ }
+
+ // --- User search ---
+
+ public LiveData<List<User>> findProposalsForUsersToAssignForACL(final long accountId, long boardId, final int topX) {
+ return dataBaseAdapter.findProposalsForUsersToAssignForACL(accountId, boardId, topX);
+ }
+
+ public LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedToACL, final String constraint) {
+ return dataBaseAdapter.searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedToACL, constraint);
+ }
+
+ public LiveData<List<User>> findProposalsForUsersToAssignForCards(final long accountId, long boardId, long notAssignedToLocalCardId, final int topX) {
+ return dataBaseAdapter.findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX);
+ }
+
+ public LiveData<List<User>> searchUserByUidOrDisplayNameForCards(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, final String searchTerm) {
+ return dataBaseAdapter.searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, searchTerm);
+ }
+
+ // --- Label search ---
+
+ public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) {
+ return findProposalsForLabelsToAssign(accountId, boardId, -1L);
+ }
+
+ public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId, long notAssignedToLocalCardId) {
+ return dataBaseAdapter.findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId);
+ }
+
+ public LiveData<List<Label>> searchNotYetAssignedLabelsByTitle(@NonNull Account account, final long boardId, final long notYetAssignedToLocalCardId, @NonNull String searchTerm) {
+ return dataBaseAdapter.searchNotYetAssignedLabelsByTitle(account.getId(), boardId, notYetAssignedToLocalCardId, searchTerm);
+ }
+
+ public LiveData<User> getUserByLocalId(long accountId, long localId) {
+ return dataBaseAdapter.getUserByLocalId(accountId, localId);
+ }
+
+ public LiveData<User> getUserByUid(long accountId, String uid) {
+ return dataBaseAdapter.getUserByUid(accountId, uid);
+ }
+
+ @WorkerThread
+ public User getUserByUidDirectly(long accountId, String uid) {
+ return dataBaseAdapter.getUserByUidDirectly(accountId, uid);
+ }
+
+ public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
+ return dataBaseAdapter.getBoardByRemoteId(accountId, remoteId);
+ }
+
+ @WorkerThread
+ public Board getBoardByRemoteIdDirectly(long accountId, long remoteId) {
+ return dataBaseAdapter.getBoardByRemoteIdDirectly(accountId, remoteId);
+ }
+
+ public LiveData<Stack> getStackByRemoteId(long accountId, long localBoardId, long remoteId) {
+ return dataBaseAdapter.getStackByRemoteId(accountId, localBoardId, remoteId);
+ }
+
+ public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) {
+ return dataBaseAdapter.getCardByRemoteID(accountId, remoteId);
+ }
+
+ @WorkerThread
+ public Optional<Card> getCardByRemoteIDDirectly(long accountId, long remoteId) {
+ return Optional.ofNullable(dataBaseAdapter.getCardByRemoteIDDirectly(accountId, remoteId));
+ }
+
+ public long createUser(long accountId, User user) {
+ return dataBaseAdapter.createUser(accountId, user);
+ }
+
+ public void updateUser(long accountId, @NonNull User user) {
+ dataBaseAdapter.updateUser(accountId, user, true);
+ }
+
+ protected void reorderLocally(List<FullCard> cardsOfNewStack, @NonNull FullCard movedCard, long newStackId, int newOrder) {
+ // set new stack and order
+ Card movedInnerCard = movedCard.getCard();
+ int oldOrder = movedInnerCard.getOrder();
+ long oldStackId = movedInnerCard.getStackId();
+
+
+ List<Card> changedCards = new ArrayList<>();
+
+ int startingAtOrder = newOrder;
+ if (oldStackId == newStackId) {
+ // card was only reordered in the same stack
+ movedInnerCard.setStatusEnum(movedInnerCard.getStatus() == DBStatus.LOCAL_MOVED.getId() ? DBStatus.LOCAL_MOVED : DBStatus.LOCAL_EDITED);
+ // move direction?
+ if (oldOrder > newOrder) {
+ // up
+ changedCards.add(movedCard.getCard());
+ for (FullCard cardToUpdate : cardsOfNewStack) {
+ Card cardEntity = cardToUpdate.getCard();
+ if (cardEntity.getOrder() < newOrder) {
+ continue;
+ }
+ if (cardEntity.getOrder() >= oldOrder) {
+ break;
+ }
+ changedCards.add(cardEntity);
+ }
+ } else {
+ // down
+ startingAtOrder = oldOrder;
+ for (FullCard cardToUpdate : cardsOfNewStack) {
+ Card cardEntity = cardToUpdate.getCard();
+ if (cardEntity.getOrder() <= oldOrder) {
+ continue;
+ }
+ if (cardEntity.getOrder() > newOrder) {
+ break;
+ }
+ changedCards.add(cardEntity);
+ }
+ changedCards.add(movedCard.getCard());
+ }
+ } else {
+ // card was moved to an other stack
+ movedInnerCard.setStackId(newStackId);
+ movedInnerCard.setStatusEnum(DBStatus.LOCAL_MOVED);
+ changedCards.add(movedCard.getCard());
+ for (FullCard fullCard : cardsOfNewStack) {
+ // skip unchanged cards
+ if (fullCard.getCard().getOrder() < newOrder) {
+ continue;
+ }
+ changedCards.add(fullCard.getCard());
+ }
+ }
+ reorderAscending(movedInnerCard, changedCards, startingAtOrder);
+ }
+
+ private void reorderAscending(@NonNull Card movedCard, @NonNull List<Card> cardsToReorganize, int startingAtOrder) {
+ final Instant now = Instant.now();
+ for (Card card : cardsToReorganize) {
+ card.setOrder(startingAtOrder);
+ if (card.getStatus() == DBStatus.UP_TO_DATE.getId()) {
+ card.setStatusEnum(DBStatus.LOCAL_EDITED_SILENT);
+ card.setLastModifiedLocal(now);
+ }
+ startingAtOrder++;
+ }
+ //update the moved one first, because otherwise a bunch of livedata is fired, leading the card to dispose and reappear
+ cardsToReorganize.remove(movedCard);
+ dataBaseAdapter.updateCard(movedCard, false);
+ for (Card card : cardsToReorganize) {
+ dataBaseAdapter.updateCard(card, false);
+ }
+ }
+
+ // -------------------
+ // Widgets
+ // -------------------
+
+ // # filter widget
+
+ @AnyThread
+ public void createFilterWidget(@NonNull FilterWidget filterWidget, @NonNull IResponseCallback<Integer> callback) {
+ executor.submit(() -> {
+ try {
+ int filterWidgetId = dataBaseAdapter.createFilterWidgetDirectly(filterWidget);
+ callback.onResponse(filterWidgetId);
+ } catch (Throwable t) {
+ callback.onError(t);
+ }
+ });
+ }
+
+ @AnyThread
+ public void updateFilterWidget(@NonNull FilterWidget filterWidget, @NonNull ResponseCallback<Boolean> callback) {
+ executor.submit(() -> {
+ try {
+ dataBaseAdapter.updateFilterWidgetDirectly(filterWidget);
+ callback.onResponse(Boolean.TRUE);
+ } catch (Throwable t) {
+ callback.onError(t);
+ }
+ });
+ }
+
+ @AnyThread
+ public void getFilterWidget(@NonNull Integer filterWidgetId, @NonNull IResponseCallback<FilterWidget> callback) {
+ executor.submit(() -> {
+ try {
+ callback.onResponse(dataBaseAdapter.getFilterWidgetByIdDirectly(filterWidgetId));
+ } catch (Throwable t) {
+ callback.onError(t);
+ }
+ });
+ }
+
+ @AnyThread
+ public void deleteFilterWidget(int filterWidgetId, @NonNull IResponseCallback<Boolean> callback) {
+ executor.submit(() -> {
+ try {
+ dataBaseAdapter.deleteFilterWidgetDirectly(filterWidgetId);
+ callback.onResponse(Boolean.TRUE);
+ } catch (Throwable t) {
+ callback.onError(t);
+ }
+ });
+ }
+
+ public boolean filterWidgetExists(int id) {
+ return dataBaseAdapter.filterWidgetExists(id);
+ }
+
+ @WorkerThread
+ public List<FilterWidgetCard> getCardsForFilterWidget(@NonNull Integer filterWidgetId) {
+ return dataBaseAdapter.getCardsForFilterWidget(filterWidgetId);
+ }
+
+ @WorkerThread
+ public LiveData<List<UpcomingCardsAdapterItem>> getCardsForUpcomingCards() {
+ return dataBaseAdapter.getCardsForUpcomingCard();
+ }
+
+ @WorkerThread
+ public List<UpcomingCardsAdapterItem> getCardsForUpcomingCardsForWidget() {
+ return dataBaseAdapter.getCardsForUpcomingCardForWidget();
+ }
+
+ // # single card widget
+
+ /**
+ * Can be called from a configuration screen or a picker.
+ * Creates a new entry in the database, if row with given widgetId does not yet exist.
+ */
+ @AnyThread
+ public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) {
+ executor.submit(() -> dataBaseAdapter.createSingleCardWidget(widgetId, accountId, boardId, localCardId));
+ }
+
+ @WorkerThread
+ public FullSingleCardWidgetModel getSingleCardWidgetModelDirectly(int appWidgetId) throws NoSuchElementException {
+ final FullSingleCardWidgetModel model = dataBaseAdapter.getFullSingleCardWidgetModel(appWidgetId);
+ if (model == null) {
+ throw new NoSuchElementException("There is no " + FullSingleCardWidgetModel.class.getSimpleName() + " with the given appWidgetId " + appWidgetId);
+ }
+ return model;
+ }
+
+ @AnyThread
+ public void deleteSingleCardWidgetModel(int widgetId) {
+ executor.submit(() -> dataBaseAdapter.deleteSingleCardWidget(widgetId));
+ }
+
+ public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) {
+ executor.submit(() -> dataBaseAdapter.createStackWidget(appWidgetId, accountId, stackId, darkTheme));
+ }
+
+ @WorkerThread
+ public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) throws NoSuchElementException {
+ final StackWidgetModel model = dataBaseAdapter.getStackWidgetModelDirectly(appWidgetId);
+ if (model == null) {
+ throw new NoSuchElementException();
+ }
+ return model;
+ }
+
+ public void deleteStackWidgetModel(int appWidgetId) {
+ executor.submit(() -> dataBaseAdapter.deleteStackWidget(appWidgetId));
+ }
+
+ @WorkerThread
+ public Stack getStackDirectly(long stackLocalId) {
+ return dataBaseAdapter.getStackByLocalIdDirectly(stackLocalId);
+ }
+
+ @ColorInt
+ @WorkerThread
+ public Integer getBoardColorDirectly(long accountId, long localBoardId) {
+ return dataBaseAdapter.getBoardColorDirectly(accountId, localBoardId);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java
new file mode 100644
index 000000000..83101ad3d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java
@@ -0,0 +1,79 @@
+package it.niedermann.nextcloud.deck.persistence;
+
+import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode;
+import static java.util.concurrent.CompletableFuture.supplyAsync;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.preference.PreferenceManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import it.niedermann.android.sharedpreferences.SharedPreferenceBooleanLiveData;
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+
+public class PreferencesRepository {
+
+ private final ExecutorService executor;
+ private final String PREF_KEY_THEME;
+ private final String PREF_KEY_DEBUGGING;
+ private final SharedPreferences sharedPreferences;
+ private final Context context;
+
+ public PreferencesRepository(@NonNull Context context) {
+ this(context, new ThreadPoolExecutor(0, 2, 0L, TimeUnit.SECONDS, new SynchronousQueue<>()));
+ }
+
+ public PreferencesRepository(@NonNull Context context, @NonNull ExecutorService executor) {
+ this.context = context.getApplicationContext();
+ this.executor = executor;
+ this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+ PREF_KEY_THEME = this.context.getString(R.string.pref_key_dark_theme);
+ PREF_KEY_DEBUGGING = this.context.getString(R.string.pref_key_debugging);
+ }
+
+ // ---------
+ // Debugging
+ // ---------
+
+ public CompletableFuture<Boolean> isDebugModeEnabled() {
+ return supplyAsync(() -> {
+ final boolean enabled = sharedPreferences.getBoolean(PREF_KEY_DEBUGGING, false);
+ DeckLog.log("--- Read:", PREF_KEY_DEBUGGING, "→", enabled);
+ return enabled;
+ }, executor);
+ }
+
+ public LiveData<Boolean> isDebugModeEnabled$() {
+ return new SharedPreferenceBooleanLiveData(sharedPreferences, PREF_KEY_DEBUGGING, false);
+ }
+
+ // -----
+ // Theme
+ // -----
+
+ public void setAppTheme(int setting) {
+ setDefaultNightMode(setting);
+ }
+
+ public CompletableFuture<Integer> getAppThemeSetting() {
+ return supplyAsync(() -> {
+ String mode;
+ try {
+ mode = sharedPreferences.getString(PREF_KEY_THEME, context.getString(R.string.pref_value_theme_system_default));
+ } catch (ClassCastException e) {
+ mode = sharedPreferences.getBoolean(PREF_KEY_THEME, false) ? context.getString(R.string.pref_value_theme_dark) : context.getString(R.string.pref_value_theme_light);
+ }
+ return Integer.parseInt(mode);
+ }, executor);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java
index 50de4d2e4..9067809ce 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java
@@ -12,15 +12,16 @@ import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
+import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
+import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.api.ParsedResponse;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.io.File;
import java.time.Instant;
@@ -29,12 +30,9 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -54,25 +52,18 @@ import it.niedermann.nextcloud.deck.model.JoinCardWithUser;
import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.model.Stack;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel;
import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects;
-import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel;
import it.niedermann.nextcloud.deck.model.full.FullStack;
import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment;
-import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
-import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
-import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget;
-import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard;
+import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst.UserSearchLiveData;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.DataPropagationHelper;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AbstractSyncDataProvider;
@@ -87,146 +78,81 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.LabelData
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.StackDataProvider;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithAclDownSyncDataProvider;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithStacksAndLabelsUpSyncDataProvider;
-import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterItem;
-import it.niedermann.nextcloud.deck.util.ExecutorServiceProvider;
+import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil;
+/**
+ * Extends {@link BaseRepository} by synchronization capabilities.
+ * Therefore it always requires an {@link Account} to choose the correct {@link SingleSignOnAccount} for network operations.
+ */
@SuppressWarnings("WeakerAccess")
-public class SyncManager {
+public class SyncManager extends BaseRepository {
@NonNull
- private final Context appContext;
- @NonNull
- private final DataBaseAdapter dataBaseAdapter;
- @NonNull
private final ServerAdapter serverAdapter;
@NonNull
- private final ExecutorService executor;
- @NonNull
private final SyncHelper.Factory syncHelperFactory;
@AnyThread
- public SyncManager(@NonNull Context context) {
- this(context, null);
+ public SyncManager(@NonNull Context context, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ this(context, AccountImporter.getSingleSignOnAccount(context, account.getName()), new ConnectivityUtil(context));
}
- @AnyThread
- public SyncManager(@NonNull Context context, @Nullable String ssoAccountName) {
- this(context,
- new DataBaseAdapter(context.getApplicationContext()),
- new ServerAdapter(context.getApplicationContext(), ssoAccountName),
- ExecutorServiceProvider.getExecutorService(),
- SyncHelper::new);
- LastSyncUtil.init(context.getApplicationContext());
+ private SyncManager(@NonNull Context context,
+ @NonNull SingleSignOnAccount ssoAccount,
+ @NonNull ConnectivityUtil connectivityUtil) {
+ this(context, new ServerAdapter(context.getApplicationContext(), ssoAccount, connectivityUtil), connectivityUtil, SyncHelper::new);
}
- private SyncManager(@NonNull Context context,
- @NonNull DataBaseAdapter databaseAdapter,
- @NonNull ServerAdapter serverAdapter,
- @NonNull ExecutorService executor,
- @NonNull SyncHelper.Factory syncHelperFactory) {
- this.appContext = context.getApplicationContext();
- this.dataBaseAdapter = databaseAdapter;
+ protected SyncManager(@NonNull Context context,
+ @NonNull ServerAdapter serverAdapter,
+ @NonNull ConnectivityUtil connectivityUtil,
+ @NonNull SyncHelper.Factory syncHelperFactory) {
+ super(context, connectivityUtil);
this.serverAdapter = serverAdapter;
- this.executor = executor;
this.syncHelperFactory = syncHelperFactory;
+ LastSyncUtil.init(context.getApplicationContext());
}
- @WorkerThread
- public Long getBoardLocalIdByAccountAndCardRemoteIdDirectly(long accountId, long cardRemoteId) {
- return dataBaseAdapter.getBoardLocalIdByAccountAndCardRemoteIdDirectly(accountId, cardRemoteId);
- }
-
- @WorkerThread
- public boolean synchronizeEverything() {
- List<Account> accounts = dataBaseAdapter.getAllAccountsDirectly();
- if (accounts.size() > 0) {
- final AtomicBoolean success = new AtomicBoolean(true);
- CountDownLatch latch = new CountDownLatch(accounts.size());
- try {
- for (Account account : accounts) {
- new SyncManager(dataBaseAdapter.getContext(), account.getName()).synchronize(new ResponseCallback<>(account) {
- @Override
- public void onResponse(Boolean response) {
- success.set(success.get() && Boolean.TRUE.equals(response));
- latch.countDown();
- }
-
- @Override
- public void onError(Throwable throwable) {
- success.set(false);
- super.onError(throwable);
- latch.countDown();
- }
- });
- }
- latch.await();
- return success.get();
- } catch (InterruptedException e) {
- DeckLog.logError(e);
- return false;
- }
- }
- return true;
- }
-
- @AnyThread
- public void synchronizeBoard(long localBoardId, @NonNull ResponseCallback<Boolean> responseCallback) {
- executor.submit(() -> {
- FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(responseCallback.getAccount().getId(), localBoardId);
- try {
- syncHelperFactory.create(serverAdapter, dataBaseAdapter, null)
- .setResponseCallback(responseCallback)
- .doSyncFor(new StackDataProvider(null, board));
- } catch (OfflineException e) {
- responseCallback.onError(e);
- }
- });
+ @VisibleForTesting
+ protected SyncManager(@NonNull Context context,
+ @NonNull ServerAdapter serverAdapter,
+ @NonNull ConnectivityUtil connectivityUtil,
+ @NonNull SyncHelper.Factory syncHelperFactory,
+ @NonNull DataBaseAdapter databaseAdapter,
+ @NonNull ExecutorService executor) {
+ super(context, connectivityUtil, databaseAdapter, executor);
+ this.serverAdapter = serverAdapter;
+ this.syncHelperFactory = syncHelperFactory;
+ LastSyncUtil.init(context.getApplicationContext());
}
@AnyThread
- public void synchronizeCard(@NonNull ResponseCallback<Boolean> responseCallback, @NonNull Card card) {
- executor.submit(() -> {
- FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId());
- Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId());
- try {
- syncHelperFactory.create(serverAdapter, dataBaseAdapter, null)
- .setResponseCallback(responseCallback)
- .doSyncFor(new CardDataProvider(null, board, stack));
- } catch (OfflineException e) {
- responseCallback.onError(e);
- }
- });
+ public void fetchBoardsFromServer(@NonNull ResponseCallback<ParsedResponse<List<FullBoard>>> callback) {
+ executor.submit(() -> serverAdapter.getBoards(callback));
}
@AnyThread
public LiveData<Pair<Integer, Integer>> synchronize(@NonNull ResponseCallback<Boolean> responseCallback) {
- MutableLiveData<Pair<Integer, Integer>> progress$ = new MutableLiveData<>();
- Account callbackAccount = responseCallback.getAccount();
- if (callbackAccount == null) {
- throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + ResponseCallback.class.getSimpleName() + " must not be null.");
- }
- Long callbackAccountId = callbackAccount.getId();
- if (callbackAccountId == null) {
- throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + ResponseCallback.class.getSimpleName() + " must contain a valid id, but given id was null.");
- }
+ final var progress$ = new MutableLiveData<Pair<Integer, Integer>>();
+ final var callbackAccount = responseCallback.getAccount();
+ final long callbackAccountId = callbackAccount.getId();
+
executor.submit(() -> {
refreshCapabilities(new ResponseCallback<>(responseCallback.getAccount()) {
@Override
public void onResponse(Capabilities response) {
if (response != null && !response.isMaintenanceEnabled()) {
if (response.getDeckVersion().isSupported()) {
- long accountId = callbackAccountId;
- Instant lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId);
+ final var lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId);
+ final var syncHelper = syncHelperFactory.create(serverAdapter, dataBaseAdapter, lastSyncDate);
- final SyncHelper syncHelper = syncHelperFactory.create(serverAdapter, dataBaseAdapter, lastSyncDate);
-
- ResponseCallback<Boolean> callback = new ResponseCallback<>(callbackAccount) {
+ final var callback = new ResponseCallback<Boolean>(callbackAccount) {
@Override
public void onResponse(Boolean response) {
syncHelper.setResponseCallback(new ResponseCallback<>(account) {
@Override
public void onResponse(Boolean response) {
- LastSyncUtil.setLastSyncDate(accountId, Instant.now());
+ LastSyncUtil.setLastSyncDate(callbackAccountId, Instant.now());
responseCallback.onResponse(response);
}
@@ -284,114 +210,44 @@ public class SyncManager {
return progress$;
}
-//
-// private <T> IResponseCallback<T> wrapCallForUi(IResponseCallback<T> responseCallback) {
-// Account account = responseCallback.getAccount();
-// if (account == null || account.getId() == null) {
-// throw new IllegalArgumentException("Bro. Please just give me a damn Account!");
-// }
-// return new IResponseCallback<T>(responseCallback.getAccount()) {
-// @Override
-// public void onResponse(T response) {
-// sourceActivity.runOnUiThread(() -> {
-// fillAccountIDs(response);
-// responseCallback.onResponse(response);
-// });
-// }
-//
-// @Override
-// public void onError(Throwable throwable) {
-// responseCallback.onError(throwable);
-// }
-// };
-// }
-
-// private <T extends AbstractRemoteEntity> T applyUpdatesFromRemote(T localEntity, T remoteEntity, Long accountId) {
-// if (!localEntity.getId().equals(remoteEntity.getId())
-// || !accountId.equals(localEntity.getAccountId())) {
-// throw new IllegalArgumentException("IDs of Account or Entity are not matching! WTF are you doin?!");
-// }
-// remoteEntity.setLastModifiedLocal(remoteEntity.getLastModified()); // not an error! local-modification = remote-mod
-// remoteEntity.setLocalId(localEntity.getLocalId());
-// return remoteEntity;
-// }
-
- @AnyThread
- public LiveData<Boolean> hasAccounts() {
- return dataBaseAdapter.hasAccounts();
- }
-
@AnyThread
- public void createAccount(@NonNull Account account, @NonNull IResponseCallback<Account> callback) {
+ public void synchronizeBoard(long localBoardId, @NonNull ResponseCallback<Boolean> responseCallback) {
executor.submit(() -> {
+ FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(responseCallback.getAccount().getId(), localBoardId);
try {
- final Account createdAccount = dataBaseAdapter.createAccountDirectly(account);
- if (createdAccount == null) {
- throw new RuntimeException("Created account is null. Source: " + account);
- }
- // TODO: throw this shit away
- // that's why we do this: https://github.com/nextcloud/deck/issues/3229
- serverAdapter.getBoards(new ResponseCallback<>(createdAccount) {
- @Override
- public void onResponse(ParsedResponse<List<FullBoard>> response) {
- callback.onResponse(createdAccount);
- }
-
- @SuppressLint("MissingSuperCall")
- @Override
- public void onError(Throwable throwable) {
- callback.onResponse(createdAccount);
- }
- });
- // TODO: and replace with this line:
-// callback.onResponse(createdAccount);
- } catch (Throwable t) {
- callback.onError(t);
+ syncHelperFactory.create(serverAdapter, dataBaseAdapter, null)
+ .setResponseCallback(responseCallback)
+ .doSyncFor(new StackDataProvider(null, board));
+ } catch (OfflineException e) {
+ responseCallback.onError(e);
}
});
}
- public boolean hasInternetConnection() {
- return serverAdapter.hasInternetConnection();
- }
-
@AnyThread
- public void deleteAccount(long id) {
+ public void synchronizeCard(@NonNull ResponseCallback<Boolean> responseCallback, @NonNull Card card) {
executor.submit(() -> {
- dataBaseAdapter.deleteAccount(id);
- LastSyncUtil.resetLastSyncDate(id);
+ FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId());
+ Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId());
+ try {
+ syncHelperFactory.create(serverAdapter, dataBaseAdapter, null)
+ .setResponseCallback(responseCallback)
+ .doSyncFor(new CardDataProvider(null, board, stack));
+ } catch (OfflineException e) {
+ responseCallback.onError(e);
+ }
});
}
- @UiThread
- public LiveData<Account> readAccount(long id) {
- return dataBaseAdapter.readAccount(id);
- }
-
- @WorkerThread
- public Account readAccountDirectly(long id) {
- return dataBaseAdapter.readAccountDirectly(id);
- }
-
- @WorkerThread
- public Account readAccountDirectly(@Nullable String name) {
- return dataBaseAdapter.readAccountDirectly(name);
- }
-
- @UiThread
- public LiveData<Account> readAccount(@Nullable String name) {
- return dataBaseAdapter.readAccount(name);
- }
-
- @UiThread
- public LiveData<List<Account>> readAccounts() {
- return dataBaseAdapter.readAccounts();
- }
-
- @WorkerThread
- public List<Account> readAccountsDirectly() {
- return dataBaseAdapter.getAllAccountsDirectly();
- }
+// private <T extends AbstractRemoteEntity> T applyUpdatesFromRemote(T localEntity, T remoteEntity, Long accountId) {
+// if (!localEntity.getId().equals(remoteEntity.getId())
+// || !accountId.equals(localEntity.getAccountId())) {
+// throw new IllegalArgumentException("IDs of Account or Entity are not matching! WTF are you doin?!");
+// }
+// remoteEntity.setLastModifiedLocal(remoteEntity.getLastModified()); // not an error! local-modification = remote-mod
+// remoteEntity.setLocalId(localEntity.getLocalId());
+// return remoteEntity;
+// }
/**
* <p>
@@ -487,66 +343,10 @@ public class SyncManager {
});
}
- /**
- * @param accountId ID of the account
- * @return all {@link Board}s no matter if {@link Board#archived} or not.
- */
- @SuppressWarnings("JavadocReference")
- @AnyThread
- public LiveData<List<Board>> getBoards(long accountId) {
- return dataBaseAdapter.getBoards(accountId);
- }
-
- /**
- * @param localProjectId LocalId of the OcsProject
- * @return all {@link OcsProjectResource}s of the Project
- */
- @AnyThread
- public LiveData<List<OcsProjectResource>> getResourcesForProject(long localProjectId) {
- return dataBaseAdapter.getResourcesByLocalProjectId(localProjectId);
- }
-
- /**
- * @param accountId ID of the account
- * @param archived Decides whether only archived or not-archived boards for the specified account will be returned
- * @return all archived or non-archived <code>Board</code>s depending on <code>archived</code> parameter
- */
- @AnyThread
- public LiveData<List<Board>> getBoards(long accountId, boolean archived) {
- return dataBaseAdapter.getBoards(accountId, archived);
- }
-
- /**
- * @param accountId ID of the account
- * @param archived Decides whether only archived or not-archived boards for the specified account will be returned
- * @return all archived or non-archived <code>FullBoard</code>s depending on <code>archived</code> parameter
- */
- @AnyThread
- public LiveData<List<FullBoard>> getFullBoards(long accountId, boolean archived) {
- return dataBaseAdapter.getFullBoards(accountId, archived);
- }
-
- /**
- * Get all non-archived <code>FullBoard</code>s with edit permissions for the specified account.
- *
- * @param accountId ID of the account
- * @return all non-archived <code>Board</code>s with edit permission
- */
- @AnyThread
- public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) {
- return dataBaseAdapter.getBoardsWithEditPermission(accountId);
- }
-
- @AnyThread
- public LiveData<Boolean> hasArchivedBoards(long accountId) {
- return dataBaseAdapter.hasArchivedBoards(accountId);
- }
-
@AnyThread
- public void createBoard(long accountId, @NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) {
+ public void createBoard(@NonNull Account account, @NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) {
executor.submit(() -> {
- final Account account = dataBaseAdapter.getAccountByIdDirectly(accountId);
- final User owner = dataBaseAdapter.getUserByUidDirectly(accountId, account.getUserName());
+ final User owner = dataBaseAdapter.getUserByUidDirectly(account.getId(), account.getUserName());
if (owner == null) {
callback.onError(new IllegalStateException("Owner is null. This can be the case if the Deck app has never before been opened in the webinterface"));
} else {
@@ -554,8 +354,8 @@ public class SyncManager {
board.setOwnerId(owner.getLocalId());
fullBoard.setOwner(owner);
fullBoard.setBoard(board);
- board.setAccountId(accountId);
- fullBoard.setAccountId(accountId);
+ board.setAccountId(account.getId());
+ fullBoard.setAccountId(account.getId());
new DataPropagationHelper(serverAdapter, dataBaseAdapter, executor).createEntity(new BoardDataProvider(), fullBoard, ResponseCallback.from(account, callback));
}
});
@@ -675,11 +475,18 @@ public class SyncManager {
}
}
}
- if (serverAdapter.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId);
- ServerAdapter serverAdapterToUse = this.serverAdapter;
- if (originAccountId != targetAccountId) {
- serverAdapterToUse = new ServerAdapter(appContext, targetAccount.getName());
+ final ServerAdapter serverAdapterToUse;
+ if (originAccountId == targetAccountId) {
+ serverAdapterToUse = this.serverAdapter;
+ } else {
+ try {
+ serverAdapterToUse = new ServerAdapter(context, AccountImporter.getSingleSignOnAccount(context, targetAccount.getName()), connectivityUtil);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ callback.onError(e);
+ return;
+ }
}
syncHelperFactory.create(serverAdapterToUse, dataBaseAdapter, null)
.setResponseCallback(new ResponseCallback<>(targetAccount) {
@@ -703,7 +510,7 @@ public class SyncManager {
@AnyThread
public LiveData<List<it.niedermann.nextcloud.deck.model.ocs.Activity>> syncActivitiesForCard(@NonNull Card card) {
executor.submit(() -> {
- if (serverAdapter.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
if (card.getId() != null) {
syncHelperFactory.create(serverAdapter, dataBaseAdapter, null)
.setResponseCallback(new ResponseCallback<>(dataBaseAdapter.getAccountByIdDirectly(card.getAccountId())) {
@@ -764,10 +571,6 @@ public class SyncManager {
});
}
- public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) {
- return dataBaseAdapter.getFullCommentsForLocalCardId(localCardId);
- }
-
@AnyThread
public void deleteBoard(@NonNull Board board, @NonNull IResponseCallback<Void> callback) {
executor.submit(() -> {
@@ -787,19 +590,6 @@ public class SyncManager {
});
}
- public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
- return dataBaseAdapter.getStacksForBoard(accountId, localBoardId);
- }
-
- public LiveData<FullStack> getStack(long accountId, long localStackId) {
- return dataBaseAdapter.getStack(accountId, localStackId);
- }
-
- @WorkerThread
- public Long getBoardLocalIdByLocalCardIdDirectly(long localCardId) {
- return dataBaseAdapter.getBoardLocalIdByLocalCardIdDirectly(localCardId);
- }
-
@AnyThread
public void createAccessControl(long accountId, @NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) {
executor.submit(() -> {
@@ -814,15 +604,6 @@ public class SyncManager {
});
}
- @WorkerThread
- public AccessControl getAccessControlByRemoteIdDirectly(long accountId, Long id) {
- return dataBaseAdapter.getAccessControlByRemoteIdDirectly(accountId, id);
- }
-
- public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long id) {
- return dataBaseAdapter.getAccessControlByLocalBoardId(accountId, id);
- }
-
@AnyThread
public void updateAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) {
executor.submit(() -> {
@@ -844,6 +625,8 @@ public class SyncManager {
public void onResponse(Void response) {
// revoked own board-access?
if (entity.getAccountId() == entity.getAccountId() && entity.getUser().getUid().equals(account.getUserName())) {
+ dataBaseAdapter.saveNeighbourOfBoard(board.getAccountId(), board.getLocalId());
+ dataBaseAdapter.removeCurrentStackId(board.getAccountId(), board.getLocalId());
dataBaseAdapter.deleteBoardPhysically(board.getBoard());
}
callback.onResponse(response);
@@ -858,17 +641,10 @@ public class SyncManager {
});
}
- public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) {
- return dataBaseAdapter.getFullBoardById(accountId, localId);
- }
-
- public Board getBoardById(Long localId) {
- return dataBaseAdapter.getBoardByLocalIdDirectly(localId);
- }
-
@AnyThread
- public void createStack(long accountId, @NonNull String title, long boardLocalId, @NonNull IResponseCallback<FullStack> callback) {
+ public void createStack(long accountId, long boardLocalId, @NonNull String title, @NonNull IResponseCallback<FullStack> callback) {
executor.submit(() -> {
+ DeckLog.info("Create Stack in account", accountId, "on board with local ID ", boardLocalId);
Stack stack = new Stack(title, boardLocalId);
Account account = dataBaseAdapter.getAccountByIdDirectly(accountId);
FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId());
@@ -883,7 +659,7 @@ public class SyncManager {
}
@AnyThread
- public void deleteStack(long accountId, long stackLocalId, long boardLocalId, @NonNull IResponseCallback<Void> callback) {
+ public void deleteStack(long accountId, long boardLocalId, long stackLocalId, @NonNull IResponseCallback<Void> callback) {
executor.submit(() -> {
Account account = dataBaseAdapter.getAccountByIdDirectly(accountId);
FullStack fullStack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackLocalId);
@@ -956,22 +732,6 @@ public class SyncManager {
});
}
- public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) {
- return dataBaseAdapter.getCardWithProjectsByLocalId(accountId, cardLocalId);
- }
-
- public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) {
- return dataBaseAdapter.getFullCardsForStack(accountId, localStackId, filter);
- }
-
- public void countCardsInStackDirectly(long accountId, long localStackId, @NonNull IResponseCallback<Integer> callback) {
- executor.submit(() -> dataBaseAdapter.countCardsInStackDirectly(accountId, localStackId, callback));
- }
-
- public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback<Integer> callback) {
- executor.submit(() -> dataBaseAdapter.countCardsWithLabel(localLabelId, callback));
- }
-
// TODO implement, see https://github.com/stefan-niedermann/nextcloud-deck/issues/395
public LiveData<List<FullCard>> getArchivedFullCardsForBoard(long accountId, long localBoardId) {
MutableLiveData<List<FullCard>> dummyData = new MutableLiveData<>();
@@ -1043,7 +803,7 @@ public class SyncManager {
}
}
- if (serverAdapter.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
syncHelperFactory.create(serverAdapter, dataBaseAdapter, null)
.setResponseCallback(new ResponseCallback<>(account) {
@Override
@@ -1153,9 +913,21 @@ public class SyncManager {
public void archiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) {
executor.submit(() -> {
try {
- FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId());
- b.getBoard().setArchived(true);
- updateBoard(b, callback);
+ final var fullBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId());
+ fullBoard.getBoard().setArchived(true);
+ updateBoard(fullBoard, new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullBoard response) {
+ dataBaseAdapter.saveNeighbourOfBoard(fullBoard.getAccountId(), fullBoard.getLocalId());
+ callback.onResponse(response);
+ }
+
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void onError(Throwable throwable) {
+ callback.onError(throwable);
+ }
+ });
} catch (Throwable e) {
callback.onError(e);
}
@@ -1168,7 +940,19 @@ public class SyncManager {
try {
FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId());
b.getBoard().setArchived(false);
- updateBoard(b, callback);
+ updateBoard(b, new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullBoard response) {
+ dataBaseAdapter.saveCurrentBoardId(b.getAccountId(), b.getLocalId());
+ callback.onResponse(response);
+ }
+
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void onError(Throwable throwable) {
+ callback.onError(throwable);
+ }
+ });
} catch (Throwable e) {
callback.onError(e);
}
@@ -1207,7 +991,7 @@ public class SyncManager {
fullCardFromDB.setCard(card.getCard());
card.getCard().setStatus(DBStatus.LOCAL_EDITED.getId());
dataBaseAdapter.updateCard(card.getCard(), false);
- if (serverAdapter.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId());
syncHelperFactory.create(serverAdapter, dataBaseAdapter, null)
.setResponseCallback(new ResponseCallback<>(account) {
@@ -1286,7 +1070,12 @@ public class SyncManager {
ServerAdapter serverToUse = serverAdapter;
if (originAccountId != targetAccountId) {
- serverToUse = new ServerAdapter(appContext, targetAccount.getName());
+ try {
+ serverToUse = new ServerAdapter(context, AccountImporter.getSingleSignOnAccount(context, targetAccount.getName()), connectivityUtil);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ callback.onError(e);
+ throw new RuntimeException(e);
+ }
}
new DataPropagationHelper(serverToUse, dataBaseAdapter, executor).createEntity(new CardPropagationDataProvider(null, targetBoard.getBoard(), targetFullStack), fullCardForServerPropagation, new ResponseCallback<>(targetAccount) {
@Override
@@ -1394,10 +1183,6 @@ public class SyncManager {
});
}
- public MutableLiveData<Label> createAndAssignLabelToCard(long accountId, @NonNull Label label, long localCardId) {
- return createAndAssignLabelToCard(accountId, label, localCardId, serverAdapter);
- }
-
@AnyThread
private MutableLiveData<Label> createAndAssignLabelToCard(long accountId, @NonNull Label label, long localCardId, ServerAdapter serverAdapterToUse) {
MutableLiveData<Label> liveData = new MutableLiveData<>();
@@ -1455,7 +1240,7 @@ public class SyncManager {
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId());
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId());
Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId());
- if (serverAdapter.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
serverAdapter.assignUserToCard(board.getId(), stack.getId(), card.getId(), user.getUid(), new ResponseCallback<>(account) {
@Override
@@ -1484,7 +1269,7 @@ public class SyncManager {
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId());
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId());
Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId());
- if (serverAdapterToUse.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
serverAdapterToUse.assignLabelToCard(board.getId(), stack.getId(), card.getId(), label.getId(), new ResponseCallback<>(account) {
@Override
@@ -1503,7 +1288,7 @@ public class SyncManager {
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId());
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId());
Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId());
- if (serverAdapter.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
serverAdapter.unassignLabelFromCard(board.getId(), stack.getId(), card.getId(), label.getId(), new ResponseCallback<>(account) {
@Override
public void onResponse(Void response) {
@@ -1518,7 +1303,7 @@ public class SyncManager {
public void unassignUserFromCard(@NonNull User user, @NonNull Card card) {
executor.submit(() -> {
dataBaseAdapter.deleteJoinedUserForCard(card.getLocalId(), user.getLocalId());
- if (serverAdapter.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId());
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId());
Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId());
@@ -1532,79 +1317,31 @@ public class SyncManager {
});
}
- public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId, long notAssignedToLocalCardId, final int topX) {
- return dataBaseAdapter.findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX);
- }
-
- public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId) {
- return dataBaseAdapter.findProposalsForUsersToAssign(accountId, boardId, -1L, -1);
- }
-
- public LiveData<List<User>> findProposalsForUsersToAssignForACL(final long accountId, long boardId, final int topX) {
- return dataBaseAdapter.findProposalsForUsersToAssignForACL(accountId, boardId, topX);
- }
-
- public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId, long notAssignedToLocalCardId) {
- return dataBaseAdapter.findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId);
- }
-
- public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) {
- return findProposalsForLabelsToAssign(accountId, boardId, -1L);
- }
-
- public LiveData<User> getUserByLocalId(long accountId, long localId) {
- return dataBaseAdapter.getUserByLocalId(accountId, localId);
- }
-
- public LiveData<User> getUserByUid(long accountId, String uid) {
- return dataBaseAdapter.getUserByUid(accountId, uid);
- }
-
- @WorkerThread
- public User getUserByUidDirectly(long accountId, String uid) {
- return dataBaseAdapter.getUserByUidDirectly(accountId, uid);
- }
-
- public LiveData<List<User>> searchUserByUidOrDisplayName(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, final String searchTerm) {
- return dataBaseAdapter.searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, searchTerm);
- }
-
- public UserSearchLiveData searchUserByUidOrDisplayNameForACL() {
- return new UserSearchLiveData(dataBaseAdapter, serverAdapter);
- }
-
- public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
- return dataBaseAdapter.getBoardByRemoteId(accountId, remoteId);
- }
-
- @WorkerThread
- public Board getBoardByRemoteIdDirectly(long accountId, long remoteId) {
- return dataBaseAdapter.getBoardByRemoteIdDirectly(accountId, remoteId);
- }
-
- public LiveData<Stack> getStackByRemoteId(long accountId, long localBoardId, long remoteId) {
- return dataBaseAdapter.getStackByRemoteId(accountId, localBoardId, remoteId);
- }
-
- public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) {
- return dataBaseAdapter.getCardByRemoteID(accountId, remoteId);
- }
-
- @WorkerThread
- public Optional<Card> getCardByRemoteIDDirectly(long accountId, long remoteId) {
- return Optional.ofNullable(dataBaseAdapter.getCardByRemoteIDDirectly(accountId, remoteId));
- }
-
- public long createUser(long accountId, User user) {
- return dataBaseAdapter.createUser(accountId, user);
- }
-
- public void updateUser(long accountId, @NonNull User user) {
- dataBaseAdapter.updateUser(accountId, user, true);
- }
+ public void triggerUserSearch(@NonNull Account account, @NonNull String constraint) {
+ executor.submit(() -> serverAdapter.searchUser(constraint, new ResponseCallback<>(account) {
+ @Override
+ public void onResponse(OcsUserList response) {
+ if (response == null || response.getUsers().isEmpty()) {
+ return;
+ }
+ for (var user : response.getUsers()) {
+ final var existingUser = dataBaseAdapter.getUserByUidDirectly(account.getId(), user.getId());
+ if (existingUser == null) {
+ User newUser = new User();
+ newUser.setStatus(DBStatus.UP_TO_DATE.getId());
+ newUser.setPrimaryKey(user.getId());
+ newUser.setUid(user.getId());
+ newUser.setDisplayname(user.getDisplayName());
+ dataBaseAdapter.createUser(account.getId(), newUser);
+ }
+ }
+ }
- public LiveData<List<Label>> searchNotYetAssignedLabelsByTitle(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, @NonNull String searchTerm) {
- return dataBaseAdapter.searchNotYetAssignedLabelsByTitle(accountId, boardId, notYetAssignedToLocalCardId, searchTerm);
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ }
+ }));
}
/**
@@ -1649,7 +1386,7 @@ public class SyncManager {
}
}
-// if (serverAdapter.hasInternetConnection()){
+// if (connectivityUtil.hasInternetConnection()){
// // call reorder
// Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(movedCard.getCard().getStackId());
// Stack newStack = newStackId == stack.getLocalId() ? stack : dataBaseAdapter.getStackByLocalIdDirectly(newStackId);
@@ -1680,7 +1417,7 @@ public class SyncManager {
reorderLocally(cardsOfNewStack, movedCard, newStackId, newOrder);
}
//FIXME: remove the sync-block, when commentblock up there is activated. (waiting for deck server bugfix)
- if (hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(movedCard.getCard().getStackId());
FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId());
Account account = dataBaseAdapter.getAccountByIdDirectly(movedCard.getCard().getAccountId());
@@ -1696,82 +1433,6 @@ public class SyncManager {
}
- private void reorderLocally(List<FullCard> cardsOfNewStack, @NonNull FullCard movedCard, long newStackId, int newOrder) {
- // set new stack and order
- Card movedInnerCard = movedCard.getCard();
- int oldOrder = movedInnerCard.getOrder();
- long oldStackId = movedInnerCard.getStackId();
-
-
- List<Card> changedCards = new ArrayList<>();
-
- int startingAtOrder = newOrder;
- if (oldStackId == newStackId) {
- // card was only reordered in the same stack
- movedInnerCard.setStatusEnum(movedInnerCard.getStatus() == DBStatus.LOCAL_MOVED.getId() ? DBStatus.LOCAL_MOVED : DBStatus.LOCAL_EDITED);
- // move direction?
- if (oldOrder > newOrder) {
- // up
- changedCards.add(movedCard.getCard());
- for (FullCard cardToUpdate : cardsOfNewStack) {
- Card cardEntity = cardToUpdate.getCard();
- if (cardEntity.getOrder() < newOrder) {
- continue;
- }
- if (cardEntity.getOrder() >= oldOrder) {
- break;
- }
- changedCards.add(cardEntity);
- }
- } else {
- // down
- startingAtOrder = oldOrder;
- for (FullCard cardToUpdate : cardsOfNewStack) {
- Card cardEntity = cardToUpdate.getCard();
- if (cardEntity.getOrder() <= oldOrder) {
- continue;
- }
- if (cardEntity.getOrder() > newOrder) {
- break;
- }
- changedCards.add(cardEntity);
- }
- changedCards.add(movedCard.getCard());
- }
- } else {
- // card was moved to an other stack
- movedInnerCard.setStackId(newStackId);
- movedInnerCard.setStatusEnum(DBStatus.LOCAL_MOVED);
- changedCards.add(movedCard.getCard());
- for (FullCard fullCard : cardsOfNewStack) {
- // skip unchanged cards
- if (fullCard.getCard().getOrder() < newOrder) {
- continue;
- }
- changedCards.add(fullCard.getCard());
- }
- }
- reorderAscending(movedInnerCard, changedCards, startingAtOrder);
- }
-
- private void reorderAscending(@NonNull Card movedCard, @NonNull List<Card> cardsToReorganize, int startingAtOrder) {
- final Instant now = Instant.now();
- for (Card card : cardsToReorganize) {
- card.setOrder(startingAtOrder);
- if (card.getStatus() == DBStatus.UP_TO_DATE.getId()) {
- card.setStatusEnum(DBStatus.LOCAL_EDITED_SILENT);
- card.setLastModifiedLocal(now);
- }
- startingAtOrder++;
- }
- //update the moved one first, because otherwise a bunch of livedata is fired, leading the card to dispose and reappear
- cardsToReorganize.remove(movedCard);
- dataBaseAdapter.updateCard(movedCard, false);
- for (Card card : cardsToReorganize) {
- dataBaseAdapter.updateCard(card, false);
- }
- }
-
/**
* FIXME clean up on error
* When uploading the exact same attachment 2 times to the same card, the server starts burning and gets mad and returns status 500
@@ -1797,12 +1458,12 @@ public class SyncManager {
}
@AnyThread
- public WrappedLiveData<Attachment> updateAttachmentForCard(long accountId, @NonNull Attachment existing, @NonNull String mimeType, @NonNull File file) {
- WrappedLiveData<Attachment> liveData = new WrappedLiveData<>();
+ public LiveData<Attachment> updateAttachmentForCard(long accountId, @NonNull Attachment existing, @NonNull String mimeType, @NonNull File file) {
+ final var liveData = new MutableLiveData<Attachment>();
executor.submit(() -> {
Attachment attachment = populateAttachmentEntityForFile(existing, existing.getCardId(), mimeType, file);
attachment.setLastModifiedLocal(Instant.now());
- if (serverAdapter.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, existing.getCardId());
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getCard().getStackId());
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId());
@@ -1817,7 +1478,8 @@ public class SyncManager {
@SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
- liveData.postError(throwable);
+ DeckLog.error(throwable);
+// liveData.postError(throwable);
}
});
}
@@ -1840,7 +1502,7 @@ public class SyncManager {
@AnyThread
public void deleteAttachmentOfCard(long accountId, long localCardId, long localAttachmentId, @NonNull IResponseCallback<Void> callback) {
executor.submit(() -> {
- if (serverAdapter.hasInternetConnection()) {
+ if (connectivityUtil.hasInternetConnection()) {
FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, localCardId);
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getCard().getStackId());
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId());
@@ -1853,135 +1515,10 @@ public class SyncManager {
});
}
- // -------------------
- // Widgets
- // -------------------
-
- // # filter widget
-
- @AnyThread
- public void createFilterWidget(@NonNull FilterWidget filterWidget, @NonNull IResponseCallback<Integer> callback) {
- executor.submit(() -> {
- try {
- int filterWidgetId = dataBaseAdapter.createFilterWidgetDirectly(filterWidget);
- callback.onResponse(filterWidgetId);
- } catch (Throwable t) {
- callback.onError(t);
- }
- });
- }
-
- @AnyThread
- public void updateFilterWidget(@NonNull FilterWidget filterWidget, @NonNull ResponseCallback<Boolean> callback) {
- executor.submit(() -> {
- try {
- dataBaseAdapter.updateFilterWidgetDirectly(filterWidget);
- callback.onResponse(Boolean.TRUE);
- } catch (Throwable t) {
- callback.onError(t);
- }
- });
- }
-
- @AnyThread
- public void getFilterWidget(@NonNull Integer filterWidgetId, @NonNull IResponseCallback<FilterWidget> callback) {
- executor.submit(() -> {
- try {
- callback.onResponse(dataBaseAdapter.getFilterWidgetByIdDirectly(filterWidgetId));
- } catch (Throwable t) {
- callback.onError(t);
- }
- });
- }
-
- @AnyThread
- public void deleteFilterWidget(int filterWidgetId, @NonNull IResponseCallback<Boolean> callback) {
- executor.submit(() -> {
- try {
- dataBaseAdapter.deleteFilterWidgetDirectly(filterWidgetId);
- callback.onResponse(Boolean.TRUE);
- } catch (Throwable t) {
- callback.onError(t);
- }
- });
- }
-
- public boolean filterWidgetExists(int id) {
- return dataBaseAdapter.filterWidgetExists(id);
- }
-
- @WorkerThread
- public List<FilterWidgetCard> getCardsForFilterWidget(@NonNull Integer filterWidgetId) {
- return dataBaseAdapter.getCardsForFilterWidget(filterWidgetId);
- }
-
- @WorkerThread
- public LiveData<List<UpcomingCardsAdapterItem>> getCardsForUpcomingCards() {
- return dataBaseAdapter.getCardsForUpcomingCard();
- }
-
- @WorkerThread
- public List<UpcomingCardsAdapterItem> getCardsForUpcomingCardsForWidget() {
- return dataBaseAdapter.getCardsForUpcomingCardForWidget();
- }
-
- // # single card widget
-
/**
- * Can be called from a configuration screen or a picker.
- * Creates a new entry in the database, if row with given widgetId does not yet exist.
+ * FIXME <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/640">GitHub Issue #640</a>
*/
- @AnyThread
- public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) {
- executor.submit(() -> dataBaseAdapter.createSingleCardWidget(widgetId, accountId, boardId, localCardId));
- }
-
- @WorkerThread
- public FullSingleCardWidgetModel getSingleCardWidgetModelDirectly(int appWidgetId) throws NoSuchElementException {
- final FullSingleCardWidgetModel model = dataBaseAdapter.getFullSingleCardWidgetModel(appWidgetId);
- if (model == null) {
- throw new NoSuchElementException("There is no " + FullSingleCardWidgetModel.class.getSimpleName() + " with the given appWidgetId " + appWidgetId);
- }
- return model;
- }
-
- @AnyThread
- public void deleteSingleCardWidgetModel(int widgetId) {
- executor.submit(() -> dataBaseAdapter.deleteSingleCardWidget(widgetId));
- }
-
- public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) {
- executor.submit(() -> dataBaseAdapter.createStackWidget(appWidgetId, accountId, stackId, darkTheme));
- }
-
- @WorkerThread
- public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) throws NoSuchElementException {
- final StackWidgetModel model = dataBaseAdapter.getStackWidgetModelDirectly(appWidgetId);
- if (model == null) {
- throw new NoSuchElementException();
- }
- return model;
- }
-
- public void deleteStackWidgetModel(int appWidgetId) {
- executor.submit(() -> dataBaseAdapter.deleteStackWidget(appWidgetId));
- }
-
- /**
- * FIXME https://github.com/stefan-niedermann/nextcloud-deck/issues/640
- */
- public static boolean ignoreExceptionOnVoidError(Throwable t) {
- return t instanceof NullPointerException && "Attempt to invoke interface method 'void io.reactivex.disposables.Disposable.dispose()' on a null object reference".equals(t.getMessage());
- }
-
- @WorkerThread
- public Stack getStackDirectly(long stackLocalId) {
- return dataBaseAdapter.getStackByLocalIdDirectly(stackLocalId);
- }
-
- @ColorInt
- @WorkerThread
- public Integer getBoardColorDirectly(long accountId, long localBoardId) {
- return dataBaseAdapter.getBoardColorDirectly(accountId, localBoardId);
+ public static boolean isNoOnVoidError(Throwable t) {
+ return !(t instanceof NullPointerException) || !"Attempt to invoke interface method 'void io.reactivex.disposables.Disposable.dispose()' on a null object reference".equals(t.getMessage());
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java
index af6e08826..a454dde1d 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncWorker.java
@@ -4,19 +4,29 @@ import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
import androidx.preference.PreferenceManager;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
+import androidx.work.ListenableWorker;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.api.ResponseCallback;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
public class SyncWorker extends Worker {
@@ -25,25 +35,62 @@ public class SyncWorker extends Worker {
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
+ private final BaseRepository baseRepository;
+ private final SharedPreferences.Editor editor;
+
public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
+ this.baseRepository = new BaseRepository(context);
+ this.editor = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit();
}
@NonNull
@Override
public Result doWork() {
- SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit();
- SyncManager syncManager = new SyncManager(getApplicationContext(), null);
- if (syncManager.hasInternetConnection()) {
- DeckLog.info("Starting background synchronization");
- sharedPreferencesEditor.putLong(getApplicationContext().getString(R.string.shared_preference_last_background_sync), System.currentTimeMillis());
- sharedPreferencesEditor.apply();
- boolean success = syncManager.synchronizeEverything();
- DeckLog.info("Finishing background synchronization. Success: ", success);
- return success ? Result.failure() : Result.success();
+ DeckLog.info("Starting background synchronization");
+ editor.putLong(getApplicationContext().getString(R.string.shared_preference_last_background_sync), System.currentTimeMillis());
+ editor.apply();
+
+ try {
+ return synchronizeEverything(getApplicationContext(), baseRepository.readAccountsDirectly());
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ return Result.failure();
+ } finally {
+ DeckLog.info("Finishing background synchronization.");
+ }
+ }
+
+ @WorkerThread
+ private ListenableWorker.Result synchronizeEverything(@NonNull Context context, @NonNull List<Account> accounts) throws NextcloudFilesAppAccountNotFoundException {
+ if (accounts.isEmpty()) {
+ return Result.success();
+ }
+ final var success = new AtomicBoolean(true);
+ final var latch = new CountDownLatch(accounts.size());
+
+ try {
+ for (Account account : accounts) {
+ new SyncManager(context, account).synchronize(new ResponseCallback<>(account) {
+ @Override
+ public void onResponse(Boolean response) {
+ success.set(success.get() && Boolean.TRUE.equals(response));
+ latch.countDown();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ success.set(false);
+ super.onError(throwable);
+ latch.countDown();
+ }
+ });
+ }
+ latch.await();
+ return success.get() ? Result.success() : Result.failure();
+ } catch (InterruptedException e) {
+ DeckLog.logError(e);
+ return Result.failure();
}
- return Result.success();
}
public static void update(@NonNull Context context) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java
index 4406ce78e..e5cdbf192 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java
@@ -4,16 +4,14 @@ import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN;
import android.content.Context;
import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
import android.net.Uri;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import com.nextcloud.android.sso.api.ParsedResponse;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.io.File;
import java.util.List;
@@ -44,6 +42,7 @@ import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser;
import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList;
import it.niedermann.nextcloud.deck.model.propagation.CardUpdate;
import it.niedermann.nextcloud.deck.model.propagation.Reorder;
+import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
@@ -51,43 +50,36 @@ import okhttp3.ResponseBody;
public class ServerAdapter {
- private final String prefKeyWifiOnly;
+ private final ConnectivityUtil connectivityUtil;
private final String prefKeyEtags;
- final SharedPreferences sharedPreferences;
-
- @NonNull
- private final Context applicationContext;
+ private final SharedPreferences sharedPreferences;
private final ApiProvider provider;
- public ServerAdapter(@NonNull Context applicationContext, @Nullable String ssoAccountName) {
- this.applicationContext = applicationContext;
- prefKeyWifiOnly = applicationContext.getResources().getString(R.string.pref_key_wifi_only);
- prefKeyEtags = applicationContext.getResources().getString(R.string.pref_key_etags);
- provider = new ApiProvider(applicationContext, ssoAccountName);
- sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext);
+ public ServerAdapter(@NonNull Context context,
+ @NonNull SingleSignOnAccount ssoAccount,
+ @NonNull ConnectivityUtil connectivityUtil) {
+ this(context, new ApiProvider(context, ssoAccount), connectivityUtil);
}
- public void ensureInternetConnection() {
- final boolean isConnected = hasInternetConnection();
- if (!isConnected) {
- throw new OfflineException();
- }
+ public ServerAdapter(@NonNull Context context,
+ @NonNull ApiProvider apiProvider,
+ @NonNull ConnectivityUtil connectivityUtil) {
+ this.prefKeyEtags = context.getResources().getString(R.string.pref_key_etags);
+ this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ this.connectivityUtil = connectivityUtil;
+ this.provider = apiProvider;
}
+ @Deprecated()
public boolean hasInternetConnection() {
- ConnectivityManager cm = (ConnectivityManager) applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (cm != null) {
- if (sharedPreferences.getBoolean(prefKeyWifiOnly, false)) {
- NetworkInfo networkInfo = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
- if (networkInfo == null) {
- return false;
- }
- return networkInfo.isConnected();
- } else {
- return cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isConnected();
- }
+ return connectivityUtil.hasInternetConnection();
+ }
+
+ public void ensureInternetConnection() {
+ final boolean isConnected = connectivityUtil.hasInternetConnection();
+ if (!isConnected) {
+ throw new OfflineException();
}
- return false;
}
// TODO what is this?
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java
index 5c438c421..284abf7d1 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java
@@ -1,11 +1,16 @@
package it.niedermann.nextcloud.deck.persistence.sync.adapters.db;
-import static androidx.lifecycle.Transformations.distinctUntilChanged;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.concurrent.CompletableFuture.supplyAsync;
+import static java.util.stream.Collectors.toList;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.database.sqlite.SQLiteConstraintException;
+import android.text.TextUtils;
import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt;
@@ -13,27 +18,34 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
+import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
+import androidx.preference.PreferenceManager;
import androidx.sqlite.db.SimpleSQLiteQuery;
-import org.jetbrains.annotations.NotNull;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.stream.Collectors;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.android.sharedpreferences.SharedPreferenceLongLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.model.AccessControl;
import it.niedermann.nextcloud.deck.model.Account;
@@ -77,27 +89,40 @@ import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetStack;
import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetUser;
import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard;
import it.niedermann.nextcloud.deck.model.widget.singlecard.SingleCardWidgetModel;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper;
import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterItem;
import it.niedermann.nextcloud.deck.ui.widget.singlecard.SingleCardWidget;
public class DataBaseAdapter {
-
@NonNull
private final DeckDatabase db;
@NonNull
private final Context context;
@NonNull
private final ExecutorService widgetNotifierExecutor;
+ @NonNull
+ private final ExecutorService executor;
+ private static final Long NOT_AVAILABLE = -1L;
+ private final SharedPreferences sharedPreferences;
+ private final SharedPreferences.Editor sharedPreferencesEditor;
+ @ColorInt
+ private final int defaultColor;
public DataBaseAdapter(@NonNull Context appContext) {
- this(appContext, DeckDatabase.getInstance(appContext), Executors.newCachedThreadPool());
+ this(appContext, DeckDatabase.getInstance(appContext), Executors.newCachedThreadPool(), Executors.newCachedThreadPool());
}
- private DataBaseAdapter(@NonNull Context applicationContext, @NonNull DeckDatabase db, @NonNull ExecutorService widgetNotifierExecutor) {
+ @VisibleForTesting
+ protected DataBaseAdapter(@NonNull Context applicationContext,
+ @NonNull DeckDatabase db,
+ @NonNull ExecutorService widgetNotifierExecutor,
+ @NonNull ExecutorService executor) {
this.context = applicationContext;
this.db = db;
this.widgetNotifierExecutor = widgetNotifierExecutor;
+ this.executor = executor;
+ this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ this.sharedPreferencesEditor = this.sharedPreferences.edit();
+ this.defaultColor = ContextCompat.getColor(context, R.color.defaultBrand);
}
@NonNull
@@ -118,11 +143,14 @@ public class DataBaseAdapter {
}
public LiveData<Boolean> hasAccounts() {
- return LiveDataHelper.postCustomValue(db.getAccountDao().countAccounts(), data -> data != null && data > 0);
+ return new ReactiveLiveData<>(db.getAccountDao().countAccounts())
+ .distinctUntilChanged()
+ .map(count -> count != null && count > 0);
}
public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
- return distinctUntilChanged(db.getBoardDao().getBoardByRemoteId(accountId, remoteId));
+ return new ReactiveLiveData<>(db.getBoardDao().getBoardByRemoteId(accountId, remoteId))
+ .distinctUntilChanged();
}
@WorkerThread
@@ -139,7 +167,8 @@ public class DataBaseAdapter {
}
public LiveData<Stack> getStackByRemoteId(long accountId, long localBoardId, long remoteId) {
- return distinctUntilChanged(db.getStackDao().getStackByRemoteId(accountId, localBoardId, remoteId));
+ return new ReactiveLiveData<>(db.getStackDao().getStackByRemoteId(accountId, localBoardId, remoteId))
+ .distinctUntilChanged();
}
public Stack getStackByLocalIdDirectly(final long localStackId) {
@@ -156,7 +185,8 @@ public class DataBaseAdapter {
}
public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) {
- return distinctUntilChanged(db.getCardDao().getCardByRemoteId(accountId, remoteId));
+ return new ReactiveLiveData<>(db.getCardDao().getCardByRemoteId(accountId, remoteId))
+ .distinctUntilChanged();
}
@WorkerThread
@@ -225,16 +255,18 @@ public class DataBaseAdapter {
return db.getCardDao().getCardByRemoteIdDirectly(accountId, remoteId);
}
- public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, FilterInformation filter) {
- if (filter == null) {
- return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardsForStack(accountId, localStackId), this::filterRelationsForCard);
- }
- return LiveDataHelper.interceptLiveData(db.getCardDao().getFilteredFullCardsForStack(getQueryForFilter(filter, accountId, localStackId)), this::filterRelationsForCard);
+ public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) {
+ return new ReactiveLiveData<>(
+ filter == null
+ ? db.getCardDao().getFullCardsForStack(accountId, localStackId)
+ : db.getCardDao().getFilteredFullCardsForStack(getQueryForFilter(filter, accountId, localStackId)))
+ .tap(this::filterRelationsForCard, executor)
+ .distinctUntilChanged();
}
private void fillSqlWithEntityListValues(StringBuilder query, Collection<Object> args, @NonNull List<? extends IRemoteEntity> entities) {
- List<Long> idList = entities.stream().map(IRemoteEntity::getLocalId).collect(Collectors.toList());
+ List<Long> idList = entities.stream().map(IRemoteEntity::getLocalId).collect(toList());
fillSqlWithListValues(query, args, idList);
}
@@ -257,19 +289,19 @@ public class DataBaseAdapter {
@AnyThread
private SimpleSQLiteQuery getQueryForFilter(FilterInformation filter, long accountId, long localStackId) {
- return getQueryForFilter(filter, Collections.singletonList(accountId), Collections.singletonList(localStackId));
+ return getQueryForFilter(filter, singletonList(accountId), singletonList(localStackId));
}
@AnyThread
- private SimpleSQLiteQuery getQueryForFilter(FilterInformation filter, List<Long> accountIds, List<Long> localStackIds) {
+ private SimpleSQLiteQuery getQueryForFilter(@NonNull FilterInformation filter, @NonNull List<Long> accountIds, @NonNull List<Long> localStackIds) {
final Collection<Object> args = new ArrayList<>();
StringBuilder query = new StringBuilder("SELECT * FROM card c WHERE 1=1 ");
- if (accountIds != null && !accountIds.isEmpty()) {
+ if (!accountIds.isEmpty()) {
query.append("and accountId in (");
fillSqlWithListValues(query, args, accountIds);
query.append(") ");
}
- if (localStackIds != null && !localStackIds.isEmpty()) {
+ if (!localStackIds.isEmpty()) {
query.append("and stackId in (");
fillSqlWithListValues(query, args, localStackIds);
query.append(") ");
@@ -335,7 +367,7 @@ public class DataBaseAdapter {
throw new IllegalArgumentException("You need to add your new EDueType value\"" + filter.getDueType() + "\" here!");
}
}
- if (filter.getFilterText() != null && !filter.getFilterText().isEmpty()) {
+ if (!TextUtils.isEmpty(filter.getFilterText())) {
query.append(" and (c.description like ? or c.title like ?) ");
String filterText = "%" + filter.getFilterText() + "%";
args.add(filterText);
@@ -385,7 +417,8 @@ public class DataBaseAdapter {
@UiThread
public LiveData<Label> getLabelByRemoteId(long accountId, long remoteId) {
- return distinctUntilChanged(db.getLabelDao().getLabelByRemoteId(accountId, remoteId));
+ return new ReactiveLiveData<>(db.getLabelDao().getLabelByRemoteId(accountId, remoteId))
+ .distinctUntilChanged();
}
@WorkerThread
@@ -529,7 +562,7 @@ public class DataBaseAdapter {
final long id = db.getAccountDao().insert(account);
widgetNotifierExecutor.submit(() -> {
- DeckLog.verbose("Adding new created", Account.class.getSimpleName(), " with ", id, " to all instances of ", EWidgetType.UPCOMING_WIDGET.name());
+ DeckLog.verbose("Adding new created", Account.class.getSimpleName(), "with", id, "to all instances of", EWidgetType.UPCOMING_WIDGET.name());
for (FilterWidget widget : getFilterWidgetsByType(EWidgetType.UPCOMING_WIDGET)) {
widget.getAccounts().add(new FilterWidgetAccount(id, false));
updateFilterWidgetDirectly(widget);
@@ -551,29 +584,29 @@ public class DataBaseAdapter {
@UiThread
public LiveData<Account> readAccount(long id) {
- return distinctUntilChanged(fillAccountsUserName(db.getAccountDao().getAccountById(id)));
+ return new ReactiveLiveData<>(db.getAccountDao().getAccountById(id))
+ .tap(account -> account.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(account.getId(), account.getUserName())), executor)
+ .distinctUntilChanged();
}
@UiThread
public LiveData<Account> readAccount(String name) {
- return distinctUntilChanged(fillAccountsUserName(db.getAccountDao().getAccountByName(name)));
+ return new ReactiveLiveData<>(db.getAccountDao().getAccountByName(name))
+ .tap(account -> account.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(account.getId(), account.getUserName())), executor)
+ .distinctUntilChanged();
}
@UiThread
public LiveData<List<Account>> readAccounts() {
- return distinctUntilChanged(fillAccountsListUserName(db.getAccountDao().getAllAccounts()));
+ return new ReactiveLiveData<>(db.getAccountDao().getAllAccounts())
+ .tap(accounts -> accounts.forEach(account -> account.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(account.getId(), account.getUserName()))), executor)
+ .distinctUntilChanged();
}
- private LiveData<Account> fillAccountsUserName(LiveData<Account> source) {
- return LiveDataHelper.interceptLiveData(distinctUntilChanged(source), data -> data.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(data.getId(), data.getUserName())));
- }
-
- private LiveData<List<Account>> fillAccountsListUserName(LiveData<List<Account>> source) {
- return LiveDataHelper.interceptLiveData(distinctUntilChanged(source), data -> {
- for (Account a : data) {
- a.setUserDisplayName(db.getUserDao().getUserNameByUidDirectly(a.getId(), a.getUserName()));
- }
- });
+ public LiveData<Integer> getAccountColor(long accountId) {
+ return new ReactiveLiveData<>(db.getAccountDao().getAccountColor(accountId))
+ .distinctUntilChanged()
+ .map(color -> color == null ? defaultColor : color);
}
@WorkerThread
@@ -588,20 +621,14 @@ public class DataBaseAdapter {
return account;
}
-
- public LiveData<List<Board>> getBoards(long accountId) {
- return distinctUntilChanged(db.getBoardDao().getBoardsForAccount(accountId));
- }
-
public LiveData<List<Board>> getBoards(long accountId, boolean archived) {
- return distinctUntilChanged(
- archived
- ? db.getBoardDao().getArchivedBoardsForAccount(accountId)
- : db.getBoardDao().getNonArchivedBoardsForAccount(accountId));
+ return new ReactiveLiveData<>(db.getBoardDao().getNotDeletedBoards(accountId, archived ? 1 : 0))
+ .distinctUntilChanged();
}
public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) {
- return distinctUntilChanged(db.getBoardDao().getBoardsWithEditPermissionsForAccount(accountId));
+ return new ReactiveLiveData<>(db.getBoardDao().getBoardsWithEditPermissionsForAccount(accountId))
+ .distinctUntilChanged();
}
@WorkerThread
@@ -612,14 +639,14 @@ public class DataBaseAdapter {
return id;
}
- public void deleteBoard(Board board, boolean setStatus) {
+ public void deleteBoard(@NonNull Board board, boolean setStatus) {
markAsDeletedIfNeeded(board, setStatus);
db.getBoardDao().update(board);
notifyAllWidgets();
notifyFilterWidgetsAboutChangedEntity(FilterWidget.EChangedEntityType.BOARD, board.getLocalId());
}
- public void deleteBoardPhysically(Board board) {
+ public void deleteBoardPhysically(@NonNull Board board) {
db.getBoardDao().delete(board);
notifyAllWidgets();
}
@@ -631,7 +658,8 @@ public class DataBaseAdapter {
}
public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
- return distinctUntilChanged(db.getStackDao().getStacksForBoard(accountId, localBoardId));
+ return new ReactiveLiveData<>(db.getStackDao().getStacksForBoard(accountId, localBoardId))
+ .distinctUntilChanged();
}
@WorkerThread
@@ -641,7 +669,8 @@ public class DataBaseAdapter {
@MainThread
public LiveData<FullStack> getStack(long accountId, long localStackId) {
- return distinctUntilChanged(db.getStackDao().getFullStack(accountId, localStackId));
+ return new ReactiveLiveData<>(db.getStackDao().getFullStack(accountId, localStackId))
+ .distinctUntilChanged();
}
@WorkerThread
@@ -685,12 +714,16 @@ public class DataBaseAdapter {
@AnyThread
public LiveData<FullCard> getCardByLocalId(long accountId, long localCardId) {
- return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardByLocalId(accountId, localCardId), this::filterRelationsForCard);
+ return new ReactiveLiveData<>(db.getCardDao().getFullCardByLocalId(accountId, localCardId))
+ .tap(this::filterRelationsForCard, executor)
+ .distinctUntilChanged();
}
@AnyThread
public LiveData<FullCardWithProjects> getCardWithProjectsByLocalId(long accountId, long localCardId) {
- return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardWithProjectsByLocalId(accountId, localCardId), this::filterRelationsForCard);
+ return new ReactiveLiveData<>(db.getCardDao().getFullCardWithProjectsByLocalId(accountId, localCardId))
+ .tap(this::filterRelationsForCard, executor)
+ .distinctUntilChanged();
}
@WorkerThread
@@ -765,7 +798,9 @@ public class DataBaseAdapter {
}
public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long localBoardId) {
- return LiveDataHelper.interceptLiveData(db.getAccessControlDao().getAccessControlByLocalBoardId(accountId, localBoardId), this::readRelationsForACL);
+ return new ReactiveLiveData<>(db.getAccessControlDao().getAccessControlByLocalBoardId(accountId, localBoardId))
+ .tap(this::readRelationsForACL, executor)
+ .distinctUntilChanged();
}
public List<AccessControl> getAccessControlByLocalBoardIdDirectly(long accountId, Long localBoardId) {
@@ -789,7 +824,8 @@ public class DataBaseAdapter {
}
public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) {
- return distinctUntilChanged(db.getBoardDao().getFullBoardById(accountId, localId));
+ return new ReactiveLiveData<>(db.getBoardDao().getFullBoardById(accountId, localId))
+ .distinctUntilChanged();
}
@WorkerThread
@@ -798,42 +834,50 @@ public class DataBaseAdapter {
}
public LiveData<User> getUserByLocalId(long accountId, long localId) {
- return db.getUserDao().getUserByLocalId(accountId, localId);
+ return new ReactiveLiveData<>(db.getUserDao().getUserByLocalId(accountId, localId))
+ .distinctUntilChanged();
}
public LiveData<User> getUserByUid(long accountId, String uid) {
- return db.getUserDao().getUserByUid(accountId, uid);
+ return new ReactiveLiveData<>(db.getUserDao().getUserByUid(accountId, uid))
+ .distinctUntilChanged();
}
public LiveData<List<User>> getUsersForAccount(final long accountId) {
- return db.getUserDao().getUsersForAccount(accountId);
+ return new ReactiveLiveData<>(db.getUserDao().getUsersForAccount(accountId))
+ .distinctUntilChanged();
}
public LiveData<List<User>> searchUserByUidOrDisplayName(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, final String searchTerm) {
validateSearchTerm(searchTerm);
- return db.getUserDao().searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%");
+ return new ReactiveLiveData<>(db.getUserDao().searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%"))
+ .distinctUntilChanged();
}
- public List<User> searchUserByUidOrDisplayNameForACLDirectly(final long accountId, final long notYetAssignedToACL, final String searchTerm) {
+ public LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedToACL, final String searchTerm) {
validateSearchTerm(searchTerm);
- return db.getUserDao().searchUserByUidOrDisplayNameForACLDirectly(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%");
+ return db.getUserDao().searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%");
}
public LiveData<List<Label>> searchNotYetAssignedLabelsByTitle(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, String searchTerm) {
validateSearchTerm(searchTerm);
- return db.getLabelDao().searchNotYetAssignedLabelsByTitle(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%");
+ return new ReactiveLiveData<>(db.getLabelDao().searchNotYetAssignedLabelsByTitle(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%"))
+ .distinctUntilChanged();
}
public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId, long notAssignedToLocalCardId, final int topX) {
- return db.getUserDao().findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX);
+ return new ReactiveLiveData<>(db.getUserDao().findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX))
+ .distinctUntilChanged();
}
public LiveData<List<User>> findProposalsForUsersToAssignForACL(final long accountId, long boardId, final int topX) {
- return db.getUserDao().findProposalsForUsersToAssignForACL(accountId, boardId, topX);
+ return new ReactiveLiveData<>(db.getUserDao().findProposalsForUsersToAssignForACL(accountId, boardId, topX))
+ .distinctUntilChanged();
}
public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId, long notAssignedToLocalCardId) {
- return db.getLabelDao().findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId);
+ return new ReactiveLiveData<>(db.getLabelDao().findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId))
+ .distinctUntilChanged();
}
@WorkerThread
@@ -923,7 +967,8 @@ public class DataBaseAdapter {
}
public LiveData<Label> getLabelByLocalId(long localLabelId) {
- return db.getLabelDao().getLabelByLocalId(localLabelId);
+ return new ReactiveLiveData<>(db.getLabelDao().getLabelByLocalId(localLabelId))
+ .distinctUntilChanged();
}
public List<FullBoard> getLocallyChangedBoards(long accountId) {
@@ -1002,7 +1047,8 @@ public class DataBaseAdapter {
}
public LiveData<List<Activity>> getActivitiesForCard(Long localCardId) {
- return db.getActivityDao().getActivitiesForCard(localCardId);
+ return new ReactiveLiveData<>(db.getActivityDao().getActivitiesForCard(localCardId))
+ .distinctUntilChanged();
}
public long createActivity(long accountId, Activity activity) {
@@ -1033,22 +1079,22 @@ public class DataBaseAdapter {
}
public LiveData<List<DeckComment>> getCommentsForLocalCardId(long localCardId) {
- return LiveDataHelper.interceptLiveData(db.getCommentDao().getCommentByLocalCardId(localCardId), (list) -> {
- for (DeckComment deckComment : list) {
- deckComment.setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getLocalId()));
- }
- });
+ return new ReactiveLiveData<>(db.getCommentDao().getCommentByLocalCardId(localCardId))
+ .tap(list -> list.forEach(comment -> comment.setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(comment.getLocalId()))), executor)
+ .distinctUntilChanged();
}
public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) {
- return LiveDataHelper.interceptLiveData(db.getCommentDao().getFullCommentByLocalCardId(localCardId), (list) -> {
- for (FullDeckComment deckComment : list) {
- deckComment.getComment().setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getLocalId()));
- if (deckComment.getParent() != null) {
- deckComment.getParent().setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getComment().getParentId()));
- }
- }
- });
+ return new ReactiveLiveData<>(db.getCommentDao().getFullCommentByLocalCardId(localCardId))
+ .tap(list -> {
+ for (FullDeckComment deckComment : list) {
+ deckComment.getComment().setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getLocalId()));
+ if (deckComment.getParent() != null) {
+ deckComment.getParent().setMentions(db.getMentionDao().getMentionsForCommentIdDirectly(deckComment.getComment().getParentId()));
+ }
+ }
+ }, executor)
+ .distinctUntilChanged();
}
@WorkerThread
@@ -1114,7 +1160,8 @@ public class DataBaseAdapter {
}
public LiveData<Long> getLocalBoardIdByCardRemoteIdAndAccountId(long cardRemoteId, long accountId) {
- return db.getBoardDao().getLocalBoardIdByCardRemoteIdAndAccountId(cardRemoteId, accountId);
+ return new ReactiveLiveData<>(db.getBoardDao().getLocalBoardIdByCardRemoteIdAndAccountId(cardRemoteId, accountId))
+ .distinctUntilChanged();
}
@WorkerThread
@@ -1138,11 +1185,14 @@ public class DataBaseAdapter {
}
public LiveData<List<FullBoard>> getFullBoards(long accountId, boolean archived) {
- return db.getBoardDao().getArchivedFullBoards(accountId, (archived ? 1 : 0));
+ return new ReactiveLiveData<>(db.getBoardDao().getNotDeletedFullBoards(accountId, archived ? 1 : 0))
+ .distinctUntilChanged();
}
public LiveData<Boolean> hasArchivedBoards(long accountId) {
- return LiveDataHelper.postCustomValue(distinctUntilChanged(db.getBoardDao().countArchivedBoards(accountId)), data -> data != null && data > 0);
+ return new ReactiveLiveData<>(db.getBoardDao().countArchivedBoards(accountId))
+ .distinctUntilChanged()
+ .map(hasArchivedBoards -> hasArchivedBoards != null && hasArchivedBoards > 0);
}
@WorkerThread
@@ -1269,14 +1319,16 @@ public class DataBaseAdapter {
}
public LiveData<List<UpcomingCardsAdapterItem>> getCardsForUpcomingCard() {
- return LiveDataHelper.postCustomValue(db.getCardDao().getUpcomingCards(), this::cardResultsToUpcomingCardsAdapterItems);
+ return new ReactiveLiveData<>(db.getCardDao().getUpcomingCards())
+ .map(this::cardResultsToUpcomingCardsAdapterItems, executor)
+ .distinctUntilChanged();
}
public List<UpcomingCardsAdapterItem> getCardsForUpcomingCardForWidget() {
return cardResultsToUpcomingCardsAdapterItems(db.getCardDao().getUpcomingCardsDirectly());
}
- @NotNull
+ @NonNull
private List<UpcomingCardsAdapterItem> cardResultsToUpcomingCardsAdapterItems(List<FullCard> cardsResult) {
filterRelationsForCard(cardsResult);
final List<UpcomingCardsAdapterItem> result = new ArrayList<>(cardsResult.size());
@@ -1302,7 +1354,7 @@ public class DataBaseAdapter {
} else filter.setDueType(EDueType.NO_FILTER);
if (filterWidget.getAccounts().isEmpty()) {
- cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, null, null)));
+ cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, emptyList(), emptyList())));
} else {
for (FilterWidgetAccount account : filterWidget.getAccounts()) {
filter.setNoAssignedUser(account.isIncludeNoUser());
@@ -1328,24 +1380,21 @@ public class DataBaseAdapter {
if (!account.getBoards().isEmpty()) {
for (FilterWidgetBoard board : account.getBoards()) {
filter.setNoAssignedLabel(board.isIncludeNoLabel());
- final List<Long> stacks;
+ final List<Long> stacks = new ArrayList<>();
for (FilterWidgetLabel label : board.getLabels()) {
Label l = new Label();
l.setLocalId(label.getLabelId());
filter.addLabel(l);
}
if (board.getStacks().isEmpty()) {
- stacks = db.getStackDao().getLocalStackIdsByLocalBoardIdDirectly(board.getBoardId());
+ stacks.addAll(db.getStackDao().getLocalStackIdsByLocalBoardIdDirectly(board.getBoardId()));
} else {
- stacks = new ArrayList<>();
- for (FilterWidgetStack stack : board.getStacks()) {
- stacks.add(stack.getStackId());
- }
+ stacks.addAll(board.getStacks().stream().map(FilterWidgetStack::getStackId).collect(toList()));
}
- cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, Collections.singletonList(account.getAccountId()), stacks)));
+ cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, singletonList(account.getAccountId()), stacks)));
}
} else {
- cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, Collections.singletonList(account.getAccountId()), null)));
+ cardsResult.addAll(db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, singletonList(account.getAccountId()), emptyList())));
}
}
}
@@ -1378,21 +1427,17 @@ public class DataBaseAdapter {
private void handleWidgetTypeExtras(FilterWidget filterWidget, Collection<FullCard> cardsResult) {
if (filterWidget.getWidgetType() == EWidgetType.UPCOMING_WIDGET) {
// https://github.com/stefan-niedermann/nextcloud-deck/issues/819 "no due" cards are only shown if they are on a shared board
- for (FullCard fullCard : new ArrayList<>(cardsResult)) {
- if (fullCard.getCard().getDueDate() == null && !db.getStackDao().isStackOnSharedBoardDirectly(fullCard.getCard().getStackId())) {
- cardsResult.remove(fullCard);
- }
- }
+ cardsResult.removeIf(fullCard -> fullCard.getCard().getDueDate() == null && !db.getStackDao().isStackOnSharedBoardDirectly(fullCard.getCard().getStackId()));
List<Long> accountIds = null;
if (!filterWidget.getAccounts().isEmpty()) {
- accountIds = filterWidget.getAccounts().stream().map(FilterWidgetAccount::getAccountId).collect(Collectors.toList());
+ accountIds = filterWidget.getAccounts().stream().map(FilterWidgetAccount::getAccountId).collect(toList());
}
// https://github.com/stefan-niedermann/nextcloud-deck/issues/822 exclude archived cards and boards
final List<Long> archivedStacks = db.getStackDao().getLocalStackIdsInArchivedBoardsByAccountIdsDirectly(accountIds);
for (Long archivedStack : archivedStacks) {
final List<FullCard> archivedCards = cardsResult.stream()
.filter(c -> c.getCard().isArchived() || archivedStack.equals(c.getCard().getStackId()))
- .collect(Collectors.toList());
+ .collect(toList());
cardsResult.removeAll(archivedCards);
}
// https://github.com/stefan-niedermann/nextcloud-deck/issues/800 all cards within non-shared boards need to be included
@@ -1420,7 +1465,8 @@ public class DataBaseAdapter {
}
public LiveData<List<Account>> readAccountsForHostWithReadAccessToBoard(String host, long boardRemoteId) {
- return db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%" + host + "%", boardRemoteId);
+ return new ReactiveLiveData<>(db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%" + host + "%", boardRemoteId))
+ .distinctUntilChanged();
}
public List<Account> readAccountsForHostWithReadAccessToBoardDirectly(String host, long boardRemoteId) {
@@ -1464,14 +1510,16 @@ public class DataBaseAdapter {
}
public LiveData<Integer> countProjectResourcesInProject(Long projectLocalId) {
- return db.getOcsProjectResourceDao().countProjectResourcesInProject(projectLocalId);
+ return new ReactiveLiveData<>(db.getOcsProjectResourceDao().countProjectResourcesInProject(projectLocalId))
+ .distinctUntilChanged();
}
public LiveData<List<OcsProjectResource>> getResourcesByLocalProjectId(Long projectLocalId) {
- return db.getOcsProjectResourceDao().getResourcesByLocalProjectId(projectLocalId);
+ return new ReactiveLiveData<>(db.getOcsProjectResourceDao().getResourcesByLocalProjectId(projectLocalId))
+ .distinctUntilChanged();
}
- public void assignCardToProjectIfMissng(Long accountId, Long localProjectId, Long remoteCardId) {
+ public void assignCardToProjectIfMissing(Long accountId, Long localProjectId, Long remoteCardId) {
final Card card = db.getCardDao().getCardByRemoteIdDirectly(accountId, remoteCardId);
if (card != null) {
final JoinCardWithProject existing = db.getJoinCardWithOcsProjectDao().getAssignmentByCardIdAndProjectIdDirectly(card.getLocalId(), localProjectId);
@@ -1502,6 +1550,12 @@ public class DataBaseAdapter {
// UpcomingWidget.notifyDatasetChanged(context);
}
+ public LiveData<Integer> getBoardColor$(long accountId, long localBoardId) {
+ return new ReactiveLiveData<>(db.getBoardDao().getBoardColor(accountId, localBoardId))
+ .map(color -> color == null ? defaultColor : color)
+ .distinctUntilChanged();
+ }
+
@ColorInt
public Integer getBoardColorDirectly(long accountId, long localBoardId) {
return db.getBoardDao().getBoardColorByLocalIdDirectly(accountId, localBoardId);
@@ -1514,4 +1568,182 @@ public class DataBaseAdapter {
public void deleteProjectResourcesByCardIdDirectly(Long localCardId) {
db.getJoinCardWithOcsProjectDao().deleteProjectResourcesByCardIdDirectly(localCardId);
}
+
+ // =============================================================================================
+ // APP STATE
+ // TODO last boards and stacks per account should be moved to a table to benefit from cascading
+ // =============================================================================================
+
+ // ---------------
+ // Current account
+ // ---------------
+
+ public void saveCurrentAccount(@NonNull Account account) {
+ executor.submit(() -> {
+ // Glide Module depends on correct account being set.
+ // TODO Use SingleSignOnURL where possible, allow passing ssoAccountName to MarkdownEditor
+ SingleAccountHelper.setCurrentAccount(context, account.getName());
+
+ DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_account), "→", account.getId());
+ sharedPreferencesEditor.putLong(context.getString(R.string.shared_preference_last_account), account.getId());
+ sharedPreferencesEditor.apply();
+ });
+ }
+
+ public void removeCurrentAccount() {
+ executor.submit(() -> {
+ // Glide Module depends on correct account being set.
+ // TODO Use SingleSignOnURL where possible, allow passing ssoAccountName to MarkdownEditor
+ SingleAccountHelper.setCurrentAccount(context, null);
+
+ DeckLog.log("--- Remove:", context.getString(R.string.shared_preference_last_account));
+ sharedPreferencesEditor.remove(context.getString(R.string.shared_preference_last_account));
+ sharedPreferencesEditor.apply();
+ });
+ }
+
+ public LiveData<Long> getCurrentAccountId$() {
+ return new ReactiveLiveData<>(new SharedPreferenceLongLiveData(sharedPreferences, this.context.getString(R.string.shared_preference_last_account), NOT_AVAILABLE))
+ .distinctUntilChanged()
+ .tap(accountId -> {
+ DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account), "→", accountId);
+ if (NOT_AVAILABLE.equals(accountId)) {
+ executor.submit(this::removeCurrentAccount);
+ }
+ });
+ }
+
+ public CompletableFuture<Long> getCurrentAccountId() {
+ return supplyAsync(() -> {
+ final long accountId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_account), NOT_AVAILABLE);
+ DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account), "→", accountId);
+
+ if (NOT_AVAILABLE.equals(accountId)) {
+ saveNeighbourOfAccount(NOT_AVAILABLE);
+ throw new CompletionException(new IllegalStateException("No current account ID set"));
+ }
+
+ return accountId;
+ }, executor);
+ }
+
+ @WorkerThread
+ public void saveNeighbourOfAccount(long currentAccountId) {
+ getAllAccountsDirectly()
+ .stream()
+ .filter(account -> currentAccountId != account.getId())
+ .findFirst()
+ .ifPresentOrElse(this::saveCurrentAccount, this::removeCurrentAccount);
+ }
+
+ @ColorInt
+ public CompletableFuture<Integer> getCurrentAccountColor(long accountId) {
+ return supplyAsync(() -> db.getAccountDao().getAccountColorDirectly(accountId), executor)
+ .thenApplyAsync(color -> color == null ? defaultColor : color, executor);
+ }
+
+ // -------------
+ // Current board
+ // -------------
+
+ public void saveCurrentBoardId(long accountId, long boardId) {
+ DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId);
+ sharedPreferencesEditor.putLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, boardId);
+ sharedPreferencesEditor.apply();
+ }
+
+ public void removeCurrentBoardId(long accountId) {
+ DeckLog.log("--- Remove:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId);
+ sharedPreferencesEditor.remove(context.getString(R.string.shared_preference_last_board_for_account_) + accountId);
+ sharedPreferencesEditor.apply();
+ }
+
+ public LiveData<Long> getCurrentBoardId$(long accountId) {
+ return new ReactiveLiveData<>(new SharedPreferenceLongLiveData(sharedPreferences,
+ this.context.getString(R.string.shared_preference_last_board_for_account_) + accountId, NOT_AVAILABLE))
+ .distinctUntilChanged()
+ .tap(boardId -> {
+ DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId);
+ if (NOT_AVAILABLE.equals(boardId)) {
+ executor.submit(() -> saveNeighbourOfBoard(accountId, NOT_AVAILABLE));
+ }
+ });
+ }
+
+ @WorkerThread
+ public void saveNeighbourOfBoard(long accountId, long currentBoardId) {
+ getNeighbour(db.getBoardDao().getNotDeletedBoardsDirectly(accountId, 0), currentBoardId)
+ .ifPresentOrElse(neighbourBoardId -> saveCurrentBoardId(accountId, neighbourBoardId), () -> removeCurrentBoardId(accountId));
+ }
+
+ public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) {
+ return supplyAsync(() -> getBoardColorDirectly(accountId, boardId), executor)
+ .thenApplyAsync(color -> color == null ? defaultColor : color, executor);
+ }
+
+ // -------------
+ // Current stack
+ // -------------
+
+ public void saveCurrentStackId(long accountId, long boardId, long stackId) {
+ DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, "→", stackId);
+ sharedPreferencesEditor.putLong(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, stackId);
+ sharedPreferencesEditor.apply();
+ }
+
+ public void removeCurrentStackId(long accountId, long boardId) {
+ DeckLog.log("--- Remove:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId);
+ sharedPreferencesEditor.remove(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId);
+ sharedPreferencesEditor.apply();
+ }
+
+ public LiveData<Long> getCurrentStackId$(long accountId, long boardId) {
+ return new ReactiveLiveData<>(new SharedPreferenceLongLiveData(sharedPreferences, context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, NOT_AVAILABLE))
+ .distinctUntilChanged()
+ .tap(stackId -> {
+ DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, "→", stackId);
+ if (NOT_AVAILABLE.equals(stackId)) {
+ executor.submit(() -> saveNeighbourOfStack(accountId, boardId, NOT_AVAILABLE));
+ }
+ });
+ }
+
+ @WorkerThread
+ public void saveNeighbourOfStack(long accountId, long boardId, long currentStackId) {
+ getNeighbour(getFullStacksForBoardDirectly(accountId, boardId), currentStackId)
+ .ifPresentOrElse(
+ neighbourStackId -> saveCurrentStackId(accountId, boardId, neighbourStackId),
+ () -> removeCurrentStackId(accountId, boardId));
+ }
+
+ /**
+ * @return the local ID of the direct neighbour of the given {@param currentId} if available. Prefers neighbours to the start of the wanted, but might also return a neighbour to the end.
+ */
+ private Optional<Long> getNeighbour(List<? extends IRemoteEntity> entities, long currentId) {
+ if (entities.size() < 1) {
+ return Optional.empty();
+ }
+
+ @Nullable Integer position = null;
+
+ for (int i = 0; i < entities.size(); i++) {
+ if (entities.get(i).getLocalId() == currentId) {
+ position = i;
+ }
+ }
+
+ // Not found, but there is an entry
+ if (position == null) {
+ return Optional.of(entities.get(0).getLocalId());
+ }
+
+ // Current entity is last entity
+ if (position == 0 && entities.size() == 1) {
+ return Optional.empty();
+ }
+
+ return Optional.of(position > 0
+ ? entities.get(position - 1).getLocalId()
+ : entities.get(position + 1).getLocalId());
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java
index 34a0515e3..739e4b9a4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java
@@ -96,6 +96,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migra
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_28_29;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_29_30;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_30_31;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_31_32;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_8_9;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migration_9_10;
@@ -134,7 +135,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration.Migra
FilterWidgetSort.class,
},
exportSchema = false,
- version = 31
+ version = 32
)
@TypeConverters({DateTypeConverter.class, EnumConverter.class})
public abstract class DeckDatabase extends RoomDatabase {
@@ -186,6 +187,7 @@ public abstract class DeckDatabase extends RoomDatabase {
.addMigrations(new Migration_28_29())
.addMigrations(new Migration_29_30(context))
.addMigrations(new Migration_30_31())
+ .addMigrations(new Migration_31_32(context))
.fallbackToDestructiveMigration()
.addCallback(ON_CREATE_CALLBACK)
.build();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDao.java
index 6ebe4a8f3..a4d1af44d 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDao.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDao.java
@@ -42,4 +42,10 @@ public interface AccountDao extends GenericDao<Account> {
@Query("SELECT * from account a where a.url like :hostLike and exists (select 1 from board b where b.id = :boardRemoteId and a.id = b.accountId)")
List<Account> readAccountsForHostWithReadAccessToBoardDirectly(String hostLike, long boardRemoteId);
+
+ @Query("SELECT a.color FROM account a where a.id = :accountId")
+ LiveData<Integer> getAccountColor(long accountId);
+
+ @Query("SELECT a.color FROM account a where a.id = :accountId")
+ Integer getAccountColorDirectly(long accountId);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java
index 066b512e9..15daf8923 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java
@@ -13,18 +13,17 @@ import it.niedermann.nextcloud.deck.model.full.FullBoard;
@Dao
public interface BoardDao extends GenericDao<Board> {
- @Query("SELECT * FROM board WHERE accountId = :accountId and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc")
- LiveData<List<Board>> getBoardsForAccount(final long accountId);
-
- @Query("SELECT * FROM board WHERE accountId = :accountId and archived = 1 and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc")
- LiveData<List<Board>> getArchivedBoardsForAccount(final long accountId);
+ @Transaction
+ @Query("SELECT * FROM board WHERE accountId = :accountId and archived = :archived and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc")
+ LiveData<List<Board>> getNotDeletedBoards(long accountId, int archived);
- @Query("SELECT * FROM board WHERE accountId = :accountId and archived = 0 and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc")
- LiveData<List<Board>> getNonArchivedBoardsForAccount(final long accountId);
+ @Transaction
+ @Query("SELECT * FROM board WHERE accountId = :accountId and archived = :archived and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc")
+ List<Board> getNotDeletedBoardsDirectly(long accountId, int archived);
@Transaction
@Query("SELECT * FROM board WHERE accountId = :accountId and archived = :archived and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc")
- LiveData<List<FullBoard>> getArchivedFullBoards(long accountId, int archived);
+ LiveData<List<FullBoard>> getNotDeletedFullBoards(long accountId, int archived);
@Query("SELECT * FROM board WHERE accountId = :accountId and id = :remoteId")
LiveData<Board> getBoardByRemoteId(final long accountId, final long remoteId);
@@ -88,4 +87,7 @@ public interface BoardDao extends GenericDao<Board> {
@Query("SELECT b.color FROM board b where b.localId = :localBoardId and b.accountId = :accountId")
Integer getBoardColorByLocalIdDirectly(long accountId, long localBoardId);
+
+ @Query("SELECT b.color FROM board b where b.localId = :localBoardId and b.accountId = :accountId")
+ LiveData<Integer> getBoardColor(long accountId, long localBoardId);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java
index 671f044b0..7fad7499e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java
@@ -55,7 +55,7 @@ public interface UserDao extends GenericDao<User> {
"and ( uid LIKE :searchTerm or displayname LIKE :searchTerm or primaryKey LIKE :searchTerm ) " +
"and u.localId <> (select b.ownerId from board b where localId = :boardId)" +
"ORDER BY u.displayname")
- List<User> searchUserByUidOrDisplayNameForACLDirectly(final long accountId, final long boardId, final String searchTerm);
+ LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long boardId, final String searchTerm);
@Query("SELECT * FROM user WHERE accountId = :accountId and uid = :uid")
User getUserByUidDirectly(final long accountId, final String uid);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_31_32.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_31_32.java
new file mode 100644
index 000000000..c26208252
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_31_32.java
@@ -0,0 +1,29 @@
+package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.migration;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.preference.PreferenceManager;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+/**
+ */
+public class Migration_31_32 extends Migration {
+
+ @NonNull
+ private final Context context;
+ public Migration_31_32(@NonNull Context context) {
+ super(31, 32);
+ this.context = context;
+ }
+
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .remove("it.niedermann.nextcloud.deck.theme_main")
+ .remove("it.niedermann.nextcloud.deck.last_account_color")
+ .apply();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/LiveDataHelper.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/LiveDataHelper.java
deleted file mode 100644
index af503b029..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/LiveDataHelper.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util;
-
-import static androidx.lifecycle.Transformations.distinctUntilChanged;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MediatorLiveData;
-import androidx.lifecycle.Observer;
-
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class LiveDataHelper {
-
- private LiveDataHelper() {
- throw new UnsupportedOperationException("This class must not be instantiated.");
- }
-
- private static final ExecutorService executor = Executors.newCachedThreadPool();
-
- public static <T> LiveData<T> interceptLiveData(LiveData<T> data, DataChangeProcessor<T> onDataChange) {
- MediatorLiveData<T> ret = new MediatorLiveData<>();
-
- ret.addSource(data, changedData ->
- executor.submit(() -> {
- onDataChange.onDataChanged(changedData);
- ret.postValue(changedData);
- })
- );
- return distinctUntilChanged(ret);
- }
-
-
- public static <I, O> LiveData<O> postCustomValue(LiveData<I> data, DataTransformator<I, O> transformator) {
- final MediatorLiveData<O> ret = new MediatorLiveData<>();
- ret.addSource(data, changedData -> executor.submit(() -> ret.postValue(transformator.transform(changedData))));
- return distinctUntilChanged(ret);
- }
-
- public static <I> MediatorLiveData<I> of(I oneShot) {
- return new MediatorLiveData<>() {
- @Override
- public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
- super.observe(owner, observer);
- executor.submit(() -> postValue(oneShot));
- }
- };
- }
-
- public static <I, O> LiveData<O> postSingleValue(LiveData<I> data, DataTransformator<I, O> transformator) {
- final MediatorLiveData<O> ret = new MediatorLiveData<>();
- ret.addSource(data, changedData -> executor.submit(() -> ret.postValue(transformator.transform(changedData))));
- return distinctUntilChanged(ret);
- }
-
- public static <T> void observeOnce(LiveData<T> liveData, LifecycleOwner owner, Observer<T> observer) {
- final Observer<T> tempObserver = new Observer<>() {
- @Override
- public void onChanged(T result) {
- liveData.removeObserver(this);
- observer.onChanged(result);
- }
- };
- liveData.observe(owner, tempObserver);
- }
-
- public interface DataChangeProcessor<T> {
- void onDataChanged(T data);
- }
-
- public interface DataTransformator<I, O> {
- O transform(I data);
- }
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java
deleted file mode 100644
index df2eac222..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util;
-
-import androidx.annotation.Nullable;
-import androidx.lifecycle.MutableLiveData;
-
-/**
- * Extends a {@link MutableLiveData} with an error state
- *
- * @param <T>
- */
-public class WrappedLiveData<T> extends MutableLiveData<T> {
- @Nullable
- private Throwable error = null;
-
- public boolean hasError() {
- return error != null;
- }
-
- public void setError(@Nullable Throwable error) {
- this.error = error;
- }
-
- @Nullable
- public Throwable getError() {
- return error;
- }
-
- public void postError(@Nullable Throwable error) {
- setError(error);
- postValue(null);
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java
deleted file mode 100644
index b9fa58b7d..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-public class Debouncer <T> {
- private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1);
- private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<>();
- private final Callback<T> callback;
- private final int interval;
-
- public Debouncer(Callback<T> c, int interval) {
- this.callback = c;
- this.interval = interval;
- }
-
- public void call(T key) {
- TimerTask task = new TimerTask(key);
-
- TimerTask prev;
- do {
- prev = delayedMap.putIfAbsent(key, task);
- if (prev == null)
- sched.schedule(task, interval, TimeUnit.MILLISECONDS);
- // Exit only if new task was added to map, or existing task was extended successfully
- } while (prev != null && !prev.extend());
- }
-
- public void terminate() {
- sched.shutdownNow();
- }
-
- public interface Callback<T> {
- void call(T key);
- }
-
- // The task that wakes up when the wait time elapses
- private class TimerTask implements Runnable {
- private final T key;
- private long dueTime;
- private final Object lock = new Object();
-
- public TimerTask(T key) {
- this.key = key;
- extend();
- }
-
- public boolean extend() {
- synchronized (lock) {
- if (dueTime < 0) // Task has been shutdown
- return false;
- dueTime = System.currentTimeMillis() + interval;
- return true;
- }
- }
-
- public void run() {
- synchronized (lock) {
- long remaining = dueTime - System.currentTimeMillis();
- if (remaining > 0) { // Re-schedule task
- sched.schedule(this, remaining, TimeUnit.MILLISECONDS);
- } else { // Mark as terminated and invoke callback
- dueTime = -1;
- try {
- callback.call(key);
- } finally {
- delayedMap.remove(key);
- }
- }
- }
- }
- }
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java
deleted file mode 100644
index e0f546399..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst;
-
-import androidx.lifecycle.MediatorLiveData;
-
-import java.util.List;
-
-import it.niedermann.nextcloud.deck.DeckLog;
-import it.niedermann.nextcloud.deck.api.ResponseCallback;
-import it.niedermann.nextcloud.deck.exceptions.OfflineException;
-import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.model.enums.DBStatus;
-import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser;
-import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter;
-
-public class UserSearchLiveData extends MediatorLiveData<List<User>> implements Debouncer.Callback<Long> {
-
- private static final int DEBOUNCE_TIME = 300; // ms
- private final DataBaseAdapter db;
- private final ServerAdapter server;
- long accountId;
- String searchTerm;
- long notYetAssignedInACL;
- private final Debouncer<Long> debouncer = new Debouncer<>(this, DEBOUNCE_TIME);
-
- public UserSearchLiveData(DataBaseAdapter db, ServerAdapter server) {
- this.db = db;
- this.server = server;
- }
-
- public UserSearchLiveData search(long accountId, long notYetAssignedInACL, String searchTerm) {
- this.accountId = accountId;
- this.searchTerm = searchTerm;
- this.notYetAssignedInACL = notYetAssignedInACL;
- new Thread(() -> debouncer.call(notYetAssignedInACL)).start();
- return this;
- }
-
-
- @Override
- public void call(Long key) {
- if (key!=notYetAssignedInACL){
- return;
- }
-
- final String term = String.copyValueOf(searchTerm.toCharArray());
-
- postCurrentFromDB(term);
-
- if (server.hasInternetConnection()) {
- try {
- Account account = db.getAccountByIdDirectly(accountId);
- server.searchUser(term, new ResponseCallback<>(account) {
- @Override
- public void onResponse(OcsUserList response) {
- if (response == null || response.getUsers().isEmpty()){
- return;
- }
- for (OcsUser user : response.getUsers()) {
- User existingUser = db.getUserByUidDirectly(accountId, user.getId());
- if (existingUser == null) {
- User newUser = new User();
- newUser.setStatus(DBStatus.UP_TO_DATE.getId());
- newUser.setPrimaryKey(user.getId());
- newUser.setUid(user.getId());
- newUser.setDisplayname(user.getDisplayName());
- db.createUser(accountId, newUser);
- }
- }
- if (!term.equals(searchTerm)) {
- return;
- }
- postCurrentFromDB(term);
- }
-
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- }
- });
- } catch (OfflineException e) {
- DeckLog.logError(e);
- }
- }
- }
-
- private void postCurrentFromDB(String term) {
- List<User> foundInDB = db.searchUserByUidOrDisplayNameForACLDirectly(accountId, notYetAssignedInACL, term);
- postValue(foundInDB);
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java
index f6dca3f07..613623da5 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java
@@ -76,7 +76,7 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> {
protected boolean removeChild(AbstractSyncDataProvider<?> child) {
boolean isRemoved = super.removeChild(child);
if (isRemoved && child.getClass() == StackDataProvider.class) {
- progressDone ++;
+ progressDone++;
updateProgress();
}
return isRemoved;
@@ -191,11 +191,11 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> {
public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, ResponseCallback<Boolean> callback) {
Long accountId = callback.getAccount().getId();
List<Label> locallyChangedLabels = dataBaseAdapter.getLocallyChangedLabels(accountId);
- AsyncUtil.awaitAsyncWork(locallyChangedLabels.size(), (countDownLatch) -> {
+ AsyncUtil.awaitAsyncWork(locallyChangedLabels.size(), latch -> {
for (Label label : locallyChangedLabels) {
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId());
label.setBoardId(board.getId());
- syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), countDownLatch);
+ syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), latch);
}
});
@@ -228,13 +228,17 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> {
}
@Override
- public void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard fullBoard) {
- dataBaseAdapter.deleteBoard(fullBoard.getBoard(), true);
+ public void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard board) {
+ dataBaseAdapter.saveNeighbourOfBoard(board.getAccountId(), board.getLocalId());
+ dataBaseAdapter.removeCurrentStackId(board.getAccountId(), board.getLocalId());
+ dataBaseAdapter.deleteBoard(board.getBoard(), true);
}
@Override
- public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard fullBoard) {
- dataBaseAdapter.deleteBoardPhysically(fullBoard.getBoard());
+ public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard board) {
+ dataBaseAdapter.saveNeighbourOfBoard(board.getAccountId(), board.getLocalId());
+ dataBaseAdapter.removeCurrentStackId(board.getAccountId(), board.getLocalId());
+ dataBaseAdapter.deleteBoardPhysically(board.getBoard());
}
@Override
@@ -251,6 +255,8 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> {
// not pushed up yet so:
continue;
}
+
+ dataBaseAdapter.saveNeighbourOfBoard(board.getAccountId(), board.getLocalId());
dataBaseAdapter.deleteBoardPhysically(board.getBoard());
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java
index ccbb01530..39d049b10 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java
@@ -88,7 +88,7 @@ public class OcsProjectDataProvider extends AbstractSyncDataProvider<OcsProject>
resource.setProjectId(entity.getLocalId());
resource.setLocalId(dataBaseAdapter.createProjectResourceDirectly(accountId, resource));
if ("deck-card".equals(resource.getType())) {
- dataBaseAdapter.assignCardToProjectIfMissng(accountId, entity.getLocalId(), resource.getId());
+ dataBaseAdapter.assignCardToProjectIfMissing(accountId, entity.getLocalId(), resource.getId());
}
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java
index cc2ded815..56fbd2506 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java
@@ -85,6 +85,8 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> {
@Override
public void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullStack entity) {
entity.getStack().setBoardId(board.getId());
+
+ dataBaseAdapter.saveNeighbourOfStack(entity.getAccountId(), entity.getStack().getBoardId(), entity.getLocalId());
dataBaseAdapter.deleteStackPhysically(entity.getStack());
}
@@ -145,6 +147,8 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> {
// not pushed up yet so:
continue;
}
+
+ dataBaseAdapter.saveNeighbourOfStack(stackToDelete.getAccountId(), stackToDelete.getStack().getBoardId(), stackToDelete.getLocalId());
dataBaseAdapter.deleteStackPhysically(stackToDelete.getStack());
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java
index faabda163..d996c6015 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java
@@ -1,19 +1,19 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.util;
+import androidx.annotation.NonNull;
+
import java.util.concurrent.CountDownLatch;
+import java.util.function.Consumer;
import it.niedermann.nextcloud.deck.DeckLog;
public class AsyncUtil {
- public interface LatchCallback {
- void doWork(CountDownLatch latch);
- }
- public static void awaitAsyncWork(int count, LatchCallback worker){
- CountDownLatch countDownLatch = new CountDownLatch(count);
- worker.doWork(countDownLatch);
+ public static void awaitAsyncWork(int count, @NonNull Consumer<CountDownLatch> worker) {
+ final var latch = new CountDownLatch(count);
+ worker.accept(latch);
try {
- countDownLatch.await();
+ latch.await();
} catch (InterruptedException e) {
DeckLog.logError(e);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/ConnectivityUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/ConnectivityUtil.java
new file mode 100644
index 000000000..fdc099753
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/ConnectivityUtil.java
@@ -0,0 +1,38 @@
+package it.niedermann.nextcloud.deck.persistence.sync.helpers.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+
+import androidx.annotation.NonNull;
+import androidx.preference.PreferenceManager;
+
+import it.niedermann.nextcloud.deck.R;
+
+public class ConnectivityUtil {
+
+ private final ConnectivityManager connectivityManager;
+ private final String prefKeyWifiOnly;
+ private final SharedPreferences sharedPreferences;
+
+ public ConnectivityUtil(@NonNull Context context) {
+ this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ this.prefKeyWifiOnly = context.getString(R.string.pref_key_wifi_only);
+ this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ public boolean hasInternetConnection() {
+ if (connectivityManager != null) {
+ if (sharedPreferences.getBoolean(prefKeyWifiOnly, false)) {
+ final var networkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+ if (networkInfo == null) {
+ return false;
+ }
+ return networkInfo.isConnected();
+ } else {
+ return connectivityManager.getActiveNetworkInfo() != null && connectivityManager.getActiveNetworkInfo().isConnected();
+ }
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java
index 4fe2db958..7a09e79a4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java
@@ -14,16 +14,22 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;
import com.nextcloud.android.sso.AccountImporter;
+import com.nextcloud.android.sso.api.ParsedResponse;
import com.nextcloud.android.sso.exceptions.AccountImportCancelledException;
import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
import com.nextcloud.android.sso.ui.UiExceptionManager;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
@@ -31,6 +37,7 @@ import it.niedermann.nextcloud.deck.api.ResponseCallback;
import it.niedermann.nextcloud.deck.databinding.ActivityImportAccountBinding;
import it.niedermann.nextcloud.deck.exceptions.OfflineException;
import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker;
@@ -43,9 +50,9 @@ public class ImportAccountActivity extends AppCompatActivity {
private String prefKeyWifiOnly;
private boolean originalWifiOnlyValue = false;
- private String sharedPreferenceLastAccount;
private String urlFragmentUpdateDeck;
+ private ImportAccountViewModel importAccountViewModel;
private ActivityImportAccountBinding binding;
@Override
@@ -54,23 +61,25 @@ public class ImportAccountActivity extends AppCompatActivity {
Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+ importAccountViewModel = new ViewModelProvider(this).get(ImportAccountViewModel.class);
binding = ActivityImportAccountBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
prefKeyWifiOnly = getString(R.string.pref_key_wifi_only);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- sharedPreferenceLastAccount = getString(R.string.shared_preference_last_account);
urlFragmentUpdateDeck = getString(R.string.url_fragment_update_deck);
originalWifiOnlyValue = sharedPreferences.getBoolean(prefKeyWifiOnly, false);
- binding.welcomeText.setText(getString(R.string.welcome_text, getString(R.string.app_name)));
+ importAccountViewModel.hasAccounts().observe(this, hasAccounts -> binding.welcomeText.setText(hasAccounts
+ ? getString(R.string.welcome_text_further_accounts)
+ : getString(R.string.welcome_text, getString(R.string.app_name))));
+
binding.addButton.setOnClickListener((v) -> {
binding.status.setText("");
binding.addButton.setEnabled(false);
binding.updateDeckButton.setVisibility(View.GONE);
- disableWifiPref();
try {
AccountImporter.pickNewAccount(this);
} catch (NextcloudFilesAppNotInstalledException e) {
@@ -99,113 +108,143 @@ public class ImportAccountActivity extends AppCompatActivity {
if (requestCode == REQUEST_AUTH_TOKEN_SSO && resultCode == RESULT_CANCELED) {
binding.addButton.setEnabled(true);
} else {
- try {
- AccountImporter.onActivityResult(requestCode, resultCode, data, ImportAccountActivity.this, new AccountImporter.IAccountAccessGranted() {
- @SuppressLint("ApplySharedPref")
- @Override
- public void accountAccessGranted(SingleSignOnAccount account) {
- runOnUiThread(() -> {
- binding.status.setText(null);
- binding.status.setVisibility(View.GONE);
- binding.progressCircular.setVisibility(View.VISIBLE);
- binding.progressText.setVisibility(View.VISIBLE);
- binding.progressCircular.setIndeterminate(true);
- binding.progressText.setText(R.string.progress_import_indeterminate);
- });
-
- SingleAccountHelper.setCurrentAccount(getApplicationContext(), account.name);
- final var syncManager = new SyncManager(ImportAccountActivity.this);
- final var accountToCreate = new Account(account.name, account.userId, account.url);
- syncManager.createAccount(accountToCreate, new IResponseCallback<>() {
- @Override
- public void onResponse(Account createdAccount) {
- // Remember last account - THIS HAS TO BE DONE SYNCHRONOUSLY
- DeckLog.log("--- Write: shared_preference_last_account | ", createdAccount.getId());
- sharedPreferences
- .edit()
- .putLong(sharedPreferenceLastAccount, createdAccount.getId())
- .commit();
-
- syncManager.refreshCapabilities(new ResponseCallback<>(createdAccount) {
- @Override
- public void onResponse(Capabilities response) {
- if (!response.isMaintenanceEnabled()) {
- if (response.getDeckVersion().isSupported()) {
- var progress$ = syncManager.synchronize(new ResponseCallback<>(account) {
- @Override
- public void onResponse(Boolean response) {
- restoreWifiPref();
- SyncWorker.update(getApplicationContext());
- setResult(RESULT_OK);
- finish();
- }
+ disableWifiPref().thenAcceptAsync(v -> {
+ try {
+ AccountImporter.onActivityResult(requestCode, resultCode, data, ImportAccountActivity.this, new AccountImporter.IAccountAccessGranted() {
+ @Override
+ public void accountAccessGranted(SingleSignOnAccount account) {
+ runOnUiThread(() -> {
+ binding.status.setText(null);
+ binding.status.setVisibility(View.GONE);
+ binding.progressCircular.setVisibility(View.VISIBLE);
+ binding.progressText.setVisibility(View.VISIBLE);
+ binding.progressCircular.setIndeterminate(true);
+ binding.progressText.setText(R.string.progress_import_indeterminate);
+ });
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- setStatusText(throwable.getMessage());
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- rollbackAccountCreation(syncManager, createdAccount.getId());
- }
- });
- runOnUiThread(() -> progress$.observe(ImportAccountActivity.this, (progress) -> {
- DeckLog.log("New progress value", progress.first, progress.second);
- if (progress.first > 0) {
- binding.progressCircular.setIndeterminate(false);
- }
- if (progress.first < progress.second) {
- binding.progressText.setText(getString(R.string.progress_import, progress.first + 1, progress.second));
+ final var accountToCreate = new Account(account.name, account.userId, account.url);
+ importAccountViewModel.createAccount(accountToCreate, new IResponseCallback<>() {
+ @Override
+ public void onResponse(Account createdAccount) {
+ try {
+ final var syncManager = new SyncManager(ImportAccountActivity.this, createdAccount);
+
+ syncManager.refreshCapabilities(new ResponseCallback<>(createdAccount) {
+ @Override
+ public void onResponse(Capabilities response) {
+ if (!response.isMaintenanceEnabled()) {
+ if (response.getDeckVersion().isSupported()) {
+ final var callback = new IResponseCallback<>() {
+ @Override
+ public void onResponse(Object response) {
+ var progress$ = syncManager.synchronize(new ResponseCallback<>(account) {
+ @Override
+ public void onResponse(Boolean response) {
+ restoreWifiPref();
+ SyncWorker.update(getApplicationContext());
+ importAccountViewModel.saveCurrentAccount(account);
+ setResult(RESULT_OK);
+ finish();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ setStatusText(throwable.getMessage());
+ runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ rollbackAccountCreation(createdAccount.getId());
+ }
+ });
+
+ runOnUiThread(() -> progress$.observe(ImportAccountActivity.this, (progress) -> {
+ DeckLog.log("New progress value", progress.first, progress.second);
+ if (progress.first > 0) {
+ binding.progressCircular.setIndeterminate(false);
+ }
+ if (progress.first < progress.second) {
+ binding.progressText.setText(getString(R.string.progress_import, progress.first + 1, progress.second));
+ }
+ binding.progressCircular.setProgress(progress.first);
+ binding.progressCircular.setMax(progress.second);
+ }));
+ }
+ };
+
+ if (response.getDeckVersion().firstCallHasDifferentResponseStructure()) {
+ syncManager.fetchBoardsFromServer(new ResponseCallback<>(account) {
+ @Override
+ public void onResponse(ParsedResponse<List<FullBoard>> response) {
+ callback.onResponse(createdAccount);
+ }
+
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void onError(Throwable throwable) {
+ // We proceed with the import anyway. It's just important that one request has been done.
+ callback.onResponse(createdAccount);
+ }
+ });
+ } else {
+ callback.onResponse(createdAccount);
+ }
+ } else {
+ setStatusText(getString(R.string.deck_outdated_please_update, response.getDeckVersion().getOriginalVersion()));
+ runOnUiThread(() -> {
+ binding.updateDeckButton.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW)
+ .setData(Uri.parse(createdAccount.getUrl() + urlFragmentUpdateDeck))));
+ binding.updateDeckButton.setVisibility(View.VISIBLE);
+ });
+ rollbackAccountCreation(createdAccount.getId());
}
- binding.progressCircular.setProgress(progress.first);
- binding.progressCircular.setMax(progress.second);
- }));
- } else {
- setStatusText(getString(R.string.deck_outdated_please_update, response.getDeckVersion().getOriginalVersion()));
- runOnUiThread(() -> {
- binding.updateDeckButton.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW)
- .setData(Uri.parse(createdAccount.getUrl() + urlFragmentUpdateDeck))));
- binding.updateDeckButton.setVisibility(View.VISIBLE);
- });
- rollbackAccountCreation(syncManager, createdAccount.getId());
+ } else {
+ setStatusText(R.string.maintenance_mode);
+ rollbackAccountCreation(createdAccount.getId());
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ if (throwable instanceof OfflineException) {
+ setStatusText(R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account);
+ } else {
+ setStatusText(throwable.getMessage());
+ runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ }
+ rollbackAccountCreation(createdAccount.getId());
}
- } else {
- setStatusText(R.string.maintenance_mode);
- rollbackAccountCreation(syncManager, createdAccount.getId());
- }
+ });
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ setStatusText(e.getMessage());
+ runOnUiThread(() -> ExceptionDialogFragment.newInstance(e, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ rollbackAccountCreation(createdAccount.getId());
}
+ }
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- if (throwable instanceof OfflineException) {
- setStatusText(R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account);
- } else {
- setStatusText(throwable.getMessage());
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- }
- rollbackAccountCreation(syncManager, createdAccount.getId());
+ @Override
+ public void onError(Throwable error) {
+ IResponseCallback.super.onError(error);
+ if (error instanceof SQLiteConstraintException) {
+ DeckLog.warn("Account already added");
+ runOnUiThread(() -> setStatusText(getString(R.string.account_already_added, accountToCreate.getName())));
+ } else {
+ runOnUiThread(() -> {
+ setStatusText(error.getMessage());
+ ExceptionDialogFragment.newInstance(error, accountToCreate).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ });
}
- });
- }
-
- @Override
- public void onError(Throwable error) {
- IResponseCallback.super.onError(error);
- if (error instanceof SQLiteConstraintException) {
- DeckLog.error("Account has already been added, this should not be the case");
+ runOnUiThread(() -> binding.addButton.setEnabled(true));
+ restoreWifiPref();
}
- setStatusText(error.getMessage());
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(error, accountToCreate).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- restoreWifiPref();
- }
- });
- }
- });
- } catch (AccountImportCancelledException e) {
- runOnUiThread(() -> binding.addButton.setEnabled(true));
- restoreWifiPref();
- DeckLog.info("Account import has been canceled.");
- }
+ });
+ }
+ });
+ } catch (AccountImportCancelledException e) {
+ runOnUiThread(() -> binding.addButton.setEnabled(true));
+ restoreWifiPref();
+ DeckLog.info("Account import has been canceled.");
+ }
+ }, ContextCompat.getMainExecutor(this));
}
}
@@ -221,14 +260,9 @@ public class ImportAccountActivity extends AppCompatActivity {
AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
- @SuppressLint("ApplySharedPref")
- private void rollbackAccountCreation(@NonNull SyncManager syncManager, final long accountId) {
+ private void rollbackAccountCreation(final long accountId) {
DeckLog.log("Rolling back account creation for " + accountId);
- syncManager.deleteAccount(accountId);
- SharedPreferences.Editor editor = sharedPreferences.edit();
- DeckLog.log("--- Remove: shared_preference_last_account |", accountId);
- editor.remove(sharedPreferenceLastAccount);
- editor.commit(); // Has to be done synchronously
+ importAccountViewModel.deleteAccount(accountId);
runOnUiThread(() -> binding.addButton.setEnabled(true));
restoreWifiPref();
}
@@ -248,12 +282,11 @@ public class ImportAccountActivity extends AppCompatActivity {
}
@SuppressLint("ApplySharedPref")
- private void disableWifiPref() {
- DeckLog.info("--- Temporarily disable sync on wifi only setting");
- sharedPreferences
- .edit()
- .putBoolean(prefKeyWifiOnly, false)
- .commit();
+ private CompletableFuture<Void> disableWifiPref() {
+ return CompletableFuture.runAsync(() -> {
+ DeckLog.info("--- Temporarily disable sync on wifi only setting");
+ sharedPreferences.edit().putBoolean(prefKeyWifiOnly, false).commit();
+ });
}
private void restoreWifiPref() {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountViewModel.java
new file mode 100644
index 000000000..6734a18f9
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountViewModel.java
@@ -0,0 +1,33 @@
+package it.niedermann.nextcloud.deck.ui;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
+
+public class ImportAccountViewModel extends BaseViewModel {
+
+ public ImportAccountViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ public LiveData<Boolean> hasAccounts() {
+ return baseRepository.hasAccounts();
+ }
+
+ public void saveCurrentAccount(@NonNull Account account) {
+ this.baseRepository.saveCurrentAccount(account);
+ }
+
+ public void createAccount(@NonNull Account account, @NonNull IResponseCallback<Account> callback) {
+ this.baseRepository.createAccount(account, callback);
+ }
+
+ public void deleteAccount(long accountId) {
+ this.baseRepository.deleteAccount(accountId);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java
deleted file mode 100644
index 6c861102b..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java
+++ /dev/null
@@ -1,1194 +0,0 @@
-package it.niedermann.nextcloud.deck.ui;
-
-import static androidx.lifecycle.Transformations.switchMap;
-import static it.niedermann.nextcloud.deck.DeckApplication.NO_ACCOUNT_ID;
-import static it.niedermann.nextcloud.deck.DeckApplication.NO_BOARD_ID;
-import static it.niedermann.nextcloud.deck.DeckApplication.NO_STACK_ID;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentBoardId;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentStackId;
-import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccount;
-import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentBoardId;
-import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentStackId;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
-import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.clearBrandColors;
-import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.saveBrandColors;
-import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ABOUT;
-import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ADD_BOARD;
-import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ARCHIVED_BOARDS;
-import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_SETTINGS;
-import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_UPCOMING_CARDS;
-
-import android.animation.AnimatorInflater;
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.database.sqlite.SQLiteConstraintException;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkRequest;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.PopupMenu;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.AnyThread;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.appcompat.app.ActionBarDrawerToggle;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.core.splashscreen.SplashScreen;
-import androidx.core.view.GravityCompat;
-import androidx.core.view.ViewCompat;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Observer;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.preference.PreferenceManager;
-import androidx.viewpager2.widget.ViewPager2;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener;
-import com.google.android.material.snackbar.Snackbar;
-import com.google.android.material.tabs.TabLayoutMediator;
-import com.nextcloud.android.sso.AccountImporter;
-import com.nextcloud.android.sso.exceptions.AccountImportCancelledException;
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
-import com.nextcloud.android.sso.exceptions.UnknownErrorException;
-
-import java.net.HttpURLConnection;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Objects;
-
-import it.niedermann.android.crosstabdnd.CrossTabDragAndDrop;
-import it.niedermann.android.tablayouthelper.TabLayoutHelper;
-import it.niedermann.android.tablayouthelper.TabTitleGenerator;
-import it.niedermann.android.util.ColorUtil;
-import it.niedermann.nextcloud.deck.DeckApplication;
-import it.niedermann.nextcloud.deck.DeckLog;
-import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.api.IResponseCallback;
-import it.niedermann.nextcloud.deck.api.ResponseCallback;
-import it.niedermann.nextcloud.deck.databinding.ActivityMainBinding;
-import it.niedermann.nextcloud.deck.databinding.NavHeaderMainBinding;
-import it.niedermann.nextcloud.deck.exceptions.OfflineException;
-import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.model.Board;
-import it.niedermann.nextcloud.deck.model.Stack;
-import it.niedermann.nextcloud.deck.model.full.FullBoard;
-import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.model.full.FullStack;
-import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
-import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
-import it.niedermann.nextcloud.deck.model.ocs.Version;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.about.AboutActivity;
-import it.niedermann.nextcloud.deck.ui.accountswitcher.AccountSwitcherDialog;
-import it.niedermann.nextcloud.deck.ui.archivedboards.ArchivedBoardsActvitiy;
-import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener;
-import it.niedermann.nextcloud.deck.ui.board.DeleteBoardListener;
-import it.niedermann.nextcloud.deck.ui.board.EditBoardDialogFragment;
-import it.niedermann.nextcloud.deck.ui.board.EditBoardListener;
-import it.niedermann.nextcloud.deck.ui.card.CardAdapter;
-import it.niedermann.nextcloud.deck.ui.card.CreateCardListener;
-import it.niedermann.nextcloud.deck.ui.card.NewCardDialog;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
-import it.niedermann.nextcloud.deck.ui.filter.FilterDialogFragment;
-import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel;
-import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel;
-import it.niedermann.nextcloud.deck.ui.settings.SettingsActivity;
-import it.niedermann.nextcloud.deck.ui.stack.DeleteStackDialogFragment;
-import it.niedermann.nextcloud.deck.ui.stack.DeleteStackListener;
-import it.niedermann.nextcloud.deck.ui.stack.EditStackDialogFragment;
-import it.niedermann.nextcloud.deck.ui.stack.EditStackListener;
-import it.niedermann.nextcloud.deck.ui.stack.OnScrollListener;
-import it.niedermann.nextcloud.deck.ui.stack.StackAdapter;
-import it.niedermann.nextcloud.deck.ui.stack.StackFragment;
-import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
-import it.niedermann.nextcloud.deck.ui.theme.ThemedSnackbar;
-import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsActivity;
-import it.niedermann.nextcloud.deck.util.CustomAppGlideModule;
-import it.niedermann.nextcloud.deck.util.DrawerMenuUtil;
-
-public class MainActivity extends AppCompatActivity implements DeleteStackListener, EditStackListener, DeleteBoardListener, EditBoardListener, ArchiveBoardListener, OnScrollListener, CreateCardListener, OnNavigationItemSelectedListener {
-
- protected ActivityMainBinding binding;
- protected NavHeaderMainBinding headerBinding;
-
- protected MainViewModel mainViewModel;
- private FilterViewModel filterViewModel;
- private PickStackViewModel pickStackViewModel;
-
- @ColorInt
- private int colorAccent;
- protected SharedPreferences sharedPreferences;
- private StackAdapter stackAdapter;
- long lastBoardId;
- @NonNull
- private List<Board> boardsList = new ArrayList<>();
- private LiveData<List<Board>> boardsLiveData;
- private Observer<List<Board>> boardsLiveDataObserver;
- private Menu listMenu;
-
- private LiveData<List<Stack>> stacksLiveData;
-
- private LiveData<Boolean> hasArchivedBoardsLiveData;
- private Observer<Boolean> hasArchivedBoardsLiveDataObserver;
-
- private boolean currentBoardHasStacks = false;
- private int currentBoardStacksCount = 0;
-
- private boolean firstAccountAdded = false;
- private ConnectivityManager.NetworkCallback networkCallback;
-
- private String addList;
- private String addBoard;
- @Nullable
- private TabLayoutMediator mediator;
- @Nullable
- private TabLayoutHelper tabLayoutHelper;
- private boolean stackMoved;
-
- private final ActivityResultLauncher<Intent> settingsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
- if (result.getResultCode() == Activity.RESULT_OK) {
- ActivityCompat.recreate(this);
- }
- });
-
- private final ActivityResultLauncher<Intent> importAccountLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
- if (result.getResultCode() == RESULT_OK) {
- firstAccountAdded = true;
- } else {
- finish();
- }
- });
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- SplashScreen.installSplashScreen(this);
-
- super.onCreate(savedInstanceState);
-
- Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this));
-
- setTheme(R.style.AppTheme);
- colorAccent = ContextCompat.getColor(this, R.color.accent);
-
- binding = ActivityMainBinding.inflate(getLayoutInflater());
- headerBinding = NavHeaderMainBinding.bind(binding.navigationView.getHeaderView(0));
- setContentView(binding.getRoot());
-
- mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
- filterViewModel = new ViewModelProvider(this).get(FilterViewModel.class);
- pickStackViewModel = new ViewModelProvider(this).get(PickStackViewModel.class);
-
- addList = getString(R.string.add_list);
- addBoard = getString(R.string.add_board);
-
- setSupportActionBar(binding.toolbar);
-
- final var toggle = new ActionBarDrawerToggle(this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
- binding.drawerLayout.addDrawerListener(toggle);
- toggle.syncState();
-
- binding.navigationView.setNavigationItemSelectedListener(this);
- sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
-
- DeckApplication.readCurrentAccountColor().observe(this, this::applyAccountTheme);
- DeckApplication.readCurrentBoardColor().observe(this, this::applyBoardTheme);
-
- binding.filterText.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- filterViewModel.setFilterText(s.toString());
- }
-
- @Override
- public void afterTextChanged(Editable s) {
-
- }
- });
-
- mainViewModel.isDebugModeEnabled().observe(this, (enabled) -> headerBinding.copyDebugLogs.setVisibility(enabled ? View.VISIBLE : View.GONE));
- headerBinding.copyDebugLogs.setOnClickListener((v) -> {
- try {
- DeckLog.shareLogAsFile(this);
- } catch (Exception e) {
- ExceptionDialogFragment.newInstance(e, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- });
- switchMap(mainViewModel.hasAccounts(), hasAccounts -> {
- if (hasAccounts) {
- return mainViewModel.readAccounts();
- } else {
- importAccountLauncher.launch(ImportAccountActivity.createIntent(this));
- return null;
- }
- }).observe(this, accounts -> {
- if (accounts == null || accounts.size() == 0) {
- // Last account has been deleted. hasAccounts LiveData will handle this, but we make sure, that branding is reset.
- saveBrandColors(this, ContextCompat.getColor(this, R.color.defaultBrand));
- return;
- }
-
- final var lastAccountId = readCurrentAccountId(this);
-
- for (var account : accounts) {
- if (lastAccountId == account.getId() || lastAccountId == NO_ACCOUNT_ID) {
- mainViewModel.setCurrentAccount(account);
- if (!firstAccountAdded) {
- DeckLog.info("Syncing the current account on app start");
- registerAutoSyncOnNetworkAvailable();
- firstAccountAdded = false;
- }
- break;
- }
- }
-
- mainViewModel.getCurrentAccountLiveData().removeObservers(this);
- mainViewModel.getCurrentAccountLiveData().observe(this, (currentAccount) -> {
- saveCurrentAccount(this, mainViewModel.getCurrentAccount());
- mainViewModel.recreateSyncManager();
-
- if (mainViewModel.getCurrentAccount().isMaintenanceEnabled()) {
- refreshCapabilities(mainViewModel.getCurrentAccount(), null);
- }
-
- lastBoardId = readCurrentBoardId(this, mainViewModel.getCurrentAccount().getId());
-
- if (boardsLiveData != null && boardsLiveDataObserver != null) {
- boardsLiveData.removeObserver(boardsLiveDataObserver);
- }
-
- boardsLiveData = mainViewModel.getBoards(currentAccount.getId(), false);
- boardsLiveDataObserver = (boards) -> {
- if (boards == null) {
- throw new IllegalStateException("List<Board> boards must not be null.");
- }
-
- boardsList = boards;
- Board currentBoard = null;
-
- if (boardsList.size() > 0) {
- boolean currentBoardIdWasInList = false;
- for (int i = 0; i < boardsList.size(); i++) {
- if (lastBoardId == boardsList.get(i).getLocalId() || lastBoardId == NO_BOARD_ID) {
- currentBoard = boardsList.get(i);
- setCurrentBoard(currentBoard);
- currentBoardIdWasInList = true;
- break;
- }
- }
- if (!currentBoardIdWasInList) {
- currentBoard = boardsList.get(0);
- setCurrentBoard(currentBoard);
- }
-
- binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
- } else {
- clearBrandColors(this);
- clearCurrentBoard();
-
- binding.filter.setOnClickListener(null);
- }
-
- final var finalCurrentBoard = currentBoard;
- if (hasArchivedBoardsLiveData != null && hasArchivedBoardsLiveDataObserver != null) {
- hasArchivedBoardsLiveData.removeObserver(hasArchivedBoardsLiveDataObserver);
- }
- hasArchivedBoardsLiveData = mainViewModel.hasArchivedBoards(currentAccount.getId());
- hasArchivedBoardsLiveDataObserver = (hasArchivedBoards) -> {
- mainViewModel.setCurrentAccountHasArchivedBoards(Boolean.TRUE.equals(hasArchivedBoards));
- inflateBoardMenu(finalCurrentBoard);
- };
- hasArchivedBoardsLiveData.observe(this, hasArchivedBoardsLiveDataObserver);
- };
- boardsLiveData.observe(this, boardsLiveDataObserver);
-
- Glide
- .with(binding.accountSwitcher.getContext())
- .load(currentAccount.getAvatarUrl(binding.accountSwitcher.getWidth()))
- .placeholder(R.drawable.ic_baseline_account_circle_24)
- .error(R.drawable.ic_baseline_account_circle_24)
- .apply(RequestOptions.circleCropTransform())
- .into(binding.accountSwitcher);
-
- DeckLog.verbose("Displaying maintenance mode info for", mainViewModel.getCurrentAccount().getName() + ":", mainViewModel.getCurrentAccount().isMaintenanceEnabled());
- binding.infoBox.setVisibility(mainViewModel.getCurrentAccount().isMaintenanceEnabled() ? View.VISIBLE : View.GONE);
- if (mainViewModel.isCurrentAccountIsSupportedVersion()) {
- binding.infoBoxVersionNotSupported.setVisibility(View.GONE);
- } else {
- binding.infoBoxVersionNotSupported.setText(getString(R.string.info_box_version_not_supported, mainViewModel.getCurrentAccount().getServerDeckVersion(), Version.minimumSupported().getOriginalVersion()));
- binding.infoBoxVersionNotSupported.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(mainViewModel.getCurrentAccount().getUrl() + getString(R.string.url_fragment_update_deck)))));
- binding.infoBoxVersionNotSupported.setVisibility(View.VISIBLE);
- }
- });
-
- stackAdapter = new StackAdapter(this);
- binding.viewPager.setAdapter(stackAdapter);
- binding.viewPager.setOffscreenPageLimit(2);
-
- final var dragAndDrop = new CrossTabDragAndDrop<StackFragment, CardAdapter, FullCard>(getResources(), ViewCompat.getLayoutDirection(binding.getRoot()) == ViewCompat.LAYOUT_DIRECTION_LTR);
- dragAndDrop.register(binding.viewPager, binding.stackTitles, getSupportFragmentManager());
- dragAndDrop.addItemMovedByDragListener((movedCard, stackId, position) -> {
- mainViewModel.reorder(mainViewModel.getCurrentAccount().getId(), movedCard, stackId, position);
- DeckLog.info("Card", movedCard.getCard().getTitle(), "was moved to Stack", stackId, "on position", position);
- });
-
- final var listMenuPopup = new PopupMenu(this, binding.listMenuButton);
- listMenu = listMenuPopup.getMenu();
- getMenuInflater().inflate(R.menu.list_menu, listMenu);
- listMenuPopup.setOnMenuItemClickListener(this::onOptionsItemSelected);
- binding.listMenuButton.setOnClickListener((v) -> listMenuPopup.show());
-
- binding.fab.setOnClickListener((v) -> {
- // TODO We should hide the FAB while the dialog is open - but how to detect the dialog has been closed?
- binding.fab.hide();
- if (this.boardsList.size() > 0) {
- try {
- NewCardDialog.newInstance(
- mainViewModel.getCurrentAccount(),
- mainViewModel.getCurrentBoardLocalId(),
- stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(),
- mainViewModel.getCurrentBoardColor()
- ).show(getSupportFragmentManager(), NewCardDialog.class.getSimpleName());
- } catch (IndexOutOfBoundsException e) {
- EditStackDialogFragment.newInstance().show(getSupportFragmentManager(), addList);
- }
- } else {
- EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard);
- }
- });
-
- binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
- @Override
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { /* Silence is gold */ }
-
- @Override
- public void onPageSelected(int position) {
- final int currentViewPagerItem = binding.viewPager.getCurrentItem();
- listMenu.findItem(R.id.move_list_left).setVisible(currentBoardHasStacks && currentViewPagerItem > 0);
- listMenu.findItem(R.id.move_list_right).setVisible(currentBoardHasStacks && currentViewPagerItem < currentBoardStacksCount - 1);
- binding.viewPager.post(() -> {
- // stackAdapter size might differ from position when an account has been deleted
- if (stackAdapter.getItemCount() > position) {
- saveCurrentStackId(getApplicationContext(), mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), stackAdapter.getItem(position).getLocalId());
- } else {
- DeckLog.logError(new IllegalStateException("Tried to save current Stack which cannot be available (stackAdapter doesn't have this position)"));
- }
- });
-
- binding.fab.extend();
- }
-
- @Override
- public void onPageScrollStateChanged(int state) {
- if (!binding.swipeRefreshLayout.isRefreshing()) {
- binding.swipeRefreshLayout.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE);
- }
- }
- });
- filterViewModel.hasActiveFilter().observe(this, (hasActiveFilter) -> binding.filterIndicator.setVisibility(hasActiveFilter ? View.VISIBLE : View.GONE));
-// binding.archivedCards.setOnClickListener((v) -> startActivity(ArchivedCardsActivity.createIntent(this, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), mainViewModel.currentBoardHasEditPermission())));
- binding.enableSearch.setOnClickListener((v) -> showFilterTextToolbar());
- binding.toolbar.setOnClickListener((v) -> showFilterTextToolbar());
-
-
- binding.swipeRefreshLayout.setOnRefreshListener(() -> {
- DeckLog.info("Triggered manual refresh");
-
- CustomAppGlideModule.clearCache(this);
-
- DeckLog.verbose("Trigger refresh capabilities for", mainViewModel.getCurrentAccount().getName());
- refreshCapabilities(mainViewModel.getCurrentAccount(), () -> {
- DeckLog.verbose("Trigger synchronization for", mainViewModel.getCurrentAccount().getName());
- mainViewModel.synchronize(new ResponseCallback<>(mainViewModel.getCurrentAccount()) {
- @Override
- public void onResponse(Boolean response) {
- DeckLog.info("End of synchronization for " + mainViewModel.getCurrentAccount().getName() + " → Stop spinner.");
- runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false));
- }
-
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- DeckLog.info("End of synchronization for " + mainViewModel.getCurrentAccount().getName() + " → Stop spinner.");
- showSyncFailedSnackbar(throwable);
- runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false));
- }
- });
- });
- });
- });
- binding.accountSwitcher.setOnClickListener((v) -> AccountSwitcherDialog.newInstance().show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName()));
- }
-
- private void applyBoardTheme(@ColorInt int color) {
- final var utils = ThemeUtils.of(color, this);
- final var scheme = ThemeUtils.createScheme(color, this);
-
- utils.deck.themeTabLayout(binding.stackTitles);
- utils.material.themeExtendedFAB(binding.fab);
- utils.androidx.themeSwipeRefreshLayout(binding.swipeRefreshLayout);
- utils.platform.colorEditText(binding.filterText);
-
- DrawableCompat.setTint(binding.filterIndicator.getDrawable(), scheme.getOnPrimaryContainer());
- }
-
- private void applyAccountTheme(@ColorInt int accountColor) {
- final var utils = ThemeUtils.of(accountColor, this);
-
- utils.platform.colorNavigationView(binding.navigationView, false);
-
- headerBinding.headerView.setBackgroundColor(accountColor);
- @ColorInt final int headerTextColor = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(accountColor);
- headerBinding.appName.setTextColor(headerTextColor);
- DrawableCompat.setTint(headerBinding.logo.getDrawable(), headerTextColor);
- DrawableCompat.setTint(headerBinding.copyDebugLogs.getDrawable(), headerTextColor);
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- this.binding = null;
- this.headerBinding = null;
- if (tabLayoutHelper != null) {
- tabLayoutHelper.release();
- }
- }
-
- @Override
- public void onCreateStack(String stackName) {
- DeckLog.info("Create Stack in account", mainViewModel.getCurrentAccount().getName(), "on board", mainViewModel.getCurrentBoardLocalId());
- mainViewModel.createStack(mainViewModel.getCurrentAccount().getId(), stackName, mainViewModel.getCurrentBoardLocalId(), new IResponseCallback<>() {
- @Override
- public void onResponse(FullStack response) {
- DeckApplication.saveCurrentStackId(MainActivity.this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), response.getLocalId());
- binding.viewPager.post(() -> {
- try {
- binding.viewPager.setCurrentItem(stackAdapter.getPosition(response.getLocalId()));
- } catch (NoSuchElementException e) {
- DeckLog.logError(e);
- }
- });
- }
-
- @Override
- public void onError(Throwable error) {
- IResponseCallback.super.onError(error);
- runOnUiThread(() -> ThemedSnackbar.make(binding.coordinatorLayout, Objects.requireNonNull(error.getLocalizedMessage()), Snackbar.LENGTH_LONG)
- .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(error, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
- .setAnchorView(binding.fab)
- .show());
- }
- });
- }
-
- @Override
- public void onUpdateStack(long localStackId, String stackName) {
- mainViewModel.updateStackTitle(localStackId, stackName, new IResponseCallback<>() {
- @Override
- public void onResponse(FullStack response) {
- DeckLog.info("Successfully updated", Stack.class.getSimpleName(), "to", stackName);
- }
-
- @Override
- public void onError(Throwable throwable) {
- IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- }
- });
- }
-
- @Override
- public void onCreateBoard(String title, @ColorInt int color) {
- if (boardsLiveData == null || boardsLiveDataObserver == null) {
- throw new IllegalStateException("Cannot create board when no one observe boards yet. boardsLiveData or observer is null.");
- }
- boardsLiveData.removeObserver(boardsLiveDataObserver);
- final var boardToCreate = new Board(title, color);
- boardToCreate.setPermissionEdit(true);
- boardToCreate.setPermissionManage(true);
-
- mainViewModel.createBoard(mainViewModel.getCurrentAccount().getId(), boardToCreate, new IResponseCallback<>() {
- @Override
- public void onResponse(FullBoard response) {
- runOnUiThread(() -> {
- if (response != null) {
- boardsList.add(response.getBoard());
- setCurrentBoard(response.getBoard());
- inflateBoardMenu(response.getBoard());
- EditStackDialogFragment.newInstance().show(getSupportFragmentManager(), addList);
- }
- boardsLiveData.observe(MainActivity.this, boardsLiveDataObserver);
- });
- }
-
- @Override
- public void onError(Throwable throwable) {
- IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ThemedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG)
- .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
- .setAnchorView(binding.fab)
- .show());
- }
- });
- }
-
- @Override
- public void onUpdateBoard(FullBoard fullBoard) {
- mainViewModel.updateBoard(fullBoard, new IResponseCallback<>() {
- @Override
- public void onResponse(FullBoard response) {
- DeckLog.info("Successfully updated board", fullBoard.getBoard().getTitle());
- }
-
- @Override
- public void onError(Throwable throwable) {
- IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- }
- });
- }
-
- private void refreshCapabilities(final Account account, @Nullable Runnable runAfter) {
- DeckLog.verbose("Refreshing capabilities for", account.getName());
- mainViewModel.refreshCapabilities(new ResponseCallback<>(account) {
- @Override
- public void onResponse(Capabilities response) {
- DeckLog.verbose("Finished refreshing capabilities for", account.getName(), "successfully.");
- if (response.isMaintenanceEnabled()) {
- DeckLog.verbose("Maintenance mode is enabled.");
- } else {
- DeckLog.verbose("Maintenance mode is disabled.");
- // If we notice after updating the capabilities, that the new version is not supported, but it was previously, recreate the activity to make sure all elements are disabled properly
- if (mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().isSupported() && !response.getDeckVersion().isSupported()) {
- ActivityCompat.recreate(MainActivity.this);
- }
- }
-
- if (runAfter != null) {
- runAfter.run();
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- DeckLog.warn("Error on refreshing capabilities for", account.getName(), "(" + throwable.getMessage() + ").");
- if (throwable.getClass() == OfflineException.class || throwable instanceof OfflineException) {
- DeckLog.info("Cannot refresh capabilities because device is offline.");
- } else {
- super.onError(throwable);
- ExceptionDialogFragment.newInstance(throwable, account).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
-
- if (runAfter != null) {
- runAfter.run();
- }
- }
- });
- }
-
- protected void clearCurrentBoard() {
- binding.toolbar.setTitle(R.string.app_name_short);
- binding.filterText.setHint(R.string.app_name_short);
- binding.swipeRefreshLayout.setVisibility(View.GONE);
- binding.listMenuButton.setVisibility(View.GONE);
- binding.emptyContentViewStacks.setVisibility(View.GONE);
- binding.emptyContentViewBoards.setVisibility(View.VISIBLE);
- }
-
- protected void setCurrentBoard(@NonNull Board board) {
- if (stacksLiveData != null) {
- stacksLiveData.removeObservers(this);
- }
- saveBrandColors(this, board.getColor());
- mainViewModel.setCurrentBoard(board);
- filterViewModel.clearFilterInformation(true);
-
- lastBoardId = board.getLocalId();
- saveCurrentBoardId(this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId());
- binding.navigationView.setCheckedItem(boardsList.indexOf(board));
-
- binding.toolbar.setTitle(board.getTitle());
- binding.filterText.setHint(getString(R.string.search_in, board.getTitle()));
-
- showEditButtonsIfPermissionsGranted();
-
- binding.emptyContentViewBoards.setVisibility(View.GONE);
- binding.swipeRefreshLayout.setVisibility(View.VISIBLE);
-
- stacksLiveData = mainViewModel.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), board.getLocalId());
- stacksLiveData.observe(this, (List<Stack> stacks) -> {
- if (stacks == null) {
- throw new IllegalStateException("Given List<FullStack> must not be null");
- }
- currentBoardStacksCount = stacks.size();
-
- if (currentBoardStacksCount == 0) {
- binding.emptyContentViewStacks.setVisibility(View.VISIBLE);
- currentBoardHasStacks = false;
- } else {
- binding.emptyContentViewStacks.setVisibility(View.GONE);
- currentBoardHasStacks = true;
- }
- listMenu.findItem(R.id.archive_cards).setVisible(currentBoardHasStacks);
-
- int stackPositionInAdapter = 0;
- stackAdapter.setStacks(stacks);
-
- final var currentStackId = readCurrentStackId(this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId());
- for (int i = 0; i < currentBoardStacksCount; i++) {
- if (stacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) {
- stackPositionInAdapter = i;
- break;
- }
- }
- final int stackPositionInAdapterClone = stackPositionInAdapter;
- final TabTitleGenerator tabTitleGenerator = position -> {
- if (stacks.size() > position) {
- return stacks.get(position).getTitle();
- } else {
- DeckLog.warn("Could not generate tab title for position " + position + " because list size is only " + currentBoardStacksCount);
- return "ERROR";
- }
- };
- final var newMediator = new TabLayoutMediator(binding.stackTitles, binding.viewPager, (tab, position) -> tab.setText(tabTitleGenerator.getTitle(position)));
- runOnUiThread(() -> {
- setStackMediator(newMediator);
- binding.viewPager.setCurrentItem(stackPositionInAdapterClone, false);
- if (stackMoved) { // Required to make sure that the correct tab will be selected after moving stacks
- binding.viewPager.post(() -> binding.viewPager.setCurrentItem(stackPositionInAdapterClone, false));
- stackMoved = false;
- }
- updateTabLayoutHelper(tabTitleGenerator);
- });
-
- listMenu.findItem(R.id.rename_list).setVisible(currentBoardHasStacks);
- listMenu.findItem(R.id.delete_list).setVisible(currentBoardHasStacks);
- });
- }
-
- @UiThread
- private void updateTabLayoutHelper(@NonNull TabTitleGenerator tabTitleGenerator) {
- if (this.tabLayoutHelper == null) {
- this.tabLayoutHelper = new TabLayoutHelper(binding.stackTitles, binding.viewPager, tabTitleGenerator);
- } else {
- tabLayoutHelper.setTabTitleGenerator(tabTitleGenerator);
- }
- }
-
- @UiThread
- private void setStackMediator(@NonNull final TabLayoutMediator newMediator) {
- if (mediator != null) {
- mediator.detach();
- }
- newMediator.attach();
- this.mediator = newMediator;
- }
-
- @UiThread
- protected void inflateBoardMenu(@Nullable Board currentBoard) {
- binding.navigationView.setItemIconTintList(null);
- final var menu = binding.navigationView.getMenu();
- menu.clear();
- DrawerMenuUtil.inflateBoards(this, menu, this.boardsList, mainViewModel.currentAccountHasArchivedBoards(), mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().isSupported());
- binding.navigationView.setCheckedItem(boardsList.indexOf(currentBoard));
- }
-
- @Override
- public boolean onNavigationItemSelected(@NonNull MenuItem item) {
- switch (item.getItemId()) {
- case MENU_ID_ABOUT:
- startActivity(AboutActivity.createIntent(this, mainViewModel.getCurrentAccount()));
- break;
- case MENU_ID_SETTINGS:
- settingsLauncher.launch(SettingsActivity.createIntent(this));
- break;
- case MENU_ID_ADD_BOARD:
- EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard);
- break;
- case MENU_ID_ARCHIVED_BOARDS:
- startActivity(ArchivedBoardsActvitiy.createIntent(MainActivity.this, mainViewModel.getCurrentAccount()));
- break;
- case MENU_ID_UPCOMING_CARDS:
- startActivity(UpcomingCardsActivity.createIntent(MainActivity.this));
- break;
- default:
- setCurrentBoard(boardsList.get(item.getItemId()));
- break;
- }
- binding.drawerLayout.closeDrawer(GravityCompat.START);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- final int itemId = item.getItemId();
- if (itemId == R.id.archive_cards) {
- final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
- final var stackLocalId = stack.getLocalId();
- mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, (numberOfCards) -> runOnUiThread(() ->
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.archive_cards)
- .setMessage(getString(FilterInformation.hasActiveFilter(filterViewModel.getFilterInformation().getValue())
- ? R.string.do_you_want_to_archive_all_cards_of_the_filtered_list
- : R.string.do_you_want_to_archive_all_cards_of_the_list, stack.getTitle()))
- .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> {
- final var filterInformation = filterViewModel.getFilterInformation().getValue();
- mainViewModel.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, filterInformation == null ? new FilterInformation() : filterInformation, new IResponseCallback<>() {
- @Override
- public void onResponse(Void response) {
- DeckLog.info("Successfully archived all cards in stack local id", stackLocalId);
- }
-
- @Override
- public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
- IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- }
- }
- });
- })
- .setNeutralButton(android.R.string.cancel, null)
- .create()
- .show()
- ));
- return true;
- } else if (itemId == R.id.add_list) {
- EditStackDialogFragment.newInstance().show(getSupportFragmentManager(), addList);
- return true;
- } else if (itemId == R.id.rename_list) {
- final var stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- observeOnce(mainViewModel.getStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, fullStack ->
- EditStackDialogFragment.newInstance(fullStack.getLocalId(), fullStack.getStack().getTitle())
- .show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
- return true;
- } else if (itemId == R.id.move_list_left || itemId == R.id.move_list_right) {
- final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- // TODO error handling
- mainViewModel.reorderStack(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), stackId, itemId == R.id.move_list_right);
- stackMoved = true;
- return true;
- } else if (itemId == R.id.delete_list) {
- final long stackLocalId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, (numberOfCards) -> runOnUiThread(() -> {
- if (numberOfCards != null && numberOfCards > 0) {
- DeleteStackDialogFragment.newInstance(stackLocalId, numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName());
- } else {
- onStackDeleted(stackLocalId);
- }
- }));
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- protected void showEditButtonsIfPermissionsGranted() {
- if (mainViewModel.currentBoardHasEditPermission()) {
- binding.fab.show();
- binding.listMenuButton.setVisibility(View.VISIBLE);
- binding.emptyContentViewStacks.showDescription();
- } else {
- binding.fab.hide();
- binding.listMenuButton.setVisibility(View.GONE);
- binding.emptyContentViewStacks.hideDescription();
- }
- }
-
- @Override
- public void onScrollUp() {
- binding.fab.extend();
- }
-
- @Override
- public void onScrollDown() {
- binding.fab.shrink();
- }
-
- @Override
- public void onBottomReached() {
- binding.fab.extend();
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- try {
- AccountImporter.onActivityResult(requestCode, resultCode, data, this, (account) -> {
- final var accountToCreate = new Account(account.name, account.userId, account.url);
- mainViewModel.createAccount(accountToCreate, new IResponseCallback<>() {
- @Override
- public void onResponse(Account createdAccount) {
- final var importSyncManager = new SyncManager(MainActivity.this, account.name);
- importSyncManager.refreshCapabilities(new ResponseCallback<>(createdAccount) {
- @SuppressLint("StringFormatInvalid")
- @Override
- public void onResponse(Capabilities response) {
- if (!response.isMaintenanceEnabled()) {
- if (response.getDeckVersion().isSupported()) {
- runOnUiThread(() -> {
- final var importSnackbar = ThemedSnackbar.make(binding.coordinatorLayout, R.string.account_is_getting_imported, Snackbar.LENGTH_INDEFINITE)
- .setAnchorView(binding.fab);
- importSnackbar.show();
- importSyncManager.synchronize(new ResponseCallback<>(createdAccount) {
- @Override
- public void onResponse(Boolean syncSuccess) {
- importSnackbar.dismiss();
- runOnUiThread(() -> ThemedSnackbar.make(binding.coordinatorLayout, getString(R.string.account_imported), Snackbar.LENGTH_LONG)
- .setAnchorView(binding.fab)
- .setAction(R.string.simple_switch, (a) -> {
- createdAccount.setColor(response.getColor());
- mainViewModel.setSyncManager(importSyncManager);
- mainViewModel.setCurrentAccount(createdAccount);
- })
- .show());
- }
-
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- runOnUiThread(() -> {
- importSnackbar.dismiss();
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- });
- }
- });
- });
- } else {
- DeckLog.warn("Cannot import account because server version is too low (" + response.getDeckVersion() + "). Minimum server version is currently", Version.minimumSupported());
- runOnUiThread(() -> new MaterialAlertDialogBuilder(MainActivity.this)
- .setTitle(R.string.update_deck)
- .setMessage(getString(R.string.deck_outdated_please_update, response.getDeckVersion().getOriginalVersion()))
- .setNegativeButton(R.string.simple_discard, null)
- .setPositiveButton(R.string.simple_update, (dialog, whichButton) -> {
- final var openURL = new Intent(Intent.ACTION_VIEW);
- openURL.setData(Uri.parse(createdAccount.getUrl() + getString(R.string.url_fragment_update_deck)));
- startActivity(openURL);
- finish();
- }).show());
- mainViewModel.deleteAccount(createdAccount.getId());
- }
- } else {
- DeckLog.warn("Cannot import account because server version is currently in maintenance mode.");
- runOnUiThread(() -> new MaterialAlertDialogBuilder(MainActivity.this)
- .setTitle(R.string.maintenance_mode)
- .setMessage(getString(R.string.maintenance_mode_explanation, createdAccount.getUrl()))
- .setPositiveButton(R.string.simple_close, null)
- .show());
- mainViewModel.deleteAccount(createdAccount.getId());
- }
- }
-
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- mainViewModel.deleteAccount(createdAccount.getId());
- if (throwable instanceof OfflineException) {
- DeckLog.warn("Cannot import account because device is currently offline.");
- runOnUiThread(() -> new MaterialAlertDialogBuilder(MainActivity.this)
- .setTitle(R.string.you_are_currently_offline)
- .setMessage(R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account)
- .setPositiveButton(R.string.simple_close, null)
- .show());
- } else {
- ExceptionDialogFragment.newInstance(throwable, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- }
- });
- }
-
- @Override
- public void onError(Throwable error) {
- IResponseCallback.super.onError(error);
- if (error instanceof SQLiteConstraintException) {
- DeckLog.warn("Account already added");
- ThemedSnackbar.make(binding.coordinatorLayout, R.string.account_already_added, Snackbar.LENGTH_LONG)
- .setAnchorView(binding.fab)
- .show();
- } else {
- ExceptionDialogFragment.newInstance(error, accountToCreate).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- }
- });
- });
- } catch (AccountImportCancelledException e) {
- DeckLog.info("Account import has been canceled.");
- }
- }
-
- @Override
- public void onBackPressed() {
- if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
- binding.drawerLayout.closeDrawer(GravityCompat.START);
- } else if (binding.searchToolbar.getVisibility() == View.VISIBLE) {
- hideFilterTextToolbar();
- } else {
- super.onBackPressed();
- }
- }
-
- private void showFilterTextToolbar() {
- binding.toolbar.setVisibility(View.GONE);
- binding.searchToolbar.setVisibility(View.VISIBLE);
- binding.searchToolbar.setNavigationOnClickListener(v1 -> onBackPressed());
- binding.enableSearch.setVisibility(View.GONE);
- binding.filterText.requestFocus();
- final var imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(binding.filterText, InputMethodManager.SHOW_IMPLICIT);
- binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_on));
- }
-
- private void hideFilterTextToolbar() {
- binding.filterText.setText(null);
- final var imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
- imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
- binding.searchToolbar.setVisibility(View.GONE);
- binding.enableSearch.setVisibility(View.VISIBLE);
- binding.toolbar.setVisibility(View.VISIBLE);
- binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_off));
- }
-
- private void registerAutoSyncOnNetworkAvailable() {
- final var connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
- final var builder = new NetworkRequest.Builder();
-
- if (connectivityManager != null) {
- if (networkCallback == null) {
- networkCallback = new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(@NonNull Network network) {
- DeckLog.log("Got Network connection");
- mainViewModel.synchronize(new ResponseCallback<>(mainViewModel.getCurrentAccount()) {
- @Override
- public void onResponse(Boolean response) {
- DeckLog.log("Auto-Sync after connection available successful");
- }
-
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- if (throwable.getClass() == OfflineException.class || throwable instanceof OfflineException) {
- DeckLog.error("Do not show sync failed snackbar because it is an ", OfflineException.class.getSimpleName(), "- assuming the user has wi-fi disabled but \"Sync only on wi-fi\" enabled");
- } else if (throwable.getClass() == UnknownErrorException.class || throwable instanceof UnknownErrorException) {
- DeckLog.error("Do not show sync failed snackbar because it is an ", UnknownErrorException.class.getSimpleName(), "- assuming a not reachable server or infrastructure issues");
- } else {
- showSyncFailedSnackbar(throwable);
- }
- }
- });
- }
-
- @Override
- public void onLost(@NonNull Network network) {
- DeckLog.log("Network lost");
- }
- };
- }
- try {
- connectivityManager.unregisterNetworkCallback(networkCallback);
- } catch (IllegalArgumentException ignored) {
- }
- connectivityManager.registerNetworkCallback(builder.build(), networkCallback);
- }
- }
-
- /**
- * Find a StackFragment by it's ID, may return null.
- *
- * @param stackId ID of the stack to find
- * @return Instance of StackFragment
- */
- @Nullable
- public StackFragment findStackFragmentById(long stackId) {
- return (StackFragment) getSupportFragmentManager().findFragmentByTag("f" + stackId);
- }
-
- /**
- * This method is called when a new Card is created
- *
- * @param createdCard The new Card's data
- */
- @Override
- public void onCardCreated(FullCard createdCard) {
- final var card = createdCard.getCard();
- DeckLog.log("Card Created! Title:" + card.getTitle() + " in stack ID: " + card.getStackId());
-
- // Scroll the given StackFragment to the bottom, so the new Card is in view.
- final var fragment = findStackFragmentById(card.getStackId());
- if (fragment != null) {
- fragment.scrollToBottom();
- }
- }
-
- @Override
- public void onDismiss(DialogInterface dialog) {
- this.binding.fab.show();
- }
-
- @Override
- public void onStackDeleted(long stackLocalId) {
- int nextStackPosition;
- try {
- nextStackPosition = stackAdapter.getNeighbourPosition(binding.viewPager.getCurrentItem());
- } catch (NoSuchElementException | IndexOutOfBoundsException e) {
- nextStackPosition = 0;
- DeckLog.logError(e);
- }
- binding.viewPager.setCurrentItem(nextStackPosition);
- mainViewModel.deleteStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, mainViewModel.getCurrentBoardLocalId(), new IResponseCallback<>() {
- @Override
- public void onResponse(Void response) {
- DeckLog.info("Successfully deleted stack with local id", stackLocalId, "and remote id", stackLocalId);
- }
-
- @Override
- public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
- IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- }
- }
- });
- }
-
- @Override
- public void onBoardDeleted(Board board) {
- final int index = this.boardsList.indexOf(board);
- if (board.getLocalId().equals(mainViewModel.getCurrentBoardLocalId())) {
- if (index > 0) { // Select first board after deletion
- setCurrentBoard(this.boardsList.get(0));
- } else if (this.boardsList.size() > 1) { // Select second board after deletion
- setCurrentBoard(this.boardsList.get(1));
- } else { // No other board is available, open create dialog
- clearBrandColors(this);
- clearCurrentBoard();
- EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard);
- }
- }
-
- mainViewModel.deleteBoard(board, new IResponseCallback<>() {
- @Override
- public void onResponse(Void response) {
- DeckLog.info("Successfully deleted board", board.getTitle());
- }
-
- @Override
- public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
- IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- }
- }
- });
-
- binding.drawerLayout.closeDrawer(GravityCompat.START);
- }
-
-
- /**
- * Displays a {@link ThemedSnackbar} for an exception of a failed sync, but only if the cause wasn't maintenance mode (this should be handled by a TextView instead of a snackbar).
- *
- * @param throwable the cause of the failed sync
- */
- @AnyThread
- private void showSyncFailedSnackbar(@NonNull Throwable throwable) {
- if (!(throwable instanceof NextcloudHttpRequestFailedException) || ((NextcloudHttpRequestFailedException) throwable).getStatusCode() != HttpURLConnection.HTTP_UNAVAILABLE) {
- runOnUiThread(() -> {
- if (binding != null) { // Can be null in case the activity has been destroyed before the synchronization process has been finished
- ThemedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG)
- .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
- .setAnchorView(binding.fab)
- .show();
- }
- });
- }
- }
-
- @Override
- public void onArchive(@NonNull Board board) {
- mainViewModel.archiveBoard(board, new IResponseCallback<>() {
- @Override
- public void onResponse(FullBoard response) {
- DeckLog.info("Successfully archived board", board.getTitle());
- }
-
- @Override
- public void onError(Throwable throwable) {
- IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
- }
- });
- }
-
- @Override
- public void onClone(Board board) {
- final String[] animals = {getString(R.string.clone_cards)};
- final boolean[] checkedItems = {false};
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.clone_board)
- .setMultiChoiceItems(animals, checkedItems, (dialog, which, isChecked) -> checkedItems[0] = isChecked)
- .setPositiveButton(R.string.simple_clone, (dialog, which) -> {
- binding.drawerLayout.closeDrawer(GravityCompat.START);
- final var snackbar = ThemedSnackbar.make(binding.coordinatorLayout, getString(R.string.cloning_board, board.getTitle()), Snackbar.LENGTH_INDEFINITE)
- .setAnchorView(binding.fab);
- snackbar.show();
- mainViewModel.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor(), checkedItems[0], new IResponseCallback<>() {
- @Override
- public void onResponse(FullBoard response) {
- runOnUiThread(() -> {
- snackbar.dismiss();
- setCurrentBoard(response.getBoard());
- ThemedSnackbar.make(binding.coordinatorLayout, getString(R.string.successfully_cloned_board, response.getBoard().getTitle()), Snackbar.LENGTH_LONG)
- .setAction(R.string.edit, v -> EditBoardDialogFragment.newInstance(response.getLocalId()).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName()))
- .setAnchorView(binding.fab)
- .show();
- });
- }
-
- @Override
- public void onError(Throwable throwable) {
- IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> {
- snackbar.dismiss();
- ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- });
- }
- });
- })
- .setNeutralButton(android.R.string.cancel, null)
- .show();
- }
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java
deleted file mode 100644
index ffa220d94..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java
+++ /dev/null
@@ -1,303 +0,0 @@
-package it.niedermann.nextcloud.deck.ui;
-
-import android.app.Application;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.preference.PreferenceManager;
-
-import java.io.File;
-import java.util.List;
-
-import it.niedermann.android.sharedpreferences.SharedPreferenceBooleanLiveData;
-import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.api.IResponseCallback;
-import it.niedermann.nextcloud.deck.api.ResponseCallback;
-import it.niedermann.nextcloud.deck.model.AccessControl;
-import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.model.Attachment;
-import it.niedermann.nextcloud.deck.model.Board;
-import it.niedermann.nextcloud.deck.model.Card;
-import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.model.Stack;
-import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.model.full.FullBoard;
-import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.model.full.FullStack;
-import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
-import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
-import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-
-@SuppressWarnings("WeakerAccess")
-public class MainViewModel extends AndroidViewModel {
-
- private SyncManager syncManager;
-
- private final MutableLiveData<Account> currentAccount = new MutableLiveData<>();
- @Nullable
- private Board currentBoard;
- private boolean currentAccountHasArchivedBoards = false;
-
- private boolean currentAccountIsSupportedVersion = false;
-
- public MainViewModel(@NonNull Application application) {
- super(application);
- this.syncManager = new SyncManager(application);
- }
-
- public LiveData<Boolean> isDebugModeEnabled() {
- return new SharedPreferenceBooleanLiveData(PreferenceManager.getDefaultSharedPreferences(getApplication()), getApplication().getString(R.string.pref_key_debugging), false);
- }
-
- public Account getCurrentAccount() {
- return currentAccount.getValue();
- }
-
- public LiveData<Account> getCurrentAccountLiveData() {
- return this.currentAccount;
- }
-
- public void setCurrentAccount(Account currentAccount) {
- this.currentAccount.setValue(currentAccount);
- this.currentAccountIsSupportedVersion = currentAccount.getServerDeckVersionAsObject().isSupported();
- }
-
- public void setCurrentBoard(@NonNull Board currentBoard) {
- this.currentBoard = currentBoard;
- }
-
- public Long getCurrentBoardLocalId() {
- if (currentBoard == null) {
- throw new IllegalStateException("getCurrentBoardLocalId() called before setCurrentBoard()");
- }
- return this.currentBoard.getLocalId();
- }
-
- @ColorInt
- public Integer getCurrentBoardColor() {
- if (currentBoard == null) {
- throw new IllegalStateException("getCurrentBoardColor() called before setCurrentBoard()");
- }
- return this.currentBoard.getColor();
- }
-
- public Long getCurrentBoardRemoteId() {
- if (currentBoard == null) {
- throw new IllegalStateException("getCurrentBoardRemoteId() called before setCurrentBoard()");
- }
- return this.currentBoard.getId();
- }
-
- public boolean currentBoardHasEditPermission() {
- return this.currentBoard != null && this.currentBoard.isPermissionEdit() && currentAccountIsSupportedVersion;
- }
-
- public boolean currentAccountHasArchivedBoards() {
- return currentAccountHasArchivedBoards;
- }
-
- public void setCurrentAccountHasArchivedBoards(boolean currentAccountHasArchivedBoards) {
- this.currentAccountHasArchivedBoards = currentAccountHasArchivedBoards;
- }
-
- public boolean isCurrentAccountIsSupportedVersion() {
- return currentAccountIsSupportedVersion;
- }
-
- public void recreateSyncManager() {
- this.syncManager = new SyncManager(getApplication());
- }
-
- public void setSyncManager(@NonNull SyncManager syncManager) {
- this.syncManager = syncManager;
- }
-
- public void synchronize(@NonNull ResponseCallback<Boolean> responseCallback) {
- syncManager.synchronize(responseCallback);
- }
-
- public void refreshCapabilities(@NonNull ResponseCallback<Capabilities> callback) {
- syncManager.refreshCapabilities(callback);
- }
-
- public LiveData<Boolean> hasAccounts() {
- return syncManager.hasAccounts();
- }
-
- public void createAccount(@NonNull Account account, @NonNull IResponseCallback<Account> callback) {
- syncManager.createAccount(account, callback);
- }
-
- public void deleteAccount(long id) {
- syncManager.deleteAccount(id);
- }
-
- public LiveData<List<Account>> readAccounts() {
- return syncManager.readAccounts();
- }
-
- public void createBoard(long accountId, @NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) {
- syncManager.createBoard(accountId, board, callback);
- }
-
- public void updateBoard(@NonNull FullBoard board, @NonNull IResponseCallback<FullBoard> callback) {
- syncManager.updateBoard(board, callback);
- }
-
- public LiveData<List<Board>> getBoards(long accountId, boolean archived) {
- return syncManager.getBoards(accountId, archived);
- }
-
- public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) {
- return syncManager.getFullBoardById(accountId, localId);
- }
-
- public void archiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) {
- syncManager.archiveBoard(board, callback);
- }
-
- public void dearchiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) {
- syncManager.dearchiveBoard(board, callback);
- }
-
- public void cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards, @NonNull IResponseCallback<FullBoard> callback) {
- syncManager.cloneBoard(originAccountId, originBoardLocalId, targetAccountId, targetBoardColor, cloneCards, callback);
- }
-
- public void deleteBoard(@NonNull Board board, @NonNull IResponseCallback<Void> callback) {
- syncManager.deleteBoard(board, callback);
- }
-
- public LiveData<Boolean> hasArchivedBoards(long accountId) {
- return syncManager.hasArchivedBoards(accountId);
- }
-
- public void createAccessControl(long accountId, @NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) {
- syncManager.createAccessControl(accountId, entity, callback);
- }
-
- public void updateAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) {
- syncManager.updateAccessControl(entity, callback);
- }
-
- public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long id) {
- return syncManager.getAccessControlByLocalBoardId(accountId, id);
- }
-
- public void deleteAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<Void> callback) {
- syncManager.deleteAccessControl(entity, callback);
- }
-
- public void createLabel(long accountId, Label label, long localBoardId, @NonNull IResponseCallback<Label> callback) {
- syncManager.createLabel(accountId, label, localBoardId, callback);
- }
-
- public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback<Integer> callback) {
- syncManager.countCardsWithLabel(localLabelId, callback);
- }
-
- public void updateLabel(@NonNull Label label, @NonNull IResponseCallback<Label> callback) {
- syncManager.updateLabel(label, callback);
- }
-
- public void deleteLabel(@NonNull Label label, @NonNull IResponseCallback<Void> callback) {
- syncManager.deleteLabel(label, callback);
- }
-
- public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
- return syncManager.getStacksForBoard(accountId, localBoardId);
- }
-
- public void createStack(long accountId, @NonNull String title, long boardLocalId, @NonNull IResponseCallback<FullStack> callback) {
- syncManager.createStack(accountId, title, boardLocalId, callback);
- }
-
- public LiveData<FullStack> getStack(long accountId, long localStackId) {
- return syncManager.getStack(accountId, localStackId);
- }
-
- public void reorderStack(long accountId, long boardLocalId, long stackLocalId, boolean moveToRight) {
- syncManager.reorderStack(accountId, boardLocalId, stackLocalId, moveToRight);
- }
-
- public void updateStackTitle(long localStackId, @NonNull String newTitle, @NonNull IResponseCallback<FullStack> callback) {
- syncManager.updateStackTitle(localStackId, newTitle, callback);
- }
-
- public void deleteStack(long accountId, long stackLocalId, long boardLocalId, @NonNull IResponseCallback<Void> callback) {
- syncManager.deleteStack(accountId, stackLocalId, boardLocalId, callback);
- }
-
- public void reorder(long accountId, @NonNull FullCard movedCard, long newStackId, int newIndex) {
- syncManager.reorder(accountId, movedCard, newStackId, newIndex);
- }
-
- public void countCardsInStack(long accountId, long localStackId, @NonNull IResponseCallback<Integer> callback) {
- syncManager.countCardsInStackDirectly(accountId, localStackId, callback);
- }
-
- public void archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation, @NonNull IResponseCallback<Void> callback) {
- syncManager.archiveCardsInStack(accountId, stackLocalId, filterInformation, callback);
- }
-
- public void updateCard(@NonNull FullCard fullCard, @NonNull IResponseCallback<FullCard> callback) {
- syncManager.updateCard(fullCard, callback);
- }
-
- public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) {
- syncManager.addCommentToCard(accountId, cardId, comment);
- }
-
- public void addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file, @NonNull IResponseCallback<Attachment> callback) {
- syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file, callback);
- }
-
- public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) {
- syncManager.addOrUpdateSingleCardWidget(widgetId, accountId, boardId, localCardId);
- }
-
- public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) {
- return syncManager.getFullCardsForStack(accountId, localStackId, filter);
- }
-
- public void moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId, @NonNull IResponseCallback<Void> callback) {
- syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, callback);
- }
-
- public LiveData<List<FullCard>> getArchivedFullCardsForBoard(long accountId, long localBoardId) {
- return syncManager.getArchivedFullCardsForBoard(accountId, localBoardId);
- }
-
- public void assignUserToCard(@NonNull User user, @NonNull Card card) {
- syncManager.assignUserToCard(user, card);
- }
-
- public void unassignUserFromCard(@NonNull User user, @NonNull Card card) {
- syncManager.unassignUserFromCard(user, card);
- }
-
- public User getUserByUidDirectly(long accountId, String uid) {
- return syncManager.getUserByUidDirectly(accountId, uid);
- }
-
- public void archiveCard(@NonNull FullCard card, @NonNull IResponseCallback<FullCard> callback) {
- syncManager.archiveCard(card, callback);
- }
-
- public void dearchiveCard(@NonNull FullCard card, @NonNull IResponseCallback<FullCard> callback) {
- syncManager.dearchiveCard(card, callback);
- }
-
- public void deleteCard(@NonNull Card card, @NonNull IResponseCallback<Void> callback) {
- syncManager.deleteCard(card, callback);
- }
-
- public void saveCard(long accountId, long boardLocalId, long stackLocalId, @NonNull FullCard fullCard, @NonNull IResponseCallback<FullCard> callback) {
- syncManager.createFullCard(accountId, boardLocalId, stackLocalId, fullCard, callback);
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java
index 358f17bbf..5b4f33462 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java
@@ -1,10 +1,7 @@
package it.niedermann.nextcloud.deck.ui;
-import static androidx.lifecycle.Transformations.switchMap;
-
import android.os.Bundle;
import android.text.Editable;
-import android.text.TextWatcher;
import android.view.View;
import androidx.annotation.NonNull;
@@ -13,8 +10,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
-import java.util.List;
-
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.ActivityPickStackBinding;
@@ -28,6 +24,7 @@ import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener;
import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
import it.niedermann.nextcloud.deck.ui.theme.Themed;
+import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher;
public abstract class PickStackActivity extends AppCompatActivity implements Themed, PickStackListener {
@@ -46,23 +43,22 @@ public abstract class PickStackActivity extends AppCompatActivity implements The
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
- switchMap(viewModel.hasAccounts(), hasAccounts -> {
- if (hasAccounts) {
- return viewModel.readAccounts();
- } else {
- // TODO After successfully importing the account, the creation will throw a TokenMissMatchException - Recreate SyncManager?
- startActivity(ImportAccountActivity.createIntent(this));
- return null;
- }
- }).observe(this, (List<Account> accounts) -> {
- if (accounts == null || accounts.size() == 0) {
- throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry");
- }
- getSupportFragmentManager()
- .beginTransaction()
- .add(R.id.fragment_container, PickStackFragment.newInstance(showBoardsWithoutEditPermission()))
- .commit();
- });
+ final var hasAccounts$ = new ReactiveLiveData<>(viewModel.hasAccounts());
+
+ hasAccounts$
+ .filter(hasAccounts -> !hasAccounts)
+ .observe(this, () -> {
+ startActivity(ImportAccountActivity.createIntent(this));
+ finish();
+ });
+
+ hasAccounts$
+ .filter(hasAccounts -> hasAccounts)
+ .observe(this, () -> getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.fragment_container, PickStackFragment.newInstance(showBoardsWithoutEditPermission()))
+ .commit());
+
binding.cancel.setOnClickListener((v) -> finish());
binding.submit.setOnClickListener((v) -> {
viewModel.setSubmitInProgress(true);
@@ -88,22 +84,7 @@ public abstract class PickStackActivity extends AppCompatActivity implements The
if (requireContent()) {
viewModel.setContentIsSatisfied(false);
binding.inputWrapper.setVisibility(View.VISIBLE);
- binding.input.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- // Nothing to do here...
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- viewModel.setContentIsSatisfied(s != null && !s.toString().trim().isEmpty());
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- // Nothing to do here...
- }
- });
+ binding.input.addTextChangedListener(new OnTextChangedWatcher(s -> viewModel.setContentIsSatisfied(s != null && !s.trim().isEmpty())));
} else {
viewModel.setContentIsSatisfied(true);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java
index 7ab189aca..32dc2b9d4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java
@@ -1,8 +1,5 @@
package it.niedermann.nextcloud.deck.ui;
-import static androidx.lifecycle.Transformations.distinctUntilChanged;
-import static androidx.lifecycle.Transformations.map;
-
import android.annotation.SuppressLint;
import android.app.Application;
import android.net.Uri;
@@ -14,25 +11,24 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
-import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
-
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Optional;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.api.ResponseCallback;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
import it.niedermann.nextcloud.deck.util.ProjectUtil;
-public class PushNotificationViewModel extends AndroidViewModel {
+public class PushNotificationViewModel extends BaseViewModel {
// Provided by Files app NotificationJob
private static final String KEY_SUBJECT = "subject";
@@ -41,12 +37,10 @@ public class PushNotificationViewModel extends AndroidViewModel {
private static final String KEY_ACCOUNT = "account";
private static final String KEY_CARD_REMOTE_ID = "objectId";
- private final SyncManager readAccountSyncManager;
private final MutableLiveData<Account> account = new MutableLiveData<>();
public PushNotificationViewModel(@NonNull Application application) {
super(application);
- this.readAccountSyncManager = new SyncManager(application);
}
@WorkerThread
@@ -63,8 +57,7 @@ public class PushNotificationViewModel extends AndroidViewModel {
.orElseThrow(() -> new IllegalArgumentException("Account not found"));
this.account.postValue(account);
- SingleAccountHelper.setCurrentAccount(getApplication(), account.getName());
- final var syncManager = new SyncManager(getApplication());
+ final var syncManager = new SyncManager(getApplication(), account);
final var card = syncManager.getCardByRemoteIDDirectly(account.getId(), cardRemoteId);
@@ -200,7 +193,7 @@ public class PushNotificationViewModel extends AndroidViewModel {
}
private Optional<Account> extractAccount(@NonNull Bundle bundle) {
- return Optional.ofNullable(readAccountSyncManager.readAccountDirectly(bundle.getString(KEY_ACCOUNT)));
+ return Optional.ofNullable(baseRepository.readAccountDirectly(bundle.getString(KEY_ACCOUNT)));
}
private Optional<Long> extractBoardLocalId(@NonNull SyncManager syncManager, long accountId, long cardRemoteId) {
@@ -230,7 +223,9 @@ public class PushNotificationViewModel extends AndroidViewModel {
}
public LiveData<Integer> getAccount() {
- return distinctUntilChanged(map(this.account, Account::getColor));
+ return new ReactiveLiveData<>(this.account)
+ .map(Account::getColor)
+ .distinctUntilChanged();
}
public interface PushNotificationCallback extends IResponseCallback<CardInformation> {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/StackChangeCallback.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/StackChangeCallback.java
new file mode 100644
index 000000000..90fd41990
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/StackChangeCallback.java
@@ -0,0 +1,71 @@
+package it.niedermann.nextcloud.deck.ui;
+
+import android.view.Menu;
+
+import androidx.annotation.NonNull;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import androidx.viewpager2.widget.ViewPager2;
+
+import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
+
+import java.util.function.Consumer;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.ui.stack.StackAdapter;
+
+public class StackChangeCallback extends ViewPager2.OnPageChangeCallback {
+
+ private final StackAdapter adapter;
+ private final ViewPager2 viewPager;
+ private final ExtendedFloatingActionButton fab;
+ private final SwipeRefreshLayout swipeRefreshLayout;
+ private final Menu menu;
+ private final Consumer<Stack> onStackSelected;
+
+ public StackChangeCallback(
+ @NonNull StackAdapter adapter,
+ @NonNull ViewPager2 viewPager,
+ @NonNull ExtendedFloatingActionButton fab,
+ @NonNull SwipeRefreshLayout swipeRefreshLayout,
+ @NonNull Menu menu,
+ @NonNull Consumer<Stack> onStackSelected
+ ) {
+ this.adapter = adapter;
+ this.viewPager = viewPager;
+ this.fab = fab;
+ this.swipeRefreshLayout = swipeRefreshLayout;
+ this.menu = menu;
+ this.onStackSelected = onStackSelected;
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ this.updateMoveItemVisibility();
+ this.viewPager.post(() -> {
+ // stackAdapter size might differ from position when an account has been deleted
+ if (this.adapter.getItemCount() > position) {
+ this.onStackSelected.accept(this.adapter.getItem(position));
+ } else {
+ DeckLog.logError(new IllegalStateException("Tried to save current Stack which cannot be available (stackAdapter doesn't have this position)"));
+ }
+ });
+ this.fab.extend();
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ if (!swipeRefreshLayout.isRefreshing()) {
+ swipeRefreshLayout.setEnabled(state == ViewPager2.SCROLL_STATE_IDLE);
+ }
+ }
+
+ public void updateMoveItemVisibility() {
+ final var currentBoardHasStacks = adapter.getItemCount() > 0;
+ final int currentViewPagerItem = viewPager.getCurrentItem();
+
+ menu.findItem(R.id.move_list_left).setVisible(currentBoardHasStacks && currentViewPagerItem > 0);
+ menu.findItem(R.id.move_list_right).setVisible(currentBoardHasStacks && currentViewPagerItem < adapter.getItemCount() - 1);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java
index cfb43c382..dc5de960f 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java
@@ -13,16 +13,16 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.tabs.TabLayoutMediator;
-import it.niedermann.nextcloud.deck.DeckApplication;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ActivityAboutBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
-public class AboutActivity extends AppCompatActivity {
- private static final String BUNDLE_KEY_ACCOUNT = "account";
+public class AboutActivity extends AppCompatActivity implements Themed {
+ private static final String KEY_ACCOUNT = "account";
private ActivityAboutBinding binding;
private final static int[] tabTitles = new int[]{
R.string.about_credits_tab_title,
@@ -35,13 +35,22 @@ public class AboutActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+ final var args = getIntent().getExtras();
+
+ if (args == null || !args.containsKey(KEY_ACCOUNT)) {
+ throw new IllegalArgumentException("Provide at least " + KEY_ACCOUNT);
+ }
+
+ final var account = (Account) args.getSerializable(KEY_ACCOUNT);
+
binding = ActivityAboutBinding.inflate(getLayoutInflater());
+
setContentView(binding.getRoot());
+ setSupportActionBar(binding.toolbar);
- DeckApplication.readCurrentAccountColor().observe(this, color -> ThemeUtils.of(color, this).deck.themeTabLayout(binding.tabLayout));
+ applyTheme(account.getColor());
- setSupportActionBar(binding.toolbar);
- binding.viewPager.setAdapter(new TabsPagerAdapter(this, (Account) getIntent().getSerializableExtra(BUNDLE_KEY_ACCOUNT)));
+ binding.viewPager.setAdapter(new TabsPagerAdapter(this, account));
new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> tab.setText(tabTitles[position])).attach();
}
@@ -51,6 +60,13 @@ public class AboutActivity extends AppCompatActivity {
this.binding = null;
}
+ @Override
+ public void applyTheme(int color) {
+ final var utils = ThemeUtils.of(color, this);
+
+ utils.deck.themeTabLayout(binding.tabLayout);
+ }
+
private static class TabsPagerAdapter extends FragmentStateAdapter {
@Nullable
@@ -70,7 +86,7 @@ public class AboutActivity extends AppCompatActivity {
case 1:
return new AboutFragmentContributingTab();
case 2:
- return new AboutFragmentLicenseTab();
+ return AboutFragmentLicenseTab.newInstance(account);
default:
throw new IllegalArgumentException("position must be between 0 and 2");
}
@@ -91,6 +107,6 @@ public class AboutActivity extends AppCompatActivity {
@NonNull
public static Intent createIntent(@NonNull Context context, @NonNull Account account) {
return new Intent(context, AboutActivity.class)
- .putExtra(BUNDLE_KEY_ACCOUNT, account);
+ .putExtra(KEY_ACCOUNT, account);
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java
index dafc54bee..5ec2c4c2c 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java
@@ -10,32 +10,37 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
-import it.niedermann.nextcloud.deck.DeckApplication;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.FragmentAboutLicenseTabBinding;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
-public class AboutFragmentLicenseTab extends Fragment {
+public class AboutFragmentLicenseTab extends Fragment implements Themed {
+ private static final String KEY_ACCOUNT = "account";
private FragmentAboutLicenseTabBinding binding;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final var args = getArguments();
+
+ if (args == null || !args.containsKey(KEY_ACCOUNT)) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided");
+ }
+
+ final Account account = (Account) requireArguments().getSerializable(KEY_ACCOUNT);
+
binding = FragmentAboutLicenseTabBinding.inflate(inflater, container, false);
setTextWithURL(binding.aboutIconsDisclaimerAppIcon, getResources(), R.string.about_icons_disclaimer_app_icon, R.string.about_app_icon_author_link_label, R.string.url_about_icon_author);
setTextWithURL(binding.aboutIconsDisclaimerMdiIcons, getResources(), R.string.about_icons_disclaimer_mdi_icons, R.string.about_icons_disclaimer_mdi, R.string.url_about_icons_disclaimer_mdi);
binding.aboutAppLicenseButton.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_license)))));
- return binding.getRoot();
- }
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- DeckApplication.readCurrentAccountColor().observe(getViewLifecycleOwner(), color ->
- ThemeUtils.of(color, requireContext()).material.colorMaterialButtonPrimaryFilled(binding.aboutAppLicenseButton));
+ applyTheme(account.getColor());
+
+ return binding.getRoot();
}
@Override
@@ -43,4 +48,21 @@ public class AboutFragmentLicenseTab extends Fragment {
super.onDestroy();
this.binding = null;
}
+
+ public static Fragment newInstance(@NonNull Account account) {
+ final var fragment = new AboutFragmentLicenseTab();
+
+ final var args = new Bundle();
+ args.putSerializable(KEY_ACCOUNT, account);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public void applyTheme(int color) {
+ final var utils = ThemeUtils.of(color, requireContext());
+
+ utils.material.colorMaterialButtonPrimaryFilled(binding.aboutAppLicenseButton);
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java
index 1f0098c4f..4a2ef80e6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java
@@ -1,104 +1,110 @@
package it.niedermann.nextcloud.deck.ui.accountswitcher;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
+import static android.app.Activity.RESULT_OK;
import android.app.Dialog;
+import android.content.Intent;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
+import android.view.View;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.android.sso.AccountImporter;
-import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted;
-import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
-import com.nextcloud.android.sso.ui.UiExceptionManager;
import java.util.Objects;
import java.util.stream.Collectors;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.android.util.DimensionUtil;
-import it.niedermann.nextcloud.deck.DeckApplication;
-import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogAccountSwitcherBinding;
-import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
+import it.niedermann.nextcloud.deck.ui.ImportAccountActivity;
import it.niedermann.nextcloud.deck.ui.manageaccounts.ManageAccountsActivity;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
public class AccountSwitcherDialog extends DialogFragment {
private AccountSwitcherAdapter adapter;
private DialogAccountSwitcherBinding binding;
- private MainViewModel viewModel;
+ private AccountViewModel accountViewModel;
+
+ private final ActivityResultLauncher<Intent> importAccountLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+ if (result.getResultCode() != RESULT_OK) {
+ requireActivity().finish();
+ }
+ });
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
binding = DialogAccountSwitcherBinding.inflate(requireActivity().getLayoutInflater());
- viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
-
- final Account currentAccount = viewModel.getCurrentAccount();
- binding.accountName.setText(
- TextUtils.isEmpty(currentAccount.getUserDisplayName())
- ? currentAccount.getUserName()
- : currentAccount.getUserDisplayName()
- );
- binding.accountHost.setText(Uri.parse(currentAccount.getUrl()).getHost());
- binding.check.setSelected(true);
-
- Glide.with(requireContext())
- .load(currentAccount.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size)))
- .placeholder(R.drawable.ic_baseline_account_circle_24)
- .error(R.drawable.ic_baseline_account_circle_24)
- .apply(RequestOptions.circleCropTransform())
- .into(binding.currentAccountItemAvatar);
-
- binding.accountLayout.setOnClickListener((v) -> dismiss());
+ accountViewModel = new ViewModelProvider(requireActivity()).get(AccountViewModel.class);
adapter = new AccountSwitcherAdapter((localAccount -> {
- viewModel.setCurrentAccount(localAccount);
+ accountViewModel.saveCurrentAccount(localAccount);
dismiss();
}));
- observeOnce(viewModel.readAccounts(), requireActivity(), (accounts) ->
- adapter.setAccounts(accounts.stream().filter(account ->
- !Objects.equals(account.getId(), viewModel.getCurrentAccount().getId())).collect(Collectors.toList())));
-
- observeOnce(DeckApplication.readCurrentBoardColor(), requireActivity(), this::applyTheme);
-
+ binding.accountLayout.setOnClickListener((v) -> dismiss());
+ binding.check.setSelected(true);
binding.accountsList.setAdapter(adapter);
-
binding.addAccount.setOnClickListener((v) -> {
- try {
- AccountImporter.pickNewAccount(requireActivity());
- } catch (NextcloudFilesAppNotInstalledException e) {
- UiExceptionManager.showDialogForException(requireContext(), e);
- DeckLog.warn("=============================================================");
- DeckLog.warn("Nextcloud app is not installed. Cannot choose account");
- DeckLog.logError(e);
- } catch (AndroidGetAccountsPermissionNotGranted e) {
- AccountImporter.requestAndroidAccountPermissionsAndPickAccount(requireActivity());
- }
+ importAccountLauncher.launch(ImportAccountActivity.createIntent(requireContext()));
dismiss();
});
-
binding.manageAccounts.setOnClickListener((v) -> {
requireActivity().startActivity(ManageAccountsActivity.createIntent(requireContext()));
dismiss();
});
+ new ReactiveLiveData<>(accountViewModel.getCurrentAccount())
+ .flatMap(currentAccount -> {
+ binding.accountName.setText(
+ TextUtils.isEmpty(currentAccount.getUserDisplayName())
+ ? currentAccount.getUserName()
+ : currentAccount.getUserDisplayName()
+ );
+ binding.accountHost.setText(Uri.parse(currentAccount.getUrl()).getHost());
+
+ Glide.with(requireContext())
+ .load(currentAccount.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size)))
+ .placeholder(R.drawable.ic_baseline_account_circle_24)
+ .error(R.drawable.ic_baseline_account_circle_24)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.currentAccountItemAvatar);
+
+ applyTheme(currentAccount.getColor());
+
+ return new ReactiveLiveData<>(accountViewModel.readAccounts())
+ .map(accounts -> accounts
+ .stream()
+ .filter(account -> !Objects.equals(account.getId(), currentAccount.getId()))
+ .collect(Collectors.toList())
+ );
+ })
+ .observe(this, adapter::setAccounts);
+
return new MaterialAlertDialogBuilder(requireContext())
.setView(binding.getRoot())
.create();
}
@Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ }
+
+ @Override
public void onDestroy() {
super.onDestroy();
this.binding = null;
@@ -109,6 +115,9 @@ public class AccountSwitcherDialog extends DialogFragment {
}
private void applyTheme(int color) {
-// applyThemeToLayerDrawable((LayerDrawable) binding.check.getDrawable(), R.id.area, mainColor);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ final var utils = ThemeUtils.of(color, requireContext());
+ utils.deck.colorSelectedCheck(binding.check.getContext(), binding.check.getDrawable());
+ }
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java
index 345a2fe23..0efa7f769 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java
@@ -1,6 +1,7 @@
package it.niedermann.nextcloud.deck.ui.accountswitcher;
import android.net.Uri;
+import android.os.Build;
import android.text.TextUtils;
import android.view.View;
@@ -15,7 +16,7 @@ import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
@@ -34,12 +35,17 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
);
binding.accountHost.setText(Uri.parse(account.getUrl()).getHost());
Glide.with(itemView.getContext())
- .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))))
+ .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))
.placeholder(R.drawable.ic_baseline_account_circle_24)
.error(R.drawable.ic_baseline_account_circle_24)
.apply(RequestOptions.circleCropTransform())
.into(binding.accountItemAvatar);
itemView.setOnClickListener((v) -> onAccountClick.accept(account));
binding.delete.setVisibility(View.GONE);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ final var utils = ThemeUtils.of(account.getColor(), itemView.getContext());
+ utils.deck.colorSelectedCheck(binding.currentAccountIndicator.getContext(), binding.currentAccountIndicator.getDrawable());
+ }
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountViewModel.java
new file mode 100644
index 000000000..4b2cf711a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountViewModel.java
@@ -0,0 +1,32 @@
+package it.niedermann.nextcloud.deck.ui.accountswitcher;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import java.util.List;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
+
+public class AccountViewModel extends BaseViewModel {
+
+ public AccountViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ public void saveCurrentAccount(@NonNull Account account) {
+ baseRepository.saveCurrentAccount(account);
+ }
+
+ public LiveData<Account> getCurrentAccount() {
+ return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$())
+ .flatMap(baseRepository::readAccount);
+ }
+
+ public LiveData<List<Account>> readAccounts() {
+ return baseRepository.readAccounts();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java
index 30f1e5d49..ba60d9666 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java
@@ -5,19 +5,22 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.appcompat.widget.PopupMenu;
-import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.nextcloud.android.common.ui.theme.utils.ColorRole;
+
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemArchivedBoardBinding;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Board;
import it.niedermann.nextcloud.deck.ui.board.DeleteBoardDialogFragment;
-import it.niedermann.nextcloud.deck.ui.board.EditBoardDialogFragment;
import it.niedermann.nextcloud.deck.ui.board.accesscontrol.AccessControlDialogFragment;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
+import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardDialogFragment;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
@SuppressWarnings("WeakerAccess")
public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder {
@@ -29,15 +32,17 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder {
this.binding = binding;
}
- void bind(boolean isSupportedVersion, Board board, FragmentManager fragmentManager, Consumer<Board> dearchiveBoardListener) {
+ void bind(@NonNull Account account, @NonNull Board board, FragmentManager fragmentManager, @NonNull Consumer<Board> dearchiveBoardListener) {
final Context context = itemView.getContext();
- binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, board.getColor()));
+ final var util = ThemeUtils.of(account.getColor(), context);
+
+ binding.boardIcon.setImageDrawable(util.deck.getColoredBoardDrawable(context, board.getColor()));
binding.boardMenu.setVisibility(View.GONE);
binding.boardTitle.setText(board.getTitle());
- if (isSupportedVersion) {
+ if (account.getServerDeckVersionAsObject().isSupported()) {
if (board.isPermissionManage()) {
binding.boardMenu.setVisibility(View.VISIBLE);
- binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, ContextCompat.getColor(context, R.color.grey600)));
+ binding.boardMenu.setImageDrawable(util.platform.tintDrawable(context, R.drawable.ic_menu, ColorRole.ON_SURFACE));
binding.boardMenu.setOnClickListener((v) -> {
PopupMenu popup = new PopupMenu(context, binding.boardMenu);
popup.getMenuInflater().inflate(R.menu.archived_board_menu, popup.getMenu());
@@ -49,10 +54,10 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder {
final String editBoard = context.getString(R.string.edit_board);
int itemId = item.getItemId();
if (itemId == SHARE_BOARD_ID) {
- AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName());
+ AccessControlDialogFragment.newInstance(account, board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName());
return true;
} else if (itemId == R.id.edit_board) {
- EditBoardDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, editBoard);
+ EditBoardDialogFragment.newInstance(account, board.getLocalId()).show(fragmentManager, editBoard);
return true;
} else if (itemId == R.id.dearchive_board) {
dearchiveBoardListener.accept(board);
@@ -67,8 +72,8 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder {
});
} else if (board.isPermissionShare()) {
binding.boardMenu.setVisibility(View.VISIBLE);
- binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, ContextCompat.getColor(context, R.color.grey600)));
- binding.boardMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName()));
+ binding.boardMenu.setImageDrawable(util.platform.tintDrawable(context, R.drawable.ic_share_grey600_18dp, ColorRole.ON_SURFACE));
+ binding.boardMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(account, board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName()));
}
binding.boardMenu.setVisibility(View.VISIBLE);
} else {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActivity.java
index 4a19b78e3..ac978adf8 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActivity.java
@@ -1,16 +1,17 @@
package it.niedermann.nextcloud.deck.ui.archivedboards;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
+import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
-import java.util.Collections;
-
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.ActivityArchivedBinding;
@@ -18,53 +19,56 @@ import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Board;
import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener;
import it.niedermann.nextcloud.deck.ui.board.DeleteBoardListener;
-import it.niedermann.nextcloud.deck.ui.board.EditBoardListener;
+import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardListener;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
-public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteBoardListener, EditBoardListener, ArchiveBoardListener {
-
- private static final String BUNDLE_KEY_ACCOUNT = "accountId";
+public class ArchivedBoardsActivity extends AppCompatActivity implements Themed, DeleteBoardListener, EditBoardListener, ArchiveBoardListener {
- private MainViewModel viewModel;
+ private static final String KEY_ACCOUNT = "account";
+ private ArchivedBoardsViewModel archivedBoardsViewModel;
private ActivityArchivedBinding binding;
private ArchivedBoardsAdapter adapter;
+ private Account account;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this));
- final Bundle args = getIntent().getExtras();
- if (args == null || !args.containsKey(BUNDLE_KEY_ACCOUNT)) {
- throw new IllegalArgumentException("Please provide at least " + BUNDLE_KEY_ACCOUNT);
- }
+ final var args = getIntent().getExtras();
- final Account account = (Account) args.getSerializable(BUNDLE_KEY_ACCOUNT);
-
- if (account == null) {
- throw new IllegalArgumentException(BUNDLE_KEY_ACCOUNT + " must not be null.");
+ if (args == null || !args.containsKey(KEY_ACCOUNT)) {
+ throw new IllegalArgumentException("Provide at least " + KEY_ACCOUNT);
}
+ account = (Account) args.getSerializable(KEY_ACCOUNT);
+
binding = ActivityArchivedBinding.inflate(getLayoutInflater());
+
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
- viewModel = new ViewModelProvider(this).get(MainViewModel.class);
- viewModel.setCurrentAccount(account);
+ archivedBoardsViewModel = new ViewModelProvider(this, new SyncViewModel.Factory(getApplication(), account)).get(ArchivedBoardsViewModel.class);
- adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), this::onArchive);
+ adapter = new ArchivedBoardsAdapter(account, getSupportFragmentManager(), this::onArchive);
binding.recyclerView.setAdapter(adapter);
- viewModel.getBoards(account.getId(), true).observe(this, (boards) -> {
- viewModel.setCurrentAccountHasArchivedBoards(boards != null && boards.size() > 0);
- adapter.setBoards(boards == null ? Collections.emptyList() : boards);
- });
+ final var archivedBoards$ = new ReactiveLiveData<>(archivedBoardsViewModel.getArchivedBoards(account.getId()));
+
+ archivedBoards$
+ .filter(boards -> boards.size() == 0)
+ .distinctUntilChanged()
+ .observe(this, this::finish);
+ archivedBoards$
+ .filter(boards -> boards.size() > 0)
+ .distinctUntilChanged()
+ .observe(this, adapter::setBoards);
}
@Override
@@ -73,16 +77,9 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB
this.binding = null;
}
- @NonNull
- public static Intent createIntent(@NonNull Context context, @NonNull Account account) {
- return new Intent(context, ArchivedBoardsActvitiy.class)
- .putExtra(BUNDLE_KEY_ACCOUNT, account)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
-
@Override
public void onBoardDeleted(Board board) {
- viewModel.deleteBoard(board, new IResponseCallback<>() {
+ archivedBoardsViewModel.deleteBoard(board, new IResponseCallback<>() {
@Override
public void onResponse(Void response) {
DeckLog.info("Successfully deleted board", board.getTitle());
@@ -90,9 +87,9 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB
@Override
public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ showExceptionDialog(throwable, account);
}
}
});
@@ -100,7 +97,7 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB
@Override
public void onUpdateBoard(FullBoard fullBoard) {
- viewModel.updateBoard(fullBoard, new IResponseCallback<>() {
+ archivedBoardsViewModel.updateBoard(fullBoard, new IResponseCallback<>() {
@Override
public void onResponse(FullBoard response) {
DeckLog.info("Successfully updated board", fullBoard.getBoard().getTitle());
@@ -109,14 +106,14 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB
@Override
public void onError(Throwable throwable) {
IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ showExceptionDialog(throwable, account);
}
});
}
@Override
public void onArchive(Board board) {
- viewModel.dearchiveBoard(board, new IResponseCallback<>() {
+ archivedBoardsViewModel.dearchiveBoard(board, new IResponseCallback<>() {
@Override
public void onResponse(FullBoard response) {
DeckLog.info("Successfully dearchived board", response.getBoard().getTitle());
@@ -125,14 +122,14 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB
@Override
public void onError(Throwable throwable) {
IResponseCallback.super.onError(throwable);
- runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ showExceptionDialog(throwable, account);
}
});
}
@Override
- public void onClone(Board board) {
- throw new IllegalStateException("Cloning boards is not available at " + ArchivedBoardsActvitiy.class.getSimpleName());
+ public void onClone(@NonNull Account account, @NonNull Board board) {
+ throw new IllegalStateException("Cloning boards is not available at " + ArchivedBoardsActivity.class.getSimpleName());
}
@Override
@@ -140,4 +137,28 @@ public class ArchivedBoardsActvitiy extends AppCompatActivity implements DeleteB
finish(); // close this activity as oppose to navigating up
return true;
}
+
+ @AnyThread
+ private void showExceptionDialog(@NonNull Throwable throwable, @Nullable Account account) {
+ ExceptionDialogFragment
+ .newInstance(throwable, account)
+ .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+
+ @NonNull
+ public static Intent createIntent(@NonNull Context context, @NonNull Account account) {
+ return new Intent(context, ArchivedBoardsActivity.class)
+ .putExtra(KEY_ACCOUNT, account)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+
+ }
+
+ @Override
+ public void applyTheme(int color) {
+ binding.emptyContentView.applyTheme(color);
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsAdapter.java
index 7bf82314c..0df6db362 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsAdapter.java
@@ -12,21 +12,23 @@ import java.util.ArrayList;
import java.util.List;
import it.niedermann.nextcloud.deck.databinding.ItemArchivedBoardBinding;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Board;
public class ArchivedBoardsAdapter extends RecyclerView.Adapter<ArchivedBoardViewHolder> {
- private final boolean isSupportedVersion;
+ @NonNull
+ private final Account account;
@NonNull
private final Consumer<Board> onDearchiveListener;
@NonNull
private final FragmentManager fragmentManager;
@NonNull
- private List<Board> boards = new ArrayList<>();
+ private final List<Board> boards = new ArrayList<>();
@SuppressWarnings("WeakerAccess")
- public ArchivedBoardsAdapter(boolean isSupportedVersion, @NonNull FragmentManager fragmentManager, @NonNull Consumer<Board> onDearchiveListener) {
- this.isSupportedVersion = isSupportedVersion;
+ public ArchivedBoardsAdapter(@NonNull Account account, @NonNull FragmentManager fragmentManager, @NonNull Consumer<Board> onDearchiveListener) {
+ this.account = account;
this.fragmentManager = fragmentManager;
this.onDearchiveListener = onDearchiveListener;
setHasStableIds(true);
@@ -45,7 +47,7 @@ public class ArchivedBoardsAdapter extends RecyclerView.Adapter<ArchivedBoardVie
@Override
public void onBindViewHolder(@NonNull ArchivedBoardViewHolder holder, int position) {
- holder.bind(isSupportedVersion, boards.get(position), fragmentManager, onDearchiveListener);
+ holder.bind(account, boards.get(position), fragmentManager, onDearchiveListener);
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsViewModel.java
new file mode 100644
index 000000000..c2cfa527a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsViewModel.java
@@ -0,0 +1,42 @@
+package it.niedermann.nextcloud.deck.ui.archivedboards;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
+import java.util.Collections;
+import java.util.List;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
+
+public class ArchivedBoardsViewModel extends SyncViewModel {
+
+ public ArchivedBoardsViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ super(application, account);
+ }
+
+ public LiveData<List<Board>> getArchivedBoards(long accountId) {
+ return new ReactiveLiveData<>(baseRepository.getBoards(accountId, true))
+ .map(boards -> boards == null ? Collections.<Board>emptyList() : boards);
+ }
+
+ public void updateBoard(@NonNull FullBoard board, @NonNull IResponseCallback<FullBoard> callback) {
+ syncManager.updateBoard(board, callback);
+ }
+
+ public void deleteBoard(@NonNull Board board, @NonNull IResponseCallback<Void> callback) {
+ syncManager.deleteBoard(board, callback);
+ }
+
+ public void dearchiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) {
+ syncManager.dearchiveBoard(board, callback);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java
deleted file mode 100644
index ae023a7d8..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package it.niedermann.nextcloud.deck.ui.archivedcards;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.lifecycle.ViewModelProvider;
-
-import it.niedermann.nextcloud.deck.databinding.ActivityArchivedBinding;
-import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
-import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel;
-
-public class ArchivedCardsActvitiy extends AppCompatActivity {
-
- private static final String BUNDLE_KEY_ACCOUNT = "accountId";
- private static final String BUNDLE_KEY_BOARD_ID = "boardId";
- private static final String BUNDLE_KEY_CAN_EDIT = "canEdit";
-
- private ActivityArchivedBinding binding;
- private ArchivedCardsAdapter adapter;
- private MainViewModel viewModel;
- private PickStackViewModel pickStackViewModel;
-
- private Account account;
- private long boardId;
- private boolean canEdit = false;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this));
-
- final Bundle args = getIntent().getExtras();
- if (args == null || !args.containsKey(BUNDLE_KEY_ACCOUNT) || !args.containsKey(BUNDLE_KEY_BOARD_ID)) {
- throw new IllegalArgumentException("Please provide at least " + BUNDLE_KEY_ACCOUNT + " and " + BUNDLE_KEY_BOARD_ID);
- }
-
- this.account = (Account) args.getSerializable(BUNDLE_KEY_ACCOUNT);
- this.boardId = args.getLong(BUNDLE_KEY_BOARD_ID);
- canEdit = args.getBoolean(BUNDLE_KEY_CAN_EDIT);
-
- if (this.account == null) {
- throw new IllegalArgumentException(BUNDLE_KEY_ACCOUNT + " must not be null.");
- }
- if (this.boardId <= 0) {
- throw new IllegalArgumentException(BUNDLE_KEY_BOARD_ID + " must a positive long value.");
- }
-
- binding = ActivityArchivedBinding.inflate(getLayoutInflater());
- viewModel = new ViewModelProvider(this).get(MainViewModel.class);
- pickStackViewModel = new ViewModelProvider(this).get(PickStackViewModel.class);
-
- setContentView(binding.getRoot());
- setSupportActionBar(binding.toolbar);
-
- viewModel.setCurrentAccount(account);
- LiveDataHelper.observeOnce(viewModel.getFullBoardById(account.getId(), boardId), this, (fullBoard) -> {
- viewModel.setCurrentBoard(fullBoard.getBoard());
-
- adapter = new ArchivedCardsAdapter(this, getSupportFragmentManager(), viewModel);
- binding.recyclerView.setAdapter(adapter);
-
- viewModel.getArchivedFullCardsForBoard(account.getId(), boardId).observe(this, (fullCards) -> adapter.setCardList(fullCards));
- });
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- this.binding = null;
- }
-
- @NonNull
- public static Intent createIntent(@NonNull Context context, @NonNull Account account, long boardId, boolean currentBoardHasEditPermission) {
- return new Intent(context, ArchivedCardsActvitiy.class)
- .putExtra(BUNDLE_KEY_ACCOUNT, account)
- .putExtra(BUNDLE_KEY_BOARD_ID, boardId)
- .putExtra(BUNDLE_KEY_CAN_EDIT, currentBoardHasEditPermission)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java
deleted file mode 100644
index ea1ff4d89..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package it.niedermann.nextcloud.deck.ui.archivedcards;
-
-import android.app.Activity;
-import android.view.MenuItem;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.FragmentManager;
-
-import it.niedermann.nextcloud.deck.DeckLog;
-import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.api.IResponseCallback;
-import it.niedermann.nextcloud.deck.model.Card;
-import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
-import it.niedermann.nextcloud.deck.ui.card.AbstractCardViewHolder;
-import it.niedermann.nextcloud.deck.ui.card.CardAdapter;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-
-public class ArchivedCardsAdapter extends CardAdapter {
-
- @SuppressWarnings("WeakerAccess")
- public ArchivedCardsAdapter(@NonNull Activity activity, @NonNull FragmentManager fragmentManager, @NonNull MainViewModel viewModel) {
- super(activity, fragmentManager, 0L, viewModel, null);
- }
-
- @Override
- public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) {
- viewHolder.bind(cardList.get(position), mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardRemoteId(), false, R.menu.archived_card_menu, this, counterMaxValue, scheme);
- }
-
- @Override
- public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) {
- int itemId = menuItem.getItemId();
- if (itemId == R.id.action_card_dearchive) {
- mainViewModel.dearchiveCard(fullCard, new IResponseCallback<>() {
- @Override
- public void onResponse(FullCard response) {
- DeckLog.info("Successfully dearchived", Card.class.getSimpleName(), fullCard.getCard().getTitle());
- }
-
- @Override
- public void onError(Throwable throwable) {
- IResponseCallback.super.onError(throwable);
- activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()));
- }
- });
- return true;
- } else if (itemId == R.id.action_card_delete) {
- mainViewModel.deleteCard(fullCard.getCard(), new IResponseCallback<>() {
- @Override
- public void onResponse(Void response) {
- DeckLog.info("Successfully deleted card", fullCard.getCard().getTitle());
- }
-
- @Override
- public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
- IResponseCallback.super.onError(throwable);
- activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()));
- }
- }
- });
- return true;
- }
- return super.onCardOptionsItemSelected(menuItem, fullCard);
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java
index 87a17470c..684f7b0a2 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java
@@ -3,23 +3,19 @@ package it.niedermann.nextcloud.deck.ui.attachments;
import android.app.Application;
import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
@SuppressWarnings("WeakerAccess")
-public class AttachmentsViewModel extends AndroidViewModel {
-
- private final SyncManager syncManager;
+public class AttachmentsViewModel extends BaseViewModel {
public AttachmentsViewModel(@NonNull Application application) {
super(application);
- this.syncManager = new SyncManager(application);
}
public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) {
- return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId);
+ return baseRepository.getFullCardWithProjectsByLocalId(accountId, cardLocalId);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java
index ff0d3e941..2e04135e4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java
@@ -1,8 +1,11 @@
package it.niedermann.nextcloud.deck.ui.board;
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Board;
public interface ArchiveBoardListener {
void onArchive(Board board);
- void onClone(Board board);
+ void onClone(@NonNull Account account, @NonNull Board board);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java
deleted file mode 100644
index c9501ffa9..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package it.niedermann.nextcloud.deck.ui.board;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import it.niedermann.nextcloud.deck.DeckLog;
-import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.model.Board;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
-
-public class BoardAdapter extends ArrayAdapter<Board> {
-
- @NonNull
- private Context context;
-
- public BoardAdapter(@NonNull Context context, @NonNull Board[] boardsList) {
- super(context, R.layout.item_board, boardsList);
- this.context = context;
- }
-
- @NonNull
- @Override
- public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
- Board board = getItem(position);
- // Check if an existing view is being reused, otherwise inflate the view
-
- if (convertView == null) {
- convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_board, parent, false);
- }
- TextView boardName = convertView.findViewById(R.id.boardName);
- if (board != null) {
- boardName.setText(board.getTitle());
- boardName.setCompoundDrawables(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, board.getColor()), null, null, null);
- } else {
- DeckLog.logError(new IllegalArgumentException("board at position " + position + "is null"));
- }
- return convertView;
- }
-
- @Override
- public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
- return getView(position, convertView, parent);
- }
-
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java
deleted file mode 100644
index 9d8fcdbde..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package it.niedermann.nextcloud.deck.ui.board;
-
-import androidx.annotation.ColorInt;
-
-import it.niedermann.nextcloud.deck.model.full.FullBoard;
-
-public interface EditBoardListener {
- void onUpdateBoard(FullBoard fullBoard);
-
- default void onCreateBoard(String title, @ColorInt int color) {
- // Creating board is not necessary
- }
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java
index c97a1e4ba..8cd5c6548 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java
@@ -9,10 +9,14 @@ import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAccessControlBinding;
import it.niedermann.nextcloud.deck.databinding.ItemAccessControlOwnerBinding;
@@ -21,7 +25,6 @@ import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
import it.niedermann.nextcloud.deck.ui.theme.Themed;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements Themed {
@@ -76,15 +79,25 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View
final AccessControl ac = accessControls.get(position);
switch (getItemViewType(position)) {
case TYPE_HEADER: {
- final OwnerViewHolder ownerHolder = (OwnerViewHolder) holder;
+ final var ownerHolder = (OwnerViewHolder) holder;
ownerHolder.binding.owner.setText(ac.getUser().getDisplayname());
- ViewUtil.addAvatar(ownerHolder.binding.avatar, account.getUrl(), ac.getUser().getUid(), R.drawable.ic_person_grey600_24dp);
+ Glide.with(ownerHolder.binding.avatar.getContext())
+ .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(ownerHolder.binding.avatar.getContext(), R.dimen.avatar_size), ac.getUser().getUid()))
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(ownerHolder.binding.avatar);
break;
}
case TYPE_ITEM:
default: {
- final AccessControlViewHolder acHolder = (AccessControlViewHolder) holder;
- ViewUtil.addAvatar(acHolder.binding.avatar, account.getUrl(), ac.getUser().getUid(), R.drawable.ic_person_grey600_24dp);
+ final var acHolder = (AccessControlViewHolder) holder;
+ Glide.with(acHolder.binding.avatar.getContext())
+ .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(acHolder.binding.avatar.getContext(), R.dimen.avatar_size), ac.getUser().getUid()))
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(acHolder.binding.avatar);
acHolder.binding.username.setText(ac.getUser().getDisplayname());
acHolder.binding.username.setCompoundDrawables(null, null, ac.getStatus() == DBStatus.LOCAL_EDITED.getId()
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java
index 7aa0dc199..72e444fb0 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java
@@ -11,11 +11,13 @@ import android.widget.AdapterView.OnItemClickListener;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import java.util.List;
@@ -24,22 +26,25 @@ import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.DialogBoardShareBinding;
import it.niedermann.nextcloud.deck.model.AccessControl;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.User;
import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.card.UserAutoCompleteAdapter;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
import it.niedermann.nextcloud.deck.ui.theme.ThemedSnackbar;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
public class AccessControlDialogFragment extends DialogFragment implements AccessControlChangedListener, OnItemClickListener {
- private MainViewModel viewModel;
+ private AccessControlViewModel accessControlViewModel;
private DialogBoardShareBinding binding;
+ private static final String KEY_ACCOUNT = "account";
private static final String KEY_BOARD_ID = "board_id";
+ private Account account;
private long boardId;
private UserAutoCompleteAdapter userAutoCompleteAdapter;
private AccessControlAdapter adapter;
@@ -49,8 +54,14 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces
super.onAttach(context);
final Bundle args = getArguments();
- if (args == null || !args.containsKey(KEY_BOARD_ID)) {
- throw new IllegalArgumentException(KEY_BOARD_ID + " must be provided as arguments");
+ if (args == null || !args.containsKey(KEY_ACCOUNT) || !args.containsKey(KEY_BOARD_ID)) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " and " + KEY_BOARD_ID + " must be provided as arguments");
+ }
+
+ this.account = (Account) args.getSerializable(KEY_ACCOUNT);
+
+ if (this.account == null) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " must not be null");
}
this.boardId = args.getLong(KEY_BOARD_ID);
@@ -65,22 +76,28 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces
public Dialog onCreateDialog(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
+ accessControlViewModel = new ViewModelProvider(requireActivity(), new SyncViewModel.Factory(requireActivity().getApplication(), account)).get(AccessControlViewModel.class);
final var dialogBuilder = new MaterialAlertDialogBuilder(requireContext());
binding = DialogBoardShareBinding.inflate(requireActivity().getLayoutInflater());
- adapter = new AccessControlAdapter(viewModel.getCurrentAccount(), this, requireContext());
+
+ adapter = new AccessControlAdapter(account, this, requireContext());
binding.peopleList.setAdapter(adapter);
- viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (FullBoard fullBoard) -> {
+ accessControlViewModel.getFullBoardById(account.getId(), boardId).observe(this, (FullBoard fullBoard) -> {
if (fullBoard != null) {
- viewModel.getAccessControlByLocalBoardId(viewModel.getCurrentAccount().getId(), boardId).observe(this, (List<AccessControl> accessControlList) -> {
+ accessControlViewModel.getAccessControlByLocalBoardId(fullBoard.getAccountId(), fullBoard.getLocalId()).observe(this, (List<AccessControl> accessControlList) -> {
final AccessControl ownerControl = new AccessControl();
ownerControl.setLocalId(HEADER_ITEM_LOCAL_ID);
ownerControl.setUser(fullBoard.getOwner());
accessControlList.add(0, ownerControl);
adapter.update(accessControlList, fullBoard.getBoard().isPermissionManage());
- userAutoCompleteAdapter = new UserAutoCompleteAdapter(requireActivity(), viewModel.getCurrentAccount(), boardId);
+ try {
+ userAutoCompleteAdapter = new UserAutoCompleteAdapter(requireActivity(), account, boardId);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ ExceptionDialogFragment.newInstance(e, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ // TODO Handle error
+ }
binding.people.setAdapter(userAutoCompleteAdapter);
binding.people.setOnItemClickListener(this);
});
@@ -91,6 +108,7 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces
dismiss();
}
});
+
return dialogBuilder
.setTitle(R.string.share_board)
.setView(binding.getRoot())
@@ -106,7 +124,7 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces
@Override
public void updateAccessControl(AccessControl accessControl) {
- viewModel.updateAccessControl(accessControl, new IResponseCallback<>() {
+ accessControlViewModel.updateAccessControl(accessControl, new IResponseCallback<>() {
@Override
public void onResponse(AccessControl response) {
DeckLog.info("Successfully updated", AccessControl.class.getSimpleName(), "for user", accessControl.getUser().getDisplayname());
@@ -115,14 +133,14 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces
@Override
public void onError(Throwable throwable) {
IResponseCallback.super.onError(throwable);
- requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ ExceptionDialogFragment.newInstance(throwable, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
});
}
@Override
public void deleteAccessControl(AccessControl ac) {
- viewModel.deleteAccessControl(ac, new IResponseCallback<>() {
+ accessControlViewModel.deleteAccessControl(ac, new IResponseCallback<>() {
@Override
public void onResponse(Void response) {
DeckLog.info("Successfully deleted access control for user", ac.getUser().getDisplayname());
@@ -130,11 +148,13 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces
@Override
public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
IResponseCallback.super.onError(throwable);
- requireActivity().runOnUiThread(() -> ThemedSnackbar.make(requireView(), getString(R.string.error_revoking_ac, ac.getUser().getDisplayname()), Snackbar.LENGTH_LONG)
- .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
- .show());
+
+ accessControlViewModel.getCurrentBoardColor(ac.getAccountId(), ac.getBoardId())
+ .thenAcceptAsync(color -> ThemedSnackbar.make(requireView(), getString(R.string.error_revoking_ac, ac.getUser().getDisplayname()), Snackbar.LENGTH_LONG, color)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+ .show(), ContextCompat.getMainExecutor(requireContext()));
}
}
});
@@ -150,7 +170,7 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces
ac.setType(0L); // https://github.com/nextcloud/deck/blob/master/docs/API.md#post-boardsboardidacl---add-new-acl-rule
ac.setUserId(user.getLocalId());
ac.setUser(user);
- viewModel.createAccessControl(viewModel.getCurrentAccount().getId(), ac, new IResponseCallback<>() {
+ accessControlViewModel.createAccessControl(account, ac, new IResponseCallback<>() {
@Override
public void onResponse(AccessControl response) {
DeckLog.info("Successfully created", AccessControl.class.getSimpleName(), "for user", user.getDisplayname());
@@ -159,7 +179,7 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces
@Override
public void onError(Throwable throwable) {
IResponseCallback.super.onError(throwable);
- requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ ExceptionDialogFragment.newInstance(throwable, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
});
binding.people.setText("");
@@ -174,10 +194,11 @@ public class AccessControlDialogFragment extends DialogFragment implements Acces
this.adapter.applyTheme(color);
}
- public static DialogFragment newInstance(long boardLocalId) {
+ public static DialogFragment newInstance(@NonNull Account account, long boardLocalId) {
final DialogFragment dialog = new AccessControlDialogFragment();
final Bundle args = new Bundle();
+ args.putSerializable(KEY_ACCOUNT, account);
args.putLong(KEY_BOARD_ID, boardLocalId);
dialog.setArguments(args);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlViewModel.java
new file mode 100644
index 000000000..d4ed8ba32
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlViewModel.java
@@ -0,0 +1,48 @@
+package it.niedermann.nextcloud.deck.ui.board.accesscontrol;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.AccessControl;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
+
+public class AccessControlViewModel extends SyncViewModel {
+
+ public AccessControlViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ super(application, account);
+ }
+
+ public LiveData<FullBoard> getFullBoardById(long accountId, long localId) {
+ return baseRepository.getFullBoardById(accountId, localId);
+ }
+
+ public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, long id) {
+ return baseRepository.getAccessControlByLocalBoardId(accountId, id);
+ }
+
+ public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) {
+ return baseRepository.getCurrentBoardColor(accountId, boardId);
+ }
+
+ public void createAccessControl(@NonNull Account account, @NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) {
+ syncManager.createAccessControl(account.getId(), entity, callback);
+ }
+
+ public void updateAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<AccessControl> callback) {
+ syncManager.updateAccessControl(entity, callback);
+ }
+
+ public void deleteAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback<Void> callback) {
+ syncManager.deleteAccessControl(entity, callback);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardDialogFragment.java
index 9e05fbd80..78abe2b2c 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardDialogFragment.java
@@ -1,7 +1,8 @@
-package it.niedermann.nextcloud.deck.ui.board;
+package it.niedermann.nextcloud.deck.ui.board.edit;
import android.app.Dialog;
import android.content.Context;
+import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -20,18 +21,22 @@ import java.util.Objects;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogTextColorInputBinding;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.full.FullBoard;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
-public class EditBoardDialogFragment extends DialogFragment {
+public class EditBoardDialogFragment extends DialogFragment implements Themed {
private DialogTextColorInputBinding binding;
+ private static final String KEY_ACCOUNT = "account";
private static final String KEY_BOARD_ID = "board_id";
private EditBoardListener editBoardListener;
+ private Account account;
+
private FullBoard fullBoard = null;
@Override
@@ -42,6 +47,14 @@ public class EditBoardDialogFragment extends DialogFragment {
} else {
throw new ClassCastException("Caller must implement " + EditBoardListener.class.getCanonicalName());
}
+
+ final var args = getArguments();
+
+ if (args == null || !args.containsKey(KEY_ACCOUNT)) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided");
+ }
+
+ this.account = (Account) args.getSerializable(KEY_ACCOUNT);
}
@NonNull
@@ -53,33 +66,40 @@ public class EditBoardDialogFragment extends DialogFragment {
final var builder = new MaterialAlertDialogBuilder(requireContext())
.setView(binding.getRoot())
.setNeutralButton(android.R.string.cancel, null);
+ final var viewModel = new ViewModelProvider(requireActivity()).get(EditBoardViewModel.class);
final var args = getArguments();
- if (args != null && args.containsKey(KEY_BOARD_ID)) {
+
+ if (args == null || (!args.containsKey(KEY_BOARD_ID) && !args.containsKey(KEY_ACCOUNT))) {
+ throw new IllegalArgumentException("Bundle must at least contain " + KEY_ACCOUNT + " or " + KEY_BOARD_ID);
+ }
+
+ if (args.containsKey(KEY_BOARD_ID)) {
+ final long boardId = args.getLong(KEY_BOARD_ID);
builder.setTitle(R.string.edit_board);
builder.setPositiveButton(R.string.simple_save, (dialog, which) -> {
this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor());
this.fullBoard.board.setTitle(binding.input.getText().toString());
this.editBoardListener.onUpdateBoard(fullBoard);
});
- final var viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
- viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), args.getLong(KEY_BOARD_ID)).observe(EditBoardDialogFragment.this, fullBoard -> {
+ viewModel.getFullBoardById(account.getId(), boardId).observe(this, fullBoard -> {
if (fullBoard.board != null) {
this.fullBoard = fullBoard;
- final var utils = ThemeUtils.of(fullBoard.getBoard().getColor(), requireContext());
+ applyTheme(fullBoard.getBoard().getColor());
final String title = this.fullBoard.getBoard().getTitle();
binding.input.setText(title);
binding.input.setSelection(title.length());
binding.colorChooser.selectColor(this.fullBoard.getBoard().getColor());
- utils.material.colorTextInputLayout(binding.inputWrapper);
}
});
} else {
builder.setTitle(R.string.add_board);
- builder.setPositiveButton(R.string.simple_add, (dialog, which) -> editBoardListener.onCreateBoard(binding.input.getText().toString(), binding.colorChooser.getSelectedColor()));
+ builder.setPositiveButton(R.string.simple_add, (dialog, which) -> editBoardListener.onCreateBoard(account, binding.input.getText().toString(), binding.colorChooser.getSelectedColor()));
binding.colorChooser.selectColor(ContextCompat.getColor(requireContext(), R.color.board_default_color));
+
+ viewModel.getAccountColor(account.getId()).observe(this, this::applyTheme);
}
return builder.create();
@@ -94,22 +114,42 @@ public class EditBoardDialogFragment extends DialogFragment {
}
@Override
+ public void onDismiss(@NonNull DialogInterface dialog) {
+ super.onDismiss(dialog);
+ editBoardListener.onDismiss(dialog);
+ }
+
+ @Override
public void onDestroy() {
super.onDestroy();
this.binding = null;
}
- public static DialogFragment newInstance(long boardId) {
+ @Override
+ public void applyTheme(int color) {
+ final var utils = ThemeUtils.of(color, requireContext());
+
+ utils.material.colorTextInputLayout(binding.inputWrapper);
+ }
+
+ public static DialogFragment newInstance(@NonNull Account account, long boardId) {
final DialogFragment dialog = new EditBoardDialogFragment();
- final Bundle args = new Bundle();
+ final var args = new Bundle();
+ args.putSerializable(KEY_ACCOUNT, account);
args.putLong(KEY_BOARD_ID, boardId);
dialog.setArguments(args);
return dialog;
}
- public static DialogFragment newInstance() {
- return new EditBoardDialogFragment();
+ public static DialogFragment newInstance(@NonNull Account account) {
+ final DialogFragment dialog = new EditBoardDialogFragment();
+
+ final var args = new Bundle();
+ args.putSerializable(KEY_ACCOUNT, account);
+ dialog.setArguments(args);
+
+ return dialog;
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardListener.java
new file mode 100644
index 000000000..d261f668e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardListener.java
@@ -0,0 +1,17 @@
+package it.niedermann.nextcloud.deck.ui.board.edit;
+
+import android.content.DialogInterface;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+
+public interface EditBoardListener extends DialogInterface.OnDismissListener {
+ void onUpdateBoard(FullBoard fullBoard);
+
+ default void onCreateBoard(@NonNull Account account, String title, @ColorInt int color) {
+ // Creating board is not necessary
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardViewModel.java
new file mode 100644
index 000000000..3b094ecf7
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardViewModel.java
@@ -0,0 +1,24 @@
+package it.niedermann.nextcloud.deck.ui.board.edit;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
+
+public class EditBoardViewModel extends BaseViewModel {
+
+ public EditBoardViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ public LiveData<FullBoard> getFullBoardById(long accountId, long localId) {
+ return baseRepository.getFullBoardById(accountId, localId);
+ }
+
+ public LiveData<Integer> getAccountColor(long accountId) {
+ return baseRepository.getAccountColor(accountId);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/LabelsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/LabelsViewModel.java
new file mode 100644
index 000000000..28fcee9e6
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/LabelsViewModel.java
@@ -0,0 +1,42 @@
+package it.niedermann.nextcloud.deck.ui.board.managelabels;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
+
+public class LabelsViewModel extends SyncViewModel {
+
+ public LabelsViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ super(application, account);
+ }
+
+ public LiveData<FullBoard> getFullBoardById(Long boardLocalId) {
+ return new ReactiveLiveData<>(baseRepository.getFullBoardById(account.getId(), boardLocalId));
+ }
+
+ public void updateLabel(@NonNull Label label, @NonNull IResponseCallback<Label> callback) {
+ syncManager.updateLabel(label, callback);
+ }
+
+ public void createLabel(@NonNull Label label, long localBoardId, @NonNull IResponseCallback<Label> callback) {
+ syncManager.createLabel(account.getId(), label, localBoardId, callback);
+ }
+
+ public void deleteLabel(@NonNull Label label, @NonNull IResponseCallback<Void> callback) {
+ syncManager.deleteLabel(label, callback);
+ }
+
+ public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback<Integer> callback) {
+ baseRepository.countCardsWithLabel(localLabelId, callback);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java
index ca3547ae7..677776fce 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java
@@ -21,22 +21,25 @@ import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.DialogBoardManageLabelsBinding;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.theme.DeleteAlertDialogBuilder;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
public class ManageLabelsDialogFragment extends ThemedDialogFragment implements ManageLabelListener, EditLabelListener {
- private MainViewModel viewModel;
+ private LabelsViewModel labelsViewModel;
private DialogBoardManageLabelsBinding binding;
private ManageLabelsAdapter adapter;
private String[] colors;
+ private static final String KEY_ACCOUNT = "account";
private static final String KEY_BOARD_ID = "board_id";
+ private Account account;
private long boardId;
@Override
@@ -44,8 +47,14 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements
super.onAttach(context);
final Bundle args = getArguments();
- if (args == null || !args.containsKey(KEY_BOARD_ID)) {
- throw new IllegalArgumentException(KEY_BOARD_ID + " must be provided as arguments");
+ if (args == null || !args.containsKey(KEY_ACCOUNT) || !args.containsKey(KEY_BOARD_ID)) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " and " + KEY_BOARD_ID + " must be provided as arguments");
+ }
+
+ this.account = (Account) args.getSerializable(KEY_ACCOUNT);
+
+ if (this.account == null) {
+ throw new IllegalStateException(KEY_ACCOUNT + " must not be null");
}
this.boardId = args.getLong(KEY_BOARD_ID);
@@ -60,13 +69,13 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements
public Dialog onCreateDialog(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
+ labelsViewModel = new ViewModelProvider(requireActivity(), new SyncViewModel.Factory(this.requireActivity().getApplication(), account)).get(LabelsViewModel.class);
final var dialogBuilder = new MaterialAlertDialogBuilder(requireContext());
binding = DialogBoardManageLabelsBinding.inflate(requireActivity().getLayoutInflater());
colors = getResources().getStringArray(R.array.board_default_colors);
adapter = new ManageLabelsAdapter(this, requireContext());
binding.labels.setAdapter(adapter);
- viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (fullBoard) -> {
+ labelsViewModel.getFullBoardById(boardId).observe(this, fullBoard -> {
if (fullBoard == null) {
throw new IllegalStateException("FullBoard should not be null");
}
@@ -80,7 +89,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements
label.setTitle(binding.addLabelTitle.getText().toString());
label.setColor(colors[new Random().nextInt(colors.length)]);
- viewModel.createLabel(viewModel.getCurrentAccount().getId(), label, boardId, new IResponseCallback<>() {
+ labelsViewModel.createLabel(label, boardId, new IResponseCallback<>() {
@Override
public void onResponse(Label response) {
requireActivity().runOnUiThread(() -> {
@@ -124,10 +133,11 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements
utils.material.colorTextInputLayout(binding.addLabelTitleWrapper);
}
- public static DialogFragment newInstance(long boardLocalId) {
+ public static DialogFragment newInstance(@NonNull Account account, long boardLocalId) {
final DialogFragment dialog = new ManageLabelsDialogFragment();
final Bundle args = new Bundle();
+ args.putSerializable(KEY_ACCOUNT, account);
args.putLong(KEY_BOARD_ID, boardLocalId);
dialog.setArguments(args);
@@ -136,7 +146,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements
@Override
public void requestDelete(@NonNull Label label) {
- viewModel.countCardsWithLabel(label.getLocalId(), (count) -> requireActivity().runOnUiThread(() -> {
+ labelsViewModel.countCardsWithLabel(label.getLocalId(), count -> requireActivity().runOnUiThread(() -> {
if (count > 0) {
new DeleteAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete_something, label.getTitle()))
@@ -151,7 +161,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements
}
private void deleteLabel(@NonNull Label label) {
- viewModel.deleteLabel(label, new IResponseCallback<>() {
+ labelsViewModel.deleteLabel(label, new IResponseCallback<>() {
@Override
public void onResponse(Void response) {
DeckLog.info("Successfully deleted label", label.getTitle());
@@ -159,7 +169,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements
@Override
public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
IResponseCallback.super.onError(throwable);
toastFromThread(throwable.getLocalizedMessage());
}
@@ -174,7 +184,7 @@ public class ManageLabelsDialogFragment extends ThemedDialogFragment implements
@Override
public void onLabelUpdated(@NonNull Label label) {
- viewModel.updateLabel(label, new IResponseCallback<>() {
+ labelsViewModel.updateLabel(label, new IResponseCallback<>() {
@Override
public void onResponse(Label label) {
DeckLog.info("Successfully update label", label.getTitle());
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java
index 34a9157ff..fb2ee19b2 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java
@@ -13,11 +13,11 @@ import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
-import androidx.core.graphics.drawable.DrawableCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.google.android.material.card.MaterialCardView;
+import com.nextcloud.android.common.ui.theme.utils.ColorRole;
import org.jetbrains.annotations.Contract;
@@ -31,12 +31,12 @@ import it.niedermann.nextcloud.deck.model.Card;
import it.niedermann.nextcloud.deck.model.User;
import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.ui.theme.DeckViewThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
import it.niedermann.nextcloud.deck.util.AttachmentUtil;
import it.niedermann.nextcloud.deck.util.DateUtil;
import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
-import scheme.Scheme;
public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder {
@@ -48,7 +48,7 @@ public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder {
* Removes all {@link OnClickListener} and {@link OnLongClickListener}
*/
@CallSuper
- public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @NonNull Scheme scheme) {
+ public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @Nullable ThemeUtils utils) {
final var context = itemView.getContext();
bindCardClickListener(null);
@@ -57,7 +57,9 @@ public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder {
getCardMenu().setVisibility(hasEditPermission ? View.VISIBLE : View.GONE);
getCardTitle().setText(fullCard.getCard().getTitle().trim());
- DrawableCompat.setTint(getNotSyncedYet().getDrawable(), scheme.getOnPrimaryContainer());
+ if (utils != null) {
+ utils.platform.colorImageView(getNotSyncedYet(), ColorRole.PRIMARY);
+ }
// TODO should be discussed with UX
// utils.material.themeCardView(getCard());
@@ -111,9 +113,8 @@ public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder {
}
private static void setupDueDate(@NonNull TextView cardDueDate, @NonNull Card card) {
- final var context = cardDueDate.getContext();
- cardDueDate.setText(DateUtil.getRelativeDateTimeString(context, card.getDueDate().toEpochMilli()));
- ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate().atZone(ZoneId.systemDefault()).toLocalDate());
+ cardDueDate.setText(DateUtil.getRelativeDateTimeString(cardDueDate.getContext(), card.getDueDate().toEpochMilli()));
+ DeckViewThemeUtils.themeDueDate(cardDueDate, card.getDueDate().atZone(ZoneId.systemDefault()).toLocalDate());
}
protected static void setupCoverImages(@NonNull Account account, @NonNull ViewGroup coverImagesHolder, @NonNull FullCard fullCard, int maxCoverImagesCount) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardActionListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardActionListener.java
new file mode 100644
index 000000000..fe5050eba
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardActionListener.java
@@ -0,0 +1,23 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+
+public interface CardActionListener {
+
+ void onArchive(@NonNull FullCard fullCard);
+
+ void onDelete(@NonNull FullCard fullCard);
+
+ void onAssignCurrentUser(@NonNull FullCard fullCard);
+
+ void onUnassignCurrentUser(@NonNull FullCard fullCard);
+
+ void onMove(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard);
+
+ void onShareLink(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard);
+
+ void onShareContent(@NonNull FullCard fullCard);
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java
index 1d003ec78..38388615b 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java
@@ -1,91 +1,68 @@
package it.niedermann.nextcloud.deck.ui.card;
import static androidx.preference.PreferenceManager.getDefaultSharedPreferences;
-import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN;
import android.app.Activity;
import android.content.ClipData;
-import android.content.Intent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import it.niedermann.android.crosstabdnd.DragAndDropAdapter;
import it.niedermann.android.crosstabdnd.DraggedItemLocalState;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.ItemCardCompactBinding;
import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding;
import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultOnlyTitleBinding;
-import it.niedermann.nextcloud.deck.model.Card;
-import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-import it.niedermann.nextcloud.deck.ui.movecard.MoveCardDialogFragment;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
-import it.niedermann.nextcloud.deck.ui.theme.Themed;
-import it.niedermann.nextcloud.deck.util.CardUtil;
-import scheme.Scheme;
-public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> implements DragAndDropAdapter<FullCard>, CardOptionsItemSelectedListener, Themed {
+public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> implements DragAndDropAdapter<FullCard>, CardOptionsItemSelectedListener {
- private final ExecutorService executor;
private final boolean compactMode;
+ @Nullable
+ private Account account;
+ @Nullable
+ private FullBoard fullBoard;
@NonNull
- protected final MainViewModel mainViewModel;
- @NonNull
- protected final FragmentManager fragmentManager;
- private final long stackId;
- @NonNull
- protected final Activity activity;
+ private final Activity activity;
@Nullable
private final SelectCardListener selectCardListener;
@NonNull
- protected List<FullCard> cardList = new ArrayList<>();
+ private final CardActionListener cardActionListener;
@NonNull
- protected String counterMaxValue;
+ private final List<FullCard> cardList = new ArrayList<>();
@NonNull
- protected Scheme scheme;
- @StringRes
- private final int shareLinkRes;
- protected final int maxCoverImages;
-
- public CardAdapter(@NonNull Activity activity, @NonNull FragmentManager fragmentManager, long stackId, @NonNull MainViewModel mainViewModel, @Nullable SelectCardListener selectCardListener) {
- this(activity, fragmentManager, stackId, mainViewModel, selectCardListener, Executors.newSingleThreadExecutor());
- }
-
- private CardAdapter(@NonNull Activity activity, @NonNull FragmentManager fragmentManager, long stackId, @NonNull MainViewModel mainViewModel, @Nullable SelectCardListener selectCardListener, @NonNull ExecutorService executor) {
+ private final String counterMaxValue;
+ @Nullable
+ private ThemeUtils utils;
+ private final int maxCoverImages;
+
+ public CardAdapter(
+ @NonNull Activity activity,
+ @NonNull CardActionListener cardActionListener,
+ @Nullable SelectCardListener selectCardListener
+ ) {
this.activity = activity;
this.counterMaxValue = this.activity.getString(R.string.counter_max_value);
- this.fragmentManager = fragmentManager;
- this.shareLinkRes = mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().getShareLinkResource();
- this.stackId = stackId;
- this.mainViewModel = mainViewModel;
+ this.cardActionListener = cardActionListener;
this.selectCardListener = selectCardListener;
- this.scheme = ThemeUtils.createScheme(ContextCompat.getColor(this.activity, R.color.defaultBrand), this.activity);
this.compactMode = getDefaultSharedPreferences(this.activity).getBoolean(this.activity.getString(R.string.pref_key_compact), false);
- this.maxCoverImages = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.pref_key_cover_images), true)
- ? activity.getResources().getInteger(R.integer.max_cover_images)
- : 0;
+ this.maxCoverImages = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.pref_key_cover_images), true) ? activity.getResources().getInteger(R.integer.max_cover_images) : 0;
setHasStableIds(true);
- this.executor = executor;
}
@Override
@@ -123,15 +100,22 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im
@Override
public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) {
+ if (account == null) {
+ throw new IllegalStateException("Tried to bind viewholder while account is still null");
+ }
+ if (fullBoard == null) {
+ throw new IllegalStateException("Tried to bind viewholder while fullBoard is still null");
+ }
+
@NonNull final var fullCard = cardList.get(position);
- viewHolder.bind(fullCard, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardRemoteId(), mainViewModel.currentBoardHasEditPermission(), R.menu.card_menu, this, counterMaxValue, scheme);
+ viewHolder.bind(fullCard, account, fullBoard.getBoard().getId(), fullBoard.board.isPermissionEdit(), R.menu.card_menu, this, counterMaxValue, utils);
// Only enable details view if there is no one waiting for selecting a card.
viewHolder.bindCardClickListener((v) -> {
if (selectCardListener == null) {
- activity.startActivity(EditActivity.createEditCardIntent(activity, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId()));
+ activity.startActivity(EditActivity.createEditCardIntent(activity, account, fullBoard.getBoard().getLocalId(), fullCard.getLocalId()));
} else {
- selectCardListener.onCardSelected(fullCard);
+ selectCardListener.onCardSelected(fullCard, fullBoard.getLocalId());
}
});
@@ -139,7 +123,7 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im
if (selectCardListener == null) {
viewHolder.bindCardLongClickListener((v) -> {
DeckLog.log("Starting drag and drop");
- v.startDrag(ClipData.newPlainText("cardid", String.valueOf(fullCard.getLocalId())),
+ v.startDragAndDrop(ClipData.newPlainText("cardid", String.valueOf(fullCard.getLocalId())),
new View.DragShadowBuilder(v),
new DraggedItemLocalState<>(fullCard, viewHolder.getDraggable(), this, position),
0
@@ -177,80 +161,51 @@ public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> im
notifyItemRemoved(position);
}
- public void setCardList(@NonNull List<FullCard> cardList) {
- this.cardList.clear();
- this.cardList.addAll(cardList);
- notifyDataSetChanged();
+ public void setAccount(@NonNull Account account) {
+ this.account = account;
}
- @Override
- public void applyTheme(int color) {
- this.scheme = ThemeUtils.createScheme(color, activity);
+ public void setFullBoard(@NonNull FullBoard fullBoard) {
+ this.fullBoard = fullBoard;
+ }
+
+ public void setCardList(@NonNull List<FullCard> cardList, @ColorInt int color) {
+ this.utils = ThemeUtils.of(color, activity);
+ this.cardList.clear();
+ this.cardList.addAll(cardList);
notifyDataSetChanged();
}
@Override
public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) {
final int itemId = menuItem.getItemId();
- final var account = mainViewModel.getCurrentAccount();
if (itemId == R.id.share_link) {
- final var shareIntent = new Intent()
- .setAction(Intent.ACTION_SEND)
- .setType(TEXT_PLAIN)
- .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle())
- .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle())
- .putExtra(Intent.EXTRA_TEXT, account.getUrl() + activity.getString(shareLinkRes, mainViewModel.getCurrentBoardRemoteId(), fullCard.getCard().getId()));
- activity.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle()));
+ if (fullBoard == null) {
+ DeckLog.warn("Can not share link to card", fullCard.getCard().getTitle(), "because fullBoard is null");
+ return false;
+ }
+ cardActionListener.onShareLink(fullBoard, fullCard);
return true;
} else if (itemId == R.id.share_content) {
- final var shareIntent = new Intent()
- .setAction(Intent.ACTION_SEND)
- .setType(TEXT_PLAIN)
- .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle())
- .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle())
- .putExtra(Intent.EXTRA_TEXT, CardUtil.getCardContentAsString(activity, fullCard));
- activity.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle()));
+ cardActionListener.onShareContent(fullCard);
} else if (itemId == R.id.action_card_assign) {
- executor.submit(() -> mainViewModel.assignUserToCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard()));
+ cardActionListener.onAssignCurrentUser(fullCard);
return true;
} else if (itemId == R.id.action_card_unassign) {
- executor.submit(() -> mainViewModel.unassignUserFromCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard()));
+ cardActionListener.onUnassignCurrentUser(fullCard);
return true;
} else if (itemId == R.id.action_card_move) {
- DeckLog.verbose("[Move card] Launch move dialog for " + Card.class.getSimpleName() + " \"" + fullCard.getCard().getTitle() + "\" (#" + fullCard.getLocalId() + ") from " + Stack.class.getSimpleName() + " #" + +stackId);
- MoveCardDialogFragment
- .newInstance(fullCard.getAccountId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getCard().getTitle(), fullCard.getLocalId(), CardUtil.cardHasCommentsOrAttachments(fullCard))
- .show(fragmentManager, MoveCardDialogFragment.class.getSimpleName());
+ if (fullBoard == null) {
+ DeckLog.warn("Can not move card", fullCard.getCard().getTitle(), "because fullBoard is null");
+ return false;
+ }
+ cardActionListener.onMove(fullBoard, fullCard);
return true;
} else if (itemId == R.id.action_card_archive) {
- mainViewModel.archiveCard(fullCard, new IResponseCallback<>() {
- @Override
- public void onResponse(FullCard response) {
- DeckLog.info("Successfully archived", Card.class.getSimpleName(), fullCard.getCard().getTitle());
- }
-
- @Override
- public void onError(Throwable throwable) {
- IResponseCallback.super.onError(throwable);
- activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()));
- }
- });
+ cardActionListener.onArchive(fullCard);
return true;
} else if (itemId == R.id.action_card_delete) {
- mainViewModel.deleteCard(fullCard.getCard(), new IResponseCallback<>() {
- @Override
- public void onResponse(Void response) {
- DeckLog.info("Successfully deleted card", fullCard.getCard().getTitle());
- }
-
- @Override
- public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
- IResponseCallback.super.onError(throwable);
- activity.runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()));
- }
- }
- });
+ cardActionListener.onDelete(fullCard);
return true;
}
return true;
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardTabAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardTabAdapter.java
index 68c95bfb6..89462c997 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardTabAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardTabAdapter.java
@@ -5,6 +5,7 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.ui.card.activities.CardActivityFragment;
import it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentsFragment;
import it.niedermann.nextcloud.deck.ui.card.comments.CardCommentsFragment;
@@ -12,10 +13,15 @@ import it.niedermann.nextcloud.deck.ui.card.details.CardDetailsFragment;
public class CardTabAdapter extends FragmentStateAdapter {
+ private final Account account;
private boolean hasCommentsAbility = false;
- public CardTabAdapter(final FragmentActivity fa) {
+ public CardTabAdapter(
+ @NonNull final FragmentActivity fa,
+ @NonNull final Account account
+ ) {
super(fa);
+ this.account = account;
}
@NonNull
@@ -23,12 +29,12 @@ public class CardTabAdapter extends FragmentStateAdapter {
public Fragment createFragment(int position) {
switch (position) {
case 0:
- return CardDetailsFragment.newInstance();
+ return CardDetailsFragment.newInstance(account);
case 1:
return CardAttachmentsFragment.newInstance();
case 2:
return hasCommentsAbility
- ? CardCommentsFragment.newInstance()
+ ? CardCommentsFragment.newInstance(account)
: CardActivityFragment.newInstance();
case 3:
if (hasCommentsAbility) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java
index fd93035de..cf947c90b 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java
@@ -18,7 +18,7 @@ import it.niedermann.nextcloud.deck.databinding.ItemCardCompactBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.model.full.FullCard;
-import scheme.Scheme;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
public class CompactCardViewHolder extends AbstractCardViewHolder {
private final ItemCardCompactBinding binding;
@@ -35,8 +35,8 @@ public class CompactCardViewHolder extends AbstractCardViewHolder {
* Removes all {@link OnClickListener} and {@link OnLongClickListener}
*/
@Override
- public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @NonNull Scheme scheme) {
- super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, scheme);
+ public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @Nullable ThemeUtils utils) {
+ super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, utils);
setupCoverImages(account, binding.coverImages, fullCard, Math.min(maxCoverImagesCount, 1));
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java
index 2641bf3ad..dc6e5a956 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java
@@ -18,7 +18,7 @@ import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.full.FullCard;
-import scheme.Scheme;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
public class DefaultCardViewHolder extends AbstractCardViewHolder {
private final ItemCardDefaultBinding binding;
@@ -35,8 +35,8 @@ public class DefaultCardViewHolder extends AbstractCardViewHolder {
* Removes all {@link OnClickListener} and {@link OnLongClickListener}
*/
@Override
- public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @NonNull Scheme scheme) {
- super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, scheme);
+ public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @Nullable ThemeUtils utils) {
+ super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, utils);
final var context = itemView.getContext();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java
index 895da6bd1..b49f38b0c 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java
@@ -1,13 +1,9 @@
package it.niedermann.nextcloud.deck.ui.card;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
-
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
-import android.text.Editable;
import android.text.InputFilter;
-import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
@@ -21,17 +17,20 @@ import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ActivityEditBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.model.ocs.Version;
-import it.niedermann.nextcloud.deck.ui.MainActivity;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.ui.main.MainActivity;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
import it.niedermann.nextcloud.deck.util.CardUtil;
+import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher;
public class EditActivity extends AppCompatActivity {
@@ -115,7 +114,12 @@ public class EditActivity extends AppCompatActivity {
if (account == null) {
throw new IllegalArgumentException(BUNDLE_KEY_ACCOUNT + " must not be null.");
}
- viewModel.setAccount(account);
+
+ try {
+ viewModel.setAccount(account);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ throw new RuntimeException(e);
+ }
final long cardLocalId = args.getLong(BUNDLE_KEY_CARD_LOCAL_ID);
if (cardLocalId <= 0L) {
@@ -127,25 +131,27 @@ public class EditActivity extends AppCompatActivity {
throw new IllegalArgumentException(BUNDLE_KEY_BOARD_LOCAL_ID + " must be a positive integer but was " + boardLocalId);
}
- observeOnce(viewModel.getFullBoardById(account.getId(), boardLocalId), EditActivity.this, (fullBoard -> {
- viewModel.setBoardColor(fullBoard.getBoard().getColor());
- viewModel.setCanEdit(fullBoard.getBoard().isPermissionEdit());
- invalidateOptionsMenu();
- observeOnce(viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardLocalId), EditActivity.this, (fullCard) -> {
- if (fullCard == null) {
- new MaterialAlertDialogBuilder(this)
- .setTitle(R.string.card_not_found)
- .setMessage(R.string.card_not_found_message)
- .setPositiveButton(R.string.simple_close, (a, b) -> super.finish())
- .show();
- } else {
- viewModel.initializeExistingCard(boardLocalId, fullCard, account.getServerDeckVersionAsObject().isSupported());
+ new ReactiveLiveData<>(viewModel.getFullBoardById(account.getId(), boardLocalId))
+ .observeOnce(EditActivity.this, fullBoard -> {
+ viewModel.setBoardColor(fullBoard.getBoard().getColor());
+ viewModel.setCanEdit(fullBoard.getBoard().isPermissionEdit());
invalidateOptionsMenu();
- setupViewPager();
- setupTitle();
- }
- });
- }));
+ new ReactiveLiveData<>(viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardLocalId))
+ .observeOnce(EditActivity.this, fullCard -> {
+ if (fullCard == null) {
+ new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.card_not_found)
+ .setMessage(R.string.card_not_found_message)
+ .setPositiveButton(R.string.simple_close, (a, b) -> super.finish())
+ .show();
+ } else {
+ viewModel.initializeExistingCard(boardLocalId, fullCard, account.getServerDeckVersionAsObject().isSupported());
+ invalidateOptionsMenu();
+ setupViewPager(account);
+ setupTitle();
+ }
+ });
+ });
DeckLog.verbose("Finished loading intent data: { accountId =", viewModel.getAccount().getId(), "cardId =", cardLocalId, "}");
}
@@ -212,11 +218,11 @@ public class EditActivity extends AppCompatActivity {
}
}
- private void setupViewPager() {
+ private void setupViewPager(@NonNull Account account) {
binding.tabLayout.removeAllTabs();
binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
- final var adapter = new CardTabAdapter(this);
+ final var adapter = new CardTabAdapter(this, account);
final var mediator = new TabLayoutMediator(binding.tabLayout, binding.pager, (tab, position) -> {
tab.setIcon(viewModel.hasCommentsAbility()
? tabIconsWithComments[position]
@@ -243,20 +249,7 @@ public class EditActivity extends AppCompatActivity {
binding.title.setFilters(new InputFilter[]{new InputFilter.LengthFilter(viewModel.getAccount().getServerDeckVersionAsObject().getCardTitleMaxLength())});
if (viewModel.canEdit()) {
binding.title.setHint(R.string.edit);
- binding.title.addTextChangedListener(new TextWatcher() {
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- viewModel.getFullCard().getCard().setTitle(binding.title.getText().toString());
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- }
- });
+ binding.title.addTextChangedListener(new OnTextChangedWatcher(s -> viewModel.getFullCard().getCard().setTitle(binding.title.getText().toString())));
} else {
binding.title.setEnabled(false);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java
index f4c652f96..7a553f80c 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java
@@ -1,7 +1,6 @@
package it.niedermann.nextcloud.deck.ui.card;
import static androidx.lifecycle.Transformations.distinctUntilChanged;
-import static androidx.lifecycle.Transformations.switchMap;
import android.app.Application;
import android.content.SharedPreferences;
@@ -10,14 +9,17 @@ import android.text.TextUtils;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
-import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
import java.io.File;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.android.sharedpreferences.SharedPreferenceBooleanLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
@@ -32,9 +34,10 @@ import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects;
import it.niedermann.nextcloud.deck.model.ocs.Activity;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
@SuppressWarnings("WeakerAccess")
-public class EditCardViewModel extends AndroidViewModel {
+public class EditCardViewModel extends BaseViewModel {
private SyncManager syncManager;
private Account account;
@@ -51,7 +54,6 @@ public class EditCardViewModel extends AndroidViewModel {
public EditCardViewModel(@NonNull Application application) {
super(application);
- this.syncManager = new SyncManager(application);
this.boardColor$.setValue(ContextCompat.getColor(application, R.color.primary));
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application);
}
@@ -60,19 +62,22 @@ public class EditCardViewModel extends AndroidViewModel {
* The result {@link LiveData} will emit <code>true</code> if the preview mode is enabled and <code>false</code> if the edit mode is enabled.
*/
public LiveData<Boolean> getDescriptionMode() {
- return distinctUntilChanged(switchMap(distinctUntilChanged(new SharedPreferenceBooleanLiveData(sharedPreferences, getApplication().getString(R.string.shared_preference_description_preview), false)), (isPreview) -> {
- // When we are in preview mode but the description of the card is empty, we explicitly switch to the edit mode
- final var fullCard = getFullCard();
- if (fullCard == null) {
- throw new IllegalStateException("Description mode must be queried after initializing " + EditCardViewModel.class.getSimpleName() + " with a card.");
- }
- if (isPreview && TextUtils.isEmpty(fullCard.getCard().getDescription())) {
- descriptionIsPreview.setValue(false);
- } else {
- descriptionIsPreview.setValue(isPreview);
- }
- return descriptionIsPreview;
- }));
+ return new ReactiveLiveData<>(new SharedPreferenceBooleanLiveData(sharedPreferences, getApplication().getString(R.string.shared_preference_description_preview), false))
+ .distinctUntilChanged()
+ .flatMap(isPreview -> {
+ // When we are in preview mode but the description of the card is empty, we explicitly switch to the edit mode
+ final var fullCard = getFullCard();
+ if (fullCard == null) {
+ throw new IllegalStateException("Description mode must be queried after initializing " + EditCardViewModel.class.getSimpleName() + " with a card.");
+ }
+ if (isPreview && TextUtils.isEmpty(fullCard.getCard().getDescription())) {
+ descriptionIsPreview.setValue(false);
+ } else {
+ descriptionIsPreview.setValue(isPreview);
+ }
+ return descriptionIsPreview;
+ })
+ .distinctUntilChanged();
}
/**
@@ -108,12 +113,16 @@ public class EditCardViewModel extends AndroidViewModel {
this.isSupportedVersion = isSupportedVersion;
}
- public void setAccount(@NonNull Account account) {
+ public void setAccount(@NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
this.account = account;
- this.syncManager = new SyncManager(getApplication(), account.getName());
+ this.syncManager = new SyncManager(getApplication(), account);
hasCommentsAbility = account.getServerDeckVersionAsObject().supportsComments();
}
+ public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) {
+ return baseRepository.getCurrentBoardColor(accountId, boardId);
+ }
+
public boolean hasChanges() {
if (fullCard == null) {
DeckLog.info("Can not check for changes because fullCard is null → assuming no changes have been made yet.");
@@ -155,7 +164,7 @@ public class EditCardViewModel extends AndroidViewModel {
}
public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) {
- return syncManager.getFullBoardById(accountId, localId);
+ return baseRepository.getFullBoardById(accountId, localId);
}
public void createLabel(long accountId, Label label, long localBoardId, @NonNull IResponseCallback<Label> callback) {
@@ -163,7 +172,7 @@ public class EditCardViewModel extends AndroidViewModel {
}
public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) {
- return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId);
+ return baseRepository.getFullCardWithProjectsByLocalId(accountId, cardLocalId);
}
/**
@@ -186,10 +195,10 @@ public class EditCardViewModel extends AndroidViewModel {
}
public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) {
- return syncManager.getCardByRemoteID(accountId, remoteId);
+ return baseRepository.getCardByRemoteID(accountId, remoteId);
}
public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
- return syncManager.getBoardByRemoteId(accountId, remoteId);
+ return baseRepository.getBoardByRemoteId(accountId, remoteId);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java
index f8fd967aa..decca0630 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java
@@ -1,50 +1,89 @@
package it.niedermann.nextcloud.deck.ui.card;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
-
+import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
+import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.Filter;
import androidx.activity.ComponentActivity;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
import java.util.Collection;
+import java.util.List;
import java.util.Random;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAutocompleteLabelBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter;
public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> {
- @Nullable
- private Label createLabel;
- private String lastFilterText;
- private boolean canManage = false;
- public LabelAutoCompleteAdapter(@NonNull ComponentActivity activity, long accountId, long boardId, long cardId) {
- super(activity, accountId, boardId, cardId);
+ @NonNull
+ protected final Context context;
+ @ColorInt
+ private final int createLabelColor;
+ private final ReactiveLiveData<Boolean> canManage$;
+
+ public LabelAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId, long cardId) throws NextcloudFilesAppAccountNotFoundException {
+ super(activity, account, boardId);
+ this.context = activity;
final String[] colors = activity.getResources().getStringArray(R.array.board_default_colors);
- @ColorInt int createLabelColor = Color.parseColor(colors[new Random().nextInt(colors.length)]);
- observeOnce(syncManager.getFullBoardById(accountId, boardId), activity, (fullBoard) -> {
- if (fullBoard.getBoard().isPermissionManage()) {
- canManage = true;
- createLabel = new Label();
- createLabel.setLocalId(ITEM_CREATE);
- createLabel.setBoardId(boardId);
- createLabel.setAccountId(accountId);
- createLabel.setColor(createLabelColor);
- }
- });
+ createLabelColor = Color.parseColor(colors[new Random().nextInt(colors.length)]);
+
+ canManage$ = new ReactiveLiveData<>(syncManager.getFullBoardById(account.getId(), boardId))
+ .map(FullBoard::getBoard)
+ .map(Board::isPermissionManage);
+
+ constraint$
+ .flatMap(constraint -> TextUtils.isEmpty(constraint)
+ ? syncManager.findProposalsForLabelsToAssign(account.getId(), boardId, cardId)
+ : syncManager.searchNotYetAssignedLabelsByTitle(account, boardId, cardId, constraint))
+ .map(this::filterExcluded)
+ .flatMap(this::addCreateLabelIfNeeded)
+ .distinctUntilChanged()
+ .observe(activity, this::publishResults);
+ }
+
+ private ReactiveLiveData<List<Label>> addCreateLabelIfNeeded(@NonNull List<Label> labels) {
+ return canManage$
+ .combineWith(() -> constraint$)
+ .map(args -> {
+ final var canManage = args.first;
+ final var constraint = args.second;
+ if (canManage && !TextUtils.isEmpty(constraint) && !labelTitleIsPresent(labels, constraint)) {
+ labels.add(createLabel(constraint));
+ }
+ return labels;
+ });
+ }
+
+ private boolean labelTitleIsPresent(@NonNull Collection<Label> labels, @NonNull CharSequence title) {
+ return labels.stream().map(Label::getTitle).anyMatch(title::equals);
+ }
+
+ @NonNull
+ private Label createLabel(String title) {
+ final var label = new Label();
+ label.setLocalId(null);
+ label.setBoardId(boardId);
+ label.setAccountId(account.getId());
+ label.setTitle(title);
+ label.setColor(createLabelColor);
+ return label;
}
@Override
@@ -61,11 +100,15 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> {
final int labelColor = label.getColor();
final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
- binding.label.setText(label.getTitle());
+ if (label.getLocalId() == null) {
+ binding.label.setText(String.format(context.getString(R.string.label_add, label.getTitle())));
+ } else {
+ binding.label.setText(label.getTitle());
+ }
binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
binding.label.setTextColor(color);
- if (ITEM_CREATE == label.getLocalId()) {
+ if (label.getLocalId() == null) {
final var plusIcon = DrawableCompat.wrap(ContextCompat.getDrawable(binding.label.getContext(), R.drawable.ic_plus));
DrawableCompat.setTint(plusIcon, color);
binding.label.setChipIcon(plusIcon);
@@ -75,49 +118,4 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> {
return binding.getRoot();
}
-
- @Override
- public Filter getFilter() {
- return new AutoCompleteFilter() {
- @Override
- protected FilterResults performFiltering(CharSequence constraint) {
- if (constraint != null) {
- lastFilterText = constraint.toString();
- activity.runOnUiThread(() -> {
- final var liveData = constraint.toString().trim().length() > 0
- ? syncManager.searchNotYetAssignedLabelsByTitle(accountId, boardId, cardId, constraint.toString())
- : syncManager.findProposalsForLabelsToAssign(accountId, boardId, cardId);
- observeOnce(liveData, activity, (labels -> {
- labels.removeAll(itemsToExclude);
- final boolean constraintLengthGreaterZero = constraint.toString().trim().length() > 0;
- if (canManage && constraintLengthGreaterZero && !labelTitleIsPresent(labels, constraint)) {
- if (createLabel == null) {
- throw new IllegalStateException("Owner has right to edit card, but createLabel is null");
- }
- createLabel.setTitle(String.format(activity.getString(R.string.label_add), constraint));
- labels.add(createLabel);
- }
- filterResults.values = labels;
- filterResults.count = labels.size();
- publishResults(constraint, filterResults);
- }));
- });
- }
- return filterResults;
- }
- };
- }
-
- private static boolean labelTitleIsPresent(@NonNull Collection<Label> labels, @NonNull CharSequence title) {
- for (final var label : labels) {
- if (label.getTitle().contentEquals(title)) {
- return true;
- }
- }
- return false;
- }
-
- public String getLastFilterText() {
- return this.lastFilterText;
- }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardDialog.java
index fd6231029..56ea190c6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardDialog.java
@@ -1,13 +1,12 @@
package it.niedermann.nextcloud.deck.ui.card;
+import static androidx.core.content.ContextCompat.getMainExecutor;
import static androidx.lifecycle.Transformations.distinctUntilChanged;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -15,7 +14,6 @@ import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.Toast;
-import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
@@ -23,36 +21,32 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.nextcloud.android.common.ui.theme.utils.ColorRole;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.DialogNewCardBinding;
import it.niedermann.nextcloud.deck.exceptions.OfflineException;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.preparecreate.PrepareCreateViewModel;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
+import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher;
-public class NewCardDialog extends DialogFragment implements DialogInterface.OnClickListener {
+public class NewCardDialog extends ThemedDialogFragment implements DialogInterface.OnClickListener {
+ private NewCardViewModel newCardViewModel;
private PrepareCreateViewModel viewModel;
-
private CreateCardListener createCardListener;
+ private DialogNewCardBinding binding;
+ private final MutableLiveData<Boolean> isPending = new MutableLiveData<>(false);
- private static final String ARG_ACCOUNT = "account";
- private static final String ARG_BOARD_LOCAL_ID = "board_id";
- private static final String ARG_STACK_LOCAL_ID = "stack_id";
- private static final String ARG_BRAND = "brand";
+ private static final String KEY_ACCOUNT = "account";
+ private static final String KEY_BOARD_ID = "board_id";
+ private static final String KEY_STACK_ID = "stack_id";
private Account account;
- private long boardLocalId;
- private long stackLocalId;
- @ColorInt
- private int color;
-
- private DialogNewCardBinding binding;
- private final MutableLiveData<Boolean> isPending = new MutableLiveData<>(false);
@Override
public void onAttach(@NonNull Context context) {
@@ -65,13 +59,13 @@ public class NewCardDialog extends DialogFragment implements DialogInterface.OnC
}
final var args = getArguments();
- if (args == null) {
- throw new IllegalArgumentException("Provide " + ARG_ACCOUNT + ", " + ARG_BOARD_LOCAL_ID + " and " + ARG_STACK_LOCAL_ID);
+
+ if (args == null || !args.containsKey(KEY_ACCOUNT)) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided");
}
- account = (Account) args.getSerializable(ARG_ACCOUNT);
- boardLocalId = args.getLong(ARG_BOARD_LOCAL_ID);
- stackLocalId = args.getLong(ARG_STACK_LOCAL_ID);
- color = args.getInt(ARG_BRAND);
+ this.account = (Account) getArguments().getSerializable(KEY_ACCOUNT);
+
+ newCardViewModel = new ViewModelProvider(requireActivity(), new SyncViewModel.Factory(requireActivity().getApplication(), account)).get(NewCardViewModel.class);
viewModel = new ViewModelProvider(requireActivity()).get(PrepareCreateViewModel.class);
}
@@ -95,34 +89,16 @@ public class NewCardDialog extends DialogFragment implements DialogInterface.OnC
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> onClick(dialog, DialogInterface.BUTTON_NEGATIVE));
});
- final var utils = ThemeUtils.of(color, requireContext());
-
- utils.material.colorTextInputLayout(binding.inputWrapper);
- utils.platform.colorCircularProgressBar(binding.progressCircular);
-
- binding.input.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- // Nothing to do
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- final boolean inputIsValid = inputIsValid(binding.input.getText());
- if (inputIsValid) {
- binding.inputWrapper.setError(null);
- }
- dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(inputIsValid);
- dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(inputIsValid);
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- // Nothing to do
+ binding.input.addTextChangedListener(new OnTextChangedWatcher(s -> {
+ final boolean inputIsValid = inputIsValid(binding.input.getText());
+ if (inputIsValid) {
+ binding.inputWrapper.setError(null);
}
- });
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(inputIsValid);
+ dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(inputIsValid);
+ }));
- distinctUntilChanged(isPending).observe(this, (isPending) -> {
+ distinctUntilChanged(isPending).observe(this, isPending -> {
if (isPending) {
binding.inputWrapper.setVisibility(View.INVISIBLE);
binding.progressCircular.setVisibility(View.VISIBLE);
@@ -180,35 +156,30 @@ public class NewCardDialog extends DialogFragment implements DialogInterface.OnC
isPending.setValue(true);
final var currentUserInput = binding.input.getText();
if (inputIsValid(currentUserInput)) {
- final var fullCard = viewModel.createFullCard(account.getServerDeckVersionAsObject(), currentUserInput.toString());
- viewModel.saveCard(account, boardLocalId, stackLocalId, fullCard, new IResponseCallback<>() {
- @Override
- public void onResponse(FullCard createdCard) {
- requireActivity().runOnUiThread(() -> {
- createCardListener.onCardCreated(createdCard);
-
- if (openOnSuccess) {
- startActivity(EditActivity.createEditCardIntent(requireContext(), account, boardLocalId, createdCard.getLocalId()));
- }
- dismiss();
- });
+ // TODO Check args in onAttach
+ final var args = getArguments();
+ assert args != null;
+ newCardViewModel.createFullCard(account.getId(), args.getLong(KEY_BOARD_ID), args.getLong(KEY_STACK_ID), currentUserInput.toString()).whenCompleteAsync((fullCard, throwable) -> {
+ if (throwable != null) {
+ isPending.setValue(false);
+ if (throwable instanceof OfflineException) {
+ Toast.makeText(requireContext(), ((OfflineException) throwable).getReason().getMessage(), Toast.LENGTH_LONG).show();
+ } else {
+ newCardViewModel.getCurrentAccount()
+ .thenAcceptAsync(account -> ExceptionDialogFragment
+ .newInstance(throwable, account)
+ .show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()), getMainExecutor(requireContext()));
+ }
+ } else {
+ createCardListener.onCardCreated(fullCard);
+ if (openOnSuccess) {
+ newCardViewModel
+ .createEditIntent(requireContext(), fullCard.getAccountId(), args.getLong(KEY_BOARD_ID), fullCard.getLocalId())
+ .thenAcceptAsync(this::startActivity, getMainExecutor(requireContext()));
+ }
+ dismiss();
}
-
- @Override
- public void onError(Throwable throwable) {
- IResponseCallback.super.onError(throwable);
- requireActivity().runOnUiThread(() -> {
- isPending.setValue(false);
- if (throwable instanceof OfflineException) {
- Toast.makeText(requireContext(), ((OfflineException) throwable).getReason().getMessage(), Toast.LENGTH_LONG).show();
- } else {
- ExceptionDialogFragment
- .newInstance(throwable, account)
- .show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- });
- }
- });
+ }, getMainExecutor(requireContext()));
} else {
binding.inputWrapper.setError(getString(R.string.title_is_mandatory));
binding.input.requestFocus();
@@ -227,14 +198,24 @@ public class NewCardDialog extends DialogFragment implements DialogInterface.OnC
return input != null && !input.toString().trim().isEmpty();
}
- public static DialogFragment newInstance(@NonNull Account account, long boardLocalId, long stackLocalId, @ColorInt int brand) {
- final var fragment = new NewCardDialog();
+ public static DialogFragment newInstance(@NonNull Account account, long boardId, long stackId) {
+ final NewCardDialog dialog = new NewCardDialog();
+
final var args = new Bundle();
- args.putSerializable(ARG_ACCOUNT, account);
- args.putLong(ARG_BOARD_LOCAL_ID, boardLocalId);
- args.putLong(ARG_STACK_LOCAL_ID, stackLocalId);
- args.putInt(ARG_BRAND, brand);
- fragment.setArguments(args);
- return fragment;
+ args.putSerializable(KEY_ACCOUNT, account);
+ args.putLong(KEY_BOARD_ID, boardId);
+ args.putLong(KEY_STACK_ID, stackId);
+ dialog.setArguments(args);
+
+ return dialog;
+
+ }
+
+ @Override
+ public void applyTheme(int color) {
+ final var utils = ThemeUtils.of(color, requireContext());
+
+ utils.material.colorTextInputLayout(binding.inputWrapper);
+ utils.platform.colorCircularProgressBar(binding.progressCircular, ColorRole.PRIMARY);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardViewModel.java
new file mode 100644
index 000000000..517c2267c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardViewModel.java
@@ -0,0 +1,76 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import static java.util.concurrent.CompletableFuture.supplyAsync;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.model.ocs.Version;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
+
+public class NewCardViewModel extends SyncViewModel {
+
+ public NewCardViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ super(application, account);
+ }
+
+ public CompletableFuture<Account> getCurrentAccount() {
+ return baseRepository.getCurrentAccountId().thenApplyAsync(baseRepository::readAccountDirectly);
+ }
+
+ public CompletableFuture<FullCard> createFullCard(long accountId, long boardId, long stackId, String content) {
+ final var result = new CompletableFuture<FullCard>();
+
+ supplyAsync(() -> baseRepository.readAccountDirectly(accountId))
+ .thenAcceptAsync(account -> syncManager.createFullCard(accountId, boardId, stackId, createFullCard(account.getServerDeckVersionAsObject(), content),
+ new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullCard response) {
+ result.complete(response);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ result.completeExceptionally(throwable);
+ }
+ }));
+ return result;
+ }
+
+ private FullCard createFullCard(@NonNull Version version, @NonNull String content) {
+ if (TextUtils.isEmpty(content)) {
+ throw new IllegalArgumentException("Content must not be empty.");
+ }
+ final var fullCard = new FullCard();
+ final var card = new Card();
+ final int maxLength = version.getCardTitleMaxLength();
+ if (content.length() > maxLength) {
+ card.setTitle(content.substring(0, maxLength).trim());
+ card.setDescription(content.substring(maxLength).trim());
+ } else {
+ card.setTitle(content);
+ card.setDescription(null);
+ }
+ fullCard.setCard(card);
+ return fullCard;
+ }
+
+ public CompletionStage<Intent> createEditIntent(@NonNull Context context, long accountId, long boardId, long cardId) {
+ return supplyAsync(() -> baseRepository.readAccountDirectly(accountId))
+ .thenApplyAsync(account -> EditActivity.createEditCardIntent(context, account, boardId, cardId));
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/SelectCardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/SelectCardListener.java
index 1198f7ebd..d20332faf 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/SelectCardListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/SelectCardListener.java
@@ -1,7 +1,9 @@
package it.niedermann.nextcloud.deck.ui.card;
+import androidx.annotation.NonNull;
+
import it.niedermann.nextcloud.deck.model.full.FullCard;
public interface SelectCardListener {
- void onCardSelected(FullCard fullCard);
+ void onCardSelected(@NonNull FullCard fullCard, long boardId);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java
index 3f6c62a52..34a1bc177 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java
@@ -1,40 +1,74 @@
package it.niedermann.nextcloud.deck.ui.card;
+import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.Filter;
import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Observer;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import java.util.List;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.android.util.DimensionUtil;
+import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAutocompleteUserBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst.UserSearchLiveData;
import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> {
+
+ private static final long NO_CARD = Long.MIN_VALUE;
@NonNull
private final Account account;
- private final UserSearchLiveData liveSearchForACL;
- private LiveData<List<User>> liveData;
- private Observer<List<User>> observer;
- public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId) {
+ /**
+ * Use this constructor to find users to be added to the ACL of a board
+ */
+ public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId) throws NextcloudFilesAppAccountNotFoundException {
this(activity, account, boardId, NO_CARD);
}
- public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId, long cardId) {
- super(activity, account.getId(), boardId, cardId);
+ /**
+ * Use this constructor to find users to be added to a specific card which are already in the ACL of the board
+ */
+ public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId, long cardId) throws NextcloudFilesAppAccountNotFoundException {
+ super(activity, account, boardId);
this.account = account;
- this.liveSearchForACL = syncManager.searchUserByUidOrDisplayNameForACL();
+
+ final ReactiveLiveData<List<User>> results$;
+
+ constraint$
+ .filter(constraint -> !TextUtils.isEmpty(constraint))
+ .debounce(300)
+ .observe(activity, constraint -> {
+ DeckLog.verbose("Triggering remote search");
+ syncManager.triggerUserSearch(account, constraint);
+ });
+
+ if (cardId == NO_CARD) {
+ // No card means this adapter is used for searching users for Board ACL
+ results$ = constraint$.flatMap(constraint -> TextUtils.isEmpty(constraint)
+ ? syncManager.findProposalsForUsersToAssignForACL(account.getId(), boardId, activity.getResources().getInteger(R.integer.max_users_suggested))
+ : syncManager.searchUserByUidOrDisplayNameForACL(account.getId(), boardId, constraint));
+ } else {
+ // Card is given, so we are searching for users to assign to a card (limited to users whom the board is shared with)
+ results$ = constraint$.flatMap(constraint -> TextUtils.isEmpty(constraint)
+ ? syncManager.findProposalsForUsersToAssignForCards(account.getId(), boardId, cardId, activity.getResources().getInteger(R.integer.max_users_suggested))
+ : syncManager.searchUserByUidOrDisplayNameForCards(account.getId(), boardId, cardId, constraint));
+ }
+
+ results$
+ .map(this::filterExcluded)
+ .distinctUntilChanged()
+ .observe(activity, this::publishResults);
}
@Override
@@ -47,41 +81,14 @@ public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> {
binding = ItemAutocompleteUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
}
- ViewUtil.addAvatar(binding.icon, account.getUrl(), getItem(position).getUid(), R.drawable.ic_person_grey600_24dp);
+ Glide.with(binding.icon.getContext())
+ .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.icon.getContext(), R.dimen.avatar_size), getItem(position).getUid()))
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.icon);
binding.label.setText(getItem(position).getDisplayname());
return binding.getRoot();
}
-
- @Override
- public Filter getFilter() {
- return new AutoCompleteFilter() {
- @Override
- protected FilterResults performFiltering(CharSequence constraint) {
- if (constraint != null) {
- activity.runOnUiThread(() -> {
- final int constraintLength = constraint.toString().trim().length();
- if (cardId == NO_CARD) {
- liveData = constraintLength > 0
- ? liveSearchForACL.search(accountId, boardId, constraint.toString())
- : syncManager.findProposalsForUsersToAssignForACL(accountId, boardId, activity.getResources().getInteger(R.integer.max_users_suggested));
- } else {
- liveData = constraintLength > 0
- ? syncManager.searchUserByUidOrDisplayName(accountId, boardId, cardId, constraint.toString())
- : syncManager.findProposalsForUsersToAssign(accountId, boardId, cardId, activity.getResources().getInteger(R.integer.max_users_suggested));
- }
- liveData.removeObservers(activity);
- observer = users -> {
- users.removeAll(itemsToExclude);
- filterResults.values = users;
- filterResults.count = users.size();
- publishResults(constraint, filterResults);
- };
- liveData.observe(activity, observer);
- });
- }
- return filterResults;
- }
- };
- }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java
index 0678357d4..f78edd3f7 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java
@@ -13,8 +13,8 @@ import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemActivityBinding;
import it.niedermann.nextcloud.deck.model.enums.ActivityType;
import it.niedermann.nextcloud.deck.model.ocs.Activity;
+import it.niedermann.nextcloud.deck.ui.theme.DeckViewThemeUtils;
import it.niedermann.nextcloud.deck.util.DateUtil;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
public class CardActivityViewHolder extends RecyclerView.ViewHolder {
public ItemActivityBinding binding;
@@ -75,13 +75,13 @@ public class CardActivityViewHolder extends RecyclerView.ViewHolder {
private static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @NonNull ActivityType type) {
switch (type) {
case ADD:
- ViewUtil.setImageColor(context, imageView, R.color.activity_create);
+ DeckViewThemeUtils.setImageColor(context, imageView, R.color.activity_create);
break;
case DELETE:
- ViewUtil.setImageColor(context, imageView, R.color.activity_delete);
+ DeckViewThemeUtils.setImageColor(context, imageView, R.color.activity_delete);
break;
default:
- ViewUtil.setImageColor(context, imageView, R.color.grey600);
+ DeckViewThemeUtils.setImageColor(context, imageView, R.color.grey600);
break;
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
index 9a46cec65..6452883d3 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
@@ -1,11 +1,10 @@
package it.niedermann.nextcloud.deck.ui.card.assignee;
-import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
-import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -87,11 +86,11 @@ public class CardAssigneeDialog extends DialogFragment {
final var circularProgressDrawable = new CircularProgressDrawable(context);
circularProgressDrawable.setStrokeWidth(5f);
circularProgressDrawable.setCenterRadius(30f);
- circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY);
+ circularProgressDrawable.setColorSchemeColors(isDarkMode(context) ? Color.LTGRAY : Color.DKGRAY);
circularProgressDrawable.start();
binding.avatar.post(() -> Glide.with(binding.avatar.getContext())
- .load(viewModel.getAccount().getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + binding.avatar.getWidth())
+ .load(viewModel.getAccount().getAvatarUrl(binding.avatar.getWidth(), user.getUid()))
.placeholder(circularProgressDrawable)
.error(R.drawable.ic_person_grey600_24dp)
.into(binding.avatar));
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java
index ddc35f76e..7b00f2a84 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java
@@ -7,10 +7,11 @@ import android.widget.ImageView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.nextcloud.android.common.ui.theme.utils.ColorRole;
+
import it.niedermann.android.util.ClipboardUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Account;
@@ -33,7 +34,7 @@ public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder {
setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), color);
itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
menuInflater.inflate(R.menu.attachment_menu, menu);
- if(EAttachmentType.DECK_FILE.equals(attachment.getType())) {
+ if (EAttachmentType.DECK_FILE.equals(attachment.getType())) {
menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> {
DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName());
return false;
@@ -55,9 +56,9 @@ public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder {
protected void setNotSyncedYetStatus(boolean synced, @ColorInt int color) {
final var notSyncedYet = getNotSyncedYetStatusIcon();
- final var scheme = ThemeUtils.createScheme(color, notSyncedYet.getContext());
+ final var utils = ThemeUtils.of(color, notSyncedYet.getContext());
- DrawableCompat.setTint(notSyncedYet.getDrawable(), scheme.getOnPrimaryContainer());
+ utils.platform.colorImageView(notSyncedYet, ColorRole.PRIMARY);
notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
index fa2ff1abe..61dc3e8ee 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
@@ -14,7 +14,6 @@ import static androidx.core.content.PermissionChecker.checkSelfPermission;
import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN;
import static java.net.HttpURLConnection.HTTP_CONFLICT;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentAdapter.VIEW_TYPE_DEFAULT;
import static it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentAdapter.VIEW_TYPE_IMAGE;
import static it.niedermann.nextcloud.deck.util.FilesUtil.copyContentUriToTempFile;
@@ -37,7 +36,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.SharedElementCallback;
-import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;
@@ -62,6 +60,7 @@ import id.zelory.compressor.constraint.FormatConstraint;
import id.zelory.compressor.constraint.QualityConstraint;
import id.zelory.compressor.constraint.ResolutionConstraint;
import id.zelory.compressor.constraint.SizeConstraint;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
@@ -106,11 +105,6 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet
private static final int REQUEST_CODE_PICK_CONTACT = 5;
private static final int REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION = 6;
- @ColorInt
- private int accentColor;
- @ColorInt
- private int primaryColor;
-
private CardAttachmentAdapter adapter;
private AbstractPickerAdapter<?> pickerAdapter;
@@ -141,8 +135,6 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet
}
return true;
});
- accentColor = ContextCompat.getColor(requireContext(), R.color.accent);
- primaryColor = ContextCompat.getColor(requireContext(), R.color.primary);
// This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance
// See https://github.com/stefan-niedermann/nextcloud-deck/issues/478
@@ -249,11 +241,12 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet
pickerAdapter = new GalleryAdapter(requireContext(), (uri, pair) -> {
previewViewModel.prepareDialog(pair.first, pair.second);
PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName());
- observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> {
- if (submitPositive) {
- onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri));
- }
- });
+ new ReactiveLiveData<>(previewViewModel.getResult())
+ .observeOnce(getViewLifecycleOwner(), submitPositive -> {
+ if (submitPositive) {
+ onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri));
+ }
+ });
}, this::openNativeCameraPicker, getViewLifecycleOwner());
if (binding.pickerRecyclerView.getItemDecorationCount() == 0) {
binding.pickerRecyclerView.addItemDecoration(galleryItemDecoration);
@@ -273,11 +266,12 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet
pickerAdapter = new ContactAdapter(requireContext(), (uri, pair) -> {
previewViewModel.prepareDialog(pair.first, pair.second);
PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName());
- observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> {
- if (submitPositive) {
- onActivityResult(REQUEST_CODE_PICK_CONTACT, RESULT_OK, new Intent().setData(uri));
- }
- });
+ new ReactiveLiveData<>(previewViewModel.getResult())
+ .observeOnce(getViewLifecycleOwner(), submitPositive -> {
+ if (submitPositive) {
+ onActivityResult(REQUEST_CODE_PICK_CONTACT, RESULT_OK, new Intent().setData(uri));
+ }
+ });
}, this::openNativeContactPicker);
removeGalleryItemDecoration();
binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
@@ -299,11 +293,12 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet
// pickerAdapter = new FileAdapter(requireContext(), (uri, pair) -> {
// previewViewModel.prepareDialog(pair.first, pair.second);
// PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName());
-// observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> {
-// if (submitPositive) {
-// onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri));
-// }
-// });
+// new ReactiveLiveData<>(previewViewModel.getResult())
+// .observeOnce(getViewLifecycleOwner(), submitPositive -> {
+// if (submitPositive) {
+// onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri));
+// }
+// });
// }, this::openNativeFilePicker);
// removeGalleryItemDecoration();
// binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
@@ -412,10 +407,11 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet
}
private void uploadNewAttachmentFromFile(@NonNull File fileToUpload, String mimeType) {
+ final int color = editViewModel.getAccount().getColor();
for (final var existingAttachment : editViewModel.getFullCard().getAttachments()) {
final String existingPath = existingAttachment.getLocalPath();
if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) {
- ThemedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
+ ThemedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG, color).show();
return;
}
}
@@ -444,17 +440,15 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet
@Override
public void onError(Throwable throwable) {
- requireActivity().runOnUiThread(() -> {
- if (throwable instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) throwable).getStatusCode() == HTTP_CONFLICT) {
- IResponseCallback.super.onError(throwable);
- // https://github.com/stefan-niedermann/nextcloud-deck/issues/534
- editViewModel.getFullCard().getAttachments().remove(a);
- adapter.removeAttachment(a);
- ThemedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
- } else {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", throwable), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- });
+ if (throwable instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) throwable).getStatusCode() == HTTP_CONFLICT) {
+ IResponseCallback.super.onError(throwable);
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/534
+ editViewModel.getFullCard().getAttachments().remove(a);
+ adapter.removeAttachment(a);
+ ThemedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG, color).show();
+ } else {
+ ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", throwable), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
}
});
}
@@ -506,7 +500,7 @@ public class CardAttachmentsFragment extends Fragment implements AttachmentDelet
@Override
public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
IResponseCallback.super.onError(throwable);
requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DeleteAttachmentDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DeleteAttachmentDialogFragment.java
index 532e59dce..60c92dde3 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DeleteAttachmentDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DeleteAttachmentDialogFragment.java
@@ -10,8 +10,9 @@ import androidx.fragment.app.DialogFragment;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Attachment;
import it.niedermann.nextcloud.deck.ui.theme.DeleteAlertDialogBuilder;
+import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment;
-public class DeleteAttachmentDialogFragment extends DialogFragment {
+public class DeleteAttachmentDialogFragment extends ThemedDialogFragment {
private static final String KEY_ATTACHMENT = "attachment";
@@ -47,6 +48,11 @@ public class DeleteAttachmentDialogFragment extends DialogFragment {
.create();
}
+ @Override
+ public void applyTheme(int color) {
+
+ }
+
public static DialogFragment newInstance(Attachment attachment) {
final var dialog = new DeleteAttachmentDialogFragment();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java
index 113c28932..eafe97fe5 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java
@@ -2,7 +2,7 @@ package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
-import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode;
import android.app.Dialog;
import android.content.DialogInterface;
@@ -39,14 +39,14 @@ public class PreviewDialog extends DialogFragment {
final var context = requireContext();
this.imageBuilder$ = this.viewModel.getImageBuilder();
- this.imageBuilder$.observe(requireActivity(), builder -> {
+ this.imageBuilder$.observe(this, builder -> {
if (builder == null) {
binding.avatar.setVisibility(GONE);
} else {
final var circularProgressDrawable = new CircularProgressDrawable(context);
circularProgressDrawable.setStrokeWidth(5f);
circularProgressDrawable.setCenterRadius(30f);
- circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY);
+ circularProgressDrawable.setColorSchemeColors(isDarkMode(context) ? Color.LTGRAY : Color.DKGRAY);
circularProgressDrawable.start();
binding.avatar.setVisibility(VISIBLE);
binding.avatar.post(() -> builder
@@ -55,7 +55,7 @@ public class PreviewDialog extends DialogFragment {
}
});
this.title$ = this.viewModel.getTitle();
- this.title$.observe(requireActivity(), title -> {
+ this.title$.observe(this, title -> {
if (TextUtils.isEmpty(title)) {
binding.title.setVisibility(GONE);
} else {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsAdapter.java
index b392d5725..6c2f5b86c 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsAdapter.java
@@ -1,13 +1,12 @@
package it.niedermann.nextcloud.deck.ui.card.comments;
-import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor;
-
import android.content.Context;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -20,11 +19,14 @@ import it.niedermann.nextcloud.deck.databinding.ItemCommentBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
-import scheme.Scheme;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
-public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHolder> {
+public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHolder> implements Themed {
- private final Scheme scheme;
+ @NonNull
+ private final Context context;
+ @Nullable
+ private ThemeUtils utils;
@NonNull
private final List<FullDeckComment> comments = new ArrayList<>();
@NonNull
@@ -40,14 +42,22 @@ public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHol
@NonNull
private final CommentEditedListener editListener;
- CardCommentsAdapter(@NonNull Context context, @NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager, CommentEditedListener editListener) {
+ CardCommentsAdapter(
+ @NonNull Context context,
+ @NonNull Account account,
+ @NonNull MenuInflater menuInflater,
+ @NonNull CommentDeletedListener deletedListener,
+ @NonNull CommentSelectAsReplyListener selectAsReplyListener,
+ @NonNull FragmentManager fragmentManager,
+ @NonNull CommentEditedListener editListener
+ ) {
+ this.context = context;
this.account = account;
this.menuInflater = menuInflater;
this.deletedListener = deletedListener;
this.selectAsReplyListener = selectAsReplyListener;
this.fragmentManager = fragmentManager;
this.editListener = editListener;
- this.scheme = ThemeUtils.createScheme(readBrandMainColor(context), context);
setHasStableIds(true);
}
@@ -65,7 +75,7 @@ public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHol
@Override
public void onBindViewHolder(@NonNull ItemCommentViewHolder holder, int position) {
final var comment = comments.get(position);
- holder.bind(comment, account, scheme, menuInflater, deletedListener, selectAsReplyListener, fragmentManager, (changedText) -> {
+ holder.bind(comment, account, utils, menuInflater, deletedListener, selectAsReplyListener, fragmentManager, (changedText) -> {
if (!Objects.equals(changedText, comment.getComment().getMessage())) {
DeckLog.info("Toggled checkbox in comment with localId", comment.getLocalId());
this.editListener.onCommentEdited(comment.getLocalId(), changedText.toString());
@@ -90,4 +100,10 @@ public class CardCommentsAdapter extends RecyclerView.Adapter<ItemCommentViewHol
public int getItemCount() {
return comments.size();
}
+
+ @Override
+ public void applyTheme(int color) {
+ this.utils = ThemeUtils.of(color, context);
+ notifyDataSetChanged();
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java
index 277eb82c2..ecefdc198 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java
@@ -3,6 +3,7 @@ package it.niedermann.nextcloud.deck.ui.card.comments;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
@@ -18,6 +19,9 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+
import java.time.Instant;
import it.niedermann.android.util.DimensionUtil;
@@ -25,6 +29,7 @@ import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabCommentsBinding;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
@@ -32,30 +37,46 @@ import it.niedermann.nextcloud.deck.ui.card.EditActivity;
import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
public class CardCommentsFragment extends Fragment implements CommentEditedListener, CommentDeletedListener, CommentSelectAsReplyListener {
+ private static final String KEY_ACCOUNT = "account";
private FragmentCardEditTabCommentsBinding binding;
- private EditCardViewModel mainViewModel;
+ private EditCardViewModel editCardViewModel;
private CommentsViewModel commentsViewModel;
private CardCommentsAdapter adapter;
- public static Fragment newInstance() {
- return new CardCommentsFragment();
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+
+ final var args = getArguments();
+
+ if (args == null || !args.containsKey(KEY_ACCOUNT)) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided.");
+ }
+
+ final var account = (Account) args.getSerializable(KEY_ACCOUNT);
+
+ if (account == null) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " must not be null.");
+ }
+
+ editCardViewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+ commentsViewModel = new ViewModelProvider(this, new SyncViewModel.Factory(requireActivity().getApplication(), account)).get(CommentsViewModel.class);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
- binding = FragmentCardEditTabCommentsBinding.inflate(inflater, container, false);
- mainViewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+ binding = FragmentCardEditTabCommentsBinding.inflate(inflater, container, false);
// This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance
// See https://github.com/stefan-niedermann/nextcloud-deck/issues/478
- if (mainViewModel.getFullCard() == null) {
+ if (editCardViewModel.getFullCard() == null) {
DeckLog.logError(new IllegalStateException("Cannot populate " + CardCommentsFragment.class.getSimpleName() + " because viewModel.getFullCard() is null"));
if (requireActivity() instanceof EditActivity) {
Toast.makeText(getContext(), R.string.error_edit_activity_killed_by_android, Toast.LENGTH_LONG).show();
@@ -66,12 +87,16 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe
return binding.getRoot();
}
- commentsViewModel = new ViewModelProvider(this).get(CommentsViewModel.class);
- adapter = new CardCommentsAdapter(requireContext(), mainViewModel.getAccount(), requireActivity().getMenuInflater(), this, this, getChildFragmentManager(), this);
+ adapter = new CardCommentsAdapter(requireContext(), editCardViewModel.getAccount(), requireActivity().getMenuInflater(), this, this, getChildFragmentManager(), this);
binding.comments.setAdapter(adapter);
binding.replyCommentCancelButton.setOnClickListener((v) -> commentsViewModel.setReplyToComment(null));
- ViewUtil.addAvatar(binding.avatar, mainViewModel.getAccount().getUrl(), mainViewModel.getAccount().getUserName(), DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp);
+ Glide.with(binding.avatar.getContext())
+ .load(editCardViewModel.getAccount().getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details)))
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
commentsViewModel.getReplyToComment().observe(getViewLifecycleOwner(), (comment) -> {
if (comment == null) {
@@ -79,10 +104,9 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe
} else {
binding.replyCommentText.setMarkdownString(comment.getComment().getMessage());
binding.replyComment.setVisibility(VISIBLE);
-// setupMentions(mainViewModel.getAccount(), comment.getComment().getMentions(), binding.replyCommentText);
}
});
- commentsViewModel.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(),
+ commentsViewModel.getFullCommentsForLocalCardId(editCardViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(),
(comments) -> {
if (comments != null && comments.size() > 0) {
binding.emptyContentView.setVisibility(GONE);
@@ -93,20 +117,20 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe
binding.comments.setVisibility(GONE);
}
});
- if (mainViewModel.canEdit()) {
+ if (editCardViewModel.canEdit()) {
binding.addCommentLayout.setVisibility(VISIBLE);
binding.fab.setOnClickListener(v -> {
// Do not handle empty comments (https://github.com/stefan-niedermann/nextcloud-deck/issues/440)
if (!TextUtils.isEmpty(binding.message.getText().toString().trim())) {
binding.emptyContentView.setVisibility(GONE);
binding.comments.setVisibility(VISIBLE);
- final DeckComment comment = new DeckComment(binding.message.getText().toString().trim(), mainViewModel.getAccount().getUserName(), Instant.now());
+ final DeckComment comment = new DeckComment(binding.message.getText().toString().trim(), editCardViewModel.getAccount().getUserName(), Instant.now());
final FullDeckComment parent = commentsViewModel.getReplyToComment().getValue();
if (parent != null) {
comment.setParentId(parent.getId());
commentsViewModel.setReplyToComment(null);
}
- commentsViewModel.addCommentToCard(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), comment);
+ commentsViewModel.addCommentToCard(editCardViewModel.getAccount().getId(), editCardViewModel.getFullCard().getLocalId(), comment);
}
binding.message.setText(null);
});
@@ -116,7 +140,7 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe
}
return true;
});
- binding.message.addTextChangedListener(new CardCommentsMentionProposer(getViewLifecycleOwner(), mainViewModel.getAccount(), mainViewModel.getBoardId(), binding.message, binding.mentionProposerWrapper, binding.mentionProposer));
+ binding.message.addTextChangedListener(new CardCommentsMentionProposer(getViewLifecycleOwner(), editCardViewModel.getAccount(), editCardViewModel.getBoardId(), binding.message, binding.mentionProposerWrapper, binding.mentionProposer));
} else {
binding.addCommentLayout.setVisibility(GONE);
}
@@ -126,11 +150,11 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- if (mainViewModel.canEdit()) {
+ if (editCardViewModel.canEdit()) {
binding.message.requestFocus();
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
}
- mainViewModel.getBoardColor().observe(getViewLifecycleOwner(), this::applyTheme);
+ editCardViewModel.getBoardColor().observe(getViewLifecycleOwner(), this::applyTheme);
}
@Override
@@ -141,12 +165,12 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe
@Override
public void onCommentEdited(Long id, String comment) {
- commentsViewModel.updateComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), id, comment);
+ commentsViewModel.updateComment(editCardViewModel.getAccount().getId(), editCardViewModel.getFullCard().getLocalId(), id, comment);
}
@Override
public void onCommentDeleted(Long localId) {
- commentsViewModel.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId, new IResponseCallback<>() {
+ commentsViewModel.deleteComment(editCardViewModel.getAccount().getId(), editCardViewModel.getFullCard().getLocalId(), localId, new IResponseCallback<>() {
@Override
public void onResponse(Void response) {
DeckLog.info("Successfully deleted comment with localId", localId);
@@ -154,9 +178,9 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe
@Override
public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
IResponseCallback.super.onError(throwable);
- requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, mainViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ requireActivity().runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, editCardViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
}
}
});
@@ -173,4 +197,14 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe
public void onSelectAsReply(FullDeckComment comment) {
commentsViewModel.setReplyToComment(comment);
}
+
+ public static Fragment newInstance(@NonNull Account account) {
+ final var fragment = new CardCommentsFragment();
+
+ final var args = new Bundle();
+ args.putSerializable(KEY_ACCOUNT, account);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java
index 60aef597b..5b5dead49 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java
@@ -1,9 +1,6 @@
package it.niedermann.nextcloud.deck.ui.card.comments;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
-
import android.annotation.SuppressLint;
-import android.net.Uri;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
@@ -21,18 +18,19 @@ import com.bumptech.glide.request.RequestOptions;
import java.util.ArrayList;
import java.util.List;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
import it.niedermann.nextcloud.deck.ui.card.comments.util.CommentsUtil;
public class CardCommentsMentionProposer implements TextWatcher {
private final int avatarSize;
@NonNull
- private final SyncManager syncManager;
+ private final BaseRepository baseRepository;
@NonNull
private final LinearLayout.LayoutParams layoutParams;
@NonNull
@@ -57,7 +55,7 @@ public class CardCommentsMentionProposer implements TextWatcher {
this.editText = editText;
this.mentionProposerWrapper = mentionProposerWrapper;
this.mentionProposer = avatarProposer;
- syncManager = new SyncManager(editText.getContext());
+ baseRepository = new BaseRepository(editText.getContext());
avatarSize = DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.avatar_size_small);
layoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize);
layoutParams.setMarginEnd(DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.spacer_1x));
@@ -79,38 +77,39 @@ public class CardCommentsMentionProposer implements TextWatcher {
this.users.clear();
} else {
if (mentionProposal.first != null && mentionProposal.second != null) {
- observeOnce(syncManager.searchUserByUidOrDisplayName(account.getId(), boardLocalId, -1L, mentionProposal.first), owner, (users) -> {
- if (!users.equals(this.users)) {
- mentionProposer.removeAllViews();
- if (users.size() > 0) {
- mentionProposerWrapper.setVisibility(View.VISIBLE);
- for (final var user : users) {
- final var avatar = new ImageView(mentionProposer.getContext());
- avatar.setLayoutParams(layoutParams);
- updateListenerOfView(avatar, s, mentionProposal, user);
-
- mentionProposer.addView(avatar);
-
- Glide.with(avatar.getContext())
- .load(account.getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + avatarSize)
- .placeholder(R.drawable.ic_person_grey600_24dp)
- .error(R.drawable.ic_person_grey600_24dp)
- .apply(RequestOptions.circleCropTransform())
- .into(avatar);
+ new ReactiveLiveData<>(baseRepository.searchUserByUidOrDisplayNameForCards(account.getId(), boardLocalId, -1L, mentionProposal.first))
+ .observeOnce(owner, users -> {
+ if (!users.equals(this.users)) {
+ mentionProposer.removeAllViews();
+ if (users.size() > 0) {
+ mentionProposerWrapper.setVisibility(View.VISIBLE);
+ for (final var user : users) {
+ final var avatar = new ImageView(mentionProposer.getContext());
+ avatar.setLayoutParams(layoutParams);
+ updateListenerOfView(avatar, s, mentionProposal, user);
+
+ mentionProposer.addView(avatar);
+
+ Glide.with(avatar.getContext())
+ .load(account.getAvatarUrl(avatarSize, user.getUid()))
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(avatar);
+ }
+ } else {
+ mentionProposerWrapper.setVisibility(View.GONE);
+ }
+ this.users.clear();
+ this.users.addAll(users);
+ } else {
+ int i = 0;
+ for (User user : users) {
+ updateListenerOfView(mentionProposer.getChildAt(i), s, mentionProposal, user);
+ i++;
+ }
}
- } else {
- mentionProposerWrapper.setVisibility(View.GONE);
- }
- this.users.clear();
- this.users.addAll(users);
- } else {
- int i = 0;
- for (User user : users) {
- updateListenerOfView(mentionProposer.getChildAt(i), s, mentionProposal, user);
- i++;
- }
- }
- });
+ });
} else {
this.users.clear();
mentionProposer.removeAllViews();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java
index 46ec36e0d..5918c9b41 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java
@@ -1,31 +1,30 @@
package it.niedermann.nextcloud.deck.ui.card.comments;
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+
import android.app.Application;
import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
import java.util.List;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-
-import static androidx.lifecycle.Transformations.distinctUntilChanged;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
@SuppressWarnings("WeakerAccess")
-public class CommentsViewModel extends AndroidViewModel {
-
- private final SyncManager syncManager;
+public class CommentsViewModel extends SyncViewModel {
private final MutableLiveData<FullDeckComment> replyToComment = new MutableLiveData<>();
- public CommentsViewModel(@NonNull Application application) {
- super(application);
- this.syncManager = new SyncManager(application);
+ public CommentsViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ super(application, account);
}
public void setReplyToComment(FullDeckComment replyToComment) {
@@ -37,7 +36,7 @@ public class CommentsViewModel extends AndroidViewModel {
}
public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) {
- return distinctUntilChanged(syncManager.getFullCommentsForLocalCardId(localCardId));
+ return distinctUntilChanged(baseRepository.getFullCommentsForLocalCardId(localCardId));
}
public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java
index f38e41f93..dc9b8dbb5 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java
@@ -5,11 +5,15 @@ import android.view.MenuInflater;
import android.view.View;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.widget.TooltipCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import com.nextcloud.android.common.ui.theme.utils.ColorRole;
+
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
@@ -23,9 +27,8 @@ import it.niedermann.nextcloud.deck.databinding.ItemCommentBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
import it.niedermann.nextcloud.deck.util.DateUtil;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
-import scheme.Scheme;
public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
private final ItemCommentBinding binding;
@@ -38,8 +41,14 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
this.binding.message.setMovementMethod(LinkMovementMethod.getInstance());
}
- public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @NonNull Scheme scheme, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager, @NonNull Consumer<CharSequence> editListener) {
- ViewUtil.addAvatar(binding.avatar, account.getUrl(), comment.getComment().getActorId(), DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp);
+ public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @Nullable ThemeUtils utils, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager, @NonNull Consumer<CharSequence> editListener) {
+ Glide.with(binding.avatar.getContext())
+ .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size), comment.getComment().getActorId()))
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
+
final var mentions = new HashMap<String, String>(comment.getComment().getMentions().size());
for (final var mention : comment.getComment().getMentions()) {
mentions.put(mention.getMentionId(), mention.getMentionDisplayName());
@@ -70,7 +79,7 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
return true;
});
menu.findItem(android.R.id.edit).setOnMenuItemClickListener(item -> {
- CardCommentsEditDialogFragment.newInstance(comment.getLocalId(), comment.getComment().getMessage()).show(fragmentManager, CardCommentsAdapter.class.getCanonicalName());
+ CardCommentsEditDialogFragment.newInstance(comment.getLocalId(), comment.getComment().getMessage()).show(fragmentManager, CardCommentsEditDialogFragment.class.getCanonicalName());
return true;
});
} else {
@@ -80,7 +89,9 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
});
TooltipCompat.setTooltipText(binding.creationDateTime, comment.getComment().getCreationDateTime().atZone(ZoneId.systemDefault()).format(dateFormatter));
- DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), scheme.getOnPrimaryContainer());
+ if (utils != null) {
+ utils.platform.colorImageView(binding.notSyncedYet, ColorRole.PRIMARY);
+ }
binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(comment.getStatusEnum()) ? View.VISIBLE : View.GONE);
if (comment.getParent() == null) {
@@ -88,7 +99,9 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
} else {
final int commentParentMaxLines = itemView.getContext().getResources().getInteger(R.integer.comment_parent_max_lines);
binding.parentContainer.setVisibility(View.VISIBLE);
- binding.parentBorder.setBackgroundColor(scheme.getOnPrimaryContainer());
+ if (utils != null) {
+ utils.platform.colorViewBackground(binding.parentBorder);
+ }
binding.parent.setText(comment.getParent().getMessage());
binding.parent.setOnClickListener((v) -> {
final boolean previouslyCollapsed = binding.parent.getMaxLines() == commentParentMaxLines;
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java
index 84e795dd2..dd5bfa4af 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java
@@ -5,14 +5,17 @@ import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAssigneeBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
public class AssigneeViewHolder extends RecyclerView.ViewHolder {
- private ItemAssigneeBinding binding;
+ private final ItemAssigneeBinding binding;
@SuppressWarnings("WeakerAccess")
public AssigneeViewHolder(ItemAssigneeBinding binding) {
@@ -21,7 +24,12 @@ public class AssigneeViewHolder extends RecyclerView.ViewHolder {
}
public void bind(@NonNull Account account, @NonNull User user, @Nullable Consumer<User> onClickListener) {
- ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), R.drawable.ic_person_grey600_24dp);
+ Glide.with(binding.avatar.getContext())
+ .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size), user.getUid()))
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
if (onClickListener != null) {
itemView.setOnClickListener((v) -> onClickListener.accept(user));
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java
index b76fae043..04874cee9 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java
@@ -27,6 +27,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
import com.google.android.material.chip.Chip;
import com.google.android.material.snackbar.Snackbar;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog;
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog.OnDateSetListener;
import com.wdullaer.materialdatetimepicker.time.TimePickerDialog;
@@ -47,6 +48,7 @@ import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabDetailsBinding;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.model.User;
import it.niedermann.nextcloud.deck.model.full.FullCard;
@@ -68,9 +70,16 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
private AssigneeAdapter adapter;
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
+ private static final String KEY_ACCOUNT = "account";
- public static Fragment newInstance() {
- return new CardDetailsFragment();
+ public static Fragment newInstance(@NonNull Account account) {
+ final var fragment = new CardDetailsFragment();
+
+ final var args = new Bundle();
+ args.putSerializable(KEY_ACCOUNT, account);
+ fragment.setArguments(args);
+
+ return fragment;
}
@Override
@@ -80,6 +89,12 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
binding = FragmentCardEditTabDetailsBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+ final var args = getArguments();
+
+ if (args == null || !args.containsKey(KEY_ACCOUNT)) {
+ throw new IllegalStateException(KEY_ACCOUNT + " must be provided");
+ }
+
// This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance
// See https://github.com/stefan-niedermann/nextcloud-deck/issues/478
if (viewModel.getFullCard() == null) {
@@ -92,7 +107,7 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
avatarLayoutParams.setMargins(0, 0, DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x), 0);
setupAssignees();
- setupLabels();
+ setupLabels((Account) args.getSerializable(KEY_ACCOUNT));
setupDueDate();
setupDescription();
setupProjects();
@@ -198,8 +213,9 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
} else {
date = LocalDate.now();
}
- ThemedDatePickerDialog.newInstance(this, date.getYear(), date.getMonthValue(), date.getDayOfMonth())
- .show(getChildFragmentManager(), ThemedDatePickerDialog.class.getCanonicalName());
+ viewModel.getCurrentBoardColor(viewModel.getAccount().getId(), viewModel.getBoardId())
+ .thenAcceptAsync(color -> ThemedDatePickerDialog.newInstance(this, date.getYear(), date.getMonthValue(), date.getDayOfMonth(), color)
+ .show(getChildFragmentManager(), ThemedDatePickerDialog.class.getCanonicalName()), ContextCompat.getMainExecutor(requireContext()));
});
binding.dueDateTime.setOnClickListener(v -> {
@@ -209,8 +225,9 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
} else {
time = LocalTime.now();
}
- ThemedTimePickerDialog.newInstance(this, time.getHour(), time.getMinute(), true)
- .show(getChildFragmentManager(), ThemedTimePickerDialog.class.getCanonicalName());
+ viewModel.getCurrentBoardColor(viewModel.getAccount().getId(), viewModel.getBoardId())
+ .thenAcceptAsync(color -> ThemedTimePickerDialog.newInstance(this, time.getHour(), time.getMinute(), true, color)
+ .show(getChildFragmentManager(), ThemedTimePickerDialog.class.getCanonicalName()), ContextCompat.getMainExecutor(requireContext()));
});
binding.clearDueDate.setOnClickListener(v -> {
@@ -226,29 +243,30 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
}
}
- private void setupLabels() {
+ private void setupLabels(@NonNull Account account) {
final long accountId = viewModel.getAccount().getId();
final long boardId = viewModel.getBoardId();
binding.labelsGroup.removeAllViews();
if (viewModel.canEdit()) {
Long localCardId = viewModel.getFullCard().getCard().getLocalId();
localCardId = localCardId == null ? -1 : localCardId;
- binding.labels.setAdapter(new LabelAutoCompleteAdapter(requireActivity(), accountId, boardId, localCardId));
+ try {
+ binding.labels.setAdapter(new LabelAutoCompleteAdapter(requireActivity(), account, boardId, localCardId));
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ ExceptionDialogFragment.newInstance(e, account).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ // TODO Handle error
+ }
binding.labels.setOnItemClickListener((adapterView, view, position, id) -> {
final var label = (Label) adapterView.getItemAtPosition(position);
- if (LabelAutoCompleteAdapter.ITEM_CREATE == label.getLocalId()) {
- final Label newLabel = new Label(label);
- newLabel.setBoardId(boardId);
- newLabel.setTitle(((LabelAutoCompleteAdapter) binding.labels.getAdapter()).getLastFilterText());
- newLabel.setLocalId(null);
- viewModel.createLabel(accountId, newLabel, boardId, new IResponseCallback<>() {
+ if (label.getLocalId() == null) {
+ viewModel.createLabel(accountId, label, boardId, new IResponseCallback<>() {
@Override
public void onResponse(Label response) {
requireActivity().runOnUiThread(() -> {
- newLabel.setLocalId(response.getLocalId());
+ label.setLocalId(response.getLocalId());
((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(response);
viewModel.getFullCard().getLabels().add(response);
- binding.labelsGroup.addView(createChipFromLabel(newLabel));
+ binding.labelsGroup.addView(createChipFromLabel(label));
binding.labelsGroup.setVisibility(VISIBLE);
});
}
@@ -256,8 +274,9 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
@Override
public void onError(Throwable throwable) {
IResponseCallback.super.onError(throwable);
- requireActivity().runOnUiThread(() -> ThemedSnackbar.make(requireView(), getString(R.string.error_create_label, newLabel.getTitle()), Snackbar.LENGTH_LONG)
- .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())).show());
+ viewModel.getCurrentBoardColor(viewModel.getAccount().getId(), viewModel.getBoardId())
+ .thenAcceptAsync(color -> ThemedSnackbar.make(requireView(), getString(R.string.error_create_label, label.getTitle()), Snackbar.LENGTH_LONG, color)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(throwable, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())).show(), ContextCompat.getMainExecutor(requireContext()));
}
});
} else {
@@ -291,7 +310,7 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
chip.setOnCloseIconClickListener(v -> {
binding.labelsGroup.removeView(chip);
viewModel.getFullCard().getLabels().remove(label);
- ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(label);
+ ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).doNotLongerExclude(label);
});
}
try {
@@ -311,7 +330,7 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
}
private void setupAssignees() {
- adapter = new AssigneeAdapter((user) -> CardAssigneeDialog.newInstance(user).show(getChildFragmentManager(), CardAssigneeDialog.class.getSimpleName()), viewModel.getAccount());
+ adapter = new AssigneeAdapter(user -> CardAssigneeDialog.newInstance(user).show(getChildFragmentManager(), CardAssigneeDialog.class.getSimpleName()), viewModel.getAccount());
binding.assignees.setAdapter(adapter);
binding.assignees.post(() -> {
@Px final int gutter = DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x);
@@ -323,7 +342,12 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
if (viewModel.canEdit()) {
Long localCardId = viewModel.getFullCard().getCard().getLocalId();
localCardId = localCardId == null ? -1 : localCardId;
- binding.people.setAdapter(new UserAutoCompleteAdapter(requireActivity(), viewModel.getAccount(), viewModel.getBoardId(), localCardId));
+ try {
+ binding.people.setAdapter(new UserAutoCompleteAdapter(requireActivity(), viewModel.getAccount(), viewModel.getBoardId(), localCardId));
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ ExceptionDialogFragment.newInstance(e, viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ // TODO Handle error
+ }
binding.people.setOnItemClickListener((adapterView, view, position, id) -> {
final var user = (User) adapterView.getItemAtPosition(position);
viewModel.getFullCard().getAssignedUsers().add(user);
@@ -404,11 +428,15 @@ public class CardDetailsFragment extends Fragment implements OnDateSetListener,
public void onUnassignUser(@NonNull User user) {
viewModel.getFullCard().getAssignedUsers().remove(user);
adapter.removeUser(user);
- ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user);
- ThemedSnackbar.make(requireView(), getString(R.string.unassigned_user, user.getDisplayname()), Snackbar.LENGTH_LONG).setAction(R.string.simple_undo, v1 -> {
- viewModel.getFullCard().getAssignedUsers().add(user);
- ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user);
- adapter.addUser(user);
- }).show();
+ ((UserAutoCompleteAdapter) binding.people.getAdapter()).doNotLongerExclude(user);
+
+ viewModel.getCurrentBoardColor(viewModel.getAccount().getId(), viewModel.getBoardId())
+ .thenAcceptAsync(color -> ThemedSnackbar.make(requireView(), getString(R.string.unassigned_user, user.getDisplayname()), Snackbar.LENGTH_LONG, color)
+ .setAction(R.string.simple_undo, v1 -> {
+ viewModel.getFullCard().getAssignedUsers().add(user);
+ ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user);
+ adapter.addUser(user);
+ })
+ .show(), ContextCompat.getMainExecutor(requireContext()));
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java
index 58e104973..a69ddf80e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java
@@ -73,7 +73,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment {
.create();
}
- public static DialogFragment newInstance(Throwable throwable, @Nullable Account account) {
+ public static DialogFragment newInstance(@NonNull Throwable throwable, @Nullable Account account) {
final var fragment = new ExceptionDialogFragment();
final var args = new Bundle();
args.putSerializable(KEY_THROWABLE, throwable);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java
index 1e52271e4..c57c80d24 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java
@@ -18,6 +18,7 @@ import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotSupportedException;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import com.nextcloud.android.sso.exceptions.TokenMismatchException;
@@ -75,6 +76,11 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> {
add(R.string.error_dialog_tip_token_mismatch_retry);
add(R.string.error_dialog_tip_clear_storage_might_help);
add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO);
+ } else if (throwable instanceof NextcloudFilesAppAccountNotFoundException) {
+ // TODO we can give better hints here...
+ add(R.string.error_dialog_tip_token_mismatch_retry);
+ add(R.string.error_dialog_tip_clear_storage_might_help);
+ add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO);
} else if (throwable instanceof NextcloudFilesAppNotSupportedException) {
add(R.string.error_dialog_min_version, new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.nextcloud.client"))
.putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_update_files_app));
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java
index 87427209d..08512c5c6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java
@@ -5,6 +5,7 @@ import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
@@ -66,7 +67,7 @@ public class FilterDialogFragment extends ThemedDialogFragment {
tab.setIcon(draft.getDueType() != EDueType.NO_FILTER ? indicator : null);
break;
default:
- throw new IllegalStateException("position must be between 0 and 2");
+ throw new IllegalStateException("position must be between 0 and 2 but was " + position);
}
});
tab.setText(tabTitles[position]);
@@ -104,7 +105,7 @@ public class FilterDialogFragment extends ThemedDialogFragment {
}
@Override
- public void applyTheme(int color) {
+ public void applyTheme(@ColorInt int color) {
final var utils = ThemeUtils.of(color, requireContext());
utils.deck.themeTabLayout(binding.tabLayout, Color.TRANSPARENT);
@@ -128,7 +129,7 @@ public class FilterDialogFragment extends ThemedDialogFragment {
case 2:
return new FilterDueTypeFragment();
default:
- throw new IllegalArgumentException("position must be between 0 and 2");
+ throw new IllegalArgumentException("position must be between 0 and 2 but was " + position);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeAdapter.java
index dab5bb9f7..7d218e19e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeAdapter.java
@@ -1,8 +1,10 @@
package it.niedermann.nextcloud.deck.ui.filter;
+import android.os.Build;
import android.view.LayoutInflater;
import android.view.ViewGroup;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
@@ -11,6 +13,8 @@ import java.util.Arrays;
import it.niedermann.nextcloud.deck.databinding.ItemFilterDuetypeBinding;
import it.niedermann.nextcloud.deck.model.enums.EDueType;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdapter.DueTypeViewHolder> {
@NonNull
@@ -18,14 +22,16 @@ public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdap
private int selectedDueTypePosition;
@Nullable
private final SelectionListener<EDueType> selectionListener;
+ @ColorInt
+ private final int color;
@SuppressWarnings("WeakerAccess")
- public FilterDueTypeAdapter(@NonNull EDueType selectedDueType, @Nullable SelectionListener<EDueType> selectionListener) {
+ public FilterDueTypeAdapter(@NonNull EDueType selectedDueType, @Nullable SelectionListener<EDueType> selectionListener, @ColorInt int color) {
super();
this.selectedDueTypePosition = Arrays.binarySearch(dueTypes, selectedDueType);
this.selectionListener = selectionListener;
+ this.color = color;
setHasStableIds(true);
- notifyDataSetChanged();
}
@NonNull
@@ -49,8 +55,8 @@ public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdap
return dueTypes.length;
}
- class DueTypeViewHolder extends RecyclerView.ViewHolder {
- private ItemFilterDuetypeBinding binding;
+ class DueTypeViewHolder extends RecyclerView.ViewHolder implements Themed {
+ private final ItemFilterDuetypeBinding binding;
DueTypeViewHolder(@NonNull ItemFilterDuetypeBinding binding) {
super(binding.getRoot());
@@ -60,6 +66,7 @@ public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdap
void bind(final EDueType dueType) {
binding.dueType.setText(dueType.toString(binding.dueType.getContext()));
itemView.setSelected(dueTypes[selectedDueTypePosition].equals(dueType));
+ applyTheme(color);
itemView.setOnClickListener(view -> {
final int oldSelection = selectedDueTypePosition;
@@ -80,5 +87,13 @@ public class FilterDueTypeAdapter extends RecyclerView.Adapter<FilterDueTypeAdap
notifyItemChanged(oldSelection);
});
}
+
+ @Override
+ public void applyTheme(int color) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ final var utils = ThemeUtils.of(color, itemView.getContext());
+ utils.deck.colorSelectedCheck(binding.selectedCheck.getContext(), binding.selectedCheck.getDrawable());
+ }
+ }
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeFragment.java
index ac23faf6c..c173433e1 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDueTypeFragment.java
@@ -28,7 +28,8 @@ public class FilterDueTypeFragment extends Fragment implements SelectionListener
filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class);
binding.dueType.setItemAnimator(null);
- binding.dueType.setAdapter(new FilterDueTypeAdapter(requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getDueType(), this));
+ filterViewModel.getCurrentBoardColor$().observe(getViewLifecycleOwner(),
+ color -> binding.dueType.setAdapter(new FilterDueTypeAdapter(requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getDueType(), this, color)));
return binding.getRoot();
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java
index 83b8f8f3e..194540139 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java
@@ -1,21 +1,26 @@
package it.niedermann.nextcloud.deck.ui.filter;
import android.content.res.ColorStateList;
+import android.os.Build;
import android.view.LayoutInflater;
import android.view.ViewGroup;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemFilterLabelBinding;
import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
@SuppressWarnings("WeakerAccess")
public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapter.LabelViewHolder> {
@@ -27,8 +32,10 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
private static final Label NOT_ASSIGNED = null;
@Nullable
private final SelectionListener<Label> selectionListener;
+ @ColorInt
+ private final int color;
- public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, boolean noAssignedLabel, @Nullable SelectionListener<Label> selectionListener) {
+ public FilterLabelsAdapter(@NonNull Collection<Label> labels, @NonNull Collection<Label> selectedLabels, boolean noAssignedLabel, @Nullable SelectionListener<Label> selectionListener, @ColorInt int color) {
super();
this.labels.add(NOT_ASSIGNED);
this.labels.addAll(labels);
@@ -37,8 +44,8 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
}
this.selectedLabels.addAll(selectedLabels);
this.selectionListener = selectionListener;
+ this.color = color;
setHasStableIds(true);
- notifyDataSetChanged();
}
@Override
@@ -67,8 +74,8 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
return labels.size();
}
- class LabelViewHolder extends RecyclerView.ViewHolder {
- private ItemFilterLabelBinding binding;
+ class LabelViewHolder extends RecyclerView.ViewHolder implements Themed {
+ private final ItemFilterLabelBinding binding;
LabelViewHolder(@NonNull ItemFilterLabelBinding binding) {
super(binding.getRoot());
@@ -80,9 +87,10 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
binding.label.setText(label.getTitle());
final int labelColor = label.getColor();
binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
- final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
- binding.label.setTextColor(color);
+ final int textColor = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
+ binding.label.setTextColor(textColor);
itemView.setSelected(selectedLabels.contains(label));
+ applyTheme(color);
bindClickListener(label);
}
@@ -93,6 +101,7 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
binding.label.setChipBackgroundColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.primary)));
binding.label.setRippleColor(null);
itemView.setSelected(selectedLabels.contains(NOT_ASSIGNED));
+ applyTheme(color);
bindClickListener(NOT_ASSIGNED);
}
@@ -113,5 +122,13 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
}
});
}
+
+ @Override
+ public void applyTheme(int color) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ final var utils = ThemeUtils.of(color, itemView.getContext());
+ utils.deck.colorSelectedCheck(binding.selectedCheck.getContext(), binding.selectedCheck.getDrawable());
+ }
+ }
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java
index b4eb17d0e..a69ced0ea 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java
@@ -1,7 +1,6 @@
package it.niedermann.nextcloud.deck.ui.filter;
import static java.util.Objects.requireNonNull;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -13,9 +12,9 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.databinding.DialogFilterLabelsBinding;
import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
public class FilterLabelsFragment extends Fragment implements SelectionListener<Label> {
@@ -26,18 +25,20 @@ public class FilterLabelsFragment extends Fragment implements SelectionListener<
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
final var binding = DialogFilterLabelsBinding.inflate(requireActivity().getLayoutInflater());
- final var mainViewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class);
- observeOnce(filterViewModel.findProposalsForLabelsToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (labels) -> {
- binding.labels.setNestedScrollingEnabled(false);
- binding.labels.setAdapter(new FilterLabelsAdapter(
- labels,
- requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(),
- requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedLabel(),
- this));
- });
+ new ReactiveLiveData<>(filterViewModel.findProposalsForLabelsToAssign())
+ .combineWith(() -> filterViewModel.getCurrentBoardColor$())
+ .observeOnce(getViewLifecycleOwner(), pair -> {
+ binding.labels.setNestedScrollingEnabled(false);
+ binding.labels.setAdapter(new FilterLabelsAdapter(
+ pair.first,
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(),
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedLabel(),
+ this,
+ pair.second));
+ });
return binding.getRoot();
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java
index 4b75b985f..b042954be 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java
@@ -1,28 +1,31 @@
package it.niedermann.nextcloud.deck.ui.filter;
+import android.os.Build;
import android.view.LayoutInflater;
import android.view.ViewGroup;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.Px;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemFilterUserBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
@SuppressWarnings("WeakerAccess")
public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.UserViewHolder> {
- @Px
- final int avatarSize;
@NonNull
private final Account account;
@Nullable
@@ -33,10 +36,11 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
private final List<User> selectedUsers = new ArrayList<>();
@Nullable
private final SelectionListener<User> selectionListener;
+ @ColorInt
+ private final int color;
- public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, boolean noAssignedUser, @Nullable SelectionListener<User> selectionListener) {
+ public FilterUserAdapter(@NonNull Account account, @NonNull Collection<User> users, @NonNull Collection<User> selectedUsers, boolean noAssignedUser, @Nullable SelectionListener<User> selectionListener, @ColorInt int color) {
super();
- this.avatarSize = avatarSize;
this.account = account;
this.users.add(NOT_ASSIGNED);
this.users.addAll(users);
@@ -45,8 +49,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
}
this.selectedUsers.addAll(selectedUsers);
this.selectionListener = selectionListener;
+ this.color = color;
setHasStableIds(true);
- notifyDataSetChanged();
}
@Override
@@ -75,8 +79,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
return users.size();
}
- class UserViewHolder extends RecyclerView.ViewHolder {
- private ItemFilterUserBinding binding;
+ class UserViewHolder extends RecyclerView.ViewHolder implements Themed {
+ private final ItemFilterUserBinding binding;
UserViewHolder(@NonNull ItemFilterUserBinding binding) {
super(binding.getRoot());
@@ -85,8 +89,14 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
void bind(@NonNull final User user) {
binding.title.setText(user.getDisplayname());
- ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp);
+ Glide.with(binding.avatar.getContext())
+ .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size), user.getUid()))
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
itemView.setSelected(selectedUsers.contains(user));
+ applyTheme(color);
bindClickListener(user);
}
@@ -96,6 +106,7 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
.load(R.drawable.ic_baseline_block_24)
.into(binding.avatar);
itemView.setSelected(selectedUsers.contains(NOT_ASSIGNED));
+ applyTheme(color);
bindClickListener(NOT_ASSIGNED);
}
@@ -116,5 +127,13 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
}
});
}
+
+ @Override
+ public void applyTheme(int color) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ final var utils = ThemeUtils.of(color, itemView.getContext());
+ utils.deck.colorSelectedCheck(binding.selectedCheck.getContext(), binding.selectedCheck.getDrawable());
+ }
+ }
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java
index 59768de39..977e4bbd2 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java
@@ -1,7 +1,6 @@
package it.niedermann.nextcloud.deck.ui.filter;
import static java.util.Objects.requireNonNull;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -10,14 +9,13 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
-import it.niedermann.android.util.DimensionUtil;
-import it.niedermann.nextcloud.deck.R;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.databinding.DialogFilterAssigneesBinding;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
public class FilterUserFragment extends Fragment implements SelectionListener<User> {
@@ -28,20 +26,20 @@ public class FilterUserFragment extends Fragment implements SelectionListener<Us
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = DialogFilterAssigneesBinding.inflate(requireActivity().getLayoutInflater());
- final var mainViewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class);
- observeOnce(filterViewModel.findProposalsForUsersToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (users) -> {
- binding.users.setNestedScrollingEnabled(false);
- binding.users.setAdapter(new FilterUserAdapter(
- DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size),
- mainViewModel.getCurrentAccount(),
- users,
- requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(),
- requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedUser(),
- this));
- });
+ new ReactiveLiveData<>(filterViewModel.findProposalsForUsersToAssign())
+ .combineWith(() -> filterViewModel.getCurrentBoardColor$())
+ .observeOnce(getViewLifecycleOwner(), pair -> {
+ binding.users.setNestedScrollingEnabled(false);
+ filterViewModel.getCurrentAccount().thenAcceptAsync(account -> binding.users.setAdapter(new FilterUserAdapter(
+ account, pair.first,
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(),
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedUser(),
+ this,
+ pair.second)), ContextCompat.getMainExecutor(requireContext()));
+ });
return binding.getRoot();
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java
index acdab52e7..09f2f4fa1 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java
@@ -1,29 +1,26 @@
package it.niedermann.nextcloud.deck.ui.filter;
-import static androidx.lifecycle.Transformations.distinctUntilChanged;
-import static androidx.lifecycle.Transformations.map;
-
import android.app.Application;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.model.User;
import it.niedermann.nextcloud.deck.model.enums.EDueType;
import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
@SuppressWarnings("WeakerAccess")
-public class FilterViewModel extends AndroidViewModel {
-
- private final SyncManager syncManager;
+public class FilterViewModel extends BaseViewModel {
@IntRange(from = 0, to = 2)
private int currentFilterTab = 0;
@@ -35,7 +32,10 @@ public class FilterViewModel extends AndroidViewModel {
public FilterViewModel(@NonNull Application application) {
super(application);
- this.syncManager = new SyncManager(application);
+ }
+
+ public CompletableFuture<Account> getCurrentAccount() {
+ return baseRepository.getCurrentAccountId().thenApplyAsync(baseRepository::readAccountDirectly);
}
public void publishFilterInformationDraft() {
@@ -60,7 +60,9 @@ public class FilterViewModel extends AndroidViewModel {
@NonNull
public LiveData<Boolean> hasActiveFilter() {
- return distinctUntilChanged(map(getFilterInformation(), FilterInformation::hasActiveFilter));
+ return new ReactiveLiveData<>(getFilterInformation())
+ .map(FilterInformation::hasActiveFilter)
+ .distinctUntilChanged();
}
public void createFilterInformationDraft() {
@@ -130,11 +132,22 @@ public class FilterViewModel extends AndroidViewModel {
return this.currentFilterTab;
}
- public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId) {
- return syncManager.findProposalsForUsersToAssign(accountId, boardId, -1L, -1);
+ // TODO Use in Filter fragments
+ public LiveData<Integer> getCurrentBoardColor$() {
+ return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$())
+ .combineWith(baseRepository::getCurrentBoardId$)
+ .flatMap(ids -> baseRepository.getBoardColor$(ids.first, ids.second));
+ }
+
+ public LiveData<List<User>> findProposalsForUsersToAssign() {
+ return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$())
+ .combineWith(baseRepository::getCurrentBoardId$)
+ .flatMap(ids -> baseRepository.findProposalsForUsersToAssignForCards(ids.first, ids.second, -1L, -1));
}
- public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) {
- return syncManager.findProposalsForLabelsToAssign(accountId, boardId, -1L);
+ public LiveData<List<Label>> findProposalsForLabelsToAssign() {
+ return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$())
+ .combineWith(baseRepository::getCurrentBoardId$)
+ .flatMap(ids -> baseRepository.findProposalsForLabelsToAssign(ids.first, ids.second, -1L));
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/DrawerMenuInflater.java
index 1683a8ef4..f85a5e490 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/DrawerMenuInflater.java
@@ -1,79 +1,94 @@
-package it.niedermann.nextcloud.deck.util;
+package it.niedermann.nextcloud.deck.ui.main;
import android.view.Menu;
import android.view.MenuItem;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.appcompat.widget.PopupMenu;
-import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener;
import it.niedermann.nextcloud.deck.ui.board.DeleteBoardDialogFragment;
-import it.niedermann.nextcloud.deck.ui.board.EditBoardDialogFragment;
import it.niedermann.nextcloud.deck.ui.board.accesscontrol.AccessControlDialogFragment;
+import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardDialogFragment;
import it.niedermann.nextcloud.deck.ui.board.managelabels.ManageLabelsDialogFragment;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
-public class DrawerMenuUtil {
+public class DrawerMenuInflater<T extends FragmentActivity & ArchiveBoardListener> {
public static final int MENU_ID_ABOUT = -1;
public static final int MENU_ID_ADD_BOARD = -2;
public static final int MENU_ID_SETTINGS = -3;
public static final int MENU_ID_ARCHIVED_BOARDS = -4;
public static final int MENU_ID_UPCOMING_CARDS = -5;
- private DrawerMenuUtil() {
- throw new UnsupportedOperationException("This class must not get instantiated");
+ private final T activity;
+ private final Menu menu;
+
+ public DrawerMenuInflater(@NonNull T activity, @NonNull Menu menu) {
+ this.activity = activity;
+ this.menu = menu;
}
- public static <T extends FragmentActivity & ArchiveBoardListener> void inflateBoards(
- @NonNull T context,
- @NonNull Menu menu,
- @NonNull List<Board> boards,
+ public Map<Integer, Long> inflateBoards(
+ @NonNull Account account,
+ @NonNull List<FullBoard> fullBoards,
+ @ColorInt int color,
boolean hasArchivedBoards,
boolean currentServerVersionIsSupported) {
- menu.add(Menu.NONE, MENU_ID_UPCOMING_CARDS, Menu.NONE, R.string.widget_upcoming_title).setIcon(R.drawable.calendar_blank_grey600_24dp);
+
+ final var utils = ThemeUtils.of(color, activity);
+ final var navigationMap = new HashMap<Integer, Long>();
+
+ menu.clear();
+ menu.add(Menu.NONE, MENU_ID_UPCOMING_CARDS, Menu.NONE, R.string.widget_upcoming_title).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.calendar_blank_grey600_24dp));
+
int index = 0;
- for (final var board : boards) {
+ for (final var fullBoard : fullBoards) {
+ navigationMap.put(index, fullBoard.getLocalId());
final var menuItem = menu
- .add(Menu.NONE, index++, Menu.NONE, board.getTitle()).setIcon(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, board.getColor()))
+ .add(Menu.NONE, index++, Menu.NONE, fullBoard.getBoard().getTitle()).setIcon(utils.deck.getColoredBoardDrawable(activity, fullBoard.getBoard().getColor()))
.setCheckable(true);
if (currentServerVersionIsSupported) {
- if (board.isPermissionManage()) {
- final var contextMenu = new AppCompatImageButton(context);
+ if (fullBoard.getBoard().isPermissionManage()) {
+ final var contextMenu = new AppCompatImageButton(activity);
contextMenu.setBackgroundDrawable(null);
- contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, ContextCompat.getColor(context, R.color.grey600)));
+ contextMenu.setImageDrawable(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_menu));
contextMenu.setOnClickListener((v) -> {
- final var popup = new PopupMenu(context, contextMenu);
+ final var popup = new PopupMenu(activity, contextMenu);
popup.getMenuInflater().inflate(R.menu.navigation_context_menu, popup.getMenu());
final int SHARE_BOARD_ID = -1;
- if (board.isPermissionShare()) {
+ if (fullBoard.getBoard().isPermissionShare()) {
popup.getMenu().add(Menu.NONE, SHARE_BOARD_ID, 5, R.string.share_board);
}
popup.setOnMenuItemClickListener((MenuItem item) -> {
- final String editBoard = context.getString(R.string.edit_board);
+ final String editBoard = activity.getString(R.string.edit_board);
int itemId = item.getItemId();
if (itemId == SHARE_BOARD_ID) {
- AccessControlDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName());
+ AccessControlDialogFragment.newInstance(account, fullBoard.getLocalId()).show(activity.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName());
return true;
} else if (itemId == R.id.edit_board) {
- EditBoardDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard);
+ EditBoardDialogFragment.newInstance(account, fullBoard.getLocalId()).show(activity.getSupportFragmentManager(), editBoard);
return true;
} else if (itemId == R.id.manage_labels) {
- ManageLabelsDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard);
+ ManageLabelsDialogFragment.newInstance(account, fullBoard.getLocalId()).show(activity.getSupportFragmentManager(), editBoard);
return true;
} else if (itemId == R.id.clone_board) {
- context.onClone(board);
+ activity.onClone(account, fullBoard.getBoard());
return true;
} else if (itemId == R.id.archive_board) {
- context.onArchive(board);
+ activity.onArchive(fullBoard.getBoard());
return true;
} else if (itemId == R.id.delete_board) {
- DeleteBoardDialogFragment.newInstance(board).show(context.getSupportFragmentManager(), DeleteBoardDialogFragment.class.getCanonicalName());
+ DeleteBoardDialogFragment.newInstance(fullBoard.getBoard()).show(activity.getSupportFragmentManager(), DeleteBoardDialogFragment.class.getCanonicalName());
return true;
}
return false;
@@ -81,25 +96,27 @@ public class DrawerMenuUtil {
popup.show();
});
menuItem.setActionView(contextMenu);
- } else if (board.isPermissionShare()) {
- final var contextMenu = new AppCompatImageButton(context);
+ } else if (fullBoard.getBoard().isPermissionShare()) {
+ final var contextMenu = new AppCompatImageButton(activity);
contextMenu.setBackgroundDrawable(null);
- contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, ContextCompat.getColor(context, R.color.grey600)));
- contextMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName()));
+ contextMenu.setImageDrawable(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_share_grey600_18dp));
+ contextMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(account, fullBoard.getLocalId()).show(activity.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName()));
menuItem.setActionView(contextMenu);
}
}
}
if (hasArchivedBoards) {
- menu.add(Menu.NONE, MENU_ID_ARCHIVED_BOARDS, Menu.NONE, R.string.archived_boards).setIcon(ViewUtil.getTintedImageView(context, R.drawable.ic_archive_white_24dp, ContextCompat.getColor(context, R.color.grey600)));
+ menu.add(Menu.NONE, MENU_ID_ARCHIVED_BOARDS, Menu.NONE, R.string.archived_boards).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_archive_white_24dp));
}
if (currentServerVersionIsSupported) {
- menu.add(Menu.NONE, MENU_ID_ADD_BOARD, Menu.NONE, R.string.add_board).setIcon(R.drawable.ic_add_grey_24dp);
+ menu.add(Menu.NONE, MENU_ID_ADD_BOARD, Menu.NONE, R.string.add_board).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_add_grey_24dp));
}
- menu.add(Menu.NONE, MENU_ID_SETTINGS, Menu.NONE, R.string.simple_settings).setIcon(R.drawable.ic_settings_grey600_24dp);
- menu.add(Menu.NONE, MENU_ID_ABOUT, Menu.NONE, R.string.about).setIcon(R.drawable.ic_info_outline_grey600_24dp);
+ menu.add(Menu.NONE, MENU_ID_SETTINGS, Menu.NONE, R.string.simple_settings).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_settings_grey600_24dp));
+ menu.add(Menu.NONE, MENU_ID_ABOUT, Menu.NONE, R.string.about).setIcon(utils.deck.themeNavigationViewIcon(activity, R.drawable.ic_info_outline_grey600_24dp));
+
+ return navigationMap;
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java
new file mode 100644
index 000000000..282b07dac
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java
@@ -0,0 +1,928 @@
+package it.niedermann.nextcloud.deck.ui.main;
+
+import static java.util.Collections.emptyList;
+
+import android.animation.AnimatorInflater;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkRequest;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.PopupMenu;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.AnyThread;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.appcompat.app.ActionBarDrawerToggle;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.splashscreen.SplashScreen;
+import androidx.core.view.GravityCompat;
+import androidx.core.view.ViewCompat;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.tabs.TabLayoutMediator;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.exceptions.UnknownErrorException;
+
+import java.net.HttpURLConnection;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import it.niedermann.android.crosstabdnd.CrossTabDragAndDrop;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.android.tablayouthelper.TabLayoutHelper;
+import it.niedermann.android.tablayouthelper.TabTitleGenerator;
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.api.ResponseCallback;
+import it.niedermann.nextcloud.deck.databinding.ActivityMainBinding;
+import it.niedermann.nextcloud.deck.databinding.NavHeaderMainBinding;
+import it.niedermann.nextcloud.deck.exceptions.OfflineException;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.model.full.FullStack;
+import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
+import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
+import it.niedermann.nextcloud.deck.model.ocs.Version;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.ImportAccountActivity;
+import it.niedermann.nextcloud.deck.ui.StackChangeCallback;
+import it.niedermann.nextcloud.deck.ui.accountswitcher.AccountSwitcherDialog;
+import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener;
+import it.niedermann.nextcloud.deck.ui.board.DeleteBoardListener;
+import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardDialogFragment;
+import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardListener;
+import it.niedermann.nextcloud.deck.ui.card.CardAdapter;
+import it.niedermann.nextcloud.deck.ui.card.CreateCardListener;
+import it.niedermann.nextcloud.deck.ui.card.NewCardDialog;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.ui.filter.FilterDialogFragment;
+import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel;
+import it.niedermann.nextcloud.deck.ui.settings.PreferencesViewModel;
+import it.niedermann.nextcloud.deck.ui.stack.DeleteStackDialogFragment;
+import it.niedermann.nextcloud.deck.ui.stack.DeleteStackListener;
+import it.niedermann.nextcloud.deck.ui.stack.EditStackDialogFragment;
+import it.niedermann.nextcloud.deck.ui.stack.EditStackListener;
+import it.niedermann.nextcloud.deck.ui.stack.OnScrollListener;
+import it.niedermann.nextcloud.deck.ui.stack.StackAdapter;
+import it.niedermann.nextcloud.deck.ui.stack.StackFragment;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.ThemedSnackbar;
+import it.niedermann.nextcloud.deck.util.CustomAppGlideModule;
+import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher;
+
+public class MainActivity extends AppCompatActivity implements DeleteStackListener, EditStackListener, DeleteBoardListener, EditBoardListener, ArchiveBoardListener, OnScrollListener, CreateCardListener {
+
+ protected ActivityMainBinding binding;
+ private NavHeaderMainBinding headerBinding;
+ private PreferencesViewModel preferencesViewModel;
+ protected MainViewModel mainViewModel;
+ private FilterViewModel filterViewModel;
+ private StackAdapter stackAdapter;
+ private DrawerMenuInflater<MainActivity> drawerMenuInflater;
+ private Menu listMenu;
+ private ConnectivityManager.NetworkCallback networkCallback;
+ private MainActivityNavigationHandler navigationHandler;
+ @Nullable
+ private TabLayoutMediator mediator;
+ @Nullable
+ private TabLayoutHelper tabLayoutHelper;
+ private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START);
+ } else if (binding.searchToolbar.getVisibility() == View.VISIBLE) {
+ hideFilterTextToolbar();
+ } else {
+ finish();
+ }
+ }
+ };
+
+ private final ActivityResultLauncher<Intent> importAccountLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+ if (result.getResultCode() != RESULT_OK) {
+ finish();
+ }
+ });
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ SplashScreen.installSplashScreen(this);
+
+ super.onCreate(savedInstanceState);
+
+ Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this));
+
+ binding = ActivityMainBinding.inflate(getLayoutInflater());
+ headerBinding = NavHeaderMainBinding.bind(binding.navigationView.getHeaderView(0));
+
+ setTheme(R.style.AppTheme);
+ setContentView(binding.getRoot());
+ setSupportActionBar(binding.toolbar);
+
+ final var toggle = new ActionBarDrawerToggle(this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
+ binding.drawerLayout.addDrawerListener(toggle);
+ toggle.syncState();
+
+ mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
+ preferencesViewModel = new ViewModelProvider(this).get(PreferencesViewModel.class);
+ filterViewModel = new ViewModelProvider(this).get(FilterViewModel.class);
+
+ navigationHandler = new MainActivityNavigationHandler(this, binding.drawerLayout, mainViewModel::saveCurrentBoardId);
+ binding.navigationView.setNavigationItemSelectedListener(navigationHandler);
+
+ stackAdapter = new StackAdapter(this);
+ binding.viewPager.setAdapter(stackAdapter);
+ binding.viewPager.setOffscreenPageLimit(2);
+ binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
+ binding.filterText.addTextChangedListener(new OnTextChangedWatcher(filterViewModel::setFilterText));
+ binding.enableSearch.setOnClickListener(v -> showFilterTextToolbar());
+ binding.toolbar.setOnClickListener(v -> showFilterTextToolbar());
+ binding.accountSwitcher.setOnClickListener(v -> AccountSwitcherDialog.newInstance().show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName()));
+
+ headerBinding.copyDebugLogs.setOnClickListener((v) -> {
+ try {
+ DeckLog.shareLogAsFile(this);
+ } catch (Exception e) {
+ showExceptionDialog(e, null);
+ }
+ });
+
+ final var dragAndDrop = new CrossTabDragAndDrop<StackFragment, CardAdapter, FullCard>(getResources(), ViewCompat.getLayoutDirection(binding.getRoot()) == ViewCompat.LAYOUT_DIRECTION_LTR);
+ dragAndDrop.register(binding.viewPager, binding.stackTitles, getSupportFragmentManager());
+ dragAndDrop.addItemMovedByDragListener((movedCard, stackId, position) -> {
+ mainViewModel.reorder(movedCard, stackId, position);
+ DeckLog.info("Card", movedCard.getCard().getTitle(), "was moved to Stack", stackId, "on position", position);
+ });
+
+ final var listMenuPopup = new PopupMenu(this, binding.listMenuButton);
+ listMenu = listMenuPopup.getMenu();
+ getMenuInflater().inflate(R.menu.list_menu, listMenu);
+ listMenuPopup.setOnMenuItemClickListener(this::onOptionsItemSelected);
+ binding.listMenuButton.setOnClickListener((v) -> listMenuPopup.show());
+
+ getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
+
+ drawerMenuInflater = new DrawerMenuInflater<>(this, binding.navigationView.getMenu());
+
+ preferencesViewModel.isDebugModeEnabled$().observe(this, enabled -> headerBinding.copyDebugLogs.setVisibility(enabled ? View.VISIBLE : View.GONE));
+ filterViewModel.hasActiveFilter().observe(this, hasActiveFilter -> binding.filterIndicator.setVisibility(hasActiveFilter ? View.VISIBLE : View.GONE));
+
+ // Flag to distinguish user initiated stack changes from stack changes derived by changing the board
+ final var boardChanged = new AtomicBoolean(true);
+ final var stackChangeCallback = new StackChangeCallback(stackAdapter,
+ binding.viewPager,
+ binding.fab,
+ binding.swipeRefreshLayout,
+ listMenu,
+ stack -> mainViewModel.saveCurrentStackId(stack.getAccountId(), stack.getBoardId(), stack.getLocalId()));
+
+ final var hasAccounts$ = new ReactiveLiveData<>(mainViewModel.hasAccounts());
+
+ hasAccounts$
+ .filter(hasAccounts -> !hasAccounts)
+ .observe(this, () -> importAccountLauncher.launch(ImportAccountActivity.createIntent(this)));
+
+ hasAccounts$
+ .filter(hasAccounts -> hasAccounts)
+ .tap(() -> binding.viewPager.unregisterOnPageChangeCallback(stackChangeCallback))
+ .tap(() -> binding.viewPager.registerOnPageChangeCallback(stackChangeCallback))
+ .flatMap(() -> mainViewModel.getCurrentAccount$())
+ .flatMap(account -> {
+ try {
+ applyAccount(account);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ showExceptionDialog(e, account);
+ // There is not much we can do here. Hide everything because this Exception means that our SyncManager instance is invalid
+ applyBoards(account, false, emptyList());
+ applyStacks(null, null, null);
+ return new MutableLiveData<>();
+ }
+ applyAccountTheme(account.getColor());
+ return new ReactiveLiveData<>(mainViewModel.getBoards(account.getId()))
+ .map(boardsAndArchived -> applyBoards(account, boardsAndArchived.second, boardsAndArchived.first))
+ .flatMap(navigationMap -> new ReactiveLiveData<>(mainViewModel.getCurrentFullBoard(account.getId()))
+ .combineWith(() -> new MutableLiveData<>(navigationMap)))
+ .flatMap(args -> {
+ applyBoard(account, args.second, args.first);
+ @Nullable final var currentBoard = args.first;
+ if (currentBoard == null) {
+ applyStacks(null, null, emptyList());
+ return new MutableLiveData<>(null);
+ } else {
+ return new ReactiveLiveData<>(mainViewModel.getStacks(account.getId(), currentBoard.getLocalId()))
+ .flatMap(stacks -> {
+ binding.viewPager.unregisterOnPageChangeCallback(stackChangeCallback);
+ boardChanged.set(true);
+ applyStacks(account, currentBoard.getLocalId(), stacks);
+ return mainViewModel.getCurrentStackId$(account.getId(), currentBoard.getLocalId());
+ });
+
+ }
+ }
+ );
+ })
+ .observe(this, currentStackId -> {
+ stackChangeCallback.updateMoveItemVisibility();
+ if (boardChanged.getAndSet(false)) {
+ applyStack(currentStackId);
+ binding.viewPager.registerOnPageChangeCallback(stackChangeCallback);
+ }
+ });
+ }
+
+ private void applyAccount(@NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ DeckLog.verbose("= Apply Account", account);
+ mainViewModel.recreateSyncManager(account);
+ registerAutoSyncOnNetworkAvailable(account);
+ navigationHandler.setCurrentAccount(account);
+
+ if (account.isMaintenanceEnabled()) {
+ refreshCapabilities(account, null);
+ }
+
+ Glide
+ .with(binding.accountSwitcher.getContext())
+ .load(account.getAvatarUrl(binding.accountSwitcher.getWidth()))
+ .placeholder(R.drawable.ic_baseline_account_circle_24)
+ .error(R.drawable.ic_baseline_account_circle_24)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.accountSwitcher);
+
+ DeckLog.verbose("Displaying maintenance mode info for", account.getName() + ":", account.isMaintenanceEnabled());
+ binding.infoBox.setVisibility(account.isMaintenanceEnabled() ? View.VISIBLE : View.GONE);
+ if (account.getServerDeckVersionAsObject().isSupported()) {
+ binding.infoBoxVersionNotSupported.setVisibility(View.GONE);
+ } else {
+ binding.infoBoxVersionNotSupported.setText(getString(R.string.info_box_version_not_supported, account.getServerDeckVersionAsObject(), Version.minimumSupported().getOriginalVersion()));
+ binding.infoBoxVersionNotSupported.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(account.getUrl() + getString(R.string.url_fragment_update_deck)))));
+ binding.infoBoxVersionNotSupported.setVisibility(View.VISIBLE);
+ }
+
+ binding.swipeRefreshLayout.setOnRefreshListener(() -> {
+ DeckLog.info("Triggered manual refresh");
+ CustomAppGlideModule.clearCache(this);
+
+ DeckLog.verbose("Trigger refresh capabilities for", account);
+ refreshCapabilities(account, () -> {
+ DeckLog.verbose("Trigger synchronization for", account);
+ mainViewModel.synchronize(account, new IResponseCallback<>() {
+ @Override
+ public void onResponse(Boolean response) {
+ DeckLog.info("End of synchronization for " + account + " → Stop spinner.");
+ runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false));
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ DeckLog.info("End of synchronization for " + account + " → Stop spinner.");
+ showSyncFailedSnackbar(account, throwable);
+ runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false));
+ }
+ });
+ });
+ });
+ }
+
+ private Map<Integer, Long> applyBoards(@NonNull Account account, boolean hasArchivedBoards, @Nullable List<FullBoard> fullBoards) {
+ DeckLog.verbose("=== Apply Boards", fullBoards, "for", account);
+ filterViewModel.clearFilterInformation(true);
+ binding.navigationView.setItemIconTintList(null);
+
+ final Map<Integer, Long> navigationMap;
+
+ if (fullBoards == null || fullBoards.isEmpty()) {
+ binding.emptyContentViewBoards.setVisibility(View.VISIBLE);
+ navigationMap = drawerMenuInflater.inflateBoards(account, emptyList(), account.getColor(), hasArchivedBoards, account.getServerDeckVersionAsObject().isSupported());
+
+ } else {
+ binding.emptyContentViewBoards.setVisibility(View.GONE);
+ navigationMap = drawerMenuInflater.inflateBoards(account, fullBoards, account.getColor(), hasArchivedBoards, account.getServerDeckVersionAsObject().isSupported());
+ }
+
+ navigationHandler.updateNavigationMap(navigationMap);
+ return navigationMap;
+ }
+
+ protected void applyBoard(@NonNull Account account, @NonNull Map<Integer, Long> navigationMap, @Nullable FullBoard currentBoard) {
+ DeckLog.verbose("===== Apply Board", currentBoard);
+ if (currentBoard == null) {
+ applyBoardTheme(account.getColor());
+ showEditButtonsIfPermissionsGranted(false, false);
+
+ binding.toolbar.setTitle(R.string.app_name_short);
+ binding.filterText.setHint(R.string.app_name_short);
+ binding.fab.setText(R.string.add_board);
+ binding.fab.setOnClickListener(v -> {
+ binding.fab.hide();
+ EditBoardDialogFragment.newInstance(account).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName());
+ });
+ } else {
+ applyBoardTheme(currentBoard.getBoard().getColor());
+ showEditButtonsIfPermissionsGranted(true, currentBoard.board.isPermissionEdit());
+
+ binding.toolbar.setTitle(currentBoard.getBoard().getTitle());
+ binding.filterText.setHint(getString(R.string.search_in, currentBoard.getBoard().getTitle()));
+ binding.fab.setText(R.string.add_list);
+ binding.fab.setOnClickListener(v -> {
+ binding.fab.hide();
+ EditStackDialogFragment.newInstance(currentBoard.getAccountId(), currentBoard.getLocalId()).show(getSupportFragmentManager(), EditStackDialogFragment.class.getSimpleName());
+ });
+
+ navigationMap
+ .entrySet()
+ .stream()
+ .filter(entry -> currentBoard.getLocalId().equals(entry.getValue()))
+ .map(Map.Entry::getKey)
+ .findFirst()
+ .ifPresent(menuItemId -> binding.navigationView.setCheckedItem(menuItemId));
+ }
+
+ }
+
+ private void applyStacks(@Nullable Account account, @Nullable Long boardId, @Nullable List<Stack> stacks) {
+ DeckLog.verbose("======= Apply Stacks", stacks, "for Board", boardId);
+ final boolean noStacksAvailable = stacks == null || stacks.isEmpty();
+
+ listMenu.findItem(R.id.archive_cards).setVisible(!noStacksAvailable);
+ listMenu.findItem(R.id.rename_list).setVisible(!noStacksAvailable);
+ listMenu.findItem(R.id.delete_list).setVisible(!noStacksAvailable);
+
+ if (account == null || noStacksAvailable) {
+ binding.emptyContentViewStacks.setVisibility(View.VISIBLE);
+
+ stackAdapter.setStacks(account, boardId, emptyList());
+ setStackMediator(new TabLayoutMediator(binding.stackTitles, binding.viewPager, (tab, position) -> tab.setText("ERROR")));
+ } else {
+ binding.emptyContentViewStacks.setVisibility(View.GONE);
+
+ binding.fab.setText(R.string.add_card);
+ binding.fab.setOnClickListener(v -> {
+ binding.fab.hide();
+ final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
+ NewCardDialog.newInstance(account, stack.getBoardId(), stack.getLocalId()).show(getSupportFragmentManager(), NewCardDialog.class.getSimpleName());
+ });
+
+ stackAdapter.setStacks(account, boardId, stacks);
+
+ final TabTitleGenerator tabTitleGenerator = position -> {
+ if (stacks.size() > position) {
+ return stacks.get(position).getTitle();
+ } else {
+ DeckLog.warn("Could not generate tab title for position " + position + " because list size is only " + stacks.size());
+ return "ERROR";
+ }
+ };
+ setStackMediator(new TabLayoutMediator(binding.stackTitles, binding.viewPager, (tab, position) -> tab.setText(tabTitleGenerator.getTitle(position))));
+ updateTabLayoutHelper(tabTitleGenerator);
+ }
+ }
+
+ private void applyStack(@Nullable Long stackId) {
+ DeckLog.verbose("========= Apply Stack", stackId);
+ if (stackId != null) {
+ try {
+ binding.viewPager.setCurrentItem(stackAdapter.getPosition(stackId), false);
+ } catch (NoSuchElementException e) {
+ DeckLog.warn(e);
+ }
+ }
+ }
+
+ private void applyBoardTheme(@ColorInt int color) {
+ final var utils = ThemeUtils.of(color, this);
+
+ utils.deck.themeTabLayout(binding.stackTitles);
+ utils.material.themeExtendedFAB(binding.fab);
+ utils.androidx.themeSwipeRefreshLayout(binding.swipeRefreshLayout);
+ utils.platform.colorEditText(binding.filterText);
+ utils.platform.tintDrawable(this, binding.filterIndicator.getDrawable());
+ binding.emptyContentViewStacks.applyTheme(color);
+ }
+
+ private void applyAccountTheme(@ColorInt int accountColor) {
+ final var utils = ThemeUtils.of(accountColor, this);
+
+ utils.platform.colorNavigationView(binding.navigationView, false);
+ binding.emptyContentViewBoards.applyTheme(accountColor);
+
+ @ColorInt final int headerTextColor = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(accountColor);
+ headerBinding.headerView.setBackgroundColor(accountColor);
+ headerBinding.appName.setTextColor(headerTextColor);
+ DrawableCompat.setTint(headerBinding.logo.getDrawable(), headerTextColor);
+ DrawableCompat.setTint(headerBinding.copyDebugLogs.getDrawable(), headerTextColor);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ this.binding = null;
+ this.headerBinding = null;
+ if (tabLayoutHelper != null) {
+ tabLayoutHelper.release();
+ }
+ }
+
+ @Override
+ public void onCreateStack(long accountId, long boardId, String stackName) {
+ mainViewModel.createStack(accountId, boardId, stackName, new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullStack response) {
+ binding.viewPager.post(() -> {
+ try {
+ binding.viewPager.setCurrentItem(stackAdapter.getPosition(response.getLocalId()));
+ mainViewModel.saveCurrentStackId(response.getEntity().getAccountId(), response.getEntity().getBoardId(), response.getEntity().getLocalId());
+ } catch (NoSuchElementException e) {
+ DeckLog.logError(e);
+ }
+ });
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ mainViewModel.getCurrentBoardColor(accountId, boardId)
+ .thenAcceptAsync(color -> ThemedSnackbar.make(binding.coordinatorLayout, Objects.requireNonNull(throwable.getLocalizedMessage()), Snackbar.LENGTH_LONG, color)
+ .setAction(R.string.simple_more, v -> showExceptionDialog(throwable, accountId))
+ .setAnchorView(binding.fab)
+ .show(), ContextCompat.getMainExecutor(MainActivity.this));
+ }
+ });
+ }
+
+ @Override
+ public void onUpdateStack(long localStackId, String stackName) {
+ mainViewModel.updateStackTitle(localStackId, stackName, new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullStack response) {
+ DeckLog.info("Successfully updated", Stack.class.getSimpleName(), "to", stackName);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ showExceptionDialog(throwable, null);
+ }
+ });
+ }
+
+ @Override
+ public void onCreateBoard(@NonNull Account account, String title, @ColorInt int color) {
+ final var boardToCreate = new Board(title, color);
+ boardToCreate.setPermissionEdit(true);
+ boardToCreate.setPermissionManage(true);
+
+ mainViewModel.createBoard(account, boardToCreate, new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullBoard response) {
+ runOnUiThread(() -> {
+ if (response != null) {
+ mainViewModel.saveCurrentBoardId(response.getAccountId(), response.getLocalId());
+ EditStackDialogFragment.newInstance(response.getAccountId(), response.getLocalId()).show(getSupportFragmentManager(), EditStackDialogFragment.class.getSimpleName());
+ }
+ });
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ ThemedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG, account.getColor())
+ .setAction(R.string.simple_more, v -> showExceptionDialog(throwable, account))
+ .setAnchorView(binding.fab)
+ .show();
+ }
+ });
+ }
+
+ @Override
+ public void onUpdateBoard(FullBoard fullBoard) {
+ mainViewModel.updateBoard(fullBoard, new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullBoard response) {
+ DeckLog.info("Successfully updated board", fullBoard.getBoard().getTitle());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ showExceptionDialog(throwable, fullBoard.getAccountId());
+ }
+ });
+ }
+
+ private void refreshCapabilities(final Account account, @Nullable Runnable runAfter) {
+ DeckLog.verbose("Refreshing capabilities for", account.getName());
+ mainViewModel.refreshCapabilities(new ResponseCallback<>(account) {
+ @Override
+ public void onResponse(Capabilities response) {
+ DeckLog.verbose("Finished refreshing capabilities for", account.getName(), "successfully.");
+ if (response.isMaintenanceEnabled()) {
+ DeckLog.verbose("Maintenance mode is enabled.");
+ } else {
+ DeckLog.verbose("Maintenance mode is disabled.");
+ // If we notice after updating the capabilities, that the new version is not supported, but it was previously, recreate the activity to make sure all elements are disabled properly
+ if (account.getServerDeckVersionAsObject().isSupported() && !response.getDeckVersion().isSupported()) {
+ ActivityCompat.recreate(MainActivity.this);
+ }
+ }
+
+ if (runAfter != null) {
+ runAfter.run();
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ DeckLog.warn("Error on refreshing capabilities for", account.getName(), "(" + throwable.getMessage() + ").");
+ if (throwable.getClass() == OfflineException.class || throwable instanceof OfflineException) {
+ DeckLog.info("Cannot refresh capabilities because device is offline.");
+ } else {
+ super.onError(throwable);
+ ExceptionDialogFragment.newInstance(throwable, account).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+
+ if (runAfter != null) {
+ runAfter.run();
+ }
+ }
+ });
+ }
+
+ @UiThread
+ private void updateTabLayoutHelper(@NonNull TabTitleGenerator tabTitleGenerator) {
+ if (this.tabLayoutHelper == null) {
+ this.tabLayoutHelper = new TabLayoutHelper(binding.stackTitles, binding.viewPager, tabTitleGenerator);
+ } else {
+ tabLayoutHelper.setTabTitleGenerator(tabTitleGenerator);
+ }
+ }
+
+ @UiThread
+ private void setStackMediator(@NonNull final TabLayoutMediator newMediator) {
+ if (mediator != null) {
+ mediator.detach();
+ }
+ newMediator.attach();
+ this.mediator = newMediator;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.archive_cards) {
+ final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
+ final var stackLocalId = stack.getLocalId();
+ mainViewModel.countCardsInStack(stack.getAccountId(), stackLocalId, numberOfCards -> runOnUiThread(() ->
+ new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.archive_cards)
+ .setMessage(getString(FilterInformation.hasActiveFilter(filterViewModel.getFilterInformation().getValue())
+ ? R.string.do_you_want_to_archive_all_cards_of_the_filtered_list
+ : R.string.do_you_want_to_archive_all_cards_of_the_list, stack.getTitle()))
+ .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> {
+ final var filterInformation = filterViewModel.getFilterInformation().getValue();
+ mainViewModel.archiveCardsInStack(stack.getAccountId(), stackLocalId, filterInformation == null ? new FilterInformation() : filterInformation, new IResponseCallback<>() {
+ @Override
+ public void onResponse(Void response) {
+ DeckLog.info("Successfully archived all cards in stack local id", stackLocalId);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
+ IResponseCallback.super.onError(throwable);
+ showExceptionDialog(throwable, stack.getAccountId());
+ }
+ }
+ });
+ })
+ .setNeutralButton(android.R.string.cancel, null)
+ .create()
+ .show()
+ ));
+ return true;
+ } else if (itemId == R.id.add_list) {
+ final Long accountId = stackAdapter.getAccount() == null ? null : stackAdapter.getAccount().getId();
+ if (accountId == null) {
+ DeckLog.warn("Can not launch stack dialog: accountId of stackAdapter is null.");
+ return false;
+ }
+ final Long boardId = stackAdapter.getBoardId();
+ if (boardId == null) {
+ DeckLog.warn("Can not launch stack dialog: boardId of stackAdapter is null.");
+ return false;
+ }
+ EditStackDialogFragment.newInstance(accountId, boardId).show(getSupportFragmentManager(), EditStackDialogFragment.class.getSimpleName());
+ return true;
+ } else if (itemId == R.id.rename_list) {
+ final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
+ new ReactiveLiveData<>(mainViewModel.getStack(stack.getAccountId(), stack.getLocalId()))
+ .observeOnce(MainActivity.this, fullStack -> EditStackDialogFragment
+ .newInstance(fullStack.getLocalId(), fullStack.getStack().getTitle())
+ .show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
+ return true;
+ } else if (itemId == R.id.move_list_left || itemId == R.id.move_list_right) {
+ final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
+ // TODO error handling
+ mainViewModel.reorderStack(stack.getAccountId(), stack.getBoardId(), stack.getLocalId(), itemId == R.id.move_list_right);
+ return true;
+ } else if (itemId == R.id.delete_list) {
+ final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
+ mainViewModel.countCardsInStack(stack.getAccountId(), stack.getLocalId(), numberOfCards -> runOnUiThread(() -> {
+ if (numberOfCards != null && numberOfCards > 0) {
+ DeleteStackDialogFragment.newInstance(stack.getAccountId(), stack.getBoardId(), stack.getLocalId(), numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName());
+ } else {
+ onDeleteStack(stack.getAccountId(), stack.getBoardId(), stack.getLocalId());
+ }
+ }));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void showEditButtonsIfPermissionsGranted(boolean currentBoardIsAvailable, boolean currentBoardHasEditPermission) {
+ if (currentBoardIsAvailable) {
+ if (!currentBoardHasEditPermission) {
+ binding.fab.hide();
+ binding.listMenuButton.setVisibility(View.GONE);
+ binding.emptyContentViewStacks.hideDescription();
+ } else {
+ binding.fab.show();
+ binding.listMenuButton.setVisibility(View.VISIBLE);
+ binding.emptyContentViewStacks.showDescription();
+ }
+ } else {
+ binding.fab.show();
+ binding.listMenuButton.setVisibility(View.GONE);
+ binding.emptyContentViewStacks.showDescription();
+ }
+ }
+
+ @Override
+ public void onScrollUp() {
+ binding.fab.extend();
+ }
+
+ @Override
+ public void onScrollDown() {
+ binding.fab.shrink();
+ }
+
+ @Override
+ public void onBottomReached() {
+ binding.fab.extend();
+ }
+
+ private void showFilterTextToolbar() {
+ binding.toolbar.setVisibility(View.GONE);
+ binding.searchToolbar.setVisibility(View.VISIBLE);
+ binding.searchToolbar.setNavigationOnClickListener(v1 -> onBackPressedCallback.handleOnBackPressed());
+ binding.enableSearch.setVisibility(View.GONE);
+ binding.filterText.requestFocus();
+ final var imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(binding.filterText, InputMethodManager.SHOW_IMPLICIT);
+ binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_on));
+ }
+
+ private void hideFilterTextToolbar() {
+ binding.filterText.setText(null);
+ final var imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
+ binding.searchToolbar.setVisibility(View.GONE);
+ binding.enableSearch.setVisibility(View.VISIBLE);
+ binding.toolbar.setVisibility(View.VISIBLE);
+ binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_off));
+ }
+
+ private void registerAutoSyncOnNetworkAvailable(@NonNull Account account) {
+ final var connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+ final var builder = new NetworkRequest.Builder();
+
+ if (connectivityManager != null) {
+ if (networkCallback != null) {
+ connectivityManager.unregisterNetworkCallback(networkCallback);
+ }
+
+ networkCallback = new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(@NonNull Network network) {
+ DeckLog.log("Got Network connection");
+ mainViewModel.synchronize(account, new IResponseCallback<>() {
+ @Override
+ public void onResponse(Boolean response) {
+ DeckLog.log("Auto-Sync after connection available successful");
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ if (throwable.getClass() == OfflineException.class || throwable instanceof OfflineException) {
+ DeckLog.error("Do not show sync failed snackbar because it is an ", OfflineException.class.getSimpleName(), "- assuming the user has wi-fi disabled but \"Sync only on wi-fi\" enabled");
+ } else if (throwable.getClass() == UnknownErrorException.class || throwable instanceof UnknownErrorException) {
+ DeckLog.error("Do not show sync failed snackbar because it is an ", UnknownErrorException.class.getSimpleName(), "- assuming a not reachable server or infrastructure issues");
+ } else {
+ showSyncFailedSnackbar(account, throwable);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ DeckLog.log("Network lost");
+ }
+ };
+ connectivityManager.registerNetworkCallback(builder.build(), networkCallback);
+ }
+ }
+
+ /**
+ * Find a StackFragment by it's ID, may return null.
+ *
+ * @param stackId ID of the stack to find
+ * @return Instance of StackFragment
+ */
+ @Nullable
+ public StackFragment findStackFragmentById(long stackId) {
+ return (StackFragment) getSupportFragmentManager().findFragmentByTag("f" + stackId);
+ }
+
+ /**
+ * This method is called when a new Card is created
+ *
+ * @param createdCard The new Card's data
+ */
+ @Override
+ public void onCardCreated(FullCard createdCard) {
+ final var card = createdCard.getCard();
+ DeckLog.log("Card Created! Title:" + card.getTitle() + " in stack ID: " + card.getStackId());
+
+ // Scroll the given StackFragment to the bottom, so the new Card is in view.
+ final var fragment = findStackFragmentById(card.getStackId());
+ if (fragment != null) {
+ fragment.scrollToBottom();
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ this.binding.fab.show();
+ }
+
+ @Override
+ public void onDeleteStack(long accountId, long boardId, long stackId) {
+ mainViewModel.deleteStack(accountId, boardId, stackId, new IResponseCallback<>() {
+ @Override
+ public void onResponse(Void response) {
+ DeckLog.info("Successfully deleted stack with local id", stackId, "and remote id", stackId);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
+ IResponseCallback.super.onError(throwable);
+ showExceptionDialog(throwable, accountId);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onBoardDeleted(Board board) {
+ mainViewModel.deleteBoard(board, new IResponseCallback<>() {
+ @Override
+ public void onResponse(Void response) {
+ DeckLog.info("Successfully deleted board", board.getTitle());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
+ IResponseCallback.super.onError(throwable);
+ showExceptionDialog(throwable, board.getAccountId());
+ }
+ }
+ });
+
+ binding.drawerLayout.closeDrawer(GravityCompat.START);
+ }
+
+ @Override
+ public void onArchive(@NonNull Board board) {
+ mainViewModel.archiveBoard(board, new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullBoard response) {
+ DeckLog.info("Successfully archived board", board.getTitle());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ showExceptionDialog(throwable, board.getAccountId());
+ }
+ });
+ }
+
+ @Override
+ public void onClone(@NonNull Account account, @NonNull Board board) {
+ final String[] cloneOptions = {getString(R.string.clone_cards)};
+ final boolean[] checkedItems = {false};
+ new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.clone_board)
+ .setMultiChoiceItems(cloneOptions, checkedItems, (dialog, which, isChecked) -> checkedItems[0] = isChecked)
+ .setPositiveButton(R.string.simple_clone, (dialog, which) -> {
+ binding.drawerLayout.closeDrawer(GravityCompat.START);
+ final var snackbar = ThemedSnackbar.make(binding.coordinatorLayout, getString(R.string.cloning_board, board.getTitle()), Snackbar.LENGTH_INDEFINITE, board.getColor())
+ .setAnchorView(binding.fab);
+ snackbar.show();
+ mainViewModel.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor(), checkedItems[0], new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullBoard response) {
+ runOnUiThread(() -> {
+ snackbar.dismiss();
+ mainViewModel.saveCurrentBoardId(response.getAccountId(), response.getLocalId());
+ ThemedSnackbar.make(binding.coordinatorLayout, getString(R.string.successfully_cloned_board, response.getBoard().getTitle()), Snackbar.LENGTH_LONG, response.getBoard().getColor())
+ .setAction(R.string.edit, v -> EditBoardDialogFragment.newInstance(account, response.getLocalId()).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName()))
+ .setAnchorView(binding.fab)
+ .show();
+ });
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ runOnUiThread(() -> {
+ snackbar.dismiss();
+ showExceptionDialog(throwable, board.getAccountId());
+ });
+ }
+ });
+ })
+ .setNeutralButton(android.R.string.cancel, null)
+ .show();
+ }
+
+
+ /**
+ * Displays a {@link ThemedSnackbar} for an exception of a failed sync, but only if the cause wasn't maintenance mode (this should be handled by a TextView instead of a snackbar).
+ *
+ * @param throwable the cause of the failed sync
+ */
+ @AnyThread
+ private void showSyncFailedSnackbar(@NonNull Account account, @NonNull Throwable throwable) {
+ if (!(throwable instanceof NextcloudHttpRequestFailedException) || ((NextcloudHttpRequestFailedException) throwable).getStatusCode() != HttpURLConnection.HTTP_UNAVAILABLE) {
+ runOnUiThread(() -> {
+ if (binding != null) { // Can be null in case the activity has been destroyed before the synchronization process has been finished
+ ThemedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG, account.getColor())
+ .setAction(R.string.simple_more, v -> showExceptionDialog(throwable, account))
+ .setAnchorView(binding.fab)
+ .show();
+ }
+ });
+ }
+ }
+
+ @AnyThread
+ protected void showExceptionDialog(@NonNull Throwable throwable, long accountId) {
+ mainViewModel.getAccount(accountId).thenAccept(account -> showExceptionDialog(throwable, account));
+ }
+
+ @AnyThread
+ protected void showExceptionDialog(@NonNull Throwable throwable, @Nullable Account account) {
+ runOnUiThread(() -> ExceptionDialogFragment
+ .newInstance(throwable, account)
+ .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivityNavigationHandler.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivityNavigationHandler.java
new file mode 100644
index 000000000..6850b70fb
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivityNavigationHandler.java
@@ -0,0 +1,120 @@
+package it.niedermann.nextcloud.deck.ui.main;
+
+import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_ABOUT;
+import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_ADD_BOARD;
+import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_ARCHIVED_BOARDS;
+import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_SETTINGS;
+import static it.niedermann.nextcloud.deck.ui.main.DrawerMenuInflater.MENU_ID_UPCOMING_CARDS;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.view.MenuItem;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.view.GravityCompat;
+import androidx.drawerlayout.widget.DrawerLayout;
+
+import com.google.android.material.navigation.NavigationView;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.ui.about.AboutActivity;
+import it.niedermann.nextcloud.deck.ui.archivedboards.ArchivedBoardsActivity;
+import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardDialogFragment;
+import it.niedermann.nextcloud.deck.ui.settings.SettingsActivity;
+import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsActivity;
+
+public class MainActivityNavigationHandler implements NavigationView.OnNavigationItemSelectedListener {
+
+ /**
+ * Keys: {@link MenuItem#getItemId()}
+ * Values: {@link FullBoard#getLocalId()}
+ */
+ private final Map<Integer, Long> navigationMap = new HashMap<>();
+ private final AppCompatActivity activity;
+ private final DrawerLayout drawerLayout;
+ private final BiConsumer<Long, Long> onBoardSelected;
+ private final ActivityResultLauncher<Intent> settingsLauncher;
+ @Nullable
+ private Account account = null;
+
+ public MainActivityNavigationHandler(
+ @NonNull AppCompatActivity activity,
+ @NonNull DrawerLayout drawerLayout,
+ @NonNull BiConsumer<Long, Long> onBoardSelected
+ ) {
+ this.activity = activity;
+ this.drawerLayout = drawerLayout;
+ this.onBoardSelected = onBoardSelected;
+ this.settingsLauncher = activity.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ ActivityCompat.recreate(activity);
+ }
+ });
+ }
+
+ @Override
+ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case MENU_ID_ABOUT:
+ if (account == null) {
+ DeckLog.warn("Current account is null, can not launch dialog to create board.");
+ return false;
+ }
+ activity.startActivity(AboutActivity.createIntent(activity, account));
+ break;
+ case MENU_ID_SETTINGS:
+ if (account == null) {
+ DeckLog.warn("Current account is null, can not launch dialog to create board.");
+ return false;
+ }
+ settingsLauncher.launch(SettingsActivity.createIntent(activity, account));
+ break;
+ case MENU_ID_ADD_BOARD:
+ if (account == null) {
+ DeckLog.warn("Current account is null, can not launch dialog to create board.");
+ return false;
+ }
+ EditBoardDialogFragment.newInstance(account).show(activity.getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName());
+ break;
+ case MENU_ID_ARCHIVED_BOARDS:
+ if (account == null) {
+ DeckLog.warn("Current account is null, can not launch dialog to create board.");
+ return false;
+ }
+ activity.startActivity(ArchivedBoardsActivity.createIntent(activity, account));
+ break;
+ case MENU_ID_UPCOMING_CARDS:
+ activity.startActivity(UpcomingCardsActivity.createIntent(activity));
+ break;
+ default:
+ if (account == null) {
+ DeckLog.warn("Current account is null, can not launch dialog to create board.");
+ return false;
+ }
+ onBoardSelected.accept(account.getId(), navigationMap.get(item.getItemId()));
+ break;
+ }
+ drawerLayout.closeDrawer(GravityCompat.START);
+ return true;
+ }
+
+ public void updateNavigationMap(@NonNull Map<Integer, Long> navigationMap) {
+ this.navigationMap.clear();
+ this.navigationMap.putAll(navigationMap);
+ }
+
+ public void setCurrentAccount(@NonNull Account account) {
+ this.account = account;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java
new file mode 100644
index 000000000..38edb933d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java
@@ -0,0 +1,256 @@
+package it.niedermann.nextcloud.deck.ui.main;
+
+import static java.util.concurrent.CompletableFuture.supplyAsync;
+
+import android.app.Application;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
+import java.io.File;
+import java.time.Instant;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.api.ResponseCallback;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Attachment;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.model.full.FullStack;
+import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
+import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
+import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
+
+@SuppressWarnings("WeakerAccess")
+public class MainViewModel extends BaseViewModel {
+
+ @Nullable
+ private SyncManager syncManager;
+
+ public MainViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ public void recreateSyncManager(@NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ try {
+ this.syncManager = new SyncManager(getApplication(), account);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ this.syncManager = null;
+ throw e;
+ }
+ }
+
+ private Exception getInvalidSyncManagerException() {
+ return new IllegalStateException("SyncManager is null");
+ }
+
+ public void synchronize(@NonNull Account account, @NonNull IResponseCallback<Boolean> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.synchronize(ResponseCallback.from(account, callback));
+ }
+ }
+
+ public void refreshCapabilities(@NonNull ResponseCallback<Capabilities> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.refreshCapabilities(callback);
+ }
+ }
+
+ public LiveData<Boolean> hasAccounts() {
+ return baseRepository.hasAccounts();
+ }
+
+ public CompletableFuture<Account> getAccount(long accountId) {
+ return supplyAsync(() -> baseRepository.readAccountDirectly(accountId), executor);
+ }
+
+ public CompletableFuture<Integer> getCurrentBoardColor(long accountId, long boardId) {
+ return baseRepository.getCurrentBoardColor(accountId, boardId);
+ }
+
+ public void saveCurrentBoardId(long accountId, long boardId) {
+ baseRepository.saveCurrentBoardId(accountId, boardId);
+ }
+
+ public void createBoard(@NonNull Account account, @NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.createBoard(account, board, callback);
+ }
+ }
+
+ public void updateBoard(@NonNull FullBoard board, @NonNull IResponseCallback<FullBoard> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.updateBoard(board, callback);
+ }
+ }
+
+ public void archiveBoard(@NonNull Board board, @NonNull IResponseCallback<FullBoard> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.archiveBoard(board, callback);
+ }
+ }
+
+ public void cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards, @NonNull IResponseCallback<FullBoard> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.cloneBoard(originAccountId, originBoardLocalId, targetAccountId, targetBoardColor, cloneCards, callback);
+ }
+ }
+
+ public void deleteBoard(@NonNull Board board, @NonNull IResponseCallback<Void> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.deleteBoard(board, callback);
+ }
+ }
+
+ public void saveCurrentStackId(long accountId, long boardId, long stackId) {
+ baseRepository.saveCurrentStackId(accountId, boardId, stackId);
+ }
+
+ public void createStack(long accountId, long boardId, @NonNull String title, @NonNull IResponseCallback<FullStack> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.createStack(accountId, boardId, title, callback);
+ }
+ }
+
+ public LiveData<FullStack> getStack(long accountId, long localStackId) {
+ if (syncManager == null) {
+ return new MutableLiveData<>();
+ }
+ return syncManager.getStack(accountId, localStackId);
+ }
+
+ public void reorderStack(long accountId, long boardId, long stackLocalId, boolean moveToRight) {
+ if (syncManager == null) {
+ DeckLog.logError(getInvalidSyncManagerException());
+ } else {
+ syncManager.reorderStack(accountId, boardId, stackLocalId, moveToRight);
+ }
+ }
+
+ public void updateStackTitle(long localStackId, @NonNull String newTitle, @NonNull IResponseCallback<FullStack> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.updateStackTitle(localStackId, newTitle, callback);
+ }
+ }
+
+ public void deleteStack(long accountId, long boardId, long stackLocalId, @NonNull IResponseCallback<Void> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.deleteStack(accountId, boardId, stackLocalId, callback);
+ }
+ }
+
+ public void reorder(@NonNull FullCard movedCard, long newStackId, int newIndex) {
+ if (syncManager == null) {
+ DeckLog.logError(getInvalidSyncManagerException());
+ } else {
+ syncManager.reorder(movedCard.getAccountId(), movedCard, newStackId, newIndex);
+ }
+ }
+
+ public void countCardsInStack(long accountId, long stackId, @NonNull IResponseCallback<Integer> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.countCardsInStackDirectly(accountId, stackId, callback);
+ }
+ }
+
+ public void archiveCardsInStack(long accountId, long stackId, @NonNull FilterInformation filterInformation, @NonNull IResponseCallback<Void> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.archiveCardsInStack(accountId, stackId, filterInformation, callback);
+ }
+ }
+
+ public void updateCard(@NonNull FullCard fullCard, @NonNull IResponseCallback<FullCard> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.updateCard(fullCard, callback);
+ }
+ }
+
+ public void addCommentToCard(long accountId, String message, long cardId) {
+ if (syncManager == null) {
+ DeckLog.logError(getInvalidSyncManagerException());
+ } else {
+ supplyAsync(() -> syncManager.readAccountDirectly(accountId))
+ .thenAcceptAsync(account -> syncManager.addCommentToCard(account.getId(), cardId, new DeckComment(message, account.getUserName(), Instant.now())));
+ }
+ }
+
+ public void addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file, @NonNull IResponseCallback<Attachment> callback) {
+ if (syncManager == null) {
+ callback.onError(getInvalidSyncManagerException());
+ } else {
+ syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file, callback);
+ }
+ }
+
+ public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) {
+ if (syncManager == null) {
+ DeckLog.logError(getInvalidSyncManagerException());
+ } else {
+ syncManager.addOrUpdateSingleCardWidget(widgetId, accountId, boardId, localCardId);
+ }
+ }
+
+ public LiveData<Account> getCurrentAccount$() {
+ return new ReactiveLiveData<>(baseRepository.getCurrentAccountId$())
+ .flatMap(baseRepository::readAccount);
+ }
+
+ public LiveData<Pair<List<FullBoard>, Boolean>> getBoards(long accountId) {
+ return new ReactiveLiveData<>(baseRepository.getFullBoards(accountId, false))
+ .combineWith(() -> baseRepository.hasArchivedBoards(accountId));
+ }
+
+ public LiveData<FullBoard> getCurrentFullBoard(long accountId) {
+ return new ReactiveLiveData<>(baseRepository.getCurrentBoardId$(accountId))
+ .flatMap(boardId -> baseRepository.getFullBoardById(accountId, boardId));
+ }
+
+ public LiveData<List<Stack>> getStacks(long accountId, long boardId) {
+ return new ReactiveLiveData<>(baseRepository.getStacksForBoard(accountId, boardId))
+ .distinctUntilChanged();
+ }
+
+ public LiveData<Long> getCurrentStackId$(long accountId, long boardId) {
+ return baseRepository.getCurrentStackId$(accountId, boardId);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java
index 1eabbd1b6..3a3465099 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java
@@ -1,6 +1,10 @@
package it.niedermann.nextcloud.deck.ui.manageaccounts;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
import android.net.Uri;
+import android.os.Build;
import android.text.TextUtils;
import android.view.View;
@@ -16,10 +20,7 @@ import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
-
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
@@ -38,7 +39,7 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
);
binding.accountHost.setText(Uri.parse(account.getUrl()).getHost());
Glide.with(itemView.getContext())
- .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))))
+ .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))
.placeholder(R.drawable.ic_baseline_account_circle_24)
.error(R.drawable.ic_baseline_account_circle_24)
.apply(RequestOptions.circleCropTransform())
@@ -56,5 +57,10 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
} else {
binding.currentAccountIndicator.setVisibility(GONE);
}
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ final var utils = ThemeUtils.of(account.getColor(), itemView.getContext());
+ utils.deck.colorSelectedCheck(binding.currentAccountIndicator.getContext(), binding.currentAccountIndicator.getDrawable());
+ }
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java
index a843551c1..d1ce2e14e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java
@@ -1,8 +1,5 @@
package it.niedermann.nextcloud.deck.ui.manageaccounts;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
-
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@@ -11,8 +8,10 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.databinding.ActivityManageAccountsBinding;
import it.niedermann.nextcloud.deck.model.Account;
@@ -34,7 +33,7 @@ public class ManageAccountsActivity extends AppCompatActivity {
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
- adapter = new ManageAccountAdapter((account) -> viewModel.setNewAccount(account), (accountPair) -> {
+ adapter = new ManageAccountAdapter((account) -> viewModel.saveCurrentAccount(account), (accountPair) -> {
if (accountPair.first != null) {
viewModel.deleteAccount(accountPair.first.getId());
} else {
@@ -42,29 +41,25 @@ public class ManageAccountsActivity extends AppCompatActivity {
}
Account newAccount = accountPair.second;
if (newAccount != null) {
- viewModel.setNewAccount(newAccount);
+ viewModel.saveCurrentAccount(newAccount);
} else {
Log.i(TAG, "Got delete account request, but new account is null. Maybe last account has been deleted?");
}
});
binding.accounts.setAdapter(adapter);
- observeOnce(viewModel.readAccount(readCurrentAccountId(this)), this, (account -> {
- adapter.setCurrentAccount(account);
- viewModel.readAccounts().observe(this, (localAccounts -> {
- if (localAccounts.size() == 0) {
- Log.i(TAG, "No accounts, finishing " + ManageAccountsActivity.class.getSimpleName());
- finish();
- } else {
- adapter.setAccounts(localAccounts);
- }
- }));
- }));
- }
-
- @Override
- public void onBackPressed() {
- onSupportNavigateUp();
+ viewModel.getCurrentAccountId().thenAcceptAsync(accountId -> new ReactiveLiveData<>(viewModel.readAccount(accountId))
+ .observeOnce(this, account -> {
+ adapter.setCurrentAccount(account);
+ viewModel.readAccounts().observe(this, (localAccounts -> {
+ if (localAccounts.size() == 0) {
+ Log.i(TAG, "No accounts, finishing " + ManageAccountsActivity.class.getSimpleName());
+ finish();
+ } else {
+ adapter.setAccounts(localAccounts);
+ }
+ }));
+ }), ContextCompat.getMainExecutor(this));
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java
index d97cf6b16..567732ddf 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java
@@ -1,42 +1,40 @@
package it.niedermann.nextcloud.deck.ui.manageaccounts;
-import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccount;
-
import android.app.Application;
import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
@SuppressWarnings("WeakerAccess")
-public class ManageAccountsViewModel extends AndroidViewModel {
-
- private SyncManager syncManager;
+public class ManageAccountsViewModel extends BaseViewModel {
public ManageAccountsViewModel(@NonNull Application application) {
super(application);
- this.syncManager = new SyncManager(application);
}
public LiveData<Account> readAccount(long id) {
- return syncManager.readAccount(id);
+ return baseRepository.readAccount(id);
}
public LiveData<List<Account>> readAccounts() {
- return syncManager.readAccounts();
+ return baseRepository.readAccounts();
+ }
+
+ public CompletableFuture<Long> getCurrentAccountId() {
+ return baseRepository.getCurrentAccountId();
}
- public void setNewAccount(@NonNull Account account) {
- saveCurrentAccount(getApplication(), account);
- syncManager = new SyncManager(getApplication());
+ public void saveCurrentAccount(@NonNull Account account) {
+ baseRepository.saveCurrentAccount(account);
}
public void deleteAccount(long id) {
- syncManager.deleteAccount(id);
+ baseRepository.deleteAccount(id);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java
index 5dd430d1d..8656d5bbb 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java
@@ -3,16 +3,21 @@ package it.niedermann.nextcloud.deck.ui.movecard;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogMoveCardBinding;
@@ -23,9 +28,9 @@ import it.niedermann.nextcloud.deck.ui.pickstack.PickStackFragment;
import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener;
import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
-import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
-public class MoveCardDialogFragment extends ThemedDialogFragment implements PickStackListener {
+public class MoveCardDialogFragment extends DialogFragment implements Themed, PickStackListener {
private static final String KEY_ORIGIN_ACCOUNT_ID = "account_id";
private static final String KEY_ORIGIN_BOARD_LOCAL_ID = "board_local_id";
@@ -37,6 +42,7 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick
private String originCardTitle;
private Long originCardLocalId;
private boolean originCardHasAttachmentsOrComments;
+ private View dialogView;
private DialogMoveCardBinding binding;
private PickStackViewModel viewModel;
@@ -74,10 +80,12 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick
originCardTitle = args.getString(KEY_ORIGIN_CARD_TITLE);
}
- @Nullable
+ @NonNull
@Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- binding = DialogMoveCardBinding.inflate(inflater);
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ final var dialogBuilder = new MaterialAlertDialogBuilder(requireContext());
+
+ binding = DialogMoveCardBinding.inflate(LayoutInflater.from(requireContext()));
binding.title.setText(getString(R.string.action_card_move_title, originCardTitle));
binding.submit.setOnClickListener((v) -> {
DeckLog.verbose("[Move card] Attempt to move to", Stack.class.getSimpleName(), "#" + selectedStack.getLocalId());
@@ -85,14 +93,24 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick
dismiss();
});
binding.cancel.setOnClickListener((v) -> dismiss());
- return binding.getRoot();
+ dialogView = binding.getRoot();
+ return dialogBuilder
+ .setView(dialogView)
+ .create();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return this.dialogView;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
getChildFragmentManager()
.beginTransaction()
- .add(R.id.fragment_container, PickStackFragment.newInstance(false))
+ .replace(R.id.fragment_container, PickStackFragment.newInstance(false), PickStackFragment.class.getSimpleName())
.commit();
}
@@ -106,12 +124,13 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick
public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) {
this.selectedAccount = account;
this.selectedBoard = board;
+ this.selectedStack = stack;
- if (board != null) {
- applyTheme(board.getColor());
- }
+ applyTheme(board == null
+ ? ContextCompat.getColor(requireContext(), R.color.accent)
+ : board.getColor()
+ );
- this.selectedStack = stack;
if (board == null || stack == null) {
binding.submit.setEnabled(false);
binding.moveWarning.setVisibility(GONE);
@@ -122,7 +141,7 @@ public class MoveCardDialogFragment extends ThemedDialogFragment implements Pick
}
@Override
- public void applyTheme(int color) {
+ public void applyTheme(@ColorInt int color) {
final var utils = ThemeUtils.of(color, requireContext());
utils.material.colorMaterialButtonText(binding.cancel);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java
index 23ed6fd6d..5784cbdf4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java
@@ -1,9 +1,6 @@
package it.niedermann.nextcloud.deck.ui.pickstack;
-import static androidx.lifecycle.Transformations.switchMap;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentBoardId;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentStackId;
+import static java.util.Collections.emptyList;
import android.content.Context;
import android.os.Bundle;
@@ -14,24 +11,30 @@ import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
import androidx.fragment.app.Fragment;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.databinding.FragmentPickStackBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Board;
import it.niedermann.nextcloud.deck.model.Stack;
-import it.niedermann.nextcloud.deck.ui.ImportAccountActivity;
import it.niedermann.nextcloud.deck.ui.preparecreate.AccountAdapter;
import it.niedermann.nextcloud.deck.ui.preparecreate.BoardAdapter;
+import it.niedermann.nextcloud.deck.ui.preparecreate.PickStackAdapter;
import it.niedermann.nextcloud.deck.ui.preparecreate.SelectedListener;
-import it.niedermann.nextcloud.deck.ui.preparecreate.StackAdapter;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
-public class PickStackFragment extends Fragment {
+public class PickStackFragment extends Fragment implements Themed, PickStackListener {
private FragmentPickStackBinding binding;
private PickStackViewModel viewModel;
@@ -41,68 +44,15 @@ public class PickStackFragment extends Fragment {
private PickStackListener pickStackListener;
private boolean showBoardsWithoutEditPermission = false;
- private long lastAccountId;
- private long lastBoardId;
- private long lastStackId;
- private ArrayAdapter<Account> accountAdapter;
- private ArrayAdapter<Board> boardAdapter;
- private ArrayAdapter<Stack> stackAdapter;
+ private ArrayAdapter<Account> pickAccountAdapter;
+ private ArrayAdapter<Board> pickBoardAdapter;
+ private PickStackAdapter pickStackAdapter;
- @Nullable
- private LiveData<List<Board>> boardsLiveData;
- @NonNull
- private final Observer<List<Board>> boardsObserver = (boards) -> {
- boardAdapter.clear();
- boardAdapter.addAll(boards);
- binding.boardSelect.setEnabled(true);
-
- if (boards.size() > 0) {
- binding.boardSelect.setEnabled(true);
-
- Board boardToSelect = null;
- for (final var board : boards) {
- if (board.getLocalId() == lastBoardId) {
- boardToSelect = board;
- break;
- }
- }
- if (boardToSelect == null) {
- boardToSelect = boards.get(0);
- }
- binding.boardSelect.setSelection(boardAdapter.getPosition(boardToSelect));
- } else {
- binding.boardSelect.setEnabled(false);
- pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), null, null);
- }
- };
-
- @Nullable
- private LiveData<List<Stack>> stacksLiveData;
- @NonNull
- private final Observer<List<Stack>> stacksObserver = (stacks) -> {
- stackAdapter.clear();
- stackAdapter.addAll(stacks);
-
- if (stacks.size() > 0) {
- binding.stackSelect.setEnabled(true);
-
- Stack stackToSelect = null;
- for (final var stack : stacks) {
- if (stack.getLocalId() == lastStackId) {
- stackToSelect = stack;
- break;
- }
- }
- if (stackToSelect == null) {
- stackToSelect = stacks.get(0);
- }
- binding.stackSelect.setSelection(stackAdapter.getPosition(stackToSelect));
- } else {
- binding.stackSelect.setEnabled(false);
- pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), null);
- }
- };
+ private final ReactiveLiveData<Void> selectionChanged$ = new ReactiveLiveData<>();
+ private final AtomicReference<Long> selectedAccount = new AtomicReference<>();
+ private final Map<Long, Long> selectedBoard = new HashMap<>();
+ private final Map<Pair<Long, Long>, Long> selectedStack = new HashMap<>();
@Override
public void onAttach(@NonNull Context context) {
@@ -127,74 +77,202 @@ public class PickStackFragment extends Fragment {
binding = FragmentPickStackBinding.inflate(getLayoutInflater());
viewModel = new ViewModelProvider(requireActivity()).get(PickStackViewModel.class);
- accountAdapter = new AccountAdapter(requireContext());
- binding.accountSelect.setAdapter(accountAdapter);
+ pickAccountAdapter = new AccountAdapter(requireContext());
+ binding.accountSelect.setAdapter(pickAccountAdapter);
binding.accountSelect.setEnabled(false);
- boardAdapter = new BoardAdapter(requireContext());
- binding.boardSelect.setAdapter(boardAdapter);
- binding.stackSelect.setEnabled(false);
- stackAdapter = new StackAdapter(requireContext());
- binding.stackSelect.setAdapter(stackAdapter);
- binding.stackSelect.setEnabled(false);
-
- switchMap(viewModel.hasAccounts(), hasAccounts -> {
- if (hasAccounts) {
- return viewModel.readAccounts();
- } else {
- // TODO After successfully importing the account, the creation will throw a TokenMissMatchException - Recreate SyncManager?
- startActivity(ImportAccountActivity.createIntent(requireContext()));
- return null;
- }
- }).observe(getViewLifecycleOwner(), (List<Account> accounts) -> {
- if (accounts == null || accounts.size() == 0) {
- throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry");
- }
-
- lastAccountId = readCurrentAccountId(requireContext());
- lastBoardId = readCurrentBoardId(requireContext(), lastAccountId);
- lastStackId = readCurrentStackId(requireContext(), lastAccountId, lastBoardId);
-
- accountAdapter.clear();
- accountAdapter.addAll(accounts);
- binding.accountSelect.setEnabled(true);
+ binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
+ selectedAccount.set(parent.getSelectedItemId());
+ selectionChanged$.setValue(null);
+ });
- for (Account account : accounts) {
- if (account.getId() == lastAccountId) {
- binding.accountSelect.setSelection(accountAdapter.getPosition(account));
- break;
- }
- }
+ pickBoardAdapter = new BoardAdapter(requireContext());
+ binding.boardSelect.setAdapter(pickBoardAdapter);
+ binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
+ selectedBoard.put(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId());
+ selectionChanged$.setValue(null);
});
- binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) ->
- updateLiveDataSource(boardsLiveData, boardsObserver, showBoardsWithoutEditPermission
- ? viewModel.getBoards(parent.getSelectedItemId())
- : viewModel.getBoardsWithEditPermission(parent.getSelectedItemId())));
+ pickStackAdapter = new PickStackAdapter(stack -> {
+ selectedStack.put(new Pair<>(binding.accountSelect.getSelectedItemId(), binding.boardSelect.getSelectedItemId()), stack.getLocalId());
+ selectionChanged$.setValue(null);
+ });
+ binding.stackSelect.setAdapter(pickStackAdapter);
+
+ selectionChanged$
+ .flatMap(() -> viewModel.readAccounts())
+ .flatMap(accounts -> {
+ binding.accountSelect.setEnabled(false);
+ binding.boardSelect.setEnabled(false);
+ setAccounts(accounts);
+
+ return getSelectedAccount()
+ .map(accountIdToSelect -> selectAccount(accounts, accountIdToSelect))
+ .flatMap(accountId -> getBoards(accountId, showBoardsWithoutEditPermission)
+ .flatMap(boards -> {
+ binding.boardSelect.setEnabled(false);
+ setBoards(boards);
- binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) ->
- updateLiveDataSource(stacksLiveData, stacksObserver, viewModel.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId())));
+ return getSelectedBoard(accountId)
+ .map(boardId -> selectBoard(boards, boardId))
+ .flatMap(boardId -> getStacks(accountId, boardId)
+ .flatMap(stacks -> {
+ setStacks(stacks);
- binding.stackSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) ->
- pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), (Stack) parent.getSelectedItem()));
+ return getSelectedStack(accountId, boardId)
+ .map(stackId -> selectStack(stacks, stackId));
+ }));
+ }));
+
+ }).observe(this);
+
+ selectionChanged$.setValue(null);
return binding.getRoot();
}
+ private ReactiveLiveData<Long> getSelectedAccount() {
+ if (selectedAccount.get() == null) {
+ return new ReactiveLiveData<>(viewModel.getCurrentAccountId$());
+ } else {
+ return new ReactiveLiveData<>(selectedAccount.get());
+ }
+ }
+
+ private void setAccounts(@NonNull Collection<Account> accounts) {
+ pickAccountAdapter.clear();
+ pickAccountAdapter.addAll(accounts);
+
+ if (accounts.size() > 1) {
+ binding.accountSelect.setVisibility(View.VISIBLE);
+ binding.accountSelect.setEnabled(true);
+ } else {
+ binding.accountSelect.setVisibility(View.GONE);
+ }
+ }
+
+
+ @Nullable
+ private Long selectAccount(@NonNull Collection<Account> accounts, @Nullable Long accountIdToSelect) {
+ final var matchingAccount = accounts
+ .stream()
+ .filter(account -> Objects.equals(account.getId(), accountIdToSelect))
+ .findAny()
+ .or(() -> accounts.stream().findAny());
+
+ if (matchingAccount.isPresent()) {
+ binding.accountSelect.setSelection(pickAccountAdapter.getPosition(matchingAccount.get()));
+ return matchingAccount.get().getId();
+ } else {
+ return null;
+ }
+ }
+
+ private ReactiveLiveData<Long> getSelectedBoard(@Nullable Long accountId) {
+ if (accountId == null) {
+ return new ReactiveLiveData<>(null);
+ } else if (selectedBoard.containsKey(accountId)) {
+ return new ReactiveLiveData<>(Objects.requireNonNull(selectedBoard.get(accountId)));
+ } else {
+ return new ReactiveLiveData<>(viewModel.getCurrentBoardId$(accountId));
+ }
+ }
+
+ private ReactiveLiveData<List<Board>> getBoards(@Nullable Long accountId, boolean showBoardsWithoutEditPermission) {
+ if (accountId == null) {
+ return new ReactiveLiveData<>(emptyList());
+ } else if (showBoardsWithoutEditPermission) {
+ return new ReactiveLiveData<>(viewModel.getNotArchivedBoards(accountId));
+ } else {
+ return new ReactiveLiveData<>(viewModel.getBoardsWithEditPermission(accountId));
+ }
+ }
+
+ private void setBoards(@NonNull Collection<Board> boards) {
+ pickBoardAdapter.clear();
+ pickBoardAdapter.addAll(boards);
+
+ if (boards.size() > 1) {
+ binding.boardSelect.setVisibility(View.VISIBLE);
+ binding.boardSelect.setEnabled(true);
+ } else {
+ binding.boardSelect.setVisibility(View.GONE);
+ }
+ }
+
+ @Nullable
+ private Long selectBoard(@NonNull Collection<Board> boards, @Nullable Long boardIdToSelect) {
+ final var matchingBoard = boards
+ .stream()
+ .filter(board -> Objects.equals(board.getLocalId(), boardIdToSelect))
+ .findAny()
+ .or(() -> boards.stream().findAny());
+
+ if (matchingBoard.isPresent()) {
+ binding.boardSelect.setSelection(pickBoardAdapter.getPosition(matchingBoard.get()));
+ applyTheme(matchingBoard.get().getColor());
+ return matchingBoard.get().getLocalId();
+ } else {
+ onStackPicked((Account) binding.accountSelect.getSelectedItem(), null, null);
+ return null;
+ }
+ }
+
+ private ReactiveLiveData<List<Stack>> getStacks(@Nullable Long accountId, @Nullable Long boardId) {
+ if (accountId == null || boardId == null) {
+ return new ReactiveLiveData<>(emptyList());
+ } else {
+ return new ReactiveLiveData<>(viewModel.getStacksForBoard(accountId, boardId));
+ }
+ }
+
+ private void setStacks(@NonNull Collection<Stack> stacks) {
+ pickStackAdapter.setStacks(stacks);
+ }
+
+ private ReactiveLiveData<Long> getSelectedStack(@Nullable Long accountId, @Nullable Long boardId) {
+ if (selectedStack.containsKey(new Pair<>(accountId, boardId))) {
+ return new ReactiveLiveData<>(Objects.requireNonNull(selectedStack.get(new Pair<>(accountId, boardId))));
+ } else if (accountId == null || boardId == null) {
+ return new ReactiveLiveData<>(null);
+ } else {
+ return new ReactiveLiveData<>(viewModel.getCurrentStackId$(accountId, boardId));
+ }
+ }
+
+ private Long selectStack(@NonNull Collection<Stack> stacks, @Nullable Long stackIdToSelect) {
+ final var matchingStack = stacks
+ .stream()
+ .filter(stack -> Objects.equals(stack.getLocalId(), stackIdToSelect))
+ .findAny()
+ .or(() -> stacks.stream().findAny());
+
+ if (matchingStack.isPresent()) {
+ pickStackAdapter.setSelection(matchingStack.get());
+ onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), matchingStack.get());
+ return matchingStack.get().getLocalId();
+ } else {
+ onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), null);
+ return null;
+ }
+ }
+
+ @Override
+ public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) {
+ DeckLog.verbose("Picked account", account.getName());
+ DeckLog.verbose("Picked board", board == null ? "null" : board.getTitle());
+ DeckLog.verbose("Picked stack", stack == null ? "null" : stack.getTitle());
+ pickStackListener.onStackPicked(account, board, stack);
+ }
+
@Override
public void onDestroy() {
super.onDestroy();
this.binding = null;
}
- /**
- * Updates the source of the given liveData and de- and reregisters the given observer.
- */
- private <T> void updateLiveDataSource(@Nullable LiveData<T> liveData, Observer<T> observer, LiveData<T> newSource) {
- if (liveData != null) {
- liveData.removeObserver(observer);
- }
- liveData = newSource;
- liveData.observe(getViewLifecycleOwner(), observer);
+ @Override
+ public void applyTheme(int color) {
+ pickStackAdapter.applyTheme(color);
}
public static PickStackFragment newInstance(boolean showBoardsWithoutEditPermission) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java
index 4999ec6f4..c4bbcb205 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java
@@ -1,10 +1,11 @@
package it.niedermann.nextcloud.deck.ui.pickstack;
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -13,14 +14,10 @@ import java.util.List;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Board;
import it.niedermann.nextcloud.deck.model.Stack;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-
-import static androidx.lifecycle.Transformations.distinctUntilChanged;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
@SuppressWarnings("WeakerAccess")
-public class PickStackViewModel extends AndroidViewModel {
-
- private final SyncManager syncManager;
+public class PickStackViewModel extends BaseViewModel {
private Account selectedAccount;
@Nullable
@@ -34,7 +31,18 @@ public class PickStackViewModel extends AndroidViewModel {
public PickStackViewModel(@NonNull Application application) {
super(application);
- this.syncManager = new SyncManager(application);
+ }
+
+ public LiveData<Long> getCurrentAccountId$() {
+ return baseRepository.getCurrentAccountId$();
+ }
+
+ public LiveData<Long> getCurrentBoardId$(long accountId) {
+ return baseRepository.getCurrentBoardId$(accountId);
+ }
+
+ public LiveData<Long> getCurrentStackId$(long accountId, long boardId) {
+ return baseRepository.getCurrentStackId$(accountId, boardId);
}
public LiveData<Boolean> submitButtonEnabled() {
@@ -81,22 +89,22 @@ public class PickStackViewModel extends AndroidViewModel {
}
public LiveData<Boolean> hasAccounts() {
- return syncManager.hasAccounts();
+ return baseRepository.hasAccounts();
}
public LiveData<List<Account>> readAccounts() {
- return syncManager.readAccounts();
+ return baseRepository.readAccounts();
}
- public LiveData<List<Board>> getBoards(long accountId) {
- return syncManager.getBoards(accountId);
+ public LiveData<List<Board>> getNotArchivedBoards(long accountId) {
+ return baseRepository.getBoards(accountId, false);
}
public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) {
- return syncManager.getBoardsWithEditPermission(accountId);
+ return baseRepository.getBoardsWithEditPermission(accountId);
}
public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
- return syncManager.getStacksForBoard(accountId, localBoardId);
+ return baseRepository.getStacksForBoard(accountId, localBoardId);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java
index 78dff7711..bf919cd66 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java
@@ -16,7 +16,6 @@ import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateAccountBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
public class AccountAdapter extends AbstractAdapter<Account> {
@@ -40,17 +39,17 @@ public class AccountAdapter extends AbstractAdapter<Account> {
binding = ItemPrepareCreateAccountBinding.bind(convertView);
}
- final var item = getItem(position);
- if (item != null) {
- binding.username.setText(item.getUserDisplayName());
+ final var account = getItem(position);
+ if (account != null) {
+ binding.username.setText(account.getUserDisplayName());
try {
- binding.instance.setText(new URL(item.getUrl()).getHost());
+ binding.instance.setText(new URL(account.getUrl()).getHost());
} catch (Throwable t) {
- binding.instance.setText(item.getUrl());
+ binding.instance.setText(account.getUrl());
}
Glide.with(getContext())
- .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size))))
+ .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size)))
.placeholder(R.drawable.ic_baseline_account_circle_24)
.error(R.drawable.ic_baseline_account_circle_24)
.apply(RequestOptions.circleCropTransform())
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java
index e0a7c68c0..87713a105 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java
@@ -5,12 +5,13 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateBoardBinding;
import it.niedermann.nextcloud.deck.model.Board;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
public class BoardAdapter extends AbstractAdapter<Board> {
@@ -37,7 +38,8 @@ public class BoardAdapter extends AbstractAdapter<Board> {
final var board = getItem(position);
if (board != null) {
binding.boardTitle.setText(board.getTitle());
- binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, board.getColor()));
+ final var utils = ThemeUtils.of(ContextCompat.getColor(getContext(), R.color.defaultBrand), getContext());
+ binding.avatar.setImageDrawable(utils.deck.getColoredBoardDrawable(binding.avatar.getContext(), board.getColor()));
} else {
DeckLog.logError(new IllegalArgumentException("No item for position " + position));
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackAdapter.java
new file mode 100644
index 000000000..ab25c8ba9
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackAdapter.java
@@ -0,0 +1,95 @@
+package it.niedermann.nextcloud.deck.ui.preparecreate;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateStackBinding;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
+
+public class PickStackAdapter extends RecyclerView.Adapter<PickStackViewHolder> implements Themed {
+
+ @Nullable
+ @ColorInt
+ Integer color;
+ @Nullable
+ private Stack selectedStack = null;
+ private final List<Stack> stacks = new ArrayList<>();
+ @NonNull
+ private final Consumer<Stack> onStackSelectedListener;
+
+ public PickStackAdapter(@NonNull Consumer<Stack> onStackSelectedListener) {
+ this.onStackSelectedListener = onStackSelectedListener;
+ setHasStableIds(true);
+ }
+
+ @NonNull
+ @Override
+ public PickStackViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new PickStackViewHolder(ItemPrepareCreateStackBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull PickStackViewHolder holder, int position) {
+ holder.bind(stacks.get(position), stack -> {
+ setSelection(stack);
+ onStackSelectedListener.accept(selectedStack);
+ }, selectedStack, color);
+ }
+
+ @Override
+ public int getItemCount() {
+ return stacks.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return stacks.get(position).getLocalId();
+ }
+
+ public void setStacks(@NonNull Collection<Stack> stacks) {
+ this.stacks.clear();
+ this.selectedStack = null;
+ this.stacks.addAll(stacks);
+ notifyDataSetChanged();
+ }
+
+ public void setSelection(@NonNull Stack stack) {
+ final var previousPosition = getPosition(selectedStack);
+ final var nextPosition = getPosition(stack);
+ this.selectedStack = stack;
+ previousPosition.ifPresent(this::notifyItemChanged);
+ nextPosition.ifPresent(this::notifyItemChanged);
+ }
+
+ private Optional<Integer> getPosition(@Nullable Stack stack) {
+ if (stack == null) {
+ return Optional.empty();
+ }
+
+ for (int i = 0; i < stacks.size(); i++) {
+ if (stacks.get(i).getLocalId().equals(stack.getLocalId())) {
+ return Optional.of(i);
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ @Override
+ public void applyTheme(int color) {
+ this.color = color;
+ notifyDataSetChanged();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackViewHolder.java
new file mode 100644
index 000000000..499c55871
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackViewHolder.java
@@ -0,0 +1,46 @@
+package it.niedermann.nextcloud.deck.ui.preparecreate;
+
+import android.os.Build;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.function.Consumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateStackBinding;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
+
+class PickStackViewHolder extends RecyclerView.ViewHolder implements Themed {
+
+ private final ItemPrepareCreateStackBinding binding;
+
+ public PickStackViewHolder(@NonNull ItemPrepareCreateStackBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Stack stack, @NonNull Consumer<Stack> onStackSelected, @Nullable Stack selectedStack, @Nullable @ColorInt Integer color) {
+ binding.stackTitle.setText(stack.getTitle());
+ itemView.setSelected(stack.getLocalId().equals(selectedStack == null ? -1 : selectedStack.getLocalId()));
+ itemView.setOnClickListener(view -> {
+ if (!itemView.isSelected()) {
+ onStackSelected.accept(stack);
+ }
+ });
+ if (color != null) {
+ applyTheme(color);
+ }
+ }
+
+ @Override
+ public void applyTheme(@ColorInt int color) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ final var utils = ThemeUtils.of(color, itemView.getContext());
+ utils.deck.colorSelectedCheck(binding.selectedCheck.getContext(), binding.selectedCheck.getDrawable());
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java
index 659190cab..c27a05006 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java
@@ -1,9 +1,5 @@
package it.niedermann.nextcloud.deck.ui.preparecreate;
-import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccount;
-import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentBoardId;
-import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentStackId;
-
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
@@ -58,9 +54,9 @@ public class PrepareCreateActivity extends PickStackActivity {
viewModel.saveCard(account, boardId, stackId, fullCard, new IResponseCallback<>() {
@Override
public void onResponse(FullCard response) {
- saveCurrentAccount(PrepareCreateActivity.this, account);
- saveCurrentBoardId(PrepareCreateActivity.this, account.getId(), boardId);
- saveCurrentStackId(PrepareCreateActivity.this, account.getId(), boardId, stackId);
+ viewModel.saveCurrentAccount(account);
+ viewModel.saveCurrentBoardId(account.getId(), boardId);
+ viewModel.saveCurrentStackId(account.getId(), boardId, stackId);
callback.onResponse(null);
startActivity(EditActivity.createEditCardIntent(PrepareCreateActivity.this, account, boardId, response.getLocalId()));
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateViewModel.java
index fb3bd5c91..90524c22a 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateViewModel.java
@@ -5,7 +5,8 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.lifecycle.AndroidViewModel;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.model.Account;
@@ -13,16 +14,33 @@ import it.niedermann.nextcloud.deck.model.Card;
import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.model.ocs.Version;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
@SuppressWarnings("WeakerAccess")
-public class PrepareCreateViewModel extends AndroidViewModel {
+public class PrepareCreateViewModel extends BaseViewModel {
public PrepareCreateViewModel(@NonNull Application application) {
super(application);
}
+ public void saveCurrentAccount(@NonNull Account account) {
+ baseRepository.saveCurrentAccount(account);
+ }
+
+ public void saveCurrentBoardId(long accountId, long boardId) {
+ baseRepository.saveCurrentBoardId(accountId, boardId);
+ }
+
+ public void saveCurrentStackId(long accountId, long boardId, long stackId) {
+ baseRepository.saveCurrentStackId(accountId, boardId, stackId);
+ }
+
public void saveCard(@NonNull Account account, long boardLocalId, long stackLocalId, @NonNull FullCard fullCard, @NonNull IResponseCallback<FullCard> callback) {
- new SyncManager(getApplication(), account.getName()).createFullCard(account.getId(), boardLocalId, stackLocalId, fullCard, callback);
+ try {
+ new SyncManager(getApplication(), account).createFullCard(account.getId(), boardLocalId, stackLocalId, fullCard, callback);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ callback.onError(e);
+ }
}
@SuppressWarnings("ConstantConditions")
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java
deleted file mode 100644
index e3e0ad5b5..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package it.niedermann.nextcloud.deck.ui.preparecreate;
-
-import android.content.Context;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-
-import it.niedermann.nextcloud.deck.DeckLog;
-import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateStackBinding;
-import it.niedermann.nextcloud.deck.model.Stack;
-
-public class StackAdapter extends AbstractAdapter<Stack> {
-
- @SuppressWarnings("WeakerAccess")
- public StackAdapter(@NonNull Context context) {
- super(context, R.layout.item_prepare_create_stack);
- }
-
- @Override
- protected long getItemId(@NonNull Stack item) {
- return item.getLocalId();
- }
-
- @NonNull
- @Override
- public View getView(int position, View convertView, @NonNull ViewGroup parent) {
- final ItemPrepareCreateStackBinding binding;
- if (convertView == null) {
- binding = ItemPrepareCreateStackBinding.inflate(inflater, parent, false);
- } else {
- binding = ItemPrepareCreateStackBinding.bind(convertView);
- }
-
- final Stack item = getItem(position);
- if (item != null) {
- binding.stackTitle.setText(item.getTitle());
- } else {
- DeckLog.logError(new IllegalArgumentException("No item for position " + position));
- }
- return binding.getRoot();
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/PreferencesViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/PreferencesViewModel.java
new file mode 100644
index 000000000..298beebb4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/PreferencesViewModel.java
@@ -0,0 +1,39 @@
+package it.niedermann.nextcloud.deck.ui.settings;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.nextcloud.deck.persistence.PreferencesRepository;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
+
+public class PreferencesViewModel extends BaseViewModel {
+
+ private final PreferencesRepository preferencesRepository;
+
+ public PreferencesViewModel(@NonNull Application application) {
+ this(application, new PreferencesRepository(application));
+ }
+
+ public PreferencesViewModel(@NonNull Application application, @NonNull PreferencesRepository preferencesRepository) {
+ super(application);
+ this.preferencesRepository = preferencesRepository;
+ }
+
+ public LiveData<Long> getCurrentAccountId$() {
+ return baseRepository.getCurrentAccountId$();
+ }
+
+ public LiveData<Integer> getAccountColor(long accountId) {
+ return baseRepository.getAccountColor(accountId);
+ }
+
+ public void setAppTheme(int setting) {
+ preferencesRepository.setAppTheme(setting);
+ }
+
+ public LiveData<Boolean> isDebugModeEnabled$() {
+ return preferencesRepository.isDebugModeEnabled$();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java
index 07df2ac20..172b31fd1 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java
@@ -8,12 +8,15 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
-import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ActivitySettingsBinding;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
-public class SettingsActivity extends AppCompatActivity {
+public class SettingsActivity extends AppCompatActivity implements Themed {
+ private static final String KEY_ACCOUNT = "account";
private ActivitySettingsBinding binding;
@Override
@@ -21,16 +24,18 @@ public class SettingsActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+ if (!getIntent().hasExtra(KEY_ACCOUNT)) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " must be provided");
+ }
+
+ final var account = (Account) getIntent().getSerializableExtra(KEY_ACCOUNT);
+
binding = ActivitySettingsBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
+ applyTheme(account.getColor());
setSupportActionBar(binding.toolbar);
-
- setResult(RESULT_OK);
- getSupportFragmentManager()
- .beginTransaction()
- .add(R.id.settings_layout, new SettingsFragment())
- .commit();
+ setContentView(binding.getRoot());
+ setResult(RESULT_CANCELED);
}
@Override
@@ -45,8 +50,17 @@ public class SettingsActivity extends AppCompatActivity {
this.binding = null;
}
+ @Override
+ public void applyTheme(int color) {
+ final var utils = ThemeUtils.of(color, this);
+
+// utils.platform.themeStatusBar(this);
+// utils.material.themeToolbar(binding.toolbar);
+ }
+
@NonNull
- public static Intent createIntent(@NonNull Context context) {
- return new Intent(context, SettingsActivity.class);
+ public static Intent createIntent(@NonNull Context context, @NonNull Account account) {
+ return new Intent(context, SettingsActivity.class)
+ .putExtra(KEY_ACCOUNT, account);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java
index cc01781e6..79a03ab71 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java
@@ -1,7 +1,5 @@
package it.niedermann.nextcloud.deck.ui.settings;
-import static it.niedermann.nextcloud.deck.DeckApplication.setAppTheme;
-
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
@@ -9,10 +7,13 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
+import androidx.lifecycle.ViewModelProvider;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
-import it.niedermann.nextcloud.deck.DeckApplication;
+import java.util.stream.Stream;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker;
@@ -20,6 +21,7 @@ import it.niedermann.nextcloud.deck.ui.theme.ThemedSwitchPreference;
public class SettingsFragment extends PreferenceFragmentCompat {
+ private PreferencesViewModel preferencesViewModel;
private ThemedSwitchPreference wifiOnlyPref;
private ThemedSwitchPreference compactPref;
private ThemedSwitchPreference coverImagesPref;
@@ -31,6 +33,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.settings, rootKey);
+ preferencesViewModel = new ViewModelProvider(requireActivity()).get(PreferencesViewModel.class);
+
wifiOnlyPref = findPreference(getString(R.string.pref_key_wifi_only));
coverImagesPref = findPreference(getString(R.string.pref_key_cover_images));
compactPref = findPreference(getString(R.string.pref_key_compact));
@@ -61,7 +65,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
final var themePref = findPreference(getString(R.string.pref_key_dark_theme));
if (themePref != null) {
themePref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> {
- setAppTheme(Integer.parseInt((String) newValue));
+ preferencesViewModel.setAppTheme(Integer.parseInt((String) newValue));
requireActivity().setResult(Activity.RESULT_OK);
ActivityCompat.recreate(requireActivity());
return true;
@@ -75,13 +79,15 @@ public class SettingsFragment extends PreferenceFragmentCompat {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- DeckApplication.readCurrentAccountColor().observe(getViewLifecycleOwner(), (mainColor) -> {
- wifiOnlyPref.applyTheme(mainColor);
- compactPref.applyTheme(mainColor);
- coverImagesPref.applyTheme(mainColor);
- compressImageAttachmentsPref.applyTheme(mainColor);
- debuggingPref.applyTheme(mainColor);
- eTagPref.applyTheme(mainColor);
- });
+ new ReactiveLiveData<>(preferencesViewModel.getCurrentAccountId$())
+ .flatMap(preferencesViewModel::getAccountColor)
+ .observe(getViewLifecycleOwner(), color -> Stream.of(
+ wifiOnlyPref,
+ compactPref,
+ coverImagesPref,
+ compressImageAttachmentsPref,
+ debuggingPref,
+ eTagPref)
+ .forEach(pref -> pref.applyTheme(color)));
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java
index 852735352..c0ab59e75 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java
@@ -49,9 +49,9 @@ public class ShareProgressDialogFragment extends ThemedDialogFragment {
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- viewModel.getMax().observe(requireActivity(), (nextMax) -> binding.progress.setMax(nextMax));
+ viewModel.getMax().observe(this, binding.progress::setMax);
- viewModel.getProgress().observe(requireActivity(), (progress) -> {
+ viewModel.getProgress().observe(this, progress -> {
binding.progress.setProgress(progress);
binding.progressText.setText(getString(R.string.progress_count, progress, viewModel.getMaxValue()));
final Integer currentMaxValue = viewModel.getMaxValue();
@@ -63,7 +63,7 @@ public class ShareProgressDialogFragment extends ThemedDialogFragment {
}
});
- viewModel.getExceptions().observe(requireActivity(), (exceptions) -> {
+ viewModel.getExceptions().observe(this, (exceptions) -> {
final int exceptionsCount = exceptions.size();
if (exceptionsCount > 0) {
binding.errorCounter.setText(getResources().getQuantityString(R.plurals.progress_error_count, exceptionsCount, exceptionsCount));
@@ -81,7 +81,7 @@ public class ShareProgressDialogFragment extends ThemedDialogFragment {
}
});
- viewModel.getDuplicateAttachments().observe(requireActivity(), (duplicates) -> {
+ viewModel.getDuplicateAttachments().observe(this, (duplicates) -> {
final int duplicatesCount = duplicates.size();
if (duplicatesCount > 0) {
final var params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java
index 30e002c9c..b7547be0b 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java
@@ -8,7 +8,6 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
-import android.view.Menu;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -19,22 +18,21 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import java.io.File;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.exceptions.UploadAttachmentFailedException;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Attachment;
-import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
-import it.niedermann.nextcloud.deck.ui.MainActivity;
import it.niedermann.nextcloud.deck.ui.card.SelectCardListener;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+import it.niedermann.nextcloud.deck.ui.main.MainActivity;
import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
public class ShareTargetActivity extends MainActivity implements SelectCardListener {
@@ -76,12 +74,12 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
binding.toolbar.setSubtitle(receivedText);
}
} catch (Throwable throwable) {
- ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ showExceptionDialog(throwable, null);
}
}
@Override
- public void onCardSelected(FullCard fullCard) {
+ public void onCardSelected(@NonNull FullCard fullCard, long boardId) {
if (cardSelected) {
return;
}
@@ -94,7 +92,7 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
}
} catch (Throwable throwable) {
cardSelected = false;
- ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ showExceptionDialog(throwable, fullCard.getAccountId());
}
}
@@ -175,15 +173,13 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
IResponseCallback.super.onError(throwable);
runOnUiThread(() -> {
cardSelected = false;
- ExceptionDialogFragment.newInstance(throwable, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ showExceptionDialog(throwable, fullCard.getAccountId());
});
}
});
break;
case 1:
- final var currentAccount = mainViewModel.getCurrentAccount();
- final var comment = new DeckComment(receivedText.trim(), currentAccount.getUserName(), Instant.now());
- mainViewModel.addCommentToCard(currentAccount.getId(), fullCard.getLocalId(), comment);
+ mainViewModel.addCommentToCard(fullCard.getAccountId(), receivedText.trim(), fullCard.getLocalId());
Toast.makeText(getApplicationContext(), getString(R.string.share_success, "\"" + receivedText + "\"", "\"" + fullCard.getCard().getTitle() + "\""), Toast.LENGTH_LONG).show();
finish();
break;
@@ -192,14 +188,8 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
}
@Override
- protected void setCurrentBoard(@NonNull Board board) {
- super.setCurrentBoard(board);
+ protected void applyBoard(@NonNull Account account, @NonNull Map<Integer, Long> navigationMap, @Nullable FullBoard currentBoard) {
+ super.applyBoard(account, navigationMap, currentBoard);
binding.toolbar.setTitle(R.string.simple_select);
- showEditButtonsIfPermissionsGranted();
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- return true;
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackDialogFragment.java
index 4120e5e00..65e449686 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackDialogFragment.java
@@ -9,13 +9,18 @@ import androidx.fragment.app.DialogFragment;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.ui.theme.DeleteAlertDialogBuilder;
+import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment;
-public class DeleteStackDialogFragment extends DialogFragment {
+public class DeleteStackDialogFragment extends ThemedDialogFragment {
+ private static final String KEY_ACCOUNT_ID = "account_id";
+ private static final String KEY_BOARD_ID = "board_id";
private static final String KEY_STACK_ID = "stack_id";
private static final String KEY_NUMBER_CARDS = "number_cards";
private DeleteStackListener deleteStackListener;
+ private long accountId;
+ private long boardId;
private long stackId;
private int numberCards;
@@ -30,12 +35,14 @@ public class DeleteStackDialogFragment extends DialogFragment {
final Bundle args = getArguments();
- if (args == null || !args.containsKey(KEY_STACK_ID) || !args.containsKey(KEY_NUMBER_CARDS)) {
- throw new IllegalArgumentException("Please provide at least " + KEY_STACK_ID + " and " + KEY_NUMBER_CARDS + " as arguments");
- } else {
- this.stackId = args.getLong(KEY_STACK_ID);
- this.numberCards = args.getInt(KEY_NUMBER_CARDS);
+ if (args == null || !args.containsKey(KEY_ACCOUNT_ID) || !args.containsKey(KEY_BOARD_ID) || !args.containsKey(KEY_STACK_ID) || !args.containsKey(KEY_NUMBER_CARDS)) {
+ throw new IllegalArgumentException("Please provide at least " + KEY_ACCOUNT_ID + ", " + KEY_BOARD_ID + ", " + KEY_STACK_ID + " and " + KEY_NUMBER_CARDS + " as arguments");
}
+
+ this.accountId = args.getLong(KEY_ACCOUNT_ID);
+ this.boardId = args.getLong(KEY_BOARD_ID);
+ this.stackId = args.getLong(KEY_STACK_ID);
+ this.numberCards = args.getInt(KEY_NUMBER_CARDS);
}
@NonNull
@@ -44,15 +51,22 @@ public class DeleteStackDialogFragment extends DialogFragment {
return new DeleteAlertDialogBuilder(requireContext())
.setTitle(R.string.delete_list)
.setMessage(getResources().getQuantityString(R.plurals.do_you_want_to_delete_the_current_list, numberCards, numberCards))
- .setPositiveButton(R.string.simple_delete, (dialog, whichButton) -> deleteStackListener.onStackDeleted(stackId))
+ .setPositiveButton(R.string.simple_delete, (dialog, whichButton) -> deleteStackListener.onDeleteStack(accountId, boardId, stackId))
.setNeutralButton(android.R.string.cancel, null)
.create();
}
- public static DialogFragment newInstance(long stackId, int numberCards) {
+ @Override
+ public void applyTheme(int color) {
+
+ }
+
+ public static DialogFragment newInstance(long accountId, long boardId, long stackId, int numberCards) {
final var dialog = new DeleteStackDialogFragment();
final var args = new Bundle();
+ args.putLong(KEY_ACCOUNT_ID, accountId);
+ args.putLong(KEY_BOARD_ID, boardId);
args.putLong(KEY_STACK_ID, stackId);
args.putInt(KEY_NUMBER_CARDS, numberCards);
dialog.setArguments(args);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackListener.java
index f0b5d68f7..032140722 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/DeleteStackListener.java
@@ -1,5 +1,5 @@
package it.niedermann.nextcloud.deck.ui.stack;
public interface DeleteStackListener {
- void onStackDeleted(long stackLocalId);
+ void onDeleteStack(long accountId, long boardId, long stackId);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackDialogFragment.java
index 4b37a3924..829df99ea 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackDialogFragment.java
@@ -4,8 +4,6 @@ import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -24,14 +22,20 @@ import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogStackCreateBinding;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
import it.niedermann.nextcloud.deck.ui.theme.ThemedDialogFragment;
+import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher;
public class EditStackDialogFragment extends ThemedDialogFragment implements DialogInterface.OnClickListener {
+
+ private static final String KEY_ACCOUNT_ID = "account_id";
+ private static final String KEY_BOARD_ID = "board_id";
private static final String KEY_STACK_ID = "stack_id";
private static final String KEY_OLD_TITLE = "old_title";
private EditStackListener editStackListener;
-
private DialogStackCreateBinding binding;
+ private Bundle args;
+ private boolean createMode;
+
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
@@ -51,12 +55,18 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia
.setView(binding.getRoot())
.setNeutralButton(android.R.string.cancel, null);
- final var args = getArguments();
+ args = getArguments();
if (args == null) {
+ throw new IllegalArgumentException("Pass either " + KEY_ACCOUNT_ID + " and " + KEY_BOARD_ID + " for creating a new stack or " + KEY_STACK_ID + " for editing an existing stack.");
+ }
+
+ if (args.getLong(KEY_STACK_ID, -1) == -1) {
+ createMode = true;
builder.setTitle(R.string.add_list)
.setPositiveButton(R.string.simple_add, null);
} else {
+ createMode = false;
binding.input.setText(args.getString(KEY_OLD_TITLE));
builder.setTitle(R.string.rename_list)
.setPositiveButton(R.string.simple_rename, null);
@@ -69,26 +79,13 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> onClick(dialog, DialogInterface.BUTTON_POSITIVE));
});
- binding.input.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- // Nothing to do
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- final boolean inputIsValid = inputIsValid(binding.input.getText());
- if (inputIsValid) {
- binding.inputWrapper.setError(null);
- }
- dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(inputIsValid);
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- // Nothing to do
+ binding.input.addTextChangedListener(new OnTextChangedWatcher(s -> {
+ final boolean inputIsValid = inputIsValid(binding.input.getText());
+ if (inputIsValid) {
+ binding.inputWrapper.setError(null);
}
- });
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(inputIsValid);
+ }));
binding.input.setOnEditorActionListener((textView, actionId, event) -> {
//noinspection SwitchStatementWithTooFewBranches
@@ -118,21 +115,6 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia
this.binding = null;
}
- public static DialogFragment newInstance() {
- return new EditStackDialogFragment();
- }
-
- public static DialogFragment newInstance(long stackId, @Nullable String oldTitle) {
- final var dialog = new EditStackDialogFragment();
-
- final var args = new Bundle();
- args.putLong(KEY_STACK_ID, stackId);
- args.putString(KEY_OLD_TITLE, oldTitle);
-
- dialog.setArguments(args);
- return dialog;
- }
-
@Override
public void applyTheme(int color) {
final var utils = ThemeUtils.of(color, requireContext());
@@ -142,16 +124,13 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia
@Override
public void onClick(DialogInterface dialog, int which) {
- final var args = getArguments();
- final var createMode = args == null;
-
//noinspection SwitchStatementWithTooFewBranches
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
final var currentUserInput = binding.input.getText();
if (inputIsValid(currentUserInput)) {
if (createMode) {
- editStackListener.onCreateStack(binding.input.getText().toString());
+ editStackListener.onCreateStack(args.getLong(KEY_ACCOUNT_ID), args.getLong(KEY_BOARD_ID), binding.input.getText().toString());
} else {
editStackListener.onUpdateStack(args.getLong(KEY_STACK_ID), binding.input.getText().toString());
}
@@ -166,7 +145,35 @@ public class EditStackDialogFragment extends ThemedDialogFragment implements Dia
}
}
+ @Override
+ public void onDismiss(@NonNull DialogInterface dialog) {
+ super.onDismiss(dialog);
+ editStackListener.onDismiss(dialog);
+ }
+
private static boolean inputIsValid(@Nullable CharSequence input) {
return input != null && !input.toString().trim().isEmpty();
}
+
+ public static DialogFragment newInstance(long accountId, long boardId) {
+ final var dialog = new EditStackDialogFragment();
+
+ final var args = new Bundle();
+ args.putLong(KEY_ACCOUNT_ID, accountId);
+ args.putLong(KEY_BOARD_ID, boardId);
+
+ dialog.setArguments(args);
+ return dialog;
+ }
+
+ public static DialogFragment newInstance(long stackId, @Nullable String oldTitle) {
+ final var dialog = new EditStackDialogFragment();
+
+ final var args = new Bundle();
+ args.putLong(KEY_STACK_ID, stackId);
+ args.putString(KEY_OLD_TITLE, oldTitle);
+
+ dialog.setArguments(args);
+ return dialog;
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackListener.java
index 8615d7a85..b4c731bfc 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/EditStackListener.java
@@ -1,7 +1,9 @@
package it.niedermann.nextcloud.deck.ui.stack;
-public interface EditStackListener {
- void onCreateStack(String title);
+import android.content.DialogInterface;
+
+public interface EditStackListener extends DialogInterface.OnDismissListener {
+ void onCreateStack(long accountId, long boardId, String title);
void onUpdateStack(long stackId, String title);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java
index 4a26164dd..37f6a3ee7 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java
@@ -1,6 +1,7 @@
package it.niedermann.nextcloud.deck.ui.stack;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
@@ -9,9 +10,15 @@ import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Stack;
public class StackAdapter extends FragmentStateAdapter {
+
+ @Nullable
+ private Account account;
+ @Nullable
+ private Long boardId;
@NonNull
private final List<Stack> stackList = new ArrayList<>();
@@ -29,22 +36,6 @@ public class StackAdapter extends FragmentStateAdapter {
}
/**
- * @return the position of the direct neighbour of the given {@param position} if available. Prefers neighbours to the start of the wanted, but might also return a neighbour to the end.
- * @throws NoSuchElementException in case this is the only {@link Stack}.
- */
- public int getNeighbourPosition(int position) throws NoSuchElementException, IndexOutOfBoundsException {
- if (position >= stackList.size()) {
- throw new IndexOutOfBoundsException("Position " + position + " is not in the current stack list.");
- }
- if (stackList.size() < 2) {
- throw new NoSuchElementException("There is no neighbour.");
- }
- return position > 0
- ? position - 1
- : position + 1;
- }
-
- /**
* @return the position of the {@link Stack} where {@link Stack#getLocalId()} equals {@param stackLocalId}.
* @throws NoSuchElementException in case the searched {@param stackLocalId} is not in the list.
*/
@@ -75,12 +66,28 @@ public class StackAdapter extends FragmentStateAdapter {
@NonNull
@Override
public Fragment createFragment(int position) {
- return StackFragment.newInstance(stackList.get(position).getLocalId());
+ final var stack = stackList.get(position);
+ if (account == null) {
+ throw new NullPointerException("Account in " + StackAdapter.class.getSimpleName() + " is null, can not create " + StackFragment.class.getSimpleName());
+ }
+ return StackFragment.newInstance(account, stack.getBoardId(), stack.getLocalId());
}
- public void setStacks(@NonNull List<Stack> stacks) {
+ public void setStacks(@Nullable Account account, @Nullable Long boardId, @NonNull List<Stack> stacks) {
+ this.account = account;
+ this.boardId = boardId;
this.stackList.clear();
this.stackList.addAll(stacks);
notifyDataSetChanged();
}
+
+ @Nullable
+ public Account getAccount() {
+ return account;
+ }
+
+ @Nullable
+ public Long getBoardId() {
+ return boardId;
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java
index f090ac59e..302dbb0e3 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java
@@ -1,54 +1,62 @@
package it.niedermann.nextcloud.deck.ui.stack;
+import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN;
+
import android.content.Context;
+import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import java.util.List;
-
import it.niedermann.android.crosstabdnd.DragAndDropTab;
-import it.niedermann.android.util.DimensionUtil;
-import it.niedermann.nextcloud.deck.DeckApplication;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
-import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.FragmentStackBinding;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Card;
import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.MainViewModel;
+import it.niedermann.nextcloud.deck.ui.card.CardActionListener;
import it.niedermann.nextcloud.deck.ui.card.CardAdapter;
import it.niedermann.nextcloud.deck.ui.card.SelectCardListener;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel;
+import it.niedermann.nextcloud.deck.ui.movecard.MoveCardDialogFragment;
import it.niedermann.nextcloud.deck.ui.movecard.MoveCardListener;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
+import it.niedermann.nextcloud.deck.util.CardUtil;
-public class StackFragment extends Fragment implements DragAndDropTab<CardAdapter>, MoveCardListener {
+public class StackFragment extends Fragment implements Themed, DragAndDropTab<CardAdapter>, MoveCardListener, CardActionListener {
+ private static final String KEY_ACCOUNT = "account";
+ private static final String KEY_BOARD_ID = "boardId";
private static final String KEY_STACK_ID = "stackId";
private FragmentStackBinding binding;
- private MainViewModel mainViewModel;
+ private StackViewModel stackViewModel;
private FragmentActivity activity;
private OnScrollListener onScrollListener;
@Nullable
private CardAdapter adapter = null;
- private LiveData<List<FullCard>> cardsLiveData;
+ private Account account;
+ private long boardId;
private long stackId;
@Override
@@ -56,12 +64,23 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte
super.onAttach(context);
final var args = requireArguments();
+
+ if (!args.containsKey(KEY_ACCOUNT)) {
+ throw new IllegalArgumentException(KEY_ACCOUNT + " is required.");
+ }
+ account = (Account) args.getSerializable(KEY_ACCOUNT);
+
+ if (!args.containsKey(KEY_BOARD_ID)) {
+ throw new IllegalArgumentException(KEY_BOARD_ID + " is required.");
+ }
+ boardId = args.getLong(KEY_BOARD_ID);
+
if (!args.containsKey(KEY_STACK_ID)) {
throw new IllegalArgumentException(KEY_STACK_ID + " is required.");
}
-
stackId = args.getLong(KEY_STACK_ID);
+
if (context instanceof OnScrollListener) {
this.onScrollListener = (OnScrollListener) context;
}
@@ -71,23 +90,11 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = requireActivity();
binding = FragmentStackBinding.inflate(inflater, container, false);
- mainViewModel = new ViewModelProvider(activity).get(MainViewModel.class);
-
- final var filterViewModel = new ViewModelProvider(activity).get(FilterViewModel.class);
+ stackViewModel = new ViewModelProvider(requireActivity(), new SyncViewModel.Factory(this.requireActivity().getApplication(), account)).get(StackViewModel.class);
- // This might be a zombie fragment with an empty MainViewModel after Android killed the activity (but not the fragment instance
- // See https://github.com/stefan-niedermann/nextcloud-deck/issues/478
- if (mainViewModel.getCurrentAccount() == null) {
- DeckLog.logError(new IllegalStateException("Cannot populate " + StackFragment.class.getSimpleName() + " because mainViewModel.getCurrentAccount() is null"));
- return binding.getRoot();
- }
+ applyTheme(account.getColor());
- adapter = new CardAdapter(requireActivity(), getChildFragmentManager(), stackId, mainViewModel,
- (requireActivity() instanceof SelectCardListener)
- ? (SelectCardListener) requireActivity()
- : null);
- binding.recyclerView.setAdapter(adapter);
- binding.loadingSpinner.show();
+ final var filterViewModel = new ViewModelProvider(activity).get(FilterViewModel.class);
if (onScrollListener != null) {
binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@@ -104,42 +111,39 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte
});
}
- if (!mainViewModel.currentBoardHasEditPermission()) {
- binding.emptyContentView.hideDescription();
- binding.recyclerView.setPadding(
- binding.recyclerView.getPaddingTop(),
- binding.recyclerView.getPaddingEnd(),
- DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x),
- binding.recyclerView.getPaddingStart()
- );
- }
-
- final Observer<List<FullCard>> cardsObserver = (fullCards) -> activity.runOnUiThread(() -> {
- binding.loadingSpinner.hide();
- if (fullCards != null && fullCards.size() > 0) {
- binding.emptyContentView.setVisibility(View.GONE);
- adapter.setCardList(fullCards);
+ stackViewModel.currentBoardHasEditPermission(account.getId(), boardId).observe(getViewLifecycleOwner(), hasEditPermission -> {
+ if (hasEditPermission) {
+ binding.emptyContentView.showDescription();
} else {
- binding.emptyContentView.setVisibility(View.VISIBLE);
+ binding.emptyContentView.hideDescription();
}
});
- cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterViewModel.getFilterInformation().getValue());
- cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver);
+ @Nullable final var selectCardListener = (activity instanceof SelectCardListener) ? (SelectCardListener) activity : null;
- filterViewModel.getFilterInformation().observe(getViewLifecycleOwner(), (filterInformation -> {
- cardsLiveData.removeObserver(cardsObserver);
- cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterInformation);
- cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver);
- }));
+ adapter = new CardAdapter(activity, this, selectCardListener);
+ binding.recyclerView.setAdapter(adapter);
- return binding.getRoot();
- }
+ new ReactiveLiveData<>(stackViewModel.getAccount(account.getId()))
+ .tap(() -> binding.loadingSpinner.show())
+ .tap(account -> adapter.setAccount(account))
+ .flatMap(account -> stackViewModel.getFullBoard(account.getId(), boardId))
+ .tap(fullBoard -> adapter.setFullBoard(fullBoard))
+ .flatMap(filterViewModel::getFilterInformation)
+ .flatMap(filterInformation -> stackViewModel.getFullCardsForStack(account.getId(), stackId, filterInformation))
+ .combineWith(() -> stackViewModel.getBoardColor$(account.getId(), boardId))
+ .observe(getViewLifecycleOwner(), pair -> {
+ binding.loadingSpinner.hide();
+ if (pair.first != null && !pair.first.isEmpty()) {
+ binding.emptyContentView.setVisibility(View.GONE);
+ assert adapter != null;
+ adapter.setCardList(pair.first, pair.second);
+ } else {
+ binding.emptyContentView.setVisibility(View.VISIBLE);
+ }
+ });
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- DeckApplication.readCurrentBoardColor().observe(getViewLifecycleOwner(), this::applyTheme);
+ return binding.getRoot();
}
@Override
@@ -159,25 +163,9 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte
return binding.recyclerView;
}
- private void applyTheme(int color) {
- if (this.adapter != null) {
- this.adapter.applyTheme(color);
- }
- }
-
- public static Fragment newInstance(long stackId) {
- final var fragment = new StackFragment();
-
- final var args = new Bundle();
- args.putLong(KEY_STACK_ID, stackId);
- fragment.setArguments(args);
-
- return fragment;
- }
-
@Override
public void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) {
- mainViewModel.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, new IResponseCallback<>() {
+ stackViewModel.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, new IResponseCallback<>() {
@Override
public void onResponse(Void response) {
DeckLog.log("Moved", Card.class.getSimpleName(), originCardLocalId, "to", Stack.class.getSimpleName(), targetStackLocalId);
@@ -186,7 +174,7 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte
@Override
public void onError(Throwable throwable) {
IResponseCallback.super.onError(throwable);
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
ExceptionDialogFragment.newInstance(throwable, null).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
}
@@ -216,4 +204,106 @@ public class StackFragment extends Fragment implements DragAndDropTab<CardAdapte
}
});
}
+
+ @Override
+ public void onArchive(@NonNull FullCard fullCard) {
+ stackViewModel.archiveCard(fullCard, new IResponseCallback<>() {
+ @Override
+ public void onResponse(FullCard response) {
+ DeckLog.info("Successfully archived", Card.class.getSimpleName(), fullCard.getCard().getTitle());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ IResponseCallback.super.onError(throwable);
+ showExceptionDialog(throwable, fullCard.getAccountId());
+ }
+ });
+ }
+
+ @Override
+ public void onDelete(@NonNull FullCard fullCard) {
+ stackViewModel.deleteCard(fullCard.getCard(), new IResponseCallback<>() {
+ @Override
+ public void onResponse(Void response) {
+ DeckLog.info("Successfully deleted card", fullCard.getCard().getTitle());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
+ IResponseCallback.super.onError(throwable);
+ showExceptionDialog(throwable, fullCard.getAccountId());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onAssignCurrentUser(@NonNull FullCard fullCard) {
+ stackViewModel.assignUserToCard(fullCard);
+ }
+
+ @Override
+ public void onUnassignCurrentUser(@NonNull FullCard fullCard) {
+ stackViewModel.unassignUserFromCard(fullCard);
+ }
+
+ @Override
+ public void onMove(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard) {
+ DeckLog.verbose("[Move card] Launch move dialog for " + Card.class.getSimpleName() + " \"" + fullCard.getCard().getTitle() + "\" (#" + fullCard.getLocalId() + ") from " + Stack.class.getSimpleName() + " #" + +stackId);
+ MoveCardDialogFragment
+ .newInstance(fullCard.getAccountId(), fullBoard.getBoard().getLocalId(), fullCard.getCard().getTitle(), fullCard.getLocalId(), CardUtil.cardHasCommentsOrAttachments(fullCard))
+ .show(getChildFragmentManager(), MoveCardDialogFragment.class.getSimpleName());
+ }
+
+ @Override
+ public void onShareLink(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard) {
+ stackViewModel.getAccountFuture(fullCard.getAccountId()).thenAcceptAsync(account -> {
+ final int shareLinkRes = account.getServerDeckVersionAsObject().getShareLinkResource();
+ final var shareIntent = new Intent()
+ .setAction(Intent.ACTION_SEND)
+ .setType(TEXT_PLAIN)
+ .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle())
+ .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle())
+ .putExtra(Intent.EXTRA_TEXT, account.getUrl() + activity.getString(shareLinkRes, fullBoard.getBoard().getId(), fullCard.getCard().getId()));
+ activity.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle()));
+ }, ContextCompat.getMainExecutor(requireContext()));
+ }
+
+ @Override
+ public void onShareContent(@NonNull FullCard fullCard) {
+ final var shareIntent = new Intent()
+ .setAction(Intent.ACTION_SEND)
+ .setType(TEXT_PLAIN)
+ .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle())
+ .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle())
+ .putExtra(Intent.EXTRA_TEXT, CardUtil.getCardContentAsString(activity, fullCard));
+ activity.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle()));
+ }
+
+ @AnyThread
+ private void showExceptionDialog(@NonNull Throwable throwable, long accountId) {
+ stackViewModel.getAccountFuture(accountId).thenAcceptAsync(account -> ExceptionDialogFragment
+ .newInstance(throwable, account)
+ .show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()),
+ ContextCompat.getMainExecutor(requireContext()));
+ }
+
+ @Override
+ public void applyTheme(int color) {
+ binding.emptyContentView.applyTheme(color);
+ }
+
+ public static Fragment newInstance(@NonNull Account account, long boardId, long stackId) {
+ final var fragment = new StackFragment();
+
+ final var args = new Bundle();
+ args.putSerializable(KEY_ACCOUNT, account);
+ args.putLong(KEY_BOARD_ID, boardId);
+ args.putLong(KEY_STACK_ID, stackId);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackViewModel.java
new file mode 100644
index 000000000..8860fe3be
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackViewModel.java
@@ -0,0 +1,87 @@
+package it.niedermann.nextcloud.deck.ui.stack;
+
+import static java.util.concurrent.CompletableFuture.supplyAsync;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
+import it.niedermann.nextcloud.deck.ui.viewmodel.SyncViewModel;
+
+public class StackViewModel extends SyncViewModel {
+
+ public StackViewModel(@NonNull Application application, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ super(application, account);
+ }
+
+ public void moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId, @NonNull IResponseCallback<Void> callback) {
+ syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, callback);
+ }
+
+ public LiveData<Account> getAccount(long accountId) {
+ return new ReactiveLiveData<>(baseRepository.readAccount(accountId))
+ .distinctUntilChanged();
+ }
+
+ public CompletableFuture<Account> getAccountFuture(long accountId) {
+ return supplyAsync(() -> baseRepository.readAccountDirectly(accountId));
+ }
+
+ public LiveData<FullBoard> getFullBoard(long accountId, long boardId) {
+ return new ReactiveLiveData<>(baseRepository.getFullBoardById(accountId, boardId))
+ .distinctUntilChanged();
+ }
+
+ public LiveData<Integer> getBoardColor$(long accountId, long boardId) {
+ return baseRepository.getBoardColor$(accountId, boardId);
+ }
+
+ public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) {
+ return new ReactiveLiveData<>(baseRepository.getFullCardsForStack(accountId, localStackId, filter))
+ .distinctUntilChanged();
+ }
+
+ public LiveData<Boolean> currentBoardHasEditPermission(long accountId, long boardId) {
+ return new ReactiveLiveData<>(baseRepository.readAccount(accountId))
+ .flatMap(account -> account.getServerDeckVersionAsObject().isSupported()
+ ? new ReactiveLiveData<>(baseRepository.getFullBoardById(accountId, boardId)).map(fullBoard -> fullBoard != null && fullBoard.getBoard().isPermissionEdit())
+ : new ReactiveLiveData<>(false))
+ .distinctUntilChanged();
+ }
+
+ public void archiveCard(@NonNull FullCard card, @NonNull IResponseCallback<FullCard> callback) {
+ syncManager.archiveCard(card, callback);
+ }
+
+
+ public void deleteCard(@NonNull Card card, @NonNull IResponseCallback<Void> callback) {
+ syncManager.deleteCard(card, callback);
+ }
+
+ public void assignUserToCard(@NonNull FullCard fullCard) {
+ getAccountFuture(fullCard.getAccountId()).thenAcceptAsync(account -> syncManager.assignUserToCard(getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard()));
+ }
+
+ public void unassignUserFromCard(@NonNull FullCard fullCard) {
+ getAccountFuture(fullCard.getAccountId()).thenAcceptAsync(account -> syncManager.unassignUserFromCard(getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard()));
+ }
+
+ private User getUserByUidDirectly(long accountId, String uid) {
+ return baseRepository.getUserByUidDirectly(accountId, uid);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java
index a8fd87ae6..1456b159d 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java
@@ -31,7 +31,7 @@ import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
-import it.niedermann.nextcloud.deck.DeckApplication;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.databinding.ActivityTakePhotoBinding;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
@@ -63,7 +63,10 @@ public class TakePhotoActivity extends AppCompatActivity {
setContentView(binding.getRoot());
// TODO do not only rely on current board color in case a card has been opened from a widget
- DeckApplication.readCurrentBoardColor().observe(this, this::applyBoardColorBrand);
+ new ReactiveLiveData<>(viewModel.getCurrentAccountId$())
+ .combineWith(viewModel::getCurrentBoardId$)
+ .flatMap(ids -> viewModel.getBoardColor$(ids.first, ids.second))
+ .observe(this, this::applyBoardColorBrand);
cameraProviderFuture = ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java
index cadc02687..ff12de766 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java
@@ -2,17 +2,19 @@ package it.niedermann.nextcloud.deck.ui.takephoto;
import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
import static androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA;
-import static androidx.lifecycle.Transformations.map;
+
+import android.app.Application;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraSelector;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.ViewModel;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
-public class TakePhotoViewModel extends ViewModel {
+public class TakePhotoViewModel extends BaseViewModel {
@NonNull
private CameraSelector cameraSelector = DEFAULT_BACK_CAMERA;
@@ -21,6 +23,22 @@ public class TakePhotoViewModel extends ViewModel {
@NonNull
private final MutableLiveData<Boolean> torchEnabled = new MutableLiveData<>(false);
+ public TakePhotoViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ public LiveData<Long> getCurrentAccountId$() {
+ return baseRepository.getCurrentAccountId$();
+ }
+
+ public LiveData<Long> getCurrentBoardId$(long accountId) {
+ return baseRepository.getCurrentBoardId$(accountId);
+ }
+
+ public LiveData<Integer> getBoardColor$(long accountId, long boardId) {
+ return baseRepository.getBoardColor$(accountId, boardId);
+ }
+
@NonNull
public CameraSelector getCameraSelector() {
return this.cameraSelector;
@@ -50,8 +68,9 @@ public class TakePhotoViewModel extends ViewModel {
}
public LiveData<Integer> getTorchToggleButtonImageResource() {
- return map(isTorchEnabled(), enabled -> enabled
- ? R.drawable.ic_baseline_flash_off_24
- : R.drawable.ic_baseline_flash_on_24);
+ return new ReactiveLiveData<>(isTorchEnabled())
+ .map(enabled -> enabled
+ ? R.drawable.ic_baseline_flash_off_24
+ : R.drawable.ic_baseline_flash_on_24);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java
index 3e13d4256..83a508a75 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java
@@ -1,15 +1,40 @@
package it.niedermann.nextcloud.deck.ui.theme;
+import static com.nextcloud.android.common.ui.util.ColorStateListUtilsKt.buildColorStateList;
+import static java.time.temporal.ChronoUnit.DAYS;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.os.Build;
+import android.widget.ImageView;
+import android.widget.TextView;
+
import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.widget.TextViewCompat;
import com.google.android.material.tabs.TabLayout;
import com.nextcloud.android.common.ui.theme.MaterialSchemes;
import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase;
+import com.nextcloud.android.common.ui.theme.utils.AndroidViewThemeUtils;
+import com.nextcloud.android.common.ui.theme.utils.ColorRole;
import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils;
+import java.time.LocalDate;
+
+import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
+import kotlin.Pair;
/**
* UI Elements which are not yet supported by the <a href="https://github.com/nextcloud/android-common"><code>android-commons</code></a> library.
@@ -17,14 +42,17 @@ import it.niedermann.nextcloud.deck.R;
*/
public class DeckViewThemeUtils extends ViewThemeUtilsBase {
+ private final AndroidViewThemeUtils platform;
private final MaterialViewThemeUtils material;
public DeckViewThemeUtils(
@NonNull MaterialSchemes schemes,
- @NonNull MaterialViewThemeUtils material
+ @NonNull MaterialViewThemeUtils material,
+ @NonNull AndroidViewThemeUtils platform
) {
super(schemes);
this.material = material;
+ this.platform = platform;
}
/**
@@ -42,4 +70,93 @@ public class DeckViewThemeUtils extends ViewThemeUtilsBase {
this.material.themeTabLayout(tabLayout);
tabLayout.setBackgroundColor(backgroundColor);
}
+
+ public Drawable themeNavigationViewIcon(@NonNull Context context, @DrawableRes int icon) {
+ return withScheme(context, scheme -> {
+ final var colorStateListe = buildColorStateList(
+ new Pair<>(android.R.attr.state_checked, scheme.getOnSecondaryContainer()),
+ new Pair<>(-android.R.attr.state_checked, scheme.getOnSurfaceVariant())
+ );
+
+ final var drawable = ContextCompat.getDrawable(context, icon);
+ assert drawable != null;
+ final var wrapped = DrawableCompat.wrap(drawable).mutate();
+ DrawableCompat.setTintList(wrapped, colorStateListe);
+ wrapped.invalidateSelf();
+
+ return wrapped;
+ });
+ }
+
+ /**
+ * There is currently no way to retrieve the actual color used for generating the current scheme.
+ * Therefore we let pass it as argument.
+ */
+ @Nullable
+ public Drawable getColoredBoardDrawable(@NonNull Context context, @ColorInt int boardColor) {
+ final var drawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.circle_grey600_36dp, null);
+ return drawable == null ? null : platform.colorDrawable(drawable, boardColor);
+ }
+
+ /**
+ * Use <strong>only</strong> for <code>@drawable/selected_check</code>
+ */
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ public void colorSelectedCheck(@NonNull Context context, @NonNull Drawable selectedCheck) {
+ try {
+ final var check = ((StateListDrawable) selectedCheck);
+ final var checkSelectedIndex = check.findStateDrawableIndex(new int[]{android.R.attr.state_selected});
+ final var checkSelectedDrawable = check.getStateDrawable(checkSelectedIndex);
+
+ final var backgroundDrawable = ((LayerDrawable) checkSelectedDrawable).findDrawableByLayerId(R.id.background);
+ final var foregroundDrawable = ((LayerDrawable) checkSelectedDrawable).findDrawableByLayerId(R.id.foreground);
+ platform.tintDrawable(context, backgroundDrawable, ColorRole.PRIMARY);
+ platform.tintDrawable(context, foregroundDrawable, ColorRole.ON_PRIMARY);
+ } catch (Exception e) {
+ DeckLog.error(e);
+ }
+ }
+
+ @Deprecated(forRemoval = true)
+ public static void themeDueDate(@NonNull TextView cardDueDate, @NonNull LocalDate dueDate) {
+ final var context = cardDueDate.getContext();
+ final long diff = DAYS.between(LocalDate.now(), dueDate);
+
+ @ColorInt @Nullable Integer textColor = null;
+ @DrawableRes int backgroundDrawable = 0;
+
+ if (diff == 1) {
+ // due date: tomorrow
+ backgroundDrawable = R.drawable.due_tomorrow_background;
+ textColor = ContextCompat.getColor(context, R.color.due_text_tomorrow);
+ } else if (diff == 0) {
+ // due date: today
+ backgroundDrawable = R.drawable.due_today_background;
+ textColor = ContextCompat.getColor(context, R.color.due_text_today);
+ } else if (diff < 0) {
+ // due date: overdue
+ backgroundDrawable = R.drawable.due_overdue_background;
+ textColor = ContextCompat.getColor(context, R.color.due_text_overdue);
+ }
+
+ cardDueDate.setBackgroundResource(backgroundDrawable);
+ if (textColor != null) {
+ cardDueDate.setTextColor(textColor);
+ TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(textColor));
+ }
+ }
+
+ @Deprecated(forRemoval = true)
+ public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, @ColorInt int color) {
+ final var drawable = ContextCompat.getDrawable(context, imageId);
+ assert drawable != null;
+ final var wrapped = DrawableCompat.wrap(drawable).mutate();
+ DrawableCompat.setTint(wrapped, color);
+ return drawable;
+ }
+
+ @Deprecated(forRemoval = true)
+ public static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @ColorRes int colorRes) {
+ imageView.setImageTintList(ColorStateList.valueOf(ContextCompat.getColor(context, colorRes)));
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemeUtils.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemeUtils.java
index 3bd6c6bbf..45d4e25c1 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemeUtils.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemeUtils.java
@@ -1,13 +1,11 @@
package it.niedermann.nextcloud.deck.ui.theme;
-import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode;
import android.content.Context;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
-import androidx.preference.PreferenceManager;
import com.nextcloud.android.common.ui.color.ColorUtil;
import com.nextcloud.android.common.ui.theme.MaterialSchemes;
@@ -20,8 +18,6 @@ import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
-import it.niedermann.nextcloud.deck.DeckLog;
-import it.niedermann.nextcloud.deck.R;
import scheme.Scheme;
public class ThemeUtils extends ViewThemeUtilsBase {
@@ -44,7 +40,7 @@ public class ThemeUtils extends ViewThemeUtilsBase {
this.material = new MaterialViewThemeUtils(schemes, colorUtil);
this.androidx = new AndroidXViewThemeUtils(schemes, this.platform);
this.dialog = new DialogViewThemeUtils(schemes);
- this.deck = new DeckViewThemeUtils(schemes, this.material);
+ this.deck = new DeckViewThemeUtils(schemes, this.material, this.platform);
}
public static ThemeUtils of(@ColorInt int color, @NonNull Context context) {
@@ -54,28 +50,8 @@ public class ThemeUtils extends ViewThemeUtilsBase {
));
}
+ @Deprecated
public static Scheme createScheme(@ColorInt int color, @NonNull Context context) {
- return isDarkTheme(context) ? Scheme.dark(color) : Scheme.light(color);
- }
-
- @ColorInt
- public static int readBrandMainColor(@NonNull Context context) {
- final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
- DeckLog.log("--- Read:", context.getString(R.string.shared_preference_theme_main));
- return sharedPreferences.getInt(context.getString(R.string.shared_preference_theme_main), ContextCompat.getColor(context, R.color.defaultBrand));
- }
-
- public static void saveBrandColors(@NonNull Context context, @ColorInt int color) {
- final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
- DeckLog.log("--- Write:", context.getString(R.string.shared_preference_theme_main), "|", color);
- editor.putInt(context.getString(R.string.shared_preference_theme_main), color);
- editor.apply();
- }
-
- public static void clearBrandColors(@NonNull Context context) {
- final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
- DeckLog.log("--- Remove:", context.getString(R.string.shared_preference_theme_main));
- editor.remove(context.getString(R.string.shared_preference_theme_main));
- editor.apply();
+ return isDarkMode(context) ? Scheme.dark(color) : Scheme.light(color);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDatePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDatePickerDialog.java
index 6b6a5ddfa..926a394e4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDatePickerDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDatePickerDialog.java
@@ -1,7 +1,6 @@
package it.niedermann.nextcloud.deck.ui.theme;
-import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
-import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor;
+import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -19,11 +18,17 @@ import scheme.Scheme;
public class ThemedDatePickerDialog extends DatePickerDialog implements Themed {
+ private static final String BUNDLE_KEY_COLOR = "color";
+
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- final var context = requireContext();
- setThemeDark(isDarkTheme(context));
- applyTheme(readBrandMainColor(context));
+ final var args = getArguments();
+ if (args == null || !args.containsKey(BUNDLE_KEY_COLOR)) {
+ throw new IllegalArgumentException("Please provide at least local comment id");
+ }
+
+ applyTheme(args.getInt(BUNDLE_KEY_COLOR));
+ setThemeDark(isDarkMode(requireContext()));
return super.onCreateView(inflater, container, savedInstanceState);
}
@@ -47,8 +52,13 @@ public class ThemedDatePickerDialog extends DatePickerDialog implements Themed {
* @param dayOfMonth The initial day of the dialog.
* @return a new DatePickerDialog instance.
*/
- public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) {
+ public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth, @ColorInt int color) {
final var dialog = new ThemedDatePickerDialog();
+
+ final var args = new Bundle();
+ args.putInt(BUNDLE_KEY_COLOR, color);
+ dialog.setArguments(args);
+
dialog.initialize(callBack, year, monthOfYear - 1, dayOfMonth);
return dialog;
}
@@ -62,8 +72,13 @@ public class ThemedDatePickerDialog extends DatePickerDialog implements Themed {
* TimeZone of the Calendar object)
* @return a new DatePickerDialog instance
*/
- public static DatePickerDialog newInstance(OnDateSetListener callback, Calendar initialSelection) {
+ public static DatePickerDialog newInstance(OnDateSetListener callback, Calendar initialSelection, @ColorInt int color) {
final var dialog = new ThemedDatePickerDialog();
+
+ final var args = new Bundle();
+ args.putInt(BUNDLE_KEY_COLOR, color);
+ dialog.setArguments(args);
+
dialog.initialize(callback, initialSelection);
return dialog;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDialogFragment.java
index 8638780f7..6d45e4190 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedDialogFragment.java
@@ -1,19 +1,21 @@
package it.niedermann.nextcloud.deck.ui.theme;
-import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor;
-
-import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
+
public abstract class ThemedDialogFragment extends DialogFragment implements Themed {
@Override
public void onStart() {
super.onStart();
- @Nullable final var context = getContext();
- if (context != null) {
- applyTheme(readBrandMainColor(context));
- }
+ final var baseRepository = new BaseRepository(requireContext());
+
+ new ReactiveLiveData<>(baseRepository.getCurrentAccountId$())
+ .combineWith(baseRepository::getCurrentBoardId$)
+ .flatMap(ids -> baseRepository.getBoardColor$(ids.first, ids.second))
+ .observe(this, this::applyTheme);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedPreferenceCategory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedPreferenceCategory.java
index cb9dd2be1..39d79a84b 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedPreferenceCategory.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedPreferenceCategory.java
@@ -1,16 +1,15 @@
package it.niedermann.nextcloud.deck.ui.theme;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountColor;
-
import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceViewHolder;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
+
public class ThemedPreferenceCategory extends PreferenceCategory {
@Override
@@ -18,10 +17,16 @@ public class ThemedPreferenceCategory extends PreferenceCategory {
super.onBindViewHolder(holder);
final var view = holder.itemView.findViewById(android.R.id.title);
- @Nullable final Context context = getContext();
+ final var context = getContext();
+ final var repo = new BaseRepository(context);
+
if (view instanceof TextView) {
- final var scheme = ThemeUtils.createScheme(readCurrentAccountColor(context), context);
- ((TextView) view).setTextColor(scheme.getOnPrimaryContainer());
+ repo.getCurrentAccountId()
+ .thenComposeAsync(repo::getCurrentAccountColor)
+ .thenAcceptAsync(accountColor -> {
+ final var utils = ThemeUtils.of(accountColor, context);
+ utils.platform.colorTextView((TextView) view);
+ });
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedSnackbar.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedSnackbar.java
index ac6829b12..7b45ac644 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedSnackbar.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedSnackbar.java
@@ -1,7 +1,5 @@
package it.niedermann.nextcloud.deck.ui.theme;
-import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor;
-
import android.view.View;
import androidx.annotation.ColorInt;
@@ -14,8 +12,7 @@ import com.google.android.material.snackbar.Snackbar;
public class ThemedSnackbar {
@NonNull
- public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @BaseTransientBottomBar.Duration int duration) {
- @ColorInt final int color = readBrandMainColor(view.getContext());
+ public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @BaseTransientBottomBar.Duration int duration, @ColorInt int color) {
final var snackbar = Snackbar.make(view, text, duration);
final var utils = ThemeUtils.of(color, view.getContext());
@@ -25,7 +22,7 @@ public class ThemedSnackbar {
}
@NonNull
- public static Snackbar make(@NonNull View view, @StringRes int resId, @BaseTransientBottomBar.Duration int duration) {
- return make(view, view.getResources().getText(resId), duration);
+ public static Snackbar make(@NonNull View view, @StringRes int resId, @BaseTransientBottomBar.Duration int duration, @ColorInt int color) {
+ return make(view, view.getResources().getText(resId), duration, color);
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedTimePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedTimePickerDialog.java
index 3a0ce8561..d835a5723 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedTimePickerDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/ThemedTimePickerDialog.java
@@ -1,9 +1,7 @@
package it.niedermann.nextcloud.deck.ui.theme;
-import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
-import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.readBrandMainColor;
+import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode;
-import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -11,7 +9,6 @@ import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.wdullaer.materialdatetimepicker.time.TimePickerDialog;
@@ -21,13 +18,17 @@ import scheme.Scheme;
public class ThemedTimePickerDialog extends TimePickerDialog implements Themed {
+ private static final String BUNDLE_KEY_COLOR = "color";
+
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- @Nullable Context context = getContext();
- if (context != null) {
- setThemeDark(isDarkTheme(context));
- applyTheme(readBrandMainColor(context));
+ final var args = getArguments();
+ if (args == null || !args.containsKey(BUNDLE_KEY_COLOR)) {
+ throw new IllegalArgumentException("Please provide at least local comment id");
}
+
+ applyTheme(args.getInt(BUNDLE_KEY_COLOR));
+ setThemeDark(isDarkMode(requireContext()));
return super.onCreateView(inflater, container, savedInstanceState);
}
@@ -54,8 +55,13 @@ public class ThemedTimePickerDialog extends TimePickerDialog implements Themed {
*/
@SuppressWarnings({"SameParameterValue"})
public static TimePickerDialog newInstance(OnTimeSetListener callback,
- int hourOfDay, int minute, int second, boolean is24HourMode) {
+ int hourOfDay, int minute, int second, boolean is24HourMode, @ColorInt int color) {
final var dialog = new ThemedTimePickerDialog();
+
+ final var args = new Bundle();
+ args.putInt(BUNDLE_KEY_COLOR, color);
+ dialog.setArguments(args);
+
dialog.initialize(callback, hourOfDay, minute, second, is24HourMode);
return dialog;
}
@@ -70,8 +76,8 @@ public class ThemedTimePickerDialog extends TimePickerDialog implements Themed {
* @return a new TimePickerDialog instance.
*/
public static TimePickerDialog newInstance(OnTimeSetListener callback,
- int hourOfDay, int minute, boolean is24HourMode) {
- return newInstance(callback, hourOfDay, minute, 0, is24HourMode);
+ int hourOfDay, int minute, boolean is24HourMode, @ColorInt int color) {
+ return newInstance(callback, hourOfDay, minute, 0, is24HourMode, color);
}
/**
@@ -82,8 +88,8 @@ public class ThemedTimePickerDialog extends TimePickerDialog implements Themed {
* @return a new TimePickerDialog instance.
*/
@SuppressWarnings({"SameParameterValue"})
- public static TimePickerDialog newInstance(OnTimeSetListener callback, boolean is24HourMode) {
+ public static TimePickerDialog newInstance(OnTimeSetListener callback, boolean is24HourMode, @ColorInt int color) {
final var now = LocalTime.now();
- return newInstance(callback, now.getHour(), now.getMinute(), is24HourMode);
+ return newInstance(callback, now.getHour(), now.getMinute(), is24HourMode, color);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/tiles/EditCardTileService.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/tiles/EditCardTileService.java
index ad691c943..5ba3402b5 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/tiles/EditCardTileService.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/tiles/EditCardTileService.java
@@ -1,14 +1,11 @@
package it.niedermann.nextcloud.deck.ui.tiles;
-import android.annotation.TargetApi;
import android.content.Intent;
-import android.os.Build;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import it.niedermann.nextcloud.deck.ui.preparecreate.PrepareCreateActivity;
-@TargetApi(Build.VERSION_CODES.N)
public class EditCardTileService extends TileService {
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsActivity.java
index a30d928c3..2d004e733 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsActivity.java
@@ -10,6 +10,8 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.ActivityUpcomingCardsBinding;
@@ -41,8 +43,20 @@ public class UpcomingCardsActivity extends AppCompatActivity implements MoveCard
binding.loadingSpinner.show();
final var adapter = new UpcomingCardsAdapter(this, getSupportFragmentManager(),
- viewModel::assignUser,
- viewModel::unassignUser,
+ (a, c) -> {
+ try {
+ viewModel.assignUser(a, c);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ ExceptionDialogFragment.newInstance(e, a).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ },
+ (a, c) -> {
+ try {
+ viewModel.unassignUser(a, c);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ ExceptionDialogFragment.newInstance(e, a).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ },
(fullCard) -> viewModel.archiveCard(fullCard, new IResponseCallback<>() {
@Override
public void onResponse(FullCard response) {
@@ -63,7 +77,7 @@ public class UpcomingCardsActivity extends AppCompatActivity implements MoveCard
@Override
public void onError(Throwable throwable) {
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
IResponseCallback.super.onError(throwable);
runOnUiThread(() -> ExceptionDialogFragment.newInstance(throwable, null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
}
@@ -107,7 +121,7 @@ public class UpcomingCardsActivity extends AppCompatActivity implements MoveCard
@Override
public void onError(Throwable throwable) {
IResponseCallback.super.onError(throwable);
- if (!SyncManager.ignoreExceptionOnVoidError(throwable)) {
+ if (SyncManager.isNoOnVoidError(throwable)) {
ExceptionDialogFragment.newInstance(throwable, null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsAdapter.java
index 1df85af63..c9e8403a9 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsAdapter.java
@@ -32,7 +32,6 @@ import it.niedermann.nextcloud.deck.ui.card.DefaultCardOnlyTitleViewHolder;
import it.niedermann.nextcloud.deck.ui.card.DefaultCardViewHolder;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
-import scheme.Scheme;
public class UpcomingCardsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@@ -46,7 +45,7 @@ public class UpcomingCardsAdapter extends RecyclerView.Adapter<RecyclerView.View
@NonNull
protected String counterMaxValue;
@NonNull
- protected Scheme scheme;
+ protected ThemeUtils utils;
@NonNull
private final BiConsumer<Account, Card> assignCard;
@NonNull
@@ -65,7 +64,7 @@ public class UpcomingCardsAdapter extends RecyclerView.Adapter<RecyclerView.View
this.activity = activity;
this.counterMaxValue = this.activity.getString(R.string.counter_max_value);
this.fragmentManager = fragmentManager;
- this.scheme = ThemeUtils.createScheme(ContextCompat.getColor(this.activity, R.color.defaultBrand), this.activity);
+ this.utils = ThemeUtils.of(ContextCompat.getColor(this.activity, R.color.defaultBrand), this.activity);
this.compactMode = getDefaultSharedPreferences(this.activity).getBoolean(this.activity.getString(R.string.pref_key_compact), false);
this.assignCard = assignCard;
this.unassignCard = unassignCard;
@@ -155,7 +154,7 @@ public class UpcomingCardsAdapter extends RecyclerView.Adapter<RecyclerView.View
unassignCard,
archiveCard,
deleteCard
- ), counterMaxValue, scheme);
+ ), counterMaxValue, utils);
cardViewHolder.bindCardClickListener((v) -> activity.startActivity(EditActivity.createEditCardIntent(activity, cardItem.getAccount(), cardItem.getCurrentBoardLocalId(), cardItem.getFullCard().getLocalId())));
} else {
throw new IllegalStateException("Item at position " + position + " is a " + item.getClass().getSimpleName() + " but viewHolder is no " + AbstractCardViewHolder.class.getSimpleName());
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsOptionsItemSelectedListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsOptionsItemSelectedListener.java
index b5c0dd3be..3b3a99365 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsOptionsItemSelectedListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsOptionsItemSelectedListener.java
@@ -41,16 +41,17 @@ public class UpcomingCardsOptionsItemSelectedListener implements CardOptionsItem
@NonNull
private final Consumer<Card> deleteCard;
- public UpcomingCardsOptionsItemSelectedListener(@NonNull Account account,
- @NonNull Activity activity,
- @NonNull FragmentManager fragmentManager,
- @Nullable Long boardRemoteId,
- long boardLocalId,
- @NonNull BiConsumer<Account, Card> assignCard,
- @NonNull BiConsumer<Account, Card> unassignCard,
- @NonNull Consumer<FullCard> archiveCard,
- @NonNull Consumer<Card> deleteCard
- ) {
+ public UpcomingCardsOptionsItemSelectedListener
+ (@NonNull Account account,
+ @NonNull Activity activity,
+ @NonNull FragmentManager fragmentManager,
+ @Nullable Long boardRemoteId,
+ long boardLocalId,
+ @NonNull BiConsumer<Account, Card> assignCard,
+ @NonNull BiConsumer<Account, Card> unassignCard,
+ @NonNull Consumer<FullCard> archiveCard,
+ @NonNull Consumer<Card> deleteCard
+ ) {
this.account = account;
this.activity = activity;
this.fragmentManager = fragmentManager;
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsViewModel.java
index 550dc1682..93234bc60 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/upcomingcards/UpcomingCardsViewModel.java
@@ -3,52 +3,73 @@ package it.niedermann.nextcloud.deck.ui.upcomingcards;
import android.app.Application;
import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Card;
import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
@SuppressWarnings("WeakerAccess")
-public class UpcomingCardsViewModel extends AndroidViewModel {
-
- private final SyncManager syncManager;
- private final ExecutorService executor;
+public class UpcomingCardsViewModel extends BaseViewModel {
public UpcomingCardsViewModel(@NonNull Application application) {
super(application);
- this.syncManager = new SyncManager(application);
- this.executor = Executors.newCachedThreadPool();
}
public LiveData<List<UpcomingCardsAdapterItem>> getUpcomingCards() {
- return this.syncManager.getCardsForUpcomingCards();
+ return this.baseRepository.getCardsForUpcomingCards();
}
- public void assignUser(@NonNull Account account, @NonNull Card card) {
- executor.submit(() -> syncManager.assignUserToCard(syncManager.getUserByUidDirectly(card.getAccountId(), account.getUserName()), card));
+ public void assignUser(@NonNull Account account, @NonNull Card card) throws NextcloudFilesAppAccountNotFoundException {
+ final var syncManager = new SyncManager(getApplication(), account);
+ executor.submit(() -> syncManager.assignUserToCard(baseRepository.getUserByUidDirectly(card.getAccountId(), account.getUserName()), card));
}
- public void unassignUser(@NonNull Account account, @NonNull Card card) {
- executor.submit(() -> syncManager.unassignUserFromCard(syncManager.getUserByUidDirectly(card.getAccountId(), account.getUserName()), card));
+ public void unassignUser(@NonNull Account account, @NonNull Card card) throws NextcloudFilesAppAccountNotFoundException {
+ final var syncManager = new SyncManager(getApplication(), account);
+ executor.submit(() -> syncManager.unassignUserFromCard(baseRepository.getUserByUidDirectly(card.getAccountId(), account.getUserName()), card));
}
public void archiveCard(@NonNull FullCard card, @NonNull IResponseCallback<FullCard> callback) {
- syncManager.archiveCard(card, callback);
+ executor.submit(() -> {
+ final var account = baseRepository.readAccountDirectly(card.getAccountId());
+ try {
+ final var syncManager = new SyncManager(getApplication(), account);
+ syncManager.archiveCard(card, callback);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ callback.onError(e);
+ }
+ });
}
public void deleteCard(@NonNull Card card, @NonNull IResponseCallback<Void> callback) {
- syncManager.deleteCard(card, callback);
+ executor.submit(() -> {
+ final var account = baseRepository.readAccountDirectly(card.getAccountId());
+ try {
+ final var syncManager = new SyncManager(getApplication(), account);
+ syncManager.deleteCard(card, callback);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ callback.onError(e);
+ }
+ });
}
public void moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId, @NonNull IResponseCallback<Void> callback) {
- syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, callback);
+ executor.submit(() -> {
+ final var account = baseRepository.readAccountDirectly(originAccountId);
+ try {
+ final var syncManager = new SyncManager(getApplication(), account);
+ syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, callback);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ callback.onError(e);
+ }
+ });
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java
index 8345e63b9..326979744 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java
@@ -21,7 +21,7 @@ import java.util.Arrays;
import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.WidgetColorChooserBinding;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
+import it.niedermann.nextcloud.deck.ui.theme.DeckViewThemeUtils;
public class ColorChooser extends LinearLayout {
@@ -60,17 +60,17 @@ public class ColorChooser extends LinearLayout {
image.setLayoutParams(params);
image.setOnClickListener((imageView) -> {
if (previouslySelectedImageView != null) { // null when first selection
- previouslySelectedImageView.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor));
+ previouslySelectedImageView.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor));
}
- image.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_check_36dp, color));
+ image.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_alpha_check_36dp, color));
selectedColor = color;
this.previouslySelectedColor = color;
this.previouslySelectedImageView = image;
- binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color)));
+ binding.customColorChooser.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color)));
binding.customColorPicker.setVisibility(View.GONE);
binding.brightnessSlide.setVisibility(View.GONE);
});
- image.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, color));
+ image.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, color));
binding.colorPicker.addView(image, binding.colorPicker.getChildCount() - 1);
}
@@ -79,21 +79,20 @@ public class ColorChooser extends LinearLayout {
binding.customColorPicker.setVisibility(View.VISIBLE);
binding.brightnessSlide.setVisibility(View.VISIBLE);
if (previouslySelectedImageView != null) {
- previouslySelectedImageView.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, selectedColor));
+ previouslySelectedImageView.setImageDrawable(DeckViewThemeUtils.getTintedImageView(context, R.drawable.circle_grey600_36dp, selectedColor));
previouslySelectedImageView = null;
}
});
binding.customColorPicker.setColorListener((ColorEnvelopeListener) (envelope, fromUser) -> {
if (previouslySelectedImageView != null) {
- previouslySelectedImageView.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor));
+ previouslySelectedImageView.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor));
previouslySelectedImageView = null;
}
- @ColorInt
- final int customColor = envelope.getColor();
+ @ColorInt final int customColor = envelope.getColor();
selectedColor = customColor;
previouslySelectedColor = customColor;
- binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.circle_alpha_colorize_36dp, selectedColor));
+ binding.customColorChooser.setImageDrawable(DeckViewThemeUtils.getTintedImageView(context, R.drawable.circle_alpha_colorize_36dp, selectedColor));
});
}
@@ -102,14 +101,14 @@ public class ColorChooser extends LinearLayout {
selectedColor = newColor;
for (int i = 0; i < colors.length; i++) {
if (colors[i] == newColor) {
- binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color)));
+ binding.customColorChooser.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color)));
binding.colorPicker.getChildAt(i).performClick();
newColorIsCustomColor = false;
break;
}
}
if (newColorIsCustomColor) {
- binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, this.selectedColor));
+ binding.customColorChooser.setImageDrawable(DeckViewThemeUtils.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, this.selectedColor));
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/EmptyContentView.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/EmptyContentView.java
index bd0bb68d9..2b3557694 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/EmptyContentView.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/EmptyContentView.java
@@ -9,10 +9,14 @@ import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
+import com.nextcloud.android.common.ui.theme.utils.ColorRole;
+
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.WidgetEmptyContentViewBinding;
+import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.theme.Themed;
-public class EmptyContentView extends RelativeLayout {
+public class EmptyContentView extends RelativeLayout implements Themed {
private static final int NO_DESCRIPTION = -1;
@@ -23,11 +27,8 @@ public class EmptyContentView extends RelativeLayout {
binding = WidgetEmptyContentViewBinding.inflate(LayoutInflater.from(context), this, true);
- final var styles = context.obtainStyledAttributes(attrs,
- R.styleable.EmptyContentView, 0, 0);
-
+ final var styles = context.obtainStyledAttributes(attrs, R.styleable.EmptyContentView, 0, 0);
@StringRes int descriptionRes = styles.getResourceId(R.styleable.EmptyContentView_description, NO_DESCRIPTION);
-
binding.title.setText(getResources().getString(styles.getResourceId(R.styleable.EmptyContentView_title, R.string.no_content)));
if (descriptionRes == NO_DESCRIPTION) {
binding.description.setVisibility(View.GONE);
@@ -45,4 +46,13 @@ public class EmptyContentView extends RelativeLayout {
public void showDescription() {
binding.description.setVisibility(View.VISIBLE);
}
+
+ @Override
+ public void applyTheme(int color) {
+ final var utils = ThemeUtils.of(color, getContext());
+
+// utils.platform.colorImageView(binding.image, ColorRole.SECONDARY_CONTAINER);
+ utils.platform.colorTextView(binding.title, ColorRole.ON_SURFACE);
+ utils.platform.colorTextView(binding.description, ColorRole.ON_SURFACE);
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java
index 9d15df896..c1d5af3a2 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java
@@ -2,7 +2,6 @@ package it.niedermann.nextcloud.deck.ui.view;
import android.content.Context;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.util.AttributeSet;
import android.widget.ImageView;
import android.widget.RelativeLayout;
@@ -69,7 +68,7 @@ public class OverlappingAvatars extends RelativeLayout {
addView(avatar);
avatar.requestLayout();
Glide.with(context)
- .load(account.getUrl() + "/index.php/avatar/" + Uri.encode(assignedUsers.get(avatarCount).getUid()) + "/" + avatarSize)
+ .load(account.getAvatarUrl(avatarSize, assignedUsers.get(avatarCount).getUid()))
.placeholder(R.drawable.ic_person_grey600_24dp)
.error(R.drawable.ic_person_grey600_24dp)
.apply(RequestOptions.circleCropTransform())
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/BaseViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/BaseViewModel.java
new file mode 100644
index 000000000..d76007be4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/BaseViewModel.java
@@ -0,0 +1,40 @@
+package it.niedermann.nextcloud.deck.ui.viewmodel;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.ViewModel;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
+
+/**
+ * To be used for {@link ViewModel}s which need an {@link BaseRepository} instance
+ */
+public abstract class BaseViewModel extends AndroidViewModel {
+
+ protected final Application application;
+ protected final BaseRepository baseRepository;
+ protected final ExecutorService executor;
+
+ public BaseViewModel(@NonNull Application application) {
+ this(application, new BaseRepository(application));
+ }
+
+ public BaseViewModel(@NonNull Application application,
+ @NonNull BaseRepository baseRepository) {
+ this(application, baseRepository, Executors.newCachedThreadPool());
+ }
+
+ public BaseViewModel(@NonNull Application application,
+ @NonNull BaseRepository baseRepository,
+ @NonNull ExecutorService executor) {
+ super(application);
+ this.application = application;
+ this.baseRepository = baseRepository;
+ this.executor = executor;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/SyncViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/SyncViewModel.java
new file mode 100644
index 000000000..543c9ba92
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/SyncViewModel.java
@@ -0,0 +1,80 @@
+package it.niedermann.nextcloud.deck.ui.viewmodel;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.archivedboards.ArchivedBoardsViewModel;
+import it.niedermann.nextcloud.deck.ui.board.accesscontrol.AccessControlViewModel;
+import it.niedermann.nextcloud.deck.ui.board.managelabels.LabelsViewModel;
+import it.niedermann.nextcloud.deck.ui.card.NewCardViewModel;
+import it.niedermann.nextcloud.deck.ui.card.comments.CommentsViewModel;
+import it.niedermann.nextcloud.deck.ui.stack.StackViewModel;
+
+/**
+ * To be used for {@link ViewModel}s which need an {@link SyncManager} instance
+ */
+public abstract class SyncViewModel extends BaseViewModel {
+
+ protected final Account account;
+ protected final SyncManager syncManager;
+
+ public SyncViewModel(@NonNull Application application,
+ @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException {
+ this(application, account, new SyncManager(application, account));
+ }
+
+ public SyncViewModel(@NonNull Application application,
+ @NonNull Account account,
+ @NonNull SyncManager syncManager) {
+ super(application, syncManager);
+ this.account = account;
+ this.syncManager = syncManager;
+ }
+
+ public static class Factory implements ViewModelProvider.Factory {
+
+ private final Application application;
+ private final Account account;
+
+ public Factory(@NonNull Application application, @NonNull Account account) {
+ this.application = application;
+ this.account = account;
+ }
+
+ @SuppressWarnings("unchecked")
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ try {
+ if (modelClass == AccessControlViewModel.class) {
+ return (T) new AccessControlViewModel(application, account);
+ }
+ if (modelClass == ArchivedBoardsViewModel.class) {
+ return (T) new ArchivedBoardsViewModel(application, account);
+ }
+ if (modelClass == CommentsViewModel.class) {
+ return (T) new CommentsViewModel(application, account);
+ }
+ if (modelClass == LabelsViewModel.class) {
+ return (T) new LabelsViewModel(application, account);
+ }
+ if (modelClass == NewCardViewModel.class) {
+ return (T) new NewCardViewModel(application, account);
+ }
+ if (modelClass == StackViewModel.class) {
+ return (T) new StackViewModel(application, account);
+ }
+ throw new IllegalArgumentException(getClass().getSimpleName() + " can not instantiate " + modelClass.getSimpleName());
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidget.java
index 56ea508c0..0d0acd7c4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidget.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidget.java
@@ -1,5 +1,7 @@
package it.niedermann.nextcloud.deck.ui.widget.filter;
+import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE;
+
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
@@ -15,9 +17,7 @@ import java.util.concurrent.Executors;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-
-import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
public class FilterWidget extends AppWidgetProvider {
public static final String ACCOUNT_KEY = "filter_widget_account";
@@ -25,7 +25,7 @@ public class FilterWidget extends AppWidgetProvider {
final ExecutorService executor = Executors.newCachedThreadPool();
static void updateAppWidget(@NonNull ExecutorService executor, @NonNull Context context, AppWidgetManager awm, int[] appWidgetIds, Account account) {
- final SyncManager syncManager = new SyncManager(context);
+ final var baseRepository = new BaseRepository(context);
for (int appWidgetId : appWidgetIds) {
executor.submit(() -> {
try {
@@ -73,10 +73,10 @@ public class FilterWidget extends AppWidgetProvider {
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
- final SyncManager syncManager = new SyncManager(context);
+ final var baseRepository = new BaseRepository(context);
for (int appWidgetId : appWidgetIds) {
- syncManager.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + FilterWidget.class.getSimpleName() + " with id " + appWidgetId));
+ baseRepository.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + FilterWidget.class.getSimpleName() + " with id " + appWidgetId));
}
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetFactory.java
index fbc3900f7..77ff718f0 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetFactory.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetFactory.java
@@ -15,12 +15,12 @@ import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
public class FilterWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
private final Context context;
private final int appWidgetId;
- private final SyncManager syncManager;
+ private final BaseRepository baseRepository;
@NonNull
private final List<FilterWidgetCard> data = new ArrayList<>();
@@ -28,7 +28,7 @@ public class FilterWidgetFactory implements RemoteViewsService.RemoteViewsFactor
FilterWidgetFactory(Context context, Intent intent) {
this.context = context;
this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
- this.syncManager = new SyncManager(context);
+ this.baseRepository = new BaseRepository(context);
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetViewModel.java
index e5afe1a0f..3f90acebb 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/filter/FilterWidgetViewModel.java
@@ -3,24 +3,20 @@ package it.niedermann.nextcloud.deck.ui.widget.filter;
import android.app.Application;
import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
-public class FilterWidgetViewModel extends AndroidViewModel {
+public class FilterWidgetViewModel extends BaseViewModel {
@NonNull
- private final SyncManager syncManager;
- @NonNull
private final MutableLiveData<FilterWidget> config$ = new MutableLiveData<>(new FilterWidget());
public FilterWidgetViewModel(@NonNull Application application) {
super(application);
- this.syncManager = new SyncManager(application);
}
public LiveData<FilterWidget> getFilterWidgetConfiguration() {
@@ -28,6 +24,6 @@ public class FilterWidgetViewModel extends AndroidViewModel {
}
public void updateFilterWidget(@NonNull IResponseCallback<Integer> callback) {
- syncManager.createFilterWidget(config$.getValue(), callback);
+ baseRepository.createFilterWidget(config$.getValue(), callback);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java
index ef1451c23..1ccdfc0be 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java
@@ -1,31 +1,29 @@
package it.niedermann.nextcloud.deck.ui.widget.singlecard;
-import static it.niedermann.nextcloud.deck.ui.theme.ThemeUtils.saveBrandColors;
-
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.os.Bundle;
-import android.view.Menu;
-import android.view.View;
-import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Map;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.ui.MainActivity;
import it.niedermann.nextcloud.deck.ui.card.SelectCardListener;
-import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils;
+import it.niedermann.nextcloud.deck.ui.main.MainActivity;
public class SelectCardForWidgetActivity extends MainActivity implements SelectCardListener {
private int appWidgetId;
- @ColorInt private int originalBrandColor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
final var intent = getIntent();
if (intent == null) {
finish();
@@ -40,39 +38,22 @@ public class SelectCardForWidgetActivity extends MainActivity implements SelectC
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish();
}
- originalBrandColor = ThemeUtils.readBrandMainColor(this);
}
@Override
- public void onCardSelected(FullCard fullCard) {
- mainViewModel.addOrUpdateSingleCardWidget(appWidgetId, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId());
- final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
+ public void onCardSelected(@NonNull FullCard fullCard, long boardId) {
+ mainViewModel.addOrUpdateSingleCardWidget(appWidgetId, fullCard.getAccountId(), boardId, fullCard.getLocalId());
+ final var intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
getApplicationContext(), SingleCardWidget.class)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
- setResult(RESULT_OK, updateIntent);
- getApplicationContext().sendBroadcast(updateIntent);
- saveBrandColors(this, originalBrandColor);
+ setResult(RESULT_OK, intent);
+ getApplicationContext().sendBroadcast(intent);
finish();
}
@Override
- protected void setCurrentBoard(@NonNull Board board) {
- super.setCurrentBoard(board);
- binding.listMenuButton.setVisibility(View.GONE);
- binding.fab.setVisibility(View.GONE);
+ protected void applyBoard(@NonNull Account account, @NonNull Map<Integer, Long> navigationMap, @Nullable FullBoard currentBoard) {
+ super.applyBoard(account, navigationMap, currentBoard);
binding.toolbar.setTitle(R.string.simple_select);
}
-
- @Override
- protected void showEditButtonsIfPermissionsGranted() {
- binding.fab.hide();
- binding.listMenuButton.setVisibility(View.GONE);
- binding.emptyContentViewStacks.hideDescription();
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- return true;
- }
-
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java
index bdfd111d0..b7d6eac44 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java
@@ -23,7 +23,7 @@ import java.util.concurrent.Executors;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Card;
import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
import it.niedermann.nextcloud.deck.util.DateUtil;
@@ -32,12 +32,12 @@ public class SingleCardWidget extends AppWidgetProvider {
private final ExecutorService executor = Executors.newCachedThreadPool();
void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) {
- final SyncManager syncManager = new SyncManager(context);
+ final var baseRepository = new BaseRepository(context);
for (int appWidgetId : appWidgetIds) {
executor.submit(() -> {
try {
- final FullSingleCardWidgetModel fullModel = syncManager.getSingleCardWidgetModelDirectly(appWidgetId);
+ final FullSingleCardWidgetModel fullModel = baseRepository.getSingleCardWidgetModelDirectly(appWidgetId);
final Intent intent = EditActivity.createEditCardIntent(context, fullModel.getAccount(), fullModel.getModel().getBoardId(), fullModel.getFullCard().getLocalId());
final PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, pendingIntentFlagCompat(PendingIntent.FLAG_UPDATE_CURRENT));
@@ -142,10 +142,10 @@ public class SingleCardWidget extends AppWidgetProvider {
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
- final SyncManager syncManager = new SyncManager(context);
+ final var baseRepository = new BaseRepository(context);
for (int appWidgetId : appWidgetIds) {
- syncManager.deleteSingleCardWidgetModel(appWidgetId);
+ baseRepository.deleteSingleCardWidgetModel(appWidgetId);
}
super.onDeleted(context, appWidgetIds);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetFactory.java
index db24afa43..1e5760110 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetFactory.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidgetFactory.java
@@ -15,19 +15,19 @@ import java.util.NoSuchElementException;
import it.niedermann.android.markdown.MarkdownUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
public class SingleCardWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
private final Context context;
private final int appWidgetId;
- private final SyncManager syncManager;
+ private final BaseRepository baseRepository;
private FullSingleCardWidgetModel model;
public SingleCardWidgetFactory(@NonNull Context context, @NonNull Intent intent) {
this.context = context;
this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
- this.syncManager = new SyncManager(context);
+ this.baseRepository = new BaseRepository(context);
}
@Override
@@ -38,7 +38,7 @@ public class SingleCardWidgetFactory implements RemoteViewsService.RemoteViewsFa
@Override
public void onDataSetChanged() {
try {
- this.model = syncManager.getSingleCardWidgetModelDirectly(appWidgetId);
+ this.model = baseRepository.getSingleCardWidgetModelDirectly(appWidgetId);
} catch (NoSuchElementException e) {
this.model = null;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java
index 3d7d1100d..dc3e50416 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java
@@ -21,9 +21,9 @@ import java.util.concurrent.Executors;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Stack;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.MainActivity;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
+import it.niedermann.nextcloud.deck.ui.main.MainActivity;
public class StackWidget extends AppWidgetProvider {
private static final int PENDING_INTENT_OPEN_APP_RQ = 0;
@@ -57,19 +57,19 @@ public class StackWidget extends AppWidgetProvider {
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
- final SyncManager syncManager = new SyncManager(context);
+ final var baseRepository = new BaseRepository(context);
for (int appWidgetId : appWidgetIds) {
DeckLog.info("Delete", StackWidget.class.getSimpleName(), "with id", appWidgetId);
- syncManager.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + StackWidget.class.getSimpleName() + " with id " + appWidgetId));
+ baseRepository.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + StackWidget.class.getSimpleName() + " with id " + appWidgetId));
}
}
private static void updateAppWidget(@NonNull ExecutorService executor, @NonNull Context context, AppWidgetManager awm, int[] appWidgetIds) {
- final SyncManager syncManager = new SyncManager(context);
+ final var baseRepository = new BaseRepository(context);
for (int appWidgetId : appWidgetIds) {
executor.submit(() -> {
- if (syncManager.filterWidgetExists(appWidgetId)) {
+ if (baseRepository.filterWidgetExists(appWidgetId)) {
final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack);
final Intent serviceIntent = new Intent(context, StackWidgetService.class);
@@ -88,9 +88,9 @@ public class StackWidget extends AppWidgetProvider {
views.setRemoteAdapter(R.id.stack_widget_lv, serviceIntent);
views.setEmptyView(R.id.stack_widget_lv, R.id.widget_stack_placeholder_iv);
- syncManager.getFilterWidget(appWidgetId, response -> {
- final Stack stack = syncManager.getStackDirectly(response.getAccounts().get(0).getBoards().get(0).getStacks().get(0).getStackId());
- @ColorInt final Integer boardColor = syncManager.getBoardColorDirectly(response.getAccounts().get(0).getAccountId(), response.getAccounts().get(0).getBoards().get(0).getBoardId());
+ baseRepository.getFilterWidget(appWidgetId, response -> {
+ final Stack stack = baseRepository.getStackDirectly(response.getAccounts().get(0).getBoards().get(0).getStacks().get(0).getStackId());
+ @ColorInt final Integer boardColor = baseRepository.getBoardColorDirectly(response.getAccounts().get(0).getAccountId(), response.getAccounts().get(0).getBoards().get(0).getBoardId());
views.setTextViewText(R.id.widget_stack_title_tv, stack.getTitle());
views.setInt(R.id.widget_stack_header_icon, "setColorFilter", boardColor);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java
index 3df706c21..2b6248c4e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java
@@ -3,23 +3,19 @@ package it.niedermann.nextcloud.deck.ui.widget.stack;
import android.app.Application;
import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
import it.niedermann.nextcloud.deck.api.ResponseCallback;
import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.viewmodel.BaseViewModel;
@SuppressWarnings("WeakerAccess")
-public class StackWidgetConfigurationViewModel extends AndroidViewModel {
-
- private final SyncManager syncManager;
+public class StackWidgetConfigurationViewModel extends BaseViewModel {
public StackWidgetConfigurationViewModel(@NonNull Application application) {
super(application);
- this.syncManager = new SyncManager(application);
}
public void addStackWidget(@NonNull FilterWidget config, @NonNull ResponseCallback<Integer> callback) {
- syncManager.createFilterWidget(config, callback);
+ baseRepository.createFilterWidget(config, callback);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java
index e44fd9876..d5b4c8743 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java
@@ -18,13 +18,13 @@ import java.util.NoSuchElementException;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
private final Context context;
private final int appWidgetId;
- private final SyncManager syncManager;
+ private final BaseRepository baseRepository;
@NonNull
private final List<FilterWidgetCard> data = new ArrayList<>();
@@ -33,7 +33,7 @@ public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory
this.context = context;
appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
- this.syncManager = new SyncManager(context);
+ this.baseRepository = new BaseRepository(context);
}
@Override
@@ -44,7 +44,7 @@ public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory
@Override
public void onDataSetChanged() {
try {
- final List<FilterWidgetCard> response = syncManager.getCardsForFilterWidget(appWidgetId);
+ final List<FilterWidgetCard> response = baseRepository.getCardsForFilterWidget(appWidgetId);
DeckLog.verbose(StackWidget.class.getSimpleName(), "with id", appWidgetId, "fetched", response.size(), "cards from the database.");
data.clear();
Collections.sort(response, Comparator.comparingLong(value -> value.getCard().getCard().getOrder()));
@@ -78,7 +78,7 @@ public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory
widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_stack_entry);
widget_entry.setTextViewText(R.id.widget_entry_content_tv, filterWidgetCard.getCard().getCard().getTitle());
- final Intent intent = EditActivity.createEditCardIntent(context, syncManager.readAccountDirectly(filterWidgetCard.getCard().getAccountId()), filterWidgetCard.getStack().getBoardId(), filterWidgetCard.getCard().getLocalId());
+ final Intent intent = EditActivity.createEditCardIntent(context, baseRepository.readAccountDirectly(filterWidgetCard.getCard().getAccountId()), filterWidgetCard.getStack().getBoardId(), filterWidgetCard.getCard().getLocalId());
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
widget_entry.setOnClickFillInIntent(R.id.widget_stack_entry, intent);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidget.java
index bbb1b0af3..fffc0a3c3 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidget.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidget.java
@@ -30,7 +30,7 @@ import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget;
import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetAccount;
import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetSort;
import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidgetUser;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
public class UpcomingWidget extends AppWidgetProvider {
@@ -43,23 +43,23 @@ public class UpcomingWidget extends AppWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
- final SyncManager syncManager = new SyncManager(context);
+ final BaseRepository baseRepository = new BaseRepository(context);
for (int appWidgetId : appWidgetIds) {
executor.submit(() -> {
- if (syncManager.filterWidgetExists(appWidgetId)) {
+ if (baseRepository.filterWidgetExists(appWidgetId)) {
DeckLog.warn(UpcomingWidget.class.getSimpleName(), "with id", appWidgetId, "already exists, perform update instead.");
updateAppWidget(executor, context, appWidgetManager, appWidgetIds);
} else {
- final List<Account> accountsList = syncManager.readAccountsDirectly();
+ final List<Account> accountsList = baseRepository.readAccountsDirectly();
final FilterWidget config = new FilterWidget(appWidgetId, EWidgetType.UPCOMING_WIDGET);
config.setSorts(new FilterWidgetSort(ESortCriteria.DUE_DATE, true));
config.setAccounts(accountsList.stream().map(account -> {
final FilterWidgetAccount fwa = new FilterWidgetAccount(account.getId(), false);
- fwa.setUsers(new FilterWidgetUser(syncManager.getUserByUidDirectly(account.getId(), account.getUserName()).getLocalId()));
+ fwa.setUsers(new FilterWidgetUser(baseRepository.getUserByUidDirectly(account.getId(), account.getUserName()).getLocalId()));
return fwa;
}).collect(Collectors.toList()));
- syncManager.createFilterWidget(config, new IResponseCallback<>() {
+ baseRepository.createFilterWidget(config, new IResponseCallback<>() {
@Override
public void onResponse(Integer response) {
DeckLog.verbose("Successfully created", UpcomingWidget.class.getSimpleName(), "with id", appWidgetId);
@@ -96,8 +96,8 @@ public class UpcomingWidget extends AppWidgetProvider {
} else if (PENDING_INTENT_ACTION_EDIT.equals(intent.getAction())) {
if (intent.hasExtra(PENDING_INTENT_PARAM_ACCOUNT_ID) && intent.hasExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID)) {
executor.submit(() -> {
- final SyncManager syncManager = new SyncManager(context);
- context.startActivity(EditActivity.createEditCardIntent(context, syncManager.readAccountDirectly(intent.getLongExtra(PENDING_INTENT_PARAM_ACCOUNT_ID, -1)), syncManager.getBoardLocalIdByLocalCardIdDirectly(intent.getLongExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID, -1)), intent.getLongExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID, -1)));
+ final var baseRepository = new BaseRepository(context);
+ context.startActivity(EditActivity.createEditCardIntent(context, baseRepository.readAccountDirectly(intent.getLongExtra(PENDING_INTENT_PARAM_ACCOUNT_ID, -1)), baseRepository.getBoardLocalIdByLocalCardIdDirectly(intent.getLongExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID, -1)), intent.getLongExtra(PENDING_INTENT_PARAM_LOCAL_CARD_ID, -1)));
});
} else {
DeckLog.error(PENDING_INTENT_PARAM_ACCOUNT_ID, "and", PENDING_INTENT_PARAM_LOCAL_CARD_ID, "must be provided for action", PENDING_INTENT_ACTION_EDIT);
@@ -110,11 +110,11 @@ public class UpcomingWidget extends AppWidgetProvider {
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
- final SyncManager syncManager = new SyncManager(context);
+ final var baseRepository = new BaseRepository(context);
for (int appWidgetId : appWidgetIds) {
DeckLog.info("Delete", UpcomingWidget.class.getSimpleName(), "with id", appWidgetId);
- syncManager.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + UpcomingWidget.class.getSimpleName() + " with id " + appWidgetId));
+ baseRepository.deleteFilterWidget(appWidgetId, response -> DeckLog.verbose("Successfully deleted " + UpcomingWidget.class.getSimpleName() + " with id " + appWidgetId));
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetFactory.java
index c23e4ca3d..9f98bf6cb 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetFactory.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/upcoming/UpcomingWidgetFactory.java
@@ -16,7 +16,7 @@ import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.BaseRepository;
import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterItem;
import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterSectionItem;
import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsUtil;
@@ -24,7 +24,7 @@ import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsUtil;
public class UpcomingWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
private final Context context;
private final int appWidgetId;
- private final SyncManager syncManager;
+ private final BaseRepository baseRepository;
private final int headerHorizontalPadding;
private final int headerVerticalPaddingNth;
@@ -34,7 +34,7 @@ public class UpcomingWidgetFactory implements RemoteViewsService.RemoteViewsFact
UpcomingWidgetFactory(@NonNull Context context, Intent intent) {
this.context = context;
this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
- this.syncManager = new SyncManager(context);
+ this.baseRepository = new BaseRepository(context);
this.headerHorizontalPadding = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1hx);
this.headerVerticalPaddingNth = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_2x);
}
@@ -47,7 +47,7 @@ public class UpcomingWidgetFactory implements RemoteViewsService.RemoteViewsFact
@Override
public void onDataSetChanged() {
try {
- final List<UpcomingCardsAdapterItem> response = syncManager.getCardsForUpcomingCardsForWidget();
+ final List<UpcomingCardsAdapterItem> response = baseRepository.getCardsForUpcomingCardsForWidget();
DeckLog.verbose(UpcomingWidgetFactory.class.getSimpleName(), "with id", appWidgetId, "fetched", response.size(), "cards from the database.");
data.clear();
data.addAll(UpcomingCardsUtil.addDueDateSeparators(context, response));
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/AutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/AutoCompleteAdapter.java
index 578f7f616..6ce764191 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/AutoCompleteAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/AutoCompleteAdapter.java
@@ -1,40 +1,49 @@
package it.niedermann.nextcloud.deck.util;
+import static java.util.stream.Collectors.toList;
+
+import android.content.Context;
import android.widget.BaseAdapter;
import android.widget.Filter;
import android.widget.Filterable;
-import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.viewbinding.ViewBinding;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
public abstract class AutoCompleteAdapter<ItemType extends IRemoteEntity> extends BaseAdapter implements Filterable {
- public static final long NO_CARD = Long.MIN_VALUE;
- public static final long ITEM_CREATE = Long.MIN_VALUE;
- @NonNull
- protected final ComponentActivity activity;
@NonNull
private List<ItemType> itemList = new ArrayList<>();
@NonNull
- protected List<ItemType> itemsToExclude = new ArrayList<>();
+ private final List<ItemType> itemsToExclude = new ArrayList<>();
@NonNull
protected SyncManager syncManager;
- protected final long accountId;
+ protected final Account account;
protected final long boardId;
- protected final long cardId;
+ protected final ReactiveLiveData<String> constraint$ = new ReactiveLiveData<String>();
+ private final AutoCompleteFilter filter = new AutoCompleteFilter() {
+ @Override
+ protected Filter.FilterResults performFiltering(CharSequence constraint) {
+ constraint$.postValue(constraint == null ? "" : constraint.toString());
+ return filterResults;
+ }
+ };
- protected AutoCompleteAdapter(@NonNull ComponentActivity activity, long accountId, long boardId, long cardId) {
- this.activity = activity;
- this.accountId = accountId;
+ protected AutoCompleteAdapter(@NonNull Context context, @NonNull Account account, long boardId) throws NextcloudFilesAppAccountNotFoundException {
+ this.account = account;
this.boardId = boardId;
- this.cardId = cardId;
- this.syncManager = new SyncManager(activity);
+ this.syncManager = new SyncManager(context, account);
}
@Override
@@ -49,7 +58,20 @@ public abstract class AutoCompleteAdapter<ItemType extends IRemoteEntity> extend
@Override
public long getItemId(int position) {
- return itemList.get(position).getLocalId();
+ // Create proposals do have null as local ID
+ final var localId = itemList.get(position).getLocalId();
+ return localId == null ? Long.MIN_VALUE : localId;
+ }
+
+ protected List<ItemType> filterExcluded(@NonNull List<ItemType> users) {
+ return users.stream().filter(this::userIsNotInExclusionList).collect(toList());
+ }
+
+ private boolean userIsNotInExclusionList(@NonNull ItemType user) {
+ return itemsToExclude
+ .stream()
+ .map(IRemoteEntity::getLocalId)
+ .noneMatch(idToExclude -> Objects.equals(user.getLocalId(), idToExclude));
}
protected static class ViewHolder<ViewBindingType extends ViewBinding> {
@@ -64,7 +86,7 @@ public abstract class AutoCompleteAdapter<ItemType extends IRemoteEntity> extend
protected final Filter.FilterResults filterResults = new Filter.FilterResults();
@Override
- protected void publishResults(CharSequence constraint, FilterResults results) {
+ public void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
if (!itemList.equals(results.values)) {
//noinspection unchecked
@@ -75,13 +97,33 @@ public abstract class AutoCompleteAdapter<ItemType extends IRemoteEntity> extend
notifyDataSetInvalidated();
}
}
+
+ private void publishResults(List<ItemType> list) {
+ DeckLog.verbose("New result list", list.stream().map(IRemoteEntity::toString).collect(toList()));
+ filterResults.values = list;
+ filterResults.count = list.size();
+ publishResults("", filterResults);
+ }
+
+ public Filter.FilterResults getFilter() {
+ return filterResults;
+ }
+ }
+
+ protected void publishResults(List<ItemType> list) {
+ filter.publishResults(list);
+ }
+
+ @Override
+ public Filter getFilter() {
+ return filter;
}
public void exclude(ItemType item) {
this.itemsToExclude.add(item);
}
- public void include(ItemType item) {
+ public void doNotLongerExclude(ItemType item) {
this.itemsToExclude.remove(item);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ExecutorServiceProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ExecutorServiceProvider.java
index a2a90d4e0..93575f401 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ExecutorServiceProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ExecutorServiceProvider.java
@@ -1,10 +1,16 @@
package it.niedermann.nextcloud.deck.util;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+/**
+ * If we really want <strong>this</strong>, we should default to {@link Executors#newWorkStealingPool()}.
+ * Though I recommend to distinguish between blocking threads and non-blocking threads (like network operations), where it does not make sense to limit it to available CPU cores.
+ */
+@Deprecated(forRemoval = true)
public class ExecutorServiceProvider {
private static final int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/OnTextChangedWatcher.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/OnTextChangedWatcher.java
new file mode 100644
index 000000000..81a21fbc8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/OnTextChangedWatcher.java
@@ -0,0 +1,35 @@
+package it.niedermann.nextcloud.deck.util;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+
+import androidx.annotation.NonNull;
+
+import java.util.function.Consumer;
+
+/**
+ * Simple {@link TextWatcher} which only listens on {@link #onTextChanged(CharSequence, int, int, int)} and is therefore usable as {@link FunctionalInterface}
+ */
+public class OnTextChangedWatcher implements TextWatcher {
+
+ private final Consumer<String> consumer;
+
+ public OnTextChangedWatcher(@NonNull Consumer<String> consumer) {
+ this.consumer = consumer;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ consumer.accept(s.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java
index 6c4b091fc..8eb0c27b7 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java
@@ -23,8 +23,13 @@ public class VCardUtil {
final var cr = context.getContentResolver();
try (final var cursor = cr.query(contactUri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
- final String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY));
- return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey);
+ final var columnIndex = cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
+ if (columnIndex >= 0) {
+ final String lookupKey = cursor.getString(columnIndex);
+ return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey);
+ } else {
+ throw new NoSuchElementException("Could not find column index for " + ContactsContract.Contacts.LOOKUP_KEY);
+ }
} else {
throw new NoSuchElementException("Cursor has zero entries");
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java
deleted file mode 100644
index 12f3418c3..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package it.niedermann.nextcloud.deck.util;
-
-import static java.time.temporal.ChronoUnit.DAYS;
-import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.ColorRes;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Px;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.core.widget.TextViewCompat;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-
-import java.time.LocalDate;
-
-import it.niedermann.android.util.DimensionUtil;
-import it.niedermann.nextcloud.deck.R;
-
-public final class ViewUtil {
-
- private ViewUtil() {
- throw new UnsupportedOperationException("This class must not get instantiated");
- }
-
- public static void addAvatar(@NonNull ImageView avatar, @NonNull String baseUrl, @NonNull String userId, @DrawableRes int errorResource) {
- addAvatar(avatar, baseUrl, userId, DimensionUtil.INSTANCE.dpToPx(avatar.getContext(), R.dimen.avatar_size), errorResource);
- }
-
- public static void addAvatar(@NonNull ImageView avatar, @NonNull String baseUrl, @NonNull String userId, @Px int avatarSizeInPx, @DrawableRes int errorResource) {
- final String uri = baseUrl + "/index.php/avatar/" + Uri.encode(userId) + "/" + avatarSizeInPx;
- Glide.with(avatar.getContext())
- .load(uri)
- .placeholder(errorResource)
- .error(errorResource)
- .apply(RequestOptions.circleCropTransform())
- .into(avatar);
- }
-
- public static void themeDueDate(@NonNull Context context, @NonNull TextView cardDueDate, @NonNull LocalDate dueDate) {
- long diff = DAYS.between(LocalDate.now(), dueDate);
-
- int backgroundDrawable = 0;
- int textColor = isDarkTheme(context) ? R.color.dark_fg_primary : R.color.grey600;
-
- if (diff == 1) {
- // due date: tomorrow
- backgroundDrawable = R.drawable.due_tomorrow_background;
- } else if (diff == 0) {
- // due date: today
- backgroundDrawable = R.drawable.due_today_background;
- } else if (diff < 0) {
- // due date: overdue
- backgroundDrawable = R.drawable.due_overdue_background;
- textColor = R.color.overdue_text_color;
- }
-
- cardDueDate.setBackgroundResource(backgroundDrawable);
- cardDueDate.setTextColor(ContextCompat.getColor(context, textColor));
- TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(ContextCompat.getColor(context, textColor)));
- }
-
- public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, @ColorInt int color) {
- final var drawable = ContextCompat.getDrawable(context, imageId);
- assert drawable != null;
- final var wrapped = DrawableCompat.wrap(drawable).mutate();
- DrawableCompat.setTint(wrapped, color);
- return drawable;
- }
-
- public static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @ColorRes int colorRes) {
- imageView.setImageTintList(ColorStateList.valueOf(ContextCompat.getColor(context, colorRes)));
- }
-}
diff --git a/app/src/main/res/drawable/selected.xml b/app/src/main/res/drawable/selected_check.xml
index f28dd14cd..60184ea55 100644
--- a/app/src/main/res/drawable/selected.xml
+++ b/app/src/main/res/drawable/selected_check.xml
@@ -2,13 +2,13 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:enterFadeDuration="@android:integer/config_shortAnimTime" android:state_selected="true">
<layer-list>
- <item>
+ <item android:id="@+id/background">
<shape android:shape="oval">
<solid android:color="@color/defaultBrand" />
<stroke android:width="1dp" android:color="@android:color/white" />
</shape>
</item>
- <item android:drawable="@drawable/circle_alpha_check_36dp" />
+ <item android:id="@+id/foreground" android:drawable="@drawable/circle_alpha_check_36dp" />
</layer-list>
</item>
</selector> \ No newline at end of file
diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml
index decf08fcb..1bc25e332 100644
--- a/app/src/main/res/layout/activity_about.xml
+++ b/app/src/main/res/layout/activity_about.xml
@@ -10,10 +10,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?android:actionBarSize"
+ android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_arrow_back_white_24dp"
tools:title="@string/about" />
diff --git a/app/src/main/res/layout/activity_archived.xml b/app/src/main/res/layout/activity_archived.xml
index d5e9646a8..b540b5f1f 100644
--- a/app/src/main/res/layout/activity_archived.xml
+++ b/app/src/main/res/layout/activity_archived.xml
@@ -10,10 +10,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?android:actionBarSize"
+ android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_arrow_back_white_24dp"
tools:title="@string/archived_cards" />
</com.google.android.material.appbar.AppBarLayout>
diff --git a/app/src/main/res/layout/activity_attachments.xml b/app/src/main/res/layout/activity_attachments.xml
index af6d472f4..784dd1090 100644
--- a/app/src/main/res/layout/activity_attachments.xml
+++ b/app/src/main/res/layout/activity_attachments.xml
@@ -12,7 +12,7 @@
android:layout_height="wrap_content"
android:background="@android:color/background_dark">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/app/src/main/res/layout/activity_edit.xml b/app/src/main/res/layout/activity_edit.xml
index 0bb4cba59..1da20f6c8 100644
--- a/app/src/main/res/layout/activity_edit.xml
+++ b/app/src/main/res/layout/activity_edit.xml
@@ -10,7 +10,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -27,7 +27,7 @@
android:maxLines="5"
tools:text="@tools:sample/lorem" />
- </androidx.appcompat.widget.Toolbar>
+ </com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
diff --git a/app/src/main/res/layout/activity_exception.xml b/app/src/main/res/layout/activity_exception.xml
index c851e8f82..4923c701b 100644
--- a/app/src/main/res/layout/activity_exception.xml
+++ b/app/src/main/res/layout/activity_exception.xml
@@ -10,10 +10,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?android:actionBarSize"
+ android:layout_height="wrap_content"
tools:title="@string/simple_exception" />
</com.google.android.material.appbar.AppBarLayout>
diff --git a/app/src/main/res/layout/activity_filter_widget.xml b/app/src/main/res/layout/activity_filter_widget.xml
index cf125c65d..fbd600073 100644
--- a/app/src/main/res/layout/activity_filter_widget.xml
+++ b/app/src/main/res/layout/activity_filter_widget.xml
@@ -11,10 +11,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?android:actionBarSize"
+ android:layout_height="wrap_content"
app:title="@string/add_filter_widget" />
</com.google.android.material.appbar.AppBarLayout>
diff --git a/app/src/main/res/layout/activity_import_account.xml b/app/src/main/res/layout/activity_import_account.xml
index 7575a3be2..b247804ed 100644
--- a/app/src/main/res/layout/activity_import_account.xml
+++ b/app/src/main/res/layout/activity_import_account.xml
@@ -30,7 +30,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
- android:layout_centerVertical="true"
android:layout_marginBottom="48dp"
android:gravity="center_horizontal"
android:textSize="24sp"
@@ -71,6 +70,7 @@
android:layout_marginTop="@dimen/spacer_4x"
android:indeterminate="true"
android:indeterminateTint="@color/defaultBrand"
+ android:progressTint="@color/defaultBrand"
android:visibility="gone" />
<TextView
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index a02b252ac..81cdd33ca 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -11,7 +11,7 @@
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".ui.MainActivity">
+ tools:context=".ui.main.MainActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
@@ -108,7 +108,7 @@
android:background="?attr/colorPrimary"
tools:title="Deck">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -118,7 +118,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/searchToolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
@@ -138,7 +138,7 @@
android:inputType="text"
android:maxLines="1"
tools:hint="@string/app_name_short" />
- </androidx.appcompat.widget.Toolbar>
+ </com.google.android.material.appbar.MaterialToolbar>
<ImageButton
android:id="@+id/enableSearch"
@@ -221,6 +221,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
+ android:background="@null"
app:tabGravity="center"
app:tabMode="fixed" />
@@ -246,6 +247,7 @@
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
android:text="@string/add_card"
+ android:visibility="gone"
app:icon="@drawable/ic_add_white_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/activity_manage_accounts.xml b/app/src/main/res/layout/activity_manage_accounts.xml
index bbf1b75c4..882baeb1b 100644
--- a/app/src/main/res/layout/activity_manage_accounts.xml
+++ b/app/src/main/res/layout/activity_manage_accounts.xml
@@ -10,10 +10,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?attr/actionBarSize"
+ android:layout_height="wrap_content"
app:contentInsetStartWithNavigation="0dp"
app:navigationIcon="@drawable/ic_arrow_back_white_24dp"
app:title="@string/manage_accounts"
diff --git a/app/src/main/res/layout/activity_pick_stack.xml b/app/src/main/res/layout/activity_pick_stack.xml
index 8c456fc13..f86bdb45f 100644
--- a/app/src/main/res/layout/activity_pick_stack.xml
+++ b/app/src/main/res/layout/activity_pick_stack.xml
@@ -15,10 +15,10 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?android:actionBarSize"
+ android:layout_height="wrap_content"
app:title="@string/add_card" />
</com.google.android.material.appbar.AppBarLayout>
@@ -50,25 +50,24 @@
</com.google.android.material.textfield.TextInputLayout>
- <ScrollView
+ <androidx.core.widget.NestedScrollView
android:id="@+id/fragment_container_wrapper"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_above="@+id/buttonBar"
android:layout_below="@id/appBarLayout"
- android:padding="@dimen/spacer_2x"
+ android:paddingHorizontal="@dimen/spacer_2x"
app:layout_constraintBottom_toTopOf="@id/buttonBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/inputWrapper"
- app:layout_constraintVertical_weight="1"
- tools:background="@color/bg_highlighted">
+ app:layout_constraintVertical_weight="1">
- <FrameLayout
+ <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
- </ScrollView>
+ </androidx.core.widget.NestedScrollView>
<LinearLayout
android:id="@+id/buttonBar"
@@ -95,11 +94,11 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/submit"
style="@style/Widget.Material3.Button"
- android:enabled="false"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacer_1x"
android:layout_weight=".5"
+ android:enabled="false"
android:text="@string/simple_add"
app:backgroundTint="@color/defaultBrand" />
</LinearLayout>
diff --git a/app/src/main/res/layout/activity_push_notification.xml b/app/src/main/res/layout/activity_push_notification.xml
index d50553892..4aca3a744 100644
--- a/app/src/main/res/layout/activity_push_notification.xml
+++ b/app/src/main/res/layout/activity_push_notification.xml
@@ -11,10 +11,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?android:actionBarSize"
+ android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_arrow_back_white_24dp"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 07f7a62fe..c04a45705 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -1,21 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/settings_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
- <com.google.android.material.appbar.AppBarLayout
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ app:navigationIcon="@drawable/ic_arrow_back_white_24dp"
+ app:title="@string/simple_settings" />
- <androidx.appcompat.widget.Toolbar
- android:id="@+id/toolbar"
- android:layout_width="match_parent"
- android:layout_height="?android:actionBarSize"
- app:navigationIcon="@drawable/ic_arrow_back_white_24dp"
- app:title="@string/simple_settings" />
- </com.google.android.material.appbar.AppBarLayout>
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/settingsFragment"
+ android:name="it.niedermann.nextcloud.deck.ui.settings.SettingsFragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/activity_upcoming_cards.xml b/app/src/main/res/layout/activity_upcoming_cards.xml
index 4e3f36736..23516ff78 100644
--- a/app/src/main/res/layout/activity_upcoming_cards.xml
+++ b/app/src/main/res/layout/activity_upcoming_cards.xml
@@ -11,10 +11,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <androidx.appcompat.widget.Toolbar
+ <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?android:actionBarSize"
+ android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_arrow_back_white_24dp"
tools:title="@string/widget_upcoming_title" />
</com.google.android.material.appbar.AppBarLayout>
diff --git a/app/src/main/res/layout/dialog_account_switcher.xml b/app/src/main/res/layout/dialog_account_switcher.xml
index 9ca6cba5d..048fc0bcd 100644
--- a/app/src/main/res/layout/dialog_account_switcher.xml
+++ b/app/src/main/res/layout/dialog_account_switcher.xml
@@ -60,7 +60,7 @@
android:scaleType="center"
android:scaleX=".7"
android:scaleY=".7"
- app:srcCompat="@drawable/selected" />
+ app:srcCompat="@drawable/selected_check" />
</LinearLayout>
<View
diff --git a/app/src/main/res/layout/dialog_move_card.xml b/app/src/main/res/layout/dialog_move_card.xml
index 0466ed915..a8d70fcdd 100644
--- a/app/src/main/res/layout/dialog_move_card.xml
+++ b/app/src/main/res/layout/dialog_move_card.xml
@@ -1,18 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
- android:paddingStart="@dimen/spacer_2x"
+ android:paddingHorizontal="@dimen/spacer_2x"
android:paddingTop="@dimen/spacer_2x"
- android:paddingEnd="@dimen/spacer_2x"
android:paddingBottom="@dimen/spacer_1x">
<TextView
android:id="@+id/title"
- style="@style/TextAppearance.AppCompat.Title"
+ style="@style/TextAppearance.Material3.TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacer_1x"
@@ -20,24 +19,22 @@
android:maxLines="5"
tools:text="@string/action_card_move_title" />
- <ScrollView
- android:id="@+id/scrollView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@id/title"
+ <androidx.core.widget.NestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
android:layout_marginTop="@dimen/spacer_2x">
- <FrameLayout
+ <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
- android:layout_width="wrap_content"
+ android:layout_width="match_parent"
android:layout_height="wrap_content" />
- </ScrollView>
+ </androidx.core.widget.NestedScrollView>
<TextView
android:id="@+id/move_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_below="@id/scrollView"
android:layout_marginTop="@dimen/spacer_2x"
android:drawablePadding="@dimen/spacer_3x"
android:paddingStart="@dimen/spacer_3x"
@@ -52,7 +49,6 @@
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_below="@id/move_warning"
android:layout_marginTop="@dimen/spacer_1x"
android:orientation="horizontal"
app:justifyContent="space_between">
@@ -76,4 +72,4 @@
android:text="@string/simple_move"
android:textColor="@color/defaultBrand" />
</com.google.android.flexbox.FlexboxLayout>
-</RelativeLayout> \ No newline at end of file
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_about_credits_tab.xml b/app/src/main/res/layout/fragment_about_credits_tab.xml
index fc026e5e1..02bb701ee 100644
--- a/app/src/main/res/layout/fragment_about_credits_tab.xml
+++ b/app/src/main/res/layout/fragment_about_credits_tab.xml
@@ -118,17 +118,5 @@
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@string/about_translators_transifex" />
-
- <TextView
- style="?android:attr/listSeparatorTextViewStyle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/about_testers_title" />
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="10dp"
- android:text="@string/about_testers" />
</LinearLayout>
</ScrollView> \ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_pick_stack.xml b/app/src/main/res/layout/fragment_pick_stack.xml
index 9769969ff..d543db414 100644
--- a/app/src/main/res/layout/fragment_pick_stack.xml
+++ b/app/src/main/res/layout/fragment_pick_stack.xml
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="wrap_content"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
@@ -10,19 +11,22 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:prompt="@string/choose_account"
- tools:listitem="@layout/item_prepare_create_account" />
+ android:visibility="gone"
+ tools:listitem="@layout/item_prepare_create_account"
+ tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/board_select"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:prompt="@string/choose_board"
- tools:listitem="@layout/item_board" />
+ tools:listitem="@layout/item_prepare_create_board" />
- <androidx.appcompat.widget.AppCompatSpinner
+ <androidx.recyclerview.widget.RecyclerView
android:id="@+id/stack_select"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:prompt="@string/choose_list"
- tools:listitem="@layout/item_board" />
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ tools:itemCount="4"
+ tools:listitem="@layout/item_prepare_create_stack" />
</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_stack.xml b/app/src/main/res/layout/fragment_stack.xml
index 481143860..c7d08ae8a 100644
--- a/app/src/main/res/layout/fragment_stack.xml
+++ b/app/src/main/res/layout/fragment_stack.xml
@@ -27,7 +27,7 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="@dimen/spacer_1x"
- android:paddingBottom="80dp"
+ android:paddingBottom="@dimen/stack_bottom_padding"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
diff --git a/app/src/main/res/layout/item_account_choose.xml b/app/src/main/res/layout/item_account_choose.xml
index c2d7ed0fb..b5a6dd726 100644
--- a/app/src/main/res/layout/item_account_choose.xml
+++ b/app/src/main/res/layout/item_account_choose.xml
@@ -29,7 +29,7 @@
android:layout_height="12dp"
android:layout_gravity="end|bottom"
android:visibility="gone"
- app:srcCompat="@drawable/selected"
+ app:srcCompat="@drawable/selected_check"
tools:src="@drawable/ic_check_grey600_24dp"
tools:visibility="visible" />
</FrameLayout>
diff --git a/app/src/main/res/layout/item_board.xml b/app/src/main/res/layout/item_board.xml
deleted file mode 100644
index 57e5bcbc3..000000000
--- a/app/src/main/res/layout/item_board.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/boardName"
- android:padding="@dimen/spacer_2x"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textColor="@android:color/white"
- android:background="@android:color/transparent"
- tools:text="Awesome board name" /> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_card_compact.xml b/app/src/main/res/layout/item_card_compact.xml
index 2886a675e..b7060e201 100644
--- a/app/src/main/res/layout/item_card_compact.xml
+++ b/app/src/main/res/layout/item_card_compact.xml
@@ -64,12 +64,10 @@
android:id="@+id/card_due_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@drawable/due_tomorrow_background"
android:drawablePadding="@dimen/spacer_1hx"
android:gravity="center"
android:padding="@dimen/spacer_1hx"
android:paddingEnd="@dimen/spacer_1x"
- android:textColor="@color/fg_secondary"
app:drawableStartCompat="@drawable/calendar_blank_grey600_24dp"
tools:text="tomorrow" />
@@ -82,7 +80,7 @@
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/label_menu"
android:padding="@dimen/spacer_1hx"
- android:tint="?attr/colorAccent"
+ app:tint="?attr/colorAccent"
app:srcCompat="@drawable/ic_menu" />
</LinearLayout>
diff --git a/app/src/main/res/layout/item_card_default.xml b/app/src/main/res/layout/item_card_default.xml
index b472bd12c..a33df5620 100644
--- a/app/src/main/res/layout/item_card_default.xml
+++ b/app/src/main/res/layout/item_card_default.xml
@@ -66,12 +66,10 @@
android:id="@+id/card_due_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@drawable/due_tomorrow_background"
android:drawablePadding="@dimen/spacer_1hx"
android:gravity="center"
android:padding="@dimen/spacer_1hx"
android:paddingEnd="@dimen/spacer_1x"
- android:textColor="@color/fg_secondary"
app:drawableStartCompat="@drawable/calendar_blank_grey600_24dp"
tools:text="tomorrow" />
diff --git a/app/src/main/res/layout/item_card_default_only_title.xml b/app/src/main/res/layout/item_card_default_only_title.xml
index f16d096bc..868ff6594 100644
--- a/app/src/main/res/layout/item_card_default_only_title.xml
+++ b/app/src/main/res/layout/item_card_default_only_title.xml
@@ -60,12 +60,10 @@
android:id="@+id/card_due_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@drawable/due_tomorrow_background"
android:drawablePadding="@dimen/spacer_1hx"
android:gravity="center"
android:padding="@dimen/spacer_1hx"
android:paddingEnd="@dimen/spacer_1x"
- android:textColor="@color/fg_secondary"
app:drawableStartCompat="@drawable/calendar_blank_grey600_24dp"
tools:text="tomorrow" />
diff --git a/app/src/main/res/layout/item_filter_duetype.xml b/app/src/main/res/layout/item_filter_duetype.xml
index a5943ff2f..1e3f5ac8e 100644
--- a/app/src/main/res/layout/item_filter_duetype.xml
+++ b/app/src/main/res/layout/item_filter_duetype.xml
@@ -17,12 +17,13 @@
tools:text="@tools:sample/lorem" />
<androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/selected_check"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginStart="@dimen/spacer_1x"
app:layout_alignSelf="center"
app:layout_flexShrink="0"
- app:srcCompat="@drawable/selected"
+ app:srcCompat="@drawable/selected_check"
tools:src="@drawable/ic_check_grey600_24dp" />
</com.google.android.flexbox.FlexboxLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_filter_label.xml b/app/src/main/res/layout/item_filter_label.xml
index 8592e3275..49bc47fcb 100644
--- a/app/src/main/res/layout/item_filter_label.xml
+++ b/app/src/main/res/layout/item_filter_label.xml
@@ -15,12 +15,13 @@
tools:text="@tools:sample/lorem" />
<androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/selected_check"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginStart="@dimen/spacer_1x"
app:layout_alignSelf="center"
app:layout_flexShrink="0"
- app:srcCompat="@drawable/selected"
+ app:srcCompat="@drawable/selected_check"
tools:src="@drawable/ic_check_grey600_24dp" />
</com.google.android.flexbox.FlexboxLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_filter_user.xml b/app/src/main/res/layout/item_filter_user.xml
index 2f801d0a5..35324d859 100644
--- a/app/src/main/res/layout/item_filter_user.xml
+++ b/app/src/main/res/layout/item_filter_user.xml
@@ -23,10 +23,11 @@
tools:srcCompat="@tools:sample/avatars" />
<androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/selected_check"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="end|bottom"
- app:srcCompat="@drawable/selected" />
+ app:srcCompat="@drawable/selected_check" />
</FrameLayout>
<TextView
diff --git a/app/src/main/res/layout/item_prepare_create_stack.xml b/app/src/main/res/layout/item_prepare_create_stack.xml
index 516941997..303c9328d 100644
--- a/app/src/main/res/layout/item_prepare_create_stack.xml
+++ b/app/src/main/res/layout/item_prepare_create_stack.xml
@@ -1,15 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/stackTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:ellipsize="middle"
+ android:background="?attr/selectableItemBackground"
+ android:orientation="horizontal"
android:paddingStart="72dp"
android:paddingTop="20dp"
android:paddingEnd="@dimen/spacer_2x"
- android:paddingBottom="20dp"
- android:singleLine="true"
- android:textAppearance="?attr/textAppearanceListItem"
- tools:text="@tools:sample/full_names" />
+ android:paddingBottom="20dp">
+ <TextView
+ android:id="@+id/stackTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="middle"
+ android:singleLine="true"
+ android:textAppearance="?attr/textAppearanceListItem"
+ tools:text="@tools:sample/full_names" />
+
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/selected_check"
+ android:layout_width="22dp"
+ android:layout_height="22dp"
+ android:layout_marginStart="@dimen/spacer_1x"
+ app:srcCompat="@drawable/selected_check"
+ tools:src="@drawable/ic_check_grey600_24dp" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_tip.xml b/app/src/main/res/layout/item_tip.xml
index fadb62e6b..d74648df6 100644
--- a/app/src/main/res/layout/item_tip.xml
+++ b/app/src/main/res/layout/item_tip.xml
@@ -5,8 +5,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
- android:paddingTop="@dimen/spacer_1x"
- android:paddingBottom="@dimen/spacer_1x">
+ android:paddingVertical="@dimen/spacer_1x">
<TextView
android:id="@+id/tip"
diff --git a/app/src/main/res/layout/widget_empty_content_view.xml b/app/src/main/res/layout/widget_empty_content_view.xml
index 9fbac7470..ab8b3b9b9 100644
--- a/app/src/main/res/layout/widget_empty_content_view.xml
+++ b/app/src/main/res/layout/widget_empty_content_view.xml
@@ -1,30 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:hint="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- android:padding="@dimen/spacer_2x">
+ android:padding="@dimen/spacer_2x"
+ android:paddingHorizontal="@dimen/spacer_2x">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image"
- android:layout_width="match_parent"
+ android:layout_width="72dp"
android:layout_height="72dp"
android:layout_above="@+id/title"
- android:layout_gravity="center"
+ android:layout_marginBottom="@dimen/spacer_2x"
android:contentDescription="@null"
android:tint="@color/fg_secondary"
+ app:layout_constraintBottom_toTopOf="@id/title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
hint:src="@drawable/ic_app_logo" />
<TextView
android:id="@+id/title"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_centerVertical="true"
android:gravity="center"
- android:paddingVertical="@dimen/spacer_2x"
android:textAlignment="center"
android:textSize="@dimen/empty_content_font_size"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
hint:text="@string/no_cards" />
<TextView
@@ -32,8 +39,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title"
- android:layout_centerHorizontal="true"
- android:paddingHorizontal="@dimen/spacer_2x"
+ android:layout_marginTop="@dimen/spacer_2x"
android:textAlignment="center"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/title"
hint:text="@string/add_a_new_card_using_the_button" />
-</RelativeLayout> \ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 155896ff5..d3e6cb425 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -1,15 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
+
+ <!-- ======================================= -->
+ <!-- Base Theme -->
+ <!-- ======================================= -->
+
<color name="primary">@android:color/black</color>
<color name="accent">@android:color/white</color>
+
+ <!-- ======================================= -->
+ <!-- Custom styles -->
+ <!-- TODO REMOVE -->
+ <!-- ======================================= -->
+
<color name="fg_secondary">#666</color>
<color name="bg_highlighted">#212121</color>
<color name="bg_info_box">#222222</color>
<color name="bg_card">#1e1e1e</color>
- <color name="bg_card_wrapper">@color/primary</color>
<color name="defaultTextHighlightBackground">#55eeeeff</color>
+ <!-- ======================================= -->
+ <!-- Widgets -->
+ <!-- ======================================= -->
+
<color name="widget_outer_background">#dd121212</color>
<color name="widget_background">#dd000000</color>
<color name="widget_foreground">#d8d8d8</color>
+
+ <!-- ======================================= -->
+ <!-- Static colors -->
+ <!-- Are theme independent and should match -->
+ <!-- the colors of the Deck server app. -->
+ <!-- ======================================= -->
+
+ <!-- Due Date badges -->
+ <color name="due_tomorrow">#232323</color>
+ <color name="due_today">#ac7c06</color>
+ <color name="due_overdue">#aa2926</color>
+ <color name="due_text_tomorrow">#ffffff</color>
+ <color name="due_text_today">#ffffff</color>
+ <color name="due_text_overdue">#ffffff</color>
</resources>
diff --git a/app/src/main/res/values-v24/styles.xml b/app/src/main/res/values-v24/styles.xml
deleted file mode 100644
index d6835c741..000000000
--- a/app/src/main/res/values-v24/styles.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <!-- https://github.com/stefan-niedermann/nextcloud-deck/issues/444 -->
- <style name="NavigationView">
- <item name="android:ellipsize">middle</item>
- <item name="android:listDivider">@null</item>
- <item name="android:colorControlHighlight">@android:color/transparent</item>
- </style>
-</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 99bede58e..1983b6367 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,29 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
+
+ <!-- ======================================= -->
+ <!-- Base Theme -->
+ <!-- ======================================= -->
+
<color name="primary">@android:color/white</color>
- <color name="accent">#000000</color>
+ <color name="accent">@android:color/black</color>
<color name="defaultBrand">#0082C9</color>
<color name="danger">#d40000</color>
+
+ <!-- ======================================= -->
+ <!-- Custom styles -->
+ <!-- TODO REMOVE -->
+ <!-- ======================================= -->
+
+ <color name="grey600">#757575</color>
<color name="fg_secondary">#999</color>
<color name="bg_highlighted">#eee</color>
- <color name="grey600">#757575</color>
<color name="bg_info_box">#dddddd</color>
<color name="bg_card">@android:color/white</color>
- <color name="bg_card_wrapper">#fafafa</color>
- <color name="dark_fg_primary">#e5e5e5</color>
<color name="defaultTextHighlightBackground">#2233334a</color>
- <color name="activity_create">#00D400</color>
- <color name="activity_delete">#D40000</color>
+ <!-- ======================================= -->
+ <!-- Widgets -->
+ <!-- ======================================= -->
+
+ <color name="widget_outer_background">#eef9f9f9</color>
+ <color name="widget_background">#ddffffff</color>
+ <color name="widget_foreground">#222222</color>
+ <!-- ======================================= -->
+ <!-- Static colors -->
+ <!-- Are theme independent and should match -->
+ <!-- the colors of the Deck server app. -->
+ <!-- ======================================= -->
- <!-- due date colors -->
- <color name="due_tomorrow">#7fffc53a</color>
- <color name="due_today">#7feca700</color>
+ <!-- Due Date badges -->
+ <color name="due_tomorrow">#f2f2f2</color>
+ <color name="due_today">#f1c14b</color>
<color name="due_overdue">#ef6e6b</color>
- <color name="overdue_text_color">#FFFFFF</color>
+ <color name="due_text_tomorrow">#666666</color>
+ <color name="due_text_today">#333333</color>
+ <color name="due_text_overdue">#ffffff</color>
+
+ <!-- Activity -->
+ <color name="activity_create">#00D400</color>
+ <color name="activity_delete">#D40000</color>
- <!-- board color picker colors -->
+ <!-- ======================================= -->
+ <!-- Static colors -->
+ <!-- These colors are stored in the backend -->
+ <!-- and must not be altered. They are -->
+ <!-- theme independent. -->
+ <!-- ======================================= -->
+
+ <!-- Default colors for boards, labels, … -->
<color name="board_default_color">#b6469d</color>
<color name="board_default_custom_color">#616161</color>
<string-array name="board_default_colors">
@@ -39,8 +71,4 @@
<item>#5b64b3</item>
<item>#8855a8</item>
</string-array>
-
- <color name="widget_outer_background">#eef9f9f9</color>
- <color name="widget_background">#ddffffff</color>
- <color name="widget_foreground">#222222</color>
</resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 1acec4aaa..f7756172e 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -7,6 +7,8 @@
<dimen name="spacer_3x">24dp</dimen>
<dimen name="spacer_4x">32dp</dimen>
+ <dimen name="stack_bottom_padding">80dp</dimen>
+
<dimen name="compact_label_height">6dp</dimen>
<dimen name="attachments_bottom_navigation_height">80dp</dimen>
diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml
index cc2771000..ba14b8366 100644
--- a/app/src/main/res/values/setup.xml
+++ b/app/src/main/res/values/setup.xml
@@ -2,7 +2,6 @@
<resources>
<string name="shared_preference_last_sync" translatable="false">it.niedermann.nextcloud.deck.last_sync</string>
<string name="shared_preference_last_background_sync" translatable="false">it.niedermann.nextcloud.deck.last_background_sync</string>
- <string name="shared_preference_theme_main" translatable="false">it.niedermann.nextcloud.deck.theme_main</string>
<string name="shared_preference_description_preview" translatable="false">it.niedermann.nextcloud.deck.description_preview</string>
<string name="pref_key_wifi_only" translatable="false">wifiOnly</string>
@@ -46,10 +45,10 @@
<item>@string/pref_value_theme_dark</item>
</string-array>
- <!-- To be concatenated with the account id -->
<string name="shared_preference_last_account" translatable="false">it.niedermann.nextcloud.deck.last_account</string>
- <string name="shared_preference_last_account_color" translatable="false">it.niedermann.nextcloud.deck.last_account_color</string>
+ <!-- To be concatenated with the account id -->
<string name="shared_preference_last_board_for_account_" translatable="false">it.niedermann.nextcloud.deck.last_board_for_account_</string>
+ <!-- To be concatenated with the account id, underscore and board id -->
<string name="shared_preference_last_stack_for_account_and_board_" translatable="false">it.niedermann.nextcloud.deck.last_stack_for_board_</string>
<!-- Transitions -->
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 30cb73eec..e51f75722 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -52,8 +52,6 @@
<string name="about_translators_title">Translators</string>
<string name="about_translators_transifex">Nextcloud community on %1$s</string>
<string name="about_translators_transifex_label">Transifex</string>
- <string name="about_testers_title">Testers</string>
- <string name="about_testers" translatable="false">Julius, David</string>
<string name="about_source_title">Source code</string>
<string name="about_source">This project is hosted on GitHub: %1$s</string>
<string name="about_issues_title">Issues</string>
@@ -84,7 +82,7 @@
<string name="add_account">Add account</string>
<string name="choose_account">Choose account</string>
- <string name="add_card">Add new card</string>
+ <string name="add_card">Add card</string>
<string name="activity">Activity</string>
<string name="add_list">Add list</string>
<string name="rename_list">Rename list</string>
@@ -123,7 +121,7 @@
<string name="attachments">Attachments</string>
<string name="no_cards">No cards yet</string>
<string name="no_account">No account configured</string>
- <string name="account_already_added">Account already added</string>
+ <string name="account_already_added">The account %1$s has already been added</string>
<string name="account_is_getting_imported">Account is getting imported</string>
<string name="not_synced_yet">Not synced yet</string>
<string name="no_lists_yet">No lists yet</string>
@@ -179,6 +177,7 @@
<string name="title_is_mandatory">Title is required</string>
<string name="provide_at_least_a_title_or_description">Provide at least a title or description</string>
<string name="welcome_text">Welcome to %1$s</string>
+ <string name="welcome_text_further_accounts">Add another account</string>
<string name="maintenance_mode_explanation">The server %1$s is currently in maintenance mode. Please contact your administrator or try later again.</string>
<string name="share_add_to_card">Add to card</string>
<string name="share_success">Successfully added %1$s to %2$s</string>
@@ -333,7 +332,7 @@
<string name="downloads">Downloads</string>
<string name="files">Files</string>
<string name="gallery">Gallery</string>
- <string name="simple_attach">attach</string>
+ <string name="simple_attach">Attach</string>
<string name="add_stack_widget">Add list widget</string>
<string name="add_filter_widget">Add filter widget</string>
<string name="simple_order">Order</string>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e29bb0793..d65645073 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -75,7 +75,7 @@
<style name="tabStyle" parent="Widget.Material3.TabLayout">
<item name="backgroundColor">@android:color/transparent</item>
<item name="itemBackground">@android:color/transparent</item>
- <item name="tabIndicatorColor">@color/defaultBrand</item>
+ <item name="tabIndicatorColor">?attr/colorAccent</item>
<item name="tabTextColor">?attr/colorAccent</item>
<item name="tabIconTint">?attr/colorAccent</item>
</style>
@@ -105,7 +105,7 @@
<style name="NavigationView">
<!-- https://github.com/stefan-niedermann/nextcloud-deck/issues/444 -->
- <item name="android:ellipsize">end</item>
+ <item name="android:ellipsize">middle</item>
<item name="android:listDivider">@null</item>
<item name="android:colorControlHighlight">@android:color/transparent</item>
</style>
diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManagerTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManagerTest.java
index 6fcaef062..c50d5b3ae 100644
--- a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManagerTest.java
+++ b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManagerTest.java
@@ -3,7 +3,6 @@ package it.niedermann.nextcloud.deck.persistence.sync;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
@@ -38,13 +37,11 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
-import java.lang.reflect.InvocationTargetException;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
import it.niedermann.nextcloud.deck.TestUtil;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
@@ -62,11 +59,11 @@ import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
import it.niedermann.nextcloud.deck.model.ocs.Version;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AbstractSyncDataProvider;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.CardDataProvider;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.StackDataProvider;
+import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil;
@RunWith(RobolectricTestRunner.class)
public class SyncManagerTest {
@@ -78,22 +75,14 @@ public class SyncManagerTest {
private final ServerAdapter serverAdapter = mock(ServerAdapter.class);
private final DataBaseAdapter dataBaseAdapter = mock(DataBaseAdapter.class);
private final SyncHelper.Factory syncHelperFactory = mock(SyncHelper.Factory.class);
+ private final ConnectivityUtil connectivityUtil = mock(ConnectivityUtil.class);
private SyncManager syncManager;
@Before
- public void setup() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
- final var constructor = SyncManager.class.getDeclaredConstructor(Context.class,
- DataBaseAdapter.class,
- ServerAdapter.class,
- ExecutorService.class,
- SyncHelper.Factory.class);
- constructor.setAccessible(true);
- syncManager = constructor.newInstance(context,
- dataBaseAdapter,
- serverAdapter,
- MoreExecutors.newDirectExecutorService(),
- syncHelperFactory);
+ public void setup() {
+ when(dataBaseAdapter.getCurrentAccountId$()).thenReturn(new MutableLiveData<>());
+ syncManager = new SyncManager(context, serverAdapter, connectivityUtil, syncHelperFactory, dataBaseAdapter, MoreExecutors.newDirectExecutorService());
}
@Test
@@ -250,15 +239,6 @@ public class SyncManagerTest {
}
@Test
- public void testHasInternetConnection() {
- when(serverAdapter.hasInternetConnection()).thenReturn(true);
- assertTrue(syncManager.hasInternetConnection());
-
- when(serverAdapter.hasInternetConnection()).thenReturn(false);
- assertFalse(syncManager.hasInternetConnection());
- }
-
- @Test
public void testReadAccountDirectly() {
final var account = new Account(1337L, "Test", "Peter", "example.com");
when(dataBaseAdapter.readAccountDirectly(1337L)).thenReturn(account);
@@ -268,7 +248,7 @@ public class SyncManagerTest {
@Test
public void testReadAccounts() throws InterruptedException {
final var accounts = Collections.singletonList(new Account(1337L, "Test", "Peter", "example.com"));
- final var wrappedAccounts = new WrappedLiveData<List<Account>>();
+ final var wrappedAccounts = new MutableLiveData<List<Account>>();
wrappedAccounts.setValue(accounts);
when(dataBaseAdapter.readAccounts()).thenReturn(wrappedAccounts);
@@ -469,21 +449,6 @@ public class SyncManagerTest {
// Bad paths
- assertThrows(IllegalArgumentException.class, () -> syncManagerSpy.synchronize(new ResponseCallback<>(new Account(null)) {
- @Override
- public void onResponse(Boolean response) {
-
- }
- }));
-
- //noinspection ConstantConditions
- assertThrows(IllegalArgumentException.class, () -> syncManagerSpy.synchronize(new ResponseCallback<>(null) {
- @Override
- public void onResponse(Boolean response) {
-
- }
- }));
-
final var syncHelper_negative = new SyncHelperMock(false);
when(syncHelperFactory.create(any(), any(), any())).thenReturn(syncHelper_negative);
diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapterTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapterTest.java
index ae013141d..f3c84a78c 100644
--- a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapterTest.java
+++ b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapterTest.java
@@ -37,14 +37,14 @@ public class DataBaseAdapterTest {
@Before
public void createAdapter() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
- final var constructor = DataBaseAdapter.class.getDeclaredConstructor(Context.class, DeckDatabase.class, ExecutorService.class);
+ final var constructor = DataBaseAdapter.class.getDeclaredConstructor(Context.class, DeckDatabase.class, ExecutorService.class, ExecutorService.class);
if (isPrivate(constructor.getModifiers())) {
constructor.setAccessible(true);
db = Room
.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), DeckDatabase.class)
.allowMainThreadQueries()
.build();
- adapter = constructor.newInstance(ApplicationProvider.getApplicationContext(), db, MoreExecutors.newDirectExecutorService());
+ adapter = constructor.newInstance(ApplicationProvider.getApplicationContext(), db, MoreExecutors.newDirectExecutorService(), MoreExecutors.newDirectExecutorService());
}
}
diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDaoTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDaoTest.java
index 4282a5f1d..7cbf0d0d7 100644
--- a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDaoTest.java
+++ b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AccountDaoTest.java
@@ -8,16 +8,19 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
+import java.net.MalformedURLException;
+
import it.niedermann.nextcloud.deck.TestUtil;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DeckDatabaseTestUtil;
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
@RunWith(RobolectricTestRunner.class)
public class AccountDaoTest extends AbstractDaoTest {
@Test
- public void testCreate() {
+ public void testCreate() throws MalformedURLException {
final var accountToCreate = new Account();
accountToCreate.setName("test@example.com");
accountToCreate.setUserName("test");
@@ -31,7 +34,9 @@ public class AccountDaoTest extends AbstractDaoTest {
assertEquals(Integer.valueOf(Capabilities.DEFAULT_COLOR), account.getColor());
assertEquals(Integer.valueOf(Capabilities.DEFAULT_TEXT_COLOR), account.getTextColor());
assertEquals("0.6.4", account.getServerDeckVersion());
- assertEquals("https://example.com/index.php/avatar/test/1337", account.getAvatarUrl(1337));
+ final var expectedAvatarUrl = new SingleSignOnUrl("test@example.com", "https://example.com/index.php/avatar/test/1337");
+ assertEquals(expectedAvatarUrl.getSsoAccountName(), account.getAvatarUrl(1337).getSsoAccountName());
+ assertEquals(expectedAvatarUrl.toURL(), account.getAvatarUrl(1337).toURL());
assertEquals(1, db.getAccountDao().countAccountsDirectly());
assertNull(account.getEtag());
assertFalse(account.isMaintenanceEnabled());
diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDaoTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDaoTest.java
index 67bc0938a..9116fdaf1 100644
--- a/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDaoTest.java
+++ b/app/src/test/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDaoTest.java
@@ -61,7 +61,7 @@ public class BoardDaoTest extends AbstractDaoTest {
boardVisibleArchived.setArchived(true);
db.getBoardDao().update(boardInVisible1, boardInVisible2, boardInVisible3, boardVisibleArchived);
- final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getBoardsForAccount(account.getId()));
+ final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getNotDeletedBoards(account.getId(), 1));
assertEquals(4, boards.size());
assertTrue(boards.stream().anyMatch((board -> boardVisible1.getLocalId().equals(board.getLocalId()))));
assertTrue(boards.stream().anyMatch((board -> boardVisible2.getLocalId().equals(board.getLocalId()))));
@@ -92,7 +92,7 @@ public class BoardDaoTest extends AbstractDaoTest {
board4.setArchived(true);
db.getBoardDao().update(board5, board6, board7, board4);
- final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getArchivedBoardsForAccount(account.getId()));
+ final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getNotDeletedBoards(account.getId(), 1));
assertEquals(1, boards.size());
assertFalse(boards.stream().anyMatch((board -> board1.getLocalId().equals(board.getLocalId()))));
assertFalse(boards.stream().anyMatch((board -> board2.getLocalId().equals(board.getLocalId()))));
@@ -123,7 +123,7 @@ public class BoardDaoTest extends AbstractDaoTest {
board4.setArchived(true);
db.getBoardDao().update(board5, board6, board7, board4);
- final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getNonArchivedBoardsForAccount(account.getId()));
+ final var boards = TestUtil.getOrAwaitValue(db.getBoardDao().getNotDeletedBoards(account.getId(), 0));
assertEquals(3, boards.size());
assertTrue(boards.stream().anyMatch((board -> board1.getLocalId().equals(board.getLocalId()))));
assertTrue(boards.stream().anyMatch((board -> board2.getLocalId().equals(board.getLocalId()))));
diff --git a/app/src/test/resources/robolectric.properties b/app/src/test/resources/robolectric.properties
index 5d28440d3..e23ee50f1 100644
--- a/app/src/test/resources/robolectric.properties
+++ b/app/src/test/resources/robolectric.properties
@@ -1 +1 @@
-sdk=23, 30 \ No newline at end of file
+sdk=24, 30 \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/1021009.txt b/fastlane/metadata/android/en-US/changelogs/1021009.txt
index 6006910a8..485d181ba 100644
--- a/fastlane/metadata/android/en-US/changelogs/1021009.txt
+++ b/fastlane/metadata/android/en-US/changelogs/1021009.txt
@@ -1,3 +1,4 @@
- 🎨 Unify material theming with Nextcloud files app
+- ↔️ Improve 'Move Card' Dialog (#972)
- 🐞 Fix not themed filter indicator
- 🐞 Fix loading cover images in multi account setup in Upcoming cards view \ No newline at end of file
diff --git a/reactive-livedata/build.gradle b/reactive-livedata/build.gradle
new file mode 100644
index 000000000..4a15acfc1
--- /dev/null
+++ b/reactive-livedata/build.gradle
@@ -0,0 +1,28 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 33
+ buildToolsVersion "31.0.0"
+ defaultConfig {
+ minSdkVersion 22
+ targetSdkVersion 33
+ }
+ namespace 'it.niedermann.android.reactivelivedata'
+ compileOptions {
+ coreLibraryDesugaringEnabled true
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+}
+
+dependencies {
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
+
+ implementation "androidx.lifecycle:lifecycle-livedata:2.5.1"
+ implementation 'androidx.core:core:1.9.0'
+
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.robolectric:robolectric:4.9.2'
+ testImplementation 'androidx.test:core:1.5.0'
+ testImplementation 'androidx.arch.core:core-testing:2.1.0'
+}
diff --git a/reactive-livedata/src/main/AndroidManifest.xml b/reactive-livedata/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..cc947c567
--- /dev/null
+++ b/reactive-livedata/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest />
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveData.java
new file mode 100644
index 000000000..845e99e66
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveData.java
@@ -0,0 +1,216 @@
+package it.niedermann.android.reactivelivedata;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.util.Function;
+import androidx.core.util.Consumer;
+import androidx.core.util.Pair;
+import androidx.core.util.Predicate;
+import androidx.core.util.Supplier;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.Transformations;
+
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.ExecutorService;
+
+import it.niedermann.android.reactivelivedata.combinator.DoubleCombinatorLiveData;
+import it.niedermann.android.reactivelivedata.combinator.TripleCombinatorLiveData;
+import it.niedermann.android.reactivelivedata.debounce.DebounceLiveData;
+import it.niedermann.android.reactivelivedata.distinct.DistinctUntilChangedLiveData;
+import it.niedermann.android.reactivelivedata.filter.FilterLiveData;
+import it.niedermann.android.reactivelivedata.flatmap.FlatMapLiveData;
+import it.niedermann.android.reactivelivedata.map.MapLiveData;
+import it.niedermann.android.reactivelivedata.merge.MergeLiveData;
+import it.niedermann.android.reactivelivedata.take.TakeLiveData;
+import it.niedermann.android.reactivelivedata.tap.TapLiveData;
+import kotlin.Triple;
+
+/**
+ * @see ReactiveLiveDataBuilder
+ */
+public class ReactiveLiveData<T> extends MediatorLiveData<T> implements ReactiveLiveDataBuilder<T> {
+
+ public ReactiveLiveData(@Nullable LiveData<T> source) {
+ if (source == null) {
+ setValue(null);
+ } else {
+ addSource(source, this::setValue);
+ }
+ }
+
+ public ReactiveLiveData(@NonNull T value) {
+ setValue(value);
+ }
+
+ public ReactiveLiveData() {
+ super();
+ }
+
+ /**
+ * Observe without getting notified about the emitted values.
+ */
+ public void observe(@NonNull LifecycleOwner owner) {
+ super.observe(owner, val -> {
+ // Nothing to do…
+ });
+ }
+
+ /**
+ * Observe without getting getting the emitted value.
+ */
+ public void observe(@NonNull LifecycleOwner owner, @NonNull Runnable runnable) {
+ super.observe(owner, val -> runnable.run());
+ }
+
+ /**
+ * Cancel observation directly after one value has been emitted.
+ */
+ public void observeOnce(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
+ final var internalObserver = new Observer<T>() {
+ @Override
+ public void onChanged(T result) {
+ removeObserver(this);
+ observer.onChanged(result);
+ }
+ };
+
+ observe(owner, internalObserver);
+ }
+
+ /**
+ * @see Transformations#map(LiveData, Function)
+ */
+ @NonNull
+ @Override
+ public <Y> ReactiveLiveData<Y> map(@NonNull Function<T, Y> mapFunction) {
+ return new MapLiveData<>(this, mapFunction);
+ }
+
+ /**
+ * @see #map(Function) but the mapFunction will be executed on the given executor
+ */
+ public <Y> ReactiveLiveData<Y> map(@NonNull Function<T, Y> mapFunction, @NonNull ExecutorService executor) {
+ return new MapLiveData<>(this, mapFunction, executor);
+ }
+
+ /**
+ * @see Transformations#switchMap(LiveData, Function)
+ */
+ @NonNull
+ @Override
+ public <Y> ReactiveLiveData<Y> flatMap(@NonNull Function<T, LiveData<Y>> flatMapFunction) {
+ return new FlatMapLiveData<>(this, flatMapFunction);
+ }
+
+ @NonNull
+ @Override
+ public <Y> ReactiveLiveData<Y> flatMap(@NonNull Supplier<LiveData<Y>> switchMapSupplier) {
+ return new FlatMapLiveData<>(this, switchMapSupplier);
+ }
+
+ /**
+ * @see Transformations#distinctUntilChanged(LiveData)
+ */
+ @NonNull
+ @Override
+ public ReactiveLiveData<T> distinctUntilChanged() {
+ return new DistinctUntilChangedLiveData<>(this);
+ }
+
+ @NonNull
+ public ReactiveLiveData<T> filter(@NonNull Predicate<T> predicate) {
+ return new FilterLiveData<>(this, predicate);
+ }
+
+ @NonNull
+ @Override
+ public ReactiveLiveData<T> filter(@NonNull Supplier<Boolean> supplier) {
+ return new FilterLiveData<>(this, supplier);
+ }
+
+ @NonNull
+ @Override
+ public ReactiveLiveData<T> tap(@NonNull Consumer<T> consumer) {
+ return new TapLiveData<>(this, consumer);
+ }
+
+ @NonNull
+ @Override
+ public ReactiveLiveData<T> tap(@NonNull Runnable runnable) {
+ return new TapLiveData<>(this, runnable);
+ }
+
+ /**
+ * @see #tap(Consumer) but the tap consumer will be executed on the given executor
+ */
+ public ReactiveLiveData<T> tap(@NonNull Consumer<T> consumer, @NonNull ExecutorService executor) {
+ return new TapLiveData<>(this, consumer, executor);
+ }
+
+ public ReactiveLiveData<T> tap(@NonNull Runnable runnable, @NonNull ExecutorService executor) {
+ return new TapLiveData<>(this, runnable, executor);
+ }
+
+ @NonNull
+ @Override
+ public ReactiveLiveData<T> merge(@NonNull Supplier<LiveData<T>> secondSource) {
+ return new MergeLiveData<>(this, secondSource);
+ }
+
+ @NonNull
+ @Override
+ public ReactiveLiveData<T> take(int limit) {
+ return new TakeLiveData<>(this, limit);
+ }
+
+ @NonNull
+ @Override
+ public <Y> ReactiveLiveData<Pair<T, Y>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction) {
+ return new DoubleCombinatorLiveData<>(this, secondSourceFunction);
+ }
+
+ @NonNull
+ @Override
+ public <Y> ReactiveLiveData<Pair<T, Y>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier) {
+ return new DoubleCombinatorLiveData<>(this, secondSourceSupplier);
+ }
+
+ @NonNull
+ @Override
+ public <Y, Z> ReactiveLiveData<Triple<T, Y, Z>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) {
+ return new TripleCombinatorLiveData<>(this, secondSourceFunction, thirdSourceFunction);
+ }
+
+ @NonNull
+ @Override
+ public <Y, Z> ReactiveLiveData<Triple<T, Y, Z>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier) {
+ return new TripleCombinatorLiveData<>(this, secondSourceFunction, thirdSourceSupplier);
+ }
+
+ @NonNull
+ @Override
+ public <Y, Z> ReactiveLiveData<Triple<T, Y, Z>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) {
+ return new TripleCombinatorLiveData<>(this, secondSourceSupplier, thirdSourceFunction);
+ }
+
+ @NonNull
+ @Override
+ public <Y, Z> ReactiveLiveData<Triple<T, Y, Z>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier) {
+ return new TripleCombinatorLiveData<>(this, secondSourceSupplier, thirdSourceSupplier);
+ }
+
+ @NonNull
+ @Override
+ public ReactiveLiveData<T> debounce(long timeout, @NonNull ChronoUnit timeUnit) {
+ return new DebounceLiveData<>(this, timeout, timeUnit);
+ }
+
+ @NonNull
+ @Override
+ public ReactiveLiveData<T> debounce(long timeout) {
+ return new DebounceLiveData<>(this, timeout);
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataBuilder.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataBuilder.java
new file mode 100644
index 000000000..e5fcb319e
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataBuilder.java
@@ -0,0 +1,130 @@
+package it.niedermann.android.reactivelivedata;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+import androidx.core.util.Consumer;
+import androidx.core.util.Pair;
+import androidx.core.util.Predicate;
+import androidx.core.util.Supplier;
+import androidx.lifecycle.LiveData;
+
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.TimeUnit;
+
+import kotlin.Triple;
+
+/**
+ * Partial implementation of <a href="https://reactivex.io/documentation/operators.html">ReactiveX</a> features
+ */
+public interface ReactiveLiveDataBuilder<T> {
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/map.html">ReactiveX#map</a>
+ */
+ @NonNull
+ <Y> ReactiveLiveDataBuilder<Y> map(@NonNull Function<T, Y> mapFunction);
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/flatmap.html">ReactiveX#flatmap</a>
+ */
+ @NonNull
+ <Y> ReactiveLiveDataBuilder<Y> flatMap(@NonNull Function<T, LiveData<Y>> flatMapFunction);
+
+ /**
+ * @see #flatMap(Function)
+ */
+ @NonNull
+ <Y> ReactiveLiveDataBuilder<Y> flatMap(@NonNull Supplier<LiveData<Y>> flatMapSupplier);
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/distinct.html">ReactiveX#distinct</a>
+ */
+ @NonNull
+ ReactiveLiveDataBuilder<T> distinctUntilChanged();
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/filter.html">ReactiveX#filter</a>
+ */
+ @NonNull
+ ReactiveLiveDataBuilder<T> filter(@NonNull Predicate<T> predicate);
+
+ /**
+ * @see #filter(Predicate)
+ */
+ @NonNull
+ ReactiveLiveDataBuilder<T> filter(@NonNull Supplier<Boolean> supplier);
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/do.html">ReactiveX#do</a>
+ */
+ @NonNull
+ ReactiveLiveDataBuilder<T> tap(@NonNull Consumer<T> consumer);
+
+ /**
+ * @see #tap(Consumer)
+ */
+ @NonNull
+ ReactiveLiveDataBuilder<T> tap(@NonNull Runnable runnable);
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/merge.html">ReactiveX#merge</a>
+ */
+ @NonNull
+ ReactiveLiveData<T> merge(@NonNull Supplier<LiveData<T>> liveData);
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/take.html">ReactiveX#take</a>
+ */
+ @NonNull
+ ReactiveLiveDataBuilder<T> take(int limit);
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/combinelatest.html">ReactiveX#combinelatest</a>
+ */
+ @NonNull
+ <Y> ReactiveLiveDataBuilder<Pair<T, Y>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction);
+
+ /**
+ * @see #combineWith(Function)
+ */
+ @NonNull
+ <Y> ReactiveLiveDataBuilder<Pair<T, Y>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier);
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/combinelatest.html">ReactiveX#combinelatest</a>
+ */
+ @NonNull
+ <Y, Z> ReactiveLiveDataBuilder<Triple<T, Y, Z>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Function<T, LiveData<Z>> thirdSourceFunction);
+
+ /**
+ * @see #combineWith(Function)
+ */
+ @NonNull
+ <Y, Z> ReactiveLiveDataBuilder<Triple<T, Y, Z>> combineWith(@NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier);
+
+ /**
+ * @see #combineWith(Function)
+ */
+ @NonNull
+ <Y, Z> ReactiveLiveDataBuilder<Triple<T, Y, Z>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Function<T, LiveData<Z>> thirdSourceFunction);
+
+ /**
+ * @see #combineWith(Function)
+ */
+ @NonNull
+ <Y, Z> ReactiveLiveDataBuilder<Triple<T, Y, Z>> combineWith(@NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier);
+
+ /**
+ * @see <a href="https://reactivex.io/documentation/operators/debounce.html">ReactiveX#debounce</a>>
+ */
+ @NonNull
+ ReactiveLiveDataBuilder<T> debounce(long timeout, @NonNull ChronoUnit timeUnit);
+
+ /**
+ * @param timeout defaults to {@link TimeUnit#MILLISECONDS}
+ *
+ * @see #debounce(long, ChronoUnit)
+ */
+ @NonNull
+ ReactiveLiveDataBuilder<T> debounce(long timeout);
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorLiveData.java
new file mode 100644
index 000000000..63c704546
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorLiveData.java
@@ -0,0 +1,20 @@
+package it.niedermann.android.reactivelivedata.combinator;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+import androidx.core.util.Pair;
+import androidx.core.util.Supplier;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+
+public class DoubleCombinatorLiveData<T, Y> extends ReactiveLiveData<Pair<T, Y>> {
+
+ public DoubleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<Y>> secondSourceSupplier) {
+ this(source, val -> secondSourceSupplier.get());
+ }
+
+ public DoubleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Function<T, LiveData<Y>> secondSourceFunction) {
+ addSource(source, new DoubleCombinatorObserver<>(this, secondSourceFunction));
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorObserver.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorObserver.java
new file mode 100644
index 000000000..18c2affbb
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/DoubleCombinatorObserver.java
@@ -0,0 +1,45 @@
+package it.niedermann.android.reactivelivedata.combinator;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+import androidx.core.util.Pair;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.Observer;
+
+class DoubleCombinatorObserver<T, Y> implements Observer<T> {
+ private final MediatorLiveData<Pair<T, Y>> mediator;
+ private final Function<T, LiveData<Y>> secondSourceFunction;
+ private T value1;
+ private Y value2;
+
+ private LiveData<Y> secondSource;
+
+ private boolean value1emitted = false;
+ private boolean value2emitted = false;
+
+ public DoubleCombinatorObserver(@NonNull MediatorLiveData<Pair<T, Y>> mediator, @NonNull Function<T, LiveData<Y>> secondSourceFunction) {
+ this.mediator = mediator;
+ this.secondSourceFunction = secondSourceFunction;
+ }
+
+ @Override
+ public void onChanged(T emittedValue1) {
+ value1 = emittedValue1;
+ value1emitted = true;
+ if (value2emitted) {
+ mediator.setValue(new Pair<>(value1, value2));
+ }
+
+ if (secondSource == null) {
+ secondSource = secondSourceFunction.apply(emittedValue1);
+ mediator.addSource(secondSource, val2 -> {
+ value2 = val2;
+ value2emitted = true;
+ if (value1emitted) {
+ mediator.setValue(new Pair<>(value1, value2));
+ }
+ });
+ }
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorLiveData.java
new file mode 100644
index 000000000..de0353c42
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorLiveData.java
@@ -0,0 +1,28 @@
+package it.niedermann.android.reactivelivedata.combinator;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+import androidx.core.util.Supplier;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+import kotlin.Triple;
+
+public class TripleCombinatorLiveData<T, Y, Z> extends ReactiveLiveData<Triple<T, Y, Z>> {
+
+ public TripleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier) {
+ this(source, val -> secondSourceSupplier.get(), val -> thirdSourceSupplier.get());
+ }
+
+ public TripleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Supplier<LiveData<Z>> thirdSourceSupplier) {
+ this(source, secondSourceFunction, val -> thirdSourceSupplier.get());
+ }
+
+ public TripleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<Y>> secondSourceSupplier, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) {
+ this(source, val -> secondSourceSupplier.get(), thirdSourceFunction);
+ }
+
+ public TripleCombinatorLiveData(@NonNull LiveData<T> source, @NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) {
+ addSource(source, new TripleCombinatorObserver<>(this, secondSourceFunction, thirdSourceFunction));
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorObserver.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorObserver.java
new file mode 100644
index 000000000..f146fa9cc
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/combinator/TripleCombinatorObserver.java
@@ -0,0 +1,62 @@
+package it.niedermann.android.reactivelivedata.combinator;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.Observer;
+
+import kotlin.Triple;
+
+class TripleCombinatorObserver<T, Y, Z> implements Observer<T> {
+ private final MediatorLiveData<Triple<T, Y, Z>> mediator;
+ private final Function<T, LiveData<Y>> secondSourceFunction;
+ private final Function<T, LiveData<Z>> thirdSourceFunction;
+ private T value1;
+ private Y value2;
+ private Z value3;
+
+ private LiveData<Y> secondSource;
+ private LiveData<Z> thirdSource;
+
+ private boolean value1emitted = false;
+ private boolean value2emitted = false;
+ private boolean value3emitted = false;
+
+ public TripleCombinatorObserver(@NonNull MediatorLiveData<Triple<T, Y, Z>> mediator, @NonNull Function<T, LiveData<Y>> secondSourceFunction, @NonNull Function<T, LiveData<Z>> thirdSourceFunction) {
+ this.mediator = mediator;
+ this.secondSourceFunction = secondSourceFunction;
+ this.thirdSourceFunction = thirdSourceFunction;
+ }
+
+ @Override
+ public void onChanged(T emittedValue1) {
+ value1 = emittedValue1;
+ value1emitted = true;
+ if (value2emitted && value3emitted) {
+ mediator.setValue(new Triple<>(value1, value2, value3));
+ }
+
+ if (secondSource == null) {
+ secondSource = secondSourceFunction.apply(emittedValue1);
+ mediator.addSource(secondSource, val2 -> {
+ value2 = val2;
+ value2emitted = true;
+ if (value1emitted && value3emitted) {
+ mediator.setValue(new Triple<>(value1, value2, value3));
+ }
+ });
+ }
+
+ if (thirdSource == null) {
+ thirdSource = thirdSourceFunction.apply(emittedValue1);
+ mediator.addSource(thirdSource, val3 -> {
+ value3 = val3;
+ value3emitted = true;
+ if (value1emitted && value2emitted) {
+ mediator.setValue(new Triple<>(value1, value2, value3));
+ }
+ });
+ }
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceLiveData.java
new file mode 100644
index 000000000..22a67d59a
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceLiveData.java
@@ -0,0 +1,19 @@
+package it.niedermann.android.reactivelivedata.debounce;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import java.time.temporal.ChronoUnit;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+
+public class DebounceLiveData<T> extends ReactiveLiveData<T> {
+
+ public DebounceLiveData(@NonNull LiveData<T> source, long timeout) {
+ this(source, timeout, ChronoUnit.MILLIS);
+ }
+
+ public DebounceLiveData(@NonNull LiveData<T> source, long timeout, @NonNull ChronoUnit timeUnit) {
+ addSource(source, new DebounceObserver<>(this, timeout, timeUnit));
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceObserver.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceObserver.java
new file mode 100644
index 000000000..4d18cfa0e
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/debounce/DebounceObserver.java
@@ -0,0 +1,79 @@
+package it.niedermann.android.reactivelivedata.debounce;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.Observer;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+class DebounceObserver<T> implements Observer<T> {
+ private final MediatorLiveData<T> mediator;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ private final long timeout;
+ private final ChronoUnit timeUnit;
+ private T lastEmittedValue = null;
+ private Instant lastEmit = Instant.now();
+ private boolean firstEmit = true;
+ private Future<?> scheduledRecheck;
+
+ public DebounceObserver(@NonNull MediatorLiveData<T> mediator, long timeout, @NonNull ChronoUnit timeUnit) {
+ this.mediator = mediator;
+ this.timeout = timeout;
+ this.timeUnit = timeUnit;
+ }
+
+ @Override
+ public void onChanged(T value) {
+ final var now = Instant.now();
+
+ if (firstEmit) {
+ firstEmit = false;
+ emitValue(value, now);
+ } else {
+ if (lastEmit.isBefore(now.minus(timeout, timeUnit))) {
+ emitValue(value, now);
+ } else {
+ scheduleRecheck(value, getRemainingTimeToNextTimeout(now, lastEmit));
+ }
+ }
+ }
+
+ private void emitValue(T value, @NonNull Instant lastEmit) {
+ cancelScheduledRecheck();
+ mediator.postValue(value);
+ this.lastEmit = lastEmit;
+ }
+
+ private Duration getRemainingTimeToNextTimeout(@NonNull Instant now, @NonNull Instant lastEmit) {
+ final var millisSinceLastEmit = now.toEpochMilli() - lastEmit.toEpochMilli();
+ final var millisToNextEmit = Duration.of(timeout, timeUnit).toMillis() - millisSinceLastEmit;
+ return Duration.ofMillis(millisToNextEmit);
+ }
+
+ private void cancelScheduledRecheck() {
+ if (scheduledRecheck != null) {
+ scheduledRecheck.cancel(true);
+ }
+ }
+
+ private synchronized void scheduleRecheck(T newValue, @NonNull Duration sleep) {
+ cancelScheduledRecheck();
+ scheduledRecheck = executor.submit(() -> {
+ try {
+ Thread.sleep(sleep.toMillis());
+ if (!Objects.equals(lastEmittedValue, newValue)) {
+ mediator.postValue(newValue);
+ lastEmittedValue = newValue;
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+} \ No newline at end of file
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/distinct/DistinctUntilChangedLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/distinct/DistinctUntilChangedLiveData.java
new file mode 100644
index 000000000..6ec85ed85
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/distinct/DistinctUntilChangedLiveData.java
@@ -0,0 +1,14 @@
+package it.niedermann.android.reactivelivedata.distinct;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Transformations;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+
+public class DistinctUntilChangedLiveData<T> extends ReactiveLiveData<T> {
+
+ public DistinctUntilChangedLiveData(@NonNull LiveData<T> source) {
+ super(Transformations.distinctUntilChanged(source));
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/filter/FilterLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/filter/FilterLiveData.java
new file mode 100644
index 000000000..fac27030f
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/filter/FilterLiveData.java
@@ -0,0 +1,23 @@
+package it.niedermann.android.reactivelivedata.filter;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Predicate;
+import androidx.core.util.Supplier;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+
+public class FilterLiveData<T> extends ReactiveLiveData<T> {
+
+ public FilterLiveData(@NonNull LiveData<T> source, @NonNull Supplier<Boolean> supplier) {
+ this(source, val -> supplier.get());
+ }
+
+ public FilterLiveData(@NonNull LiveData<T> source, @NonNull Predicate<T> predicate) {
+ addSource(source, val -> {
+ if (predicate.test(val)) {
+ setValue(val);
+ }
+ });
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/flatmap/FlatMapLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/flatmap/FlatMapLiveData.java
new file mode 100644
index 000000000..5212ea7fa
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/flatmap/FlatMapLiveData.java
@@ -0,0 +1,20 @@
+package it.niedermann.android.reactivelivedata.flatmap;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+import androidx.core.util.Supplier;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Transformations;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+
+public class FlatMapLiveData<T, Y> extends ReactiveLiveData<Y> {
+
+ public FlatMapLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<Y>> switchMapSupplier) {
+ this(source, val -> switchMapSupplier.get());
+ }
+
+ public FlatMapLiveData(@NonNull LiveData<T> source, @NonNull Function<T, LiveData<Y>> flatMapFunction) {
+ super(Transformations.switchMap(source, flatMapFunction));
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/map/MapLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/map/MapLiveData.java
new file mode 100644
index 000000000..eaeb2b435
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/map/MapLiveData.java
@@ -0,0 +1,21 @@
+package it.niedermann.android.reactivelivedata.map;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.util.Function;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Transformations;
+
+import java.util.concurrent.ExecutorService;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+
+public class MapLiveData<T, Y> extends ReactiveLiveData<Y> {
+
+ public MapLiveData(@NonNull LiveData<T> source, @NonNull Function<T, Y> mapFunction) {
+ super(Transformations.map(source, mapFunction));
+ }
+
+ public MapLiveData(@NonNull LiveData<T> source, @NonNull Function<T, Y> mapFunction, @NonNull ExecutorService executor) {
+ addSource(source, val -> executor.submit(() -> postValue(mapFunction.apply(val))));
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/merge/MergeLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/merge/MergeLiveData.java
new file mode 100644
index 000000000..cc3c1ddd6
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/merge/MergeLiveData.java
@@ -0,0 +1,15 @@
+package it.niedermann.android.reactivelivedata.merge;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Supplier;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+
+public class MergeLiveData<T> extends ReactiveLiveData<T> {
+
+ public MergeLiveData(@NonNull LiveData<T> source, @NonNull Supplier<LiveData<T>> secondSource) {
+ addSource(source, this::setValue);
+ addSource(secondSource.get(), this::setValue);
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeLiveData.java
new file mode 100644
index 000000000..0ca536db2
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeLiveData.java
@@ -0,0 +1,13 @@
+package it.niedermann.android.reactivelivedata.take;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.android.reactivelivedata.ReactiveLiveData;
+
+public class TakeLiveData<T> extends ReactiveLiveData<T> {
+
+ public TakeLiveData(@NonNull LiveData<T> source, int limit) {
+ addSource(source, new TakeObserver<>(this, limit));
+ }
+}
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeObserver.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeObserver.java
new file mode 100644
index 000000000..0f1f4e576
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/take/TakeObserver.java
@@ -0,0 +1,37 @@
+package it.niedermann.android.reactivelivedata.take;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.Observer;
+
+class TakeObserver<T> implements Observer<T> {
+ private final MediatorLiveData<T> mediator;
+ private final int limit;
+ private int counter = 0;
+
+ public TakeObserver(@NonNull MediatorLiveData<T> mediator, int limit) {
+ if (limit == Integer.MAX_VALUE) {
+ throw new RuntimeException("limit must be lower than Integer.MAX_VALUE");
+ }
+
+ if (limit < 1) {
+ throw new RuntimeException("limit must be 1 or higher");
+ }
+
+ this.mediator = mediator;
+ this.limit = limit;
+ }
+
+ @Override
+ public void onChanged(T value) {
+ if (counter < limit) {
+ mediator.setValue(value);
+ }
+ counter++;
+
+ // Prevent integer overflow
+ if (counter == limit + 1) {
+ counter = limit;
+ }
+ }
+} \ No newline at end of file
diff --git a/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/tap/TapLiveData.java b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/tap/TapLiveData.java
new file mode 100644
index 000000000..c9d08a98d
--- /dev/null
+++ b/reactive-livedata/src/main/java/it/niedermann/android/reactivelivedata/tap/TapLiveData.java
@@ -0,0 +1,34 @@
+package it.niedermann.android.reactivelivedata.tap;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+import androidx.lifecycle.LiveData;
+
+import java.util.concurrent.ExecutorService;
+
+import it.niedermann.android.reactivelivedata.map.MapLiveData;
+
+public class TapLiveData<T> extends MapLiveData<T, T> {
+
+ public TapLiveData(@NonNull LiveData<T> source, @NonNull Runnable runnable) {
+ this(source, val -> runnable.run());
+ }
+
+ public TapLiveData(@NonNull LiveData<T> source, @NonNull Consumer<T> consumer) {
+ super(source, val -> {
+ consumer.accept(val);
+ return val;
+ });
+ }
+
+ public TapLiveData(@NonNull LiveData<T> source, @NonNull Runnable runnable, @NonNull ExecutorService executor) {
+ this(source, val -> runnable.run(), executor);
+ }
+
+ public TapLiveData(@NonNull LiveData<T> source, @NonNull Consumer<T> consumer, @NonNull ExecutorService executor) {
+ super(source, val -> {
+ consumer.accept(val);
+ return val;
+ }, executor);
+ }
+}
diff --git a/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataTest.java b/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataTest.java
new file mode 100644
index 000000000..a644a8ccf
--- /dev/null
+++ b/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/ReactiveLiveDataTest.java
@@ -0,0 +1,188 @@
+package it.niedermann.android.reactivelivedata;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static it.niedermann.android.reactivelivedata.TestUtil.getOrAwaitValue;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.core.util.Pair;
+import androidx.lifecycle.MutableLiveData;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(RobolectricTestRunner.class)
+public class ReactiveLiveDataTest {
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Test
+ public void filter() throws InterruptedException {
+ final var s1$ = new MutableLiveData<Integer>();
+
+ final var reactive1$ = new ReactiveLiveData<>(s1$)
+ .filter(val -> val < 2);
+
+ s1$.postValue(1);
+ assertEquals(Integer.valueOf(1), getOrAwaitValue(reactive1$));
+
+ s1$.postValue(2);
+ assertEquals(Integer.valueOf(1), getOrAwaitValue(reactive1$));
+ }
+
+ @Test
+ public void map() throws InterruptedException {
+ final var s1$ = new MutableLiveData<Integer>();
+
+ final var reactive1$ = new ReactiveLiveData<>(s1$)
+ .map(val -> val * 2);
+
+ s1$.postValue(1);
+ assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$));
+
+ s1$.postValue(2);
+ assertEquals(Integer.valueOf(4), getOrAwaitValue(reactive1$));
+
+ s1$.postValue(3);
+ assertEquals(Integer.valueOf(6), getOrAwaitValue(reactive1$));
+ }
+
+ @Test
+ public void flatMap() throws InterruptedException {
+ final var s0$ = new MutableLiveData<Void>(null);
+ final var s1$ = new MutableLiveData<>("Foo");
+ final var s2$ = new MutableLiveData<>("Bar");
+
+ final var reactive1$ = new ReactiveLiveData<>(s0$)
+ .flatMap(() -> s1$);
+
+ final var reactive2$ = new ReactiveLiveData<>(s0$)
+ .flatMap(() -> s2$);
+
+ assertEquals("Foo", getOrAwaitValue(reactive1$));
+ assertEquals("Bar", getOrAwaitValue(reactive2$));
+ }
+
+ @Test
+ public void flatMap_chained() throws InterruptedException {
+ final var s0$ = new MutableLiveData<Void>(null);
+ final var s1$ = new MutableLiveData<>("Foo");
+ final var s2$ = new MutableLiveData<>("Bar");
+ final var s3$ = new MutableLiveData<>("Baz");
+
+ final var reactive1$ = new ReactiveLiveData<>(s0$)
+ .flatMap(() -> s1$)
+ .flatMap(val -> "Foo".equals(val) ? s2$ : s3$);
+
+ assertEquals("Bar", getOrAwaitValue(reactive1$));
+
+ s1$.postValue("Qux");
+
+ assertEquals("Baz", getOrAwaitValue(reactive1$));
+ }
+
+ @Test
+ public void combineWith() throws InterruptedException {
+ final var s1$ = new MutableLiveData<>(5);
+ final var s2$ = new MutableLiveData<>("Foo");
+
+ final var reactive1$ = new ReactiveLiveData<>(s1$)
+ .combineWith(val -> s2$);
+
+ assertEquals(new Pair<>(5, "Foo"), getOrAwaitValue(reactive1$));
+ }
+
+ @Test
+ public void merge() throws InterruptedException {
+ final var s1$ = new MutableLiveData<>(5);
+ final var s2$ = new MutableLiveData<>(9);
+
+ final var reactive1$ = new ReactiveLiveData<>(s1$)
+ .merge(() -> s2$);
+
+ assertEquals(Integer.valueOf(5), getOrAwaitValue(reactive1$));
+ assertEquals(Integer.valueOf(9), getOrAwaitValue(reactive1$));
+ }
+
+ @Test
+ public void take() throws InterruptedException {
+ assertThrows(RuntimeException.class, () -> new ReactiveLiveData<>().take(Integer.MAX_VALUE));
+ assertThrows(RuntimeException.class, () -> new ReactiveLiveData<>().take(0));
+
+ final var s1$ = new MutableLiveData<>(0);
+
+ final var reactive1$ = new ReactiveLiveData<>(s1$)
+ .take(3);
+
+ assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$));
+
+ s1$.setValue(1);
+ assertEquals(Integer.valueOf(1), getOrAwaitValue(reactive1$));
+
+ s1$.setValue(2);
+ assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$));
+
+ s1$.setValue(3);
+ assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$));
+
+ s1$.setValue(4);
+ assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$));
+ }
+
+ @Test
+ public void debounce() throws InterruptedException {
+ final var s1$ = new MutableLiveData<>(0);
+
+ final var reactive1$ = new ReactiveLiveData<>(s1$)
+ .debounce(120);
+
+ assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 10, TimeUnit.MILLISECONDS));
+
+ for (int i = 1; i <= 6; i++) {
+ Thread.sleep(50);
+ s1$.setValue(i);
+ switch (i) {
+ case 1:
+ case 2:
+ assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 10, TimeUnit.MILLISECONDS));
+ break;
+ case 3:
+ case 4:
+ case 5:
+ assertEquals(Integer.valueOf(3), getOrAwaitValue(reactive1$, 10, TimeUnit.MILLISECONDS));
+ break;
+ case 6:
+ assertEquals(Integer.valueOf(6), getOrAwaitValue(reactive1$, 10, TimeUnit.MILLISECONDS));
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ @Test
+ public void debounce_shouldPickUpChangesAfterTheTimeoutDirectly() throws InterruptedException {
+ final var s1$ = new MutableLiveData<>(0);
+
+ final var reactive1$ = new ReactiveLiveData<>(s1$)
+ .debounce(120);
+
+ assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 0, TimeUnit.MILLISECONDS));
+
+ Thread.sleep(50);
+ s1$.setValue(1);
+ assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 0, TimeUnit.MILLISECONDS));
+
+ Thread.sleep(50);
+ s1$.setValue(2);
+ assertEquals(Integer.valueOf(0), getOrAwaitValue(reactive1$, 0, TimeUnit.MILLISECONDS));
+
+ Thread.sleep(50);
+ assertEquals(Integer.valueOf(2), getOrAwaitValue(reactive1$, 0, TimeUnit.MILLISECONDS));
+ }
+}
diff --git a/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/TestUtil.java b/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/TestUtil.java
new file mode 100644
index 000000000..8f47e15f7
--- /dev/null
+++ b/reactive-livedata/src/test/java/it/niedermann/android/reactivelivedata/TestUtil.java
@@ -0,0 +1,45 @@
+package it.niedermann.android.reactivelivedata;
+
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class TestUtil {
+
+ private TestUtil() {
+ // Util class
+ }
+
+ /**
+ * @see #getOrAwaitValue(LiveData, long, TimeUnit)
+ */
+ public static <T> T getOrAwaitValue(final LiveData<T> liveData) throws InterruptedException {
+ return getOrAwaitValue(liveData, 2, TimeUnit.SECONDS);
+ }
+
+ /**
+ * @see <a href="https://gist.github.com/JoseAlcerreca/1e9ee05dcdd6a6a6fa1cbfc125559bba">Source</a>
+ */
+ public static <T> T getOrAwaitValue(final LiveData<T> liveData, long timeout, TimeUnit unit) throws InterruptedException {
+ final var data = new Object[1];
+ final var latch = new CountDownLatch(1);
+ final var observer = new Observer<T>() {
+ @Override
+ public void onChanged(@Nullable T o) {
+ data[0] = o;
+ latch.countDown();
+ liveData.removeObserver(this);
+ }
+ };
+ liveData.observeForever(observer);
+ // Don't wait indefinitely if the LiveData is not set.
+ if (!latch.await(timeout, unit)) {
+ throw new RuntimeException("LiveData value was never set.");
+ }
+ //noinspection unchecked
+ return (T) data[0];
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index edf1d2bdb..6b84d234f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,4 @@
include ':app'
include ':cross-tab-drag-and-drop'
include ':tab-layout-helper'
+include ':reactive-livedata' \ No newline at end of file