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>2020-12-09 19:59:07 +0300
committerStefan Niedermann <info@niedermann.it>2020-12-09 19:59:07 +0300
commitdf900e53492c7b30cbb90b9180d6c3cdf59f38d9 (patch)
treeb88dc0386e6b448c95cf4fcb46fbeaf8edc8780f /app/src/main/java/it/niedermann
parent034ae108ae4ab4c273ef4d74f1bfd39fbc4d8a84 (diff)
parentf29eed9db4c0906fa7887e446cf0325718ef6827 (diff)
Merge branch 'master' into fastlanefastlane
# Conflicts: # fastlane/metadata/android/en-US/images/phoneScreenshots/1.png # fastlane/metadata/android/en-US/images/phoneScreenshots/2.png # fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
Diffstat (limited to 'app/src/main/java/it/niedermann')
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java18
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCDateAdapter.java48
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCInstantAdapter.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/IResponseCallback.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java332
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/LastSyncUtil.java14
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java26
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java20
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java11
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java81
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/Attachment.java60
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java48
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java58
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java53
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java11
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/User.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java68
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java14
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java82
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java5
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java60
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java55
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/comment/DeckComment.java15
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java66
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java45
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java24
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java135
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java50
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java27
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/OcsUserList.java13
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java71
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java70
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java811
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java57
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java294
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DateTypeConverter.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java256
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java16
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java3
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java34
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java13
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java13
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java25
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/SingleCardWidgetModelDao.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/SingleCardWidgetModelDao.java)3
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java19
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java6
-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/DataPropagationHelper.java119
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java77
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java33
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java97
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AttachmentDataProvider.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java119
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java76
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java26
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java100
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/UserDataProvider.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithAclDownSyncDataProvider.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWitAclDownSyncDataProvider.java)2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java37
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java315
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java223
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java115
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java146
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java52
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java26
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java37
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java22
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java72
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java56
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java61
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java20
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java25
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java1
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java24
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java16
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java3
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java122
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java345
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java11
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java84
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java61
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java157
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java44
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java78
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java15
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java50
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java113
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java11
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java125
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java91
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java492
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java36
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java33
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java100
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java26
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java104
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java66
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java85
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java88
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java45
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java100
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java42
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java51
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java102
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java50
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java139
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java37
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java19
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java48
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java80
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java28
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java253
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java52
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java32
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java54
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java110
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java83
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java19
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java18
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java44
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java25
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java40
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java28
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java45
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java128
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java5
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java204
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java45
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java27
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java226
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java16
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java5
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java49
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java182
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java57
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java56
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java16
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java35
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java)25
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java22
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java)19
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java121
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java68
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java134
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java11
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java127
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java38
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java165
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/DateUtil.java51
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java59
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java47
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/ExceptionUtil.java81
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java1
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java90
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/SpannableUtil.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java42
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java42
222 files changed, 9426 insertions, 2898 deletions
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 ff492c226..cf30a0971 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java
@@ -1,20 +1,17 @@
package it.niedermann.nextcloud.deck;
-import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
+import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;
-import com.jakewharton.threetenabp.AndroidThreeTen;
-
import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO;
import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES;
import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode;
-import static androidx.multidex.MultiDex.install;
-public class DeckApplication extends Application {
+public class DeckApplication extends MultiDexApplication {
public static final long NO_ACCOUNT_ID = -1L;
public static final long NO_BOARD_ID = -1L;
@@ -24,17 +21,6 @@ public class DeckApplication extends Application {
public void onCreate() {
setAppTheme(isDarkTheme(getApplicationContext()));
super.onCreate();
- AndroidThreeTen.init(this);
- }
-
- // --------
- // Multidex
- // --------
-
- @Override
- protected void attachBaseContext(Context base) {
- super.attachBaseContext(base);
- install(this);
}
// -----------------
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java
index 9883608c2..1f0148b91 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java
@@ -1,6 +1,8 @@
package it.niedermann.nextcloud.deck.api;
+import com.nextcloud.android.sso.api.ParsedResponse;
+
import java.util.List;
import io.reactivex.Observable;
@@ -33,6 +35,7 @@ import retrofit2.http.Query;
public interface DeckAPI {
String MODIFIED_SINCE_HEADER = "If-Modified-Since";
+ String IF_NONE_MATCH = "If-None-Match";
// ### BOARDS
@POST("boards")
@@ -51,7 +54,7 @@ public interface DeckAPI {
Observable<FullBoard> restoreBoard(@Path("id") long id);
@GET("boards")
- Observable<List<FullBoard>> getBoards(@Query ("details") boolean verbose, @Header(MODIFIED_SINCE_HEADER) String lastSync );
+ Observable<ParsedResponse<List<FullBoard>>> getBoards(@Query ("details") boolean verbose, @Header(MODIFIED_SINCE_HEADER) String lastSync, @Header(IF_NONE_MATCH) String eTag);
// ### Stacks
@@ -83,11 +86,11 @@ public interface DeckAPI {
@FormUrlEncoded
@PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel")
- Observable assignLabelToCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId);
+ Observable<Void> assignLabelToCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId);
@FormUrlEncoded
@PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/removeLabel")
- Observable unassignLabelFromCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId);
+ Observable<Void> unassignLabelFromCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId);
@FormUrlEncoded
@PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignUser")
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java
index 29b3706fa..4e61abfb3 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java
@@ -5,7 +5,7 @@ import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
-import java.util.Date;
+import java.time.Instant;
import java.util.List;
import it.niedermann.nextcloud.deck.model.Attachment;
@@ -16,6 +16,8 @@ import it.niedermann.nextcloud.deck.model.full.FullStack;
import it.niedermann.nextcloud.deck.model.ocs.Activity;
import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectList;
+import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs;
import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser;
import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList;
@@ -42,14 +44,17 @@ public class GsonConfig {
Type ocsUserList = new TypeToken<OcsUserList>() {}.getType();
Type ocsUser = new TypeToken<OcsUser>() {}.getType();
Type activity = new TypeToken<Activity>() {}.getType();
+ Type activityList = new TypeToken<List<Activity>>() {}.getType();
Type attachment = new TypeToken<Attachment>() {}.getType();
Type attachmentList = new TypeToken<List<Attachment>>() {}.getType();
Type comment = new TypeToken<OcsComment>() {}.getType();
+ Type projectList = new TypeToken<OcsProjectList>() {}.getType();
+ Type groupMembers = new TypeToken<GroupMemberUIDs>() {}.getType();
INSTANCE = new GsonBuilder()
.setDateFormat(DATE_PATTERN)
.setLenient()
- .registerTypeAdapter(Date.class, new GsonUTCDateAdapter())
+ .registerTypeAdapter(Instant.class, new GsonUTCInstantAdapter())
.registerTypeAdapter(boardList, new NextcloudArrayDeserializer<>("boards", FullBoard.class))
.registerTypeAdapter(board, new NextcloudDeserializer<>("board", FullBoard.class))
.registerTypeAdapter(cardList, new NextcloudArrayDeserializer<>("cards", FullCard.class))
@@ -62,9 +67,12 @@ public class GsonConfig {
.registerTypeAdapter(ocsUserList, new NextcloudDeserializer<>("ocsUserList", OcsUserList.class))
.registerTypeAdapter(ocsUser, new NextcloudDeserializer<>("ocsUser", OcsUser.class))
.registerTypeAdapter(activity, new NextcloudDeserializer<>("activity", Activity.class))
+ .registerTypeAdapter(activityList, new NextcloudDeserializer<>("activityList", Activity.class))
.registerTypeAdapter(attachmentList, new NextcloudArrayDeserializer<>("attachments", Attachment.class))
.registerTypeAdapter(attachment, new NextcloudDeserializer<>("attachment", Attachment.class))
.registerTypeAdapter(comment, new NextcloudDeserializer<>("comment", OcsComment.class))
+ .registerTypeAdapter(projectList, new NextcloudDeserializer<>("projectList", OcsProjectList.class))
+ .registerTypeAdapter(groupMembers, new NextcloudDeserializer<>("groupMembers", GroupMemberUIDs.class))
.create();
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCDateAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCDateAdapter.java
deleted file mode 100644
index 4d30b3e81..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCDateAdapter.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package it.niedermann.nextcloud.deck.api;
-
-import com.google.gson.JsonDeserializationContext;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonPrimitive;
-import com.google.gson.JsonSerializationContext;
-import com.google.gson.JsonSerializer;
-
-import java.lang.reflect.Type;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.regex.Pattern;
-
-public class GsonUTCDateAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
-
- private final DateFormat dateFormat;
- private final Pattern UNIX_TIMESTAMP = Pattern.compile("^[0-9]+$");
-
- public GsonUTCDateAdapter() {
- //This is the format I need
- dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
- //This is the key line which converts the date to UTC which cannot be accessed with the default serializer
- dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
- }
-
- @Override public synchronized JsonElement serialize(Date date, Type type, JsonSerializationContext jsonSerializationContext) {
- return new JsonPrimitive(dateFormat.format(date));
- }
-
- @Override public synchronized Date deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) {
- String dateValue = jsonElement.getAsString();
- try {
- return dateFormat.parse(dateValue);
- } catch (ParseException e) {
- // fallback to unix timestamp?
- if (UNIX_TIMESTAMP.matcher(dateValue).matches()){
- return new Date(Long.valueOf(dateValue));
- }
- throw new JsonParseException(e);
- }
- }
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCInstantAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCInstantAdapter.java
new file mode 100644
index 000000000..5a6b5b0c0
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonUTCInstantAdapter.java
@@ -0,0 +1,43 @@
+package it.niedermann.nextcloud.deck.api;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+import java.lang.reflect.Type;
+import java.text.ParseException;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.regex.Pattern;
+
+public class GsonUTCInstantAdapter implements JsonSerializer<Instant>, JsonDeserializer<Instant> {
+
+ private static final Pattern UNIX_TIMESTAMP = Pattern.compile("^[0-9]+$");
+
+ @Override
+ public synchronized JsonElement serialize(Instant date, Type type, JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(DateTimeFormatter.ISO_INSTANT.format(date));
+ }
+
+ @Override
+ public synchronized Instant deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) {
+ String dateValue = jsonElement.getAsString();
+ try {
+ final Instant parsedDate = Instant.parse(dateValue);
+ if (parsedDate == null) {
+ throw new ParseException("Parsed date is null", 0);
+ }
+ return parsedDate;
+ } catch (ParseException e) {
+ // fallback to unix timestamp?
+ if (UNIX_TIMESTAMP.matcher(dateValue).matches()) {
+ return Instant.ofEpochMilli(Long.parseLong(dateValue));
+ }
+ throw new JsonParseException(e);
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/IResponseCallback.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/IResponseCallback.java
index 0e523c318..be3eaeb22 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/api/IResponseCallback.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/IResponseCallback.java
@@ -24,12 +24,18 @@ public abstract class IResponseCallback<T> {
DeckLog.logError(throwable);
}
+ @CallSuper
+ public void onError(Throwable throwable, T locallyCreatedEntity) {
+ onError(throwable);
+ }
+
public static <T> IResponseCallback<T> getDefaultResponseCallback(Account account) {
return new IResponseCallback<T>(account) {
@Override
public void onResponse(T response) {
// Do Nothing
}
+
};
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java
index 6dedefa01..e759f27d4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java
@@ -1,20 +1,20 @@
package it.niedermann.nextcloud.deck.api;
+import android.graphics.Color;
+
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
-import org.threeten.bp.DateTimeUtils;
-import org.threeten.bp.ZonedDateTime;
-import org.threeten.bp.format.DateTimeFormatter;
-
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.exceptions.DeckException;
-import it.niedermann.nextcloud.deck.exceptions.TraceableException;
import it.niedermann.nextcloud.deck.model.AccessControl;
import it.niedermann.nextcloud.deck.model.Attachment;
import it.niedermann.nextcloud.deck.model.Board;
@@ -32,13 +32,19 @@ import it.niedermann.nextcloud.deck.model.ocs.Version;
import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
import it.niedermann.nextcloud.deck.model.ocs.comment.Mention;
import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectList;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs;
import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser;
import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList;
import static it.niedermann.nextcloud.deck.exceptions.DeckException.Hint.CAPABILITIES_VERSION_NOT_PARSABLE;
+import static it.niedermann.nextcloud.deck.exceptions.TraceableException.makeTraceableIfFails;
public class JsonToEntityParser {
+ @SuppressWarnings("unchecked")
protected static <T> T parseJsonObject(JsonObject obj, Class<T> mType) {
if (mType == FullBoard.class) {
return (T) parseBoard(obj);
@@ -55,44 +61,54 @@ public class JsonToEntityParser {
} else if (mType == OcsUserList.class) {
return (T) parseOcsUserList(obj);
} else if (mType == OcsUser.class) {
- return (T) parseOcsUser(obj);
+ return (T) parseSingleOcsUser(obj);
} else if (mType == Attachment.class) {
return (T) parseAttachment(obj);
} else if (mType == OcsComment.class) {
return (T) parseOcsComment(obj);
+ } else if (mType == GroupMemberUIDs.class) {
+ return (T) parseGroupMemberUIDs(obj);
+ } else if (mType == OcsProjectList.class) {
+ return (T) parseOcsProjectList(obj);
}
throw new IllegalArgumentException("unregistered type: " + mType.getCanonicalName());
}
- private static OcsUser parseOcsUser(JsonObject obj) {
+ private static GroupMemberUIDs parseGroupMemberUIDs(JsonObject obj) {
DeckLog.verbose(obj.toString());
- OcsUser ocsUser = new OcsUser();
- TraceableException.makeTraceableIfFails(() -> {
+ GroupMemberUIDs uids = new GroupMemberUIDs();
+ makeTraceableIfFails(() -> {
JsonElement data = obj.get("ocs").getAsJsonObject().get("data");
- if (!data.isJsonNull()) {
- JsonObject jsonObject = data.getAsJsonObject();
- if (jsonObject.has("id")) {
- ocsUser.setId(getNullAsEmptyString(jsonObject.get("id")));
- }
- if (jsonObject.has("displayname")) {
- ocsUser.setDisplayName(getNullAsEmptyString(jsonObject.get("displayname")));
+ if (!data.isJsonNull() && data.getAsJsonObject().has("users")) {
+ JsonElement users = data.getAsJsonObject().get("users");
+ if (!users.isJsonNull() && users.isJsonArray()) {
+ for (JsonElement userElement : users.getAsJsonArray()) {
+ uids.add(userElement.getAsString());
+ }
}
}
}, obj);
- return ocsUser;
+ return uids;
}
private static OcsUserList parseOcsUserList(JsonObject obj) {
DeckLog.verbose(obj.toString());
OcsUserList ocsUserList = new OcsUserList();
- TraceableException.makeTraceableIfFails(() -> {
+ makeTraceableIfFails(() -> {
JsonElement data = obj.get("ocs").getAsJsonObject().get("data");
if (!data.isJsonNull() && data.getAsJsonObject().has("users")) {
JsonElement users = data.getAsJsonObject().get("users");
if (!users.isJsonNull() && users.isJsonArray()) {
for (JsonElement userElement : users.getAsJsonArray()) {
- ocsUserList.add(userElement.getAsString());
+ JsonObject singleUserElement = userElement.getAsJsonObject();
+ OcsUser user = new OcsUser();
+ user.setDisplayName(singleUserElement.get("label").getAsString());
+ user.setId(
+ singleUserElement.get("value").getAsJsonObject()
+ .get("shareWith").getAsString()
+ );
+ ocsUserList.addUser(user);
}
}
}
@@ -101,10 +117,105 @@ public class JsonToEntityParser {
return ocsUserList;
}
+ private static OcsUser parseSingleOcsUser(JsonObject obj) {
+ DeckLog.verbose(obj.toString());
+ OcsUser ocsUser = new OcsUser();
+ makeTraceableIfFails(() -> {
+ JsonElement data = obj.get("ocs").getAsJsonObject().get("data");
+ if (!data.isJsonNull()) {
+ JsonObject user = data.getAsJsonObject();
+ if (user.has("id")) {
+ ocsUser.setId(user.get("id").getAsString());
+ }
+ if (user.has("displayname")) {
+ ocsUser.setDisplayName(user.get("displayname").getAsString());
+ }
+ }
+
+ }, obj);
+ return ocsUser;
+ }
+
+ private static OcsProjectList parseOcsProjectList(JsonObject obj) {
+ DeckLog.verbose(obj.toString());
+ OcsProjectList projectList = new OcsProjectList();
+ makeTraceableIfFails(() -> {
+ JsonElement data = obj.get("ocs").getAsJsonObject().get("data");
+ if (!data.isJsonNull() && data.isJsonArray()) {
+ JsonArray projectJsonArray = data.getAsJsonArray();
+ for (JsonElement jsonArrayElement : projectJsonArray) {
+ if (jsonArrayElement.isJsonObject()) {
+ JsonObject jsonObject = jsonArrayElement.getAsJsonObject();
+ OcsProject project = new OcsProject();
+ project.setId(jsonObject.get("id").getAsLong());
+ project.setName(getNullAsEmptyString(jsonObject.get("name")));
+ project.setResources(new ArrayList<>());
+ JsonElement jsonResources = jsonObject.get("resources");
+ if (jsonResources != null && jsonResources.isJsonArray()) {
+ JsonArray resourcesArray = jsonResources.getAsJsonArray();
+ for (JsonElement resourceElement : resourcesArray) {
+ if (resourceElement.isJsonObject()) {
+ OcsProjectResource resource = parseOcsProjectResource(resourceElement.getAsJsonObject());
+ resource.setProjectId(project.getId());
+ project.getResources().add(resource);
+ }
+ }
+ }
+ projectList.add(project);
+ }
+ }
+ }
+
+ }, obj);
+ return projectList;
+ }
+
+ private static OcsProjectResource parseOcsProjectResource(JsonObject obj) {
+ DeckLog.verbose(obj.toString());
+ OcsProjectResource resource = new OcsProjectResource();
+ makeTraceableIfFails(() -> {
+ if (obj.has("id")) {
+ String idString = obj.get("id").getAsString();
+ if (idString != null && idString.trim().length() > 0) {
+ if (idString.matches("[0-9]+")) {
+ resource.setId(Long.parseLong(idString.trim()));
+ } else {
+ resource.setIdString(idString);
+ }
+ }
+ }
+ if (obj.has("type")) {
+ resource.setType(getNullAsEmptyString(obj.get("type")));
+ }
+ if (obj.has("name")) {
+ resource.setName(getNullAsEmptyString(obj.get("name")));
+ }
+ if (obj.has("link")) {
+ resource.setLink(getNullAsEmptyString(obj.get("link")));
+ }
+ if (obj.has("iconUrl")) {
+ resource.setIconUrl(getNullAsEmptyString(obj.get("iconUrl")));
+ }
+ if (obj.has("path")) {
+ resource.setPath(obj.get("path").getAsString());
+ }
+ if (obj.has("mimetype")) {
+ resource.setMimetype(obj.get("mimetype").getAsString());
+ }
+ if (obj.has("preview-available")) {
+ resource.setPreviewAvailable(obj.get("preview-available").getAsBoolean());
+ } else {
+ resource.setPreviewAvailable(false);
+ }
+
+ }, obj);
+ return resource;
+ }
+
private static OcsComment parseOcsComment(JsonObject obj) {
DeckLog.verbose(obj.toString());
OcsComment comment = new OcsComment();
- TraceableException.makeTraceableIfFails(() -> {
+ makeTraceableIfFails(() -> {
JsonElement data = obj.get("ocs").getAsJsonObject().get("data");
if (data.isJsonArray()) {
for (JsonElement deckComment : data.getAsJsonArray()) {
@@ -121,7 +232,7 @@ public class JsonToEntityParser {
DeckLog.verbose(data.toString());
DeckComment deckComment = new DeckComment();
- TraceableException.makeTraceableIfFails(() -> {
+ makeTraceableIfFails(() -> {
JsonObject commentJson = data.getAsJsonObject();
deckComment.setId(commentJson.get("id").getAsLong());
@@ -132,17 +243,18 @@ public class JsonToEntityParser {
deckComment.setActorType(commentJson.get("actorType").getAsString());
deckComment.setCreationDateTime(getTimestampFromString(commentJson.get("creationDateTime")));
- if (commentJson.has("replyTo")){
- JsonObject replyTo = commentJson.get("replyTo").getAsJsonObject();
- deckComment.setParentId(replyTo.get("id").getAsLong());
- }
+ if (commentJson.has("replyTo")) {
+ JsonObject replyTo = commentJson.get("replyTo").getAsJsonObject();
+ deckComment.setParentId(replyTo.get("id").getAsLong());
+ }
- JsonElement mentions = commentJson.get("mentions");
- if (mentions != null && mentions.isJsonArray()) {
- for (JsonElement mention : mentions.getAsJsonArray()) {
- deckComment.addMention(parseMention(mention));
+ JsonElement mentions = commentJson.get("mentions");
+ if (mentions != null && mentions.isJsonArray()) {
+ for (JsonElement mention : mentions.getAsJsonArray()) {
+ deckComment.addMention(parseMention(mention));
+ }
}
- }}, data);
+ }, data);
return deckComment;
}
@@ -152,7 +264,7 @@ public class JsonToEntityParser {
DeckLog.verbose(mentionJson.toString());
- TraceableException.makeTraceableIfFails(() -> {
+ makeTraceableIfFails(() -> {
JsonObject mentionObject = mentionJson.getAsJsonObject();
mention.setMentionId(mentionObject.get("mentionId").getAsString());
mention.setMentionType(mentionObject.get("mentionType").getAsString());
@@ -169,9 +281,10 @@ public class JsonToEntityParser {
DeckLog.verbose(e.toString());
Board board = new Board();
- TraceableException.makeTraceableIfFails(() -> {
+ makeTraceableIfFails(() -> {
board.setTitle(getNullAsEmptyString(e.get("title")));
board.setColor(getNullAsEmptyString(e.get("color")));
+ board.setEtag(getNullAsNull(e.get("ETag")));
board.setArchived(e.get("archived").getAsBoolean());
board.setLastModified(getTimestampFromLong(e.get("lastModified")));
@@ -232,12 +345,19 @@ public class JsonToEntityParser {
}
}
- JsonElement owner = e.get("owner");
- if (owner != null) {
- if (owner.isJsonPrimitive()) {//TODO: remove if, let only else!
- DeckLog.verbose("owner is Primitive, skipping");
- } else
- fullBoard.setOwner(parseUser(owner.getAsJsonObject()));
+ if (e.has("owner")) {
+ fullBoard.setOwner(parseUser(e.get("owner")));
+ }
+ if (e.has("users")) {
+ JsonElement users = e.get("users");
+ if (users != null && !users.isJsonNull() && users.isJsonArray()) {
+ JsonArray usersArray = users.getAsJsonArray();
+ List<User> usersList = new ArrayList<>();
+ for (JsonElement userJson : usersArray) {
+ usersList.add(parseUser(userJson));
+ }
+ fullBoard.setUsers(usersList);
+ }
}
}, e);
return fullBoard;
@@ -248,8 +368,8 @@ public class JsonToEntityParser {
AccessControl acl = new AccessControl();
if (aclJson.has("participant") && !aclJson.get("participant").isJsonNull()) {
- TraceableException.makeTraceableIfFails(() -> {
- User participant = parseUser(aclJson.get("participant").getAsJsonObject());
+ makeTraceableIfFails(() -> {
+ User participant = parseUser(aclJson.get("participant"));
acl.setUser(participant);
acl.setType(aclJson.get("type").getAsLong());
acl.setBoardId(aclJson.get("boardId").getAsLong());
@@ -271,12 +391,13 @@ public class JsonToEntityParser {
FullCard fullCard = new FullCard();
Card card = new Card();
fullCard.setCard(card);
- TraceableException.makeTraceableIfFails(() -> {
+ makeTraceableIfFails(() -> {
card.setId(e.get("id").getAsLong());
card.setTitle(getNullAsEmptyString(e.get("title")));
card.setDescription(getNullAsEmptyString(e.get("description")));
card.setStackId(e.get("stackId").getAsLong());
card.setType(getNullAsEmptyString(e.get("type")));
+ card.setEtag(getNullAsNull(e.get("ETag")));
card.setLastModified(getTimestampFromLong(e.get("lastModified")));
card.setCreatedAt(getTimestampFromLong(e.get("createdAt")));
card.setDeletedAt(getTimestampFromLong(e.get("deletedAt")));
@@ -296,7 +417,7 @@ public class JsonToEntityParser {
for (JsonElement assignedUser : assignedUsers) {
JsonObject userJson = assignedUser.getAsJsonObject();
if (userJson.has("participant") && !userJson.get("participant").isJsonNull()) {
- users.add(parseUser(userJson.get("participant").getAsJsonObject()));
+ users.add(parseUser(userJson.get("participant")));
}
}
fullCard.setAssignedUsers(users);
@@ -322,10 +443,7 @@ public class JsonToEntityParser {
card.setCommentsUnread(e.get("commentsUnread").getAsInt());
JsonElement owner = e.get("owner");
if (owner != null) {
- if (owner.isJsonPrimitive()) {//TODO: remove if, let only else!
- DeckLog.verbose("owner is Primitive, skipping");
- } else
- fullCard.setOwner(parseUser(owner.getAsJsonObject()));
+ fullCard.setOwner(parseUser(owner));
}
card.setArchived(e.get("archived").getAsBoolean());
}, e);
@@ -336,16 +454,17 @@ public class JsonToEntityParser {
protected static Attachment parseAttachment(JsonObject e) {
DeckLog.verbose(e.toString());
Attachment a = new Attachment();
- TraceableException.makeTraceableIfFails(() -> {
+ makeTraceableIfFails(() -> {
a.setId(e.get("id").getAsLong());
a.setCardId(e.get("cardId").getAsLong());
a.setType(e.get("type").getAsString());
+ a.setEtag(getNullAsNull(e.get("ETag")));
a.setData(e.get("data").getAsString());
a.setLastModified(getTimestampFromLong(e.get("lastModified")));
a.setCreatedAt(getTimestampFromLong(e.get("createdAt")));
a.setCreatedBy(e.get("createdBy").getAsString());
a.setDeletedAt(getTimestampFromLong(e.get("deletedAt")));
- if (e.has("extendedData") && !e.get("extendedData").isJsonNull()) {
+ if (e.has("extendedData") && !e.get("extendedData").isJsonNull() && e.get("extendedData").isJsonObject()) {
JsonObject extendedData = e.getAsJsonObject("extendedData").getAsJsonObject();
a.setFilesize(extendedData.get("filesize").getAsLong());
a.setMimetype(extendedData.get("mimetype").getAsString());
@@ -364,14 +483,27 @@ public class JsonToEntityParser {
return a;
}
- protected static User parseUser(JsonObject e) {
- DeckLog.verbose(e.toString());
+ protected static User parseUser(JsonElement userElement) {
+ DeckLog.verbose(userElement.toString());
+ if (userElement.isJsonNull()) {
+ return null;
+ }
User user = new User();
- TraceableException.makeTraceableIfFails(() -> {
- user.setDisplayname(getNullAsEmptyString(e.get("displayname")));
- user.setPrimaryKey(getNullAsEmptyString(e.get("primaryKey")));
- user.setUid(getNullAsEmptyString(e.get("uid")));
- }, e);
+ makeTraceableIfFails(() -> {
+
+ if (userElement.isJsonPrimitive()) {
+ String uid = userElement.getAsString();
+ user.setDisplayname(uid);
+ user.setPrimaryKey(uid);
+ user.setUid(uid);
+ } else {
+ JsonObject userJson = userElement.getAsJsonObject();
+ user.setDisplayname(getNullAsEmptyString(userJson.get("displayname")));
+ user.setPrimaryKey(getNullAsEmptyString(userJson.get("primaryKey")));
+ user.setUid(getNullAsEmptyString(userJson.get("uid")));
+ }
+
+ }, userElement);
return user;
}
@@ -419,8 +551,8 @@ public class JsonToEntityParser {
}
if (caps.has("theming")) {
JsonObject theming = caps.getAsJsonObject("theming");
- capabilities.setColor(theming.get("color").getAsString());
- capabilities.setTextColor(theming.get("color-text").getAsString());
+ capabilities.setColor(getColorAsInt(theming, "color"));
+ capabilities.setTextColor(getColorAsInt(theming, "color-text"));
}
}
capabilities.setDeckVersion(Version.of(version));
@@ -429,31 +561,17 @@ public class JsonToEntityParser {
return capabilities;
}
- protected static List<Activity> parseActivity(JsonObject e) {
- DeckLog.verbose(e.toString());
- List<Activity> activityList = new ArrayList<>();
-
- TraceableException.makeTraceableIfFails(() -> {
- if (e.has("ocs")) {
- JsonObject ocs = e.getAsJsonObject("ocs");
- if (ocs.has("data")) {
- JsonArray data = ocs.getAsJsonArray("data");
- for (JsonElement activityJson : data) {
- Activity activity = new Activity();
- JsonObject activityObject = activityJson.getAsJsonObject();
-
- activity.setId(activityObject.get("activity_id").getAsLong());
- activity.setType(ActivityType.findByPath(getNullAsEmptyString(activityObject.get("icon"))).getId());
- activity.setSubject(getNullAsEmptyString(activityObject.get("subject")));
- activity.setCardId(activityObject.get("object_id").getAsLong());
- activity.setLastModified(getTimestampFromString(activityObject.get("datetime")));
-
- activityList.add(activity);
- }
- }
+ private static int getColorAsInt(JsonObject element, String field) {
+ String rawString = getNullAsEmptyString(element.get(field));
+ try {
+ if (!rawString.trim().isEmpty()) {
+ String colorAsString = ColorUtil.INSTANCE.formatColorToParsableHexString(rawString);
+ return Color.parseColor(colorAsString);
}
- }, e);
- return activityList;
+ } catch (Exception e) {
+ // Do mostly nothing, return default value
+ }
+ return Color.GRAY;
}
protected static FullStack parseStack(JsonObject e) {
@@ -461,10 +579,11 @@ public class JsonToEntityParser {
FullStack fullStack = new FullStack();
Stack stack = new Stack();
fullStack.setStack(stack);
- TraceableException.makeTraceableIfFails(() -> {
+ makeTraceableIfFails(() -> {
stack.setTitle(getNullAsEmptyString(e.get("title")));
stack.setBoardId(e.get("boardId").getAsLong());
stack.setId(e.get("id").getAsLong());
+ stack.setEtag(getNullAsNull(e.get("ETag")));
stack.setLastModified(getTimestampFromLong(e.get("lastModified")));
stack.setDeletedAt(getTimestampFromLong(e.get("deletedAt")));
if (e.has("order") && !e.get("order").isJsonNull()) {
@@ -485,37 +604,70 @@ public class JsonToEntityParser {
return fullStack;
}
+ protected static List<Activity> parseActivity(JsonObject e) {
+ DeckLog.verbose(e.toString());
+ List<Activity> activityList = new ArrayList<>();
+
+ makeTraceableIfFails(() -> {
+ if (e.has("ocs")) {
+ JsonObject ocs = e.getAsJsonObject("ocs");
+ if (ocs.has("data")) {
+ JsonArray data = ocs.getAsJsonArray("data");
+ for (JsonElement activityJson : data) {
+ Activity activity = new Activity();
+ JsonObject activityObject = activityJson.getAsJsonObject();
+
+ activity.setId(activityObject.get("activity_id").getAsLong());
+ activity.setType(ActivityType.findByPath(getNullAsEmptyString(activityObject.get("icon"))).getId());
+ activity.setSubject(getNullAsEmptyString(activityObject.get("subject")));
+ activity.setCardId(activityObject.get("object_id").getAsLong());
+ activity.setEtag(getNullAsNull(e.get("ETag")));
+ activity.setLastModified(getTimestampFromString(activityObject.get("datetime")));
+
+ activityList.add(activity);
+ }
+ }
+ }
+ }, e);
+ return activityList;
+ }
+
protected static Label parseLabel(JsonObject e) {
DeckLog.verbose(e.toString());
Label label = new Label();
- TraceableException.makeTraceableIfFails(() -> {
+ makeTraceableIfFails(() -> {
label.setId(e.get("id").getAsLong());
//todo: last modified!
// label.setLastModified(get);
label.setTitle(getNullAsEmptyString(e.get("title")));
- label.setColor(getNullAsEmptyString(e.get("color")));
+ label.setEtag(getNullAsNull(e.get("ETag")));
+ label.setColor(getColorAsInt(e, "color"));
}, e);
return label;
}
private static String getNullAsEmptyString(JsonElement jsonElement) {
- return jsonElement.isJsonNull() ? "" : jsonElement.getAsString();
+ return jsonElement == null || jsonElement.isJsonNull() ? "" : jsonElement.getAsString();
+ }
+
+ private static String getNullAsNull(JsonElement jsonElement) {
+ return jsonElement == null || jsonElement.isJsonNull() ? null : jsonElement.getAsString();
}
- private static Date getTimestampFromString(JsonElement jsonElement) {
+ private static Instant getTimestampFromString(JsonElement jsonElement) {
if (jsonElement.isJsonNull()) {
return null;
} else {
String dateAsString = jsonElement.getAsString();
- return DateTimeUtils.toDate(ZonedDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(dateAsString)).toInstant());
+ return ZonedDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(dateAsString)).toInstant();
}
}
- private static Date getTimestampFromLong(JsonElement jsonElement) {
+ private static Instant getTimestampFromLong(JsonElement jsonElement) {
if (jsonElement.isJsonNull()) {
return null;
} else {
- return new Date(jsonElement.getAsLong() * 1000);
+ return Instant.ofEpochMilli(jsonElement.getAsLong() * 1000);
}
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/LastSyncUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/LastSyncUtil.java
index 1d44bfe13..1492d78ac 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/api/LastSyncUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/LastSyncUtil.java
@@ -3,7 +3,7 @@ package it.niedermann.nextcloud.deck.api;
import android.content.Context;
import android.content.SharedPreferences;
-import java.util.Date;
+import java.time.Instant;
import it.niedermann.nextcloud.deck.R;
@@ -22,19 +22,19 @@ public class LastSyncUtil {
}
- public static long getLastSync(long accountId){
+ public static long getLastSync(long accountId) {
return INSTANCE.lastSyncPref.getLong(getSyncKeyForAccount(accountId), 0L);
}
- public static Date getLastSyncDate(long accountId){
- return new Date(getLastSync(accountId));
+ public static Instant getLastSyncDate(long accountId) {
+ return Instant.ofEpochMilli(getLastSync(accountId));
}
- public static void setLastSyncDate(long accountId, Date value){
- INSTANCE.lastSyncPref.edit().putLong(getSyncKeyForAccount(accountId), value.getTime()).apply();
+ public static void setLastSyncDate(long accountId, Instant value) {
+ INSTANCE.lastSyncPref.edit().putLong(getSyncKeyForAccount(accountId), value.toEpochMilli()).apply();
}
- public static void resetLastSyncDate(long accountId){
+ public static void resetLastSyncDate(long accountId) {
INSTANCE.lastSyncPref.edit().remove(getSyncKeyForAccount(accountId)).apply();
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java
index d69ee8513..7b93ace49 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java
@@ -1,6 +1,8 @@
package it.niedermann.nextcloud.deck.api;
+import com.nextcloud.android.sso.api.ParsedResponse;
+
import java.util.List;
import io.reactivex.Observable;
@@ -8,11 +10,14 @@ import it.niedermann.nextcloud.deck.model.ocs.Activity;
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.projects.OcsProjectList;
+import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs;
import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser;
import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
+import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.PUT;
@@ -22,13 +27,19 @@ import retrofit2.http.Query;
public interface NextcloudServerAPI {
@GET("cloud/capabilities?format=json")
- Observable<Capabilities> getCapabilities();
+ Observable<ParsedResponse<Capabilities>> getCapabilities(@Header("If-None-Match") String eTag);
+
+ @GET("collaboration/resources/deck-card/{cardId}?format=json")
+ Observable<OcsProjectList> getProjectsForCard(@Path("cardId") long cardId);
+
+ @GET("apps/files_sharing/api/v1/sharees?format=json&perPage=20&itemType=0%2C1%2C7")
+ Observable<OcsUserList> searchUser(@Query("search") String searchTerm);
- @GET("cloud/users?format=json")
- Observable<OcsUserList> getAllUsers();
+ @GET("cloud/groups/{search}?format=json")
+ Observable<GroupMemberUIDs> searchGroupMembers(@Path("search") String groupUid);
- @GET("cloud/users/{uid}?format=json")
- Observable<OcsUser> getUserDetails(@Path("uid") String uid);
+ @GET("cloud/users/{search}?format=json")
+ Observable<OcsUser> getSingleUserData(@Path("search") String userUid);
@GET("apps/activity/api/v2/activity/filter?format=json&object_type=deck_card&limit=50&since=-1&sort=asc")
Observable<List<Activity>> getActivitiesForCard(@Query("object_id") long cardId);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java
new file mode 100644
index 000000000..5169e7f5e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java
@@ -0,0 +1,26 @@
+package it.niedermann.nextcloud.deck.api.json;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+
+import it.niedermann.android.util.ColorUtil;
+
+public class JsonColorSerializer extends TypeAdapter<Integer> {
+ @Override
+ public void write(JsonWriter out, Integer value) throws IOException {
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(ColorUtil.INSTANCE.intColorToHexString(value));
+ }
+ }
+
+ @Override
+ public Integer read(JsonReader in) throws IOException {
+ // currently not needed
+ return null;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java
index 8c2904d2b..0512f8a60 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java
@@ -5,6 +5,8 @@ public class DeckException extends IllegalArgumentException {
public enum Hint {
CAPABILITIES_NOT_PARSABLE,
CAPABILITIES_VERSION_NOT_PARSABLE,
+ UNKNOWN_ACCOUNT_USER_ID,
+ DEPENDENCY_NOT_SYNCED_YET
}
private Hint hint;
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java
index a53ce8c1f..8d436477d 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java
@@ -11,22 +11,24 @@ public class TraceableException extends RuntimeException {
public static void makeTraceableIfFails(Runnable runnable, Object... args) {
try {
runnable.run();
+ } catch (TraceableException t) {
+ throw t;
} catch (Throwable t) {
- String message = "Sorry, a wild error appeared!\n" +
- "### If you want to tell us about the following issue, " +
- "please make sure to censor sensitive data beforehand! ###\n" +
- "Failed to run traceable code";
+ final StringBuilder message = new StringBuilder("Sorry, a wild error appeared!\n\n" +
+ "⚠️ If you want to tell us about the following issue, " +
+ "please make sure to censor sensitive data beforehand! ⚠️\n\n" +
+ "Failed to run traceable code");
if (args != null && args.length > 0) {
- message += " with arguments:\n";
+ message.append(" with arguments:\n");
for (Object arg : args) {
- message += (arg == null ? "null" : arg.toString())+"\n";
+ message.append(arg == null ? "null" : arg.toString()).append("\n");
}
} else {
- message += ":\n";
+ message.append(":\n");
}
- message += "Cause: " + t.getLocalizedMessage();
- TraceableException ex = new TraceableException(message, t);
+ message.append("Cause: ").append(t.getLocalizedMessage());
+ TraceableException ex = new TraceableException(message.toString(), t);
DeckLog.logError(ex);
throw ex;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java
index 299dd7ca0..e2a3ccc3b 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java
@@ -8,6 +8,7 @@ import androidx.room.Index;
import com.google.gson.annotations.SerializedName;
import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
+import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs;
@Entity(inheritSuperIndices = true,
indices = {
@@ -35,6 +36,8 @@ public class AccessControl extends AbstractRemoteEntity {
@Ignore
@SerializedName("participant")
private User user;
+ @Ignore
+ private GroupMemberUIDs groupMemberUIDs;
public AccessControl() {
super();
@@ -115,6 +118,14 @@ public class AccessControl extends AbstractRemoteEntity {
this.user = user;
}
+ public GroupMemberUIDs getGroupMemberUIDs() {
+ return groupMemberUIDs;
+ }
+
+ public void setGroupMemberUIDs(GroupMemberUIDs groupMemberUIDs) {
+ this.groupMemberUIDs = groupMemberUIDs;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
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 ba8fb72b7..8abcbced7 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
@@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.model;
import android.net.Uri;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Px;
import androidx.room.ColumnInfo;
@@ -18,7 +19,6 @@ 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.deck.util.ColorUtil;
@Entity(indices = {@Index(value = "name", unique = true)})
public class Account implements Serializable {
@@ -38,12 +38,12 @@ public class Account implements Serializable {
private String url;
@NonNull
- @ColumnInfo(defaultValue = "#0082c9")
- private String color = "#0082c9";
+ @ColumnInfo(defaultValue = "0")
+ private Integer color = 0;
@NonNull
- @ColumnInfo(defaultValue = "#ffffff")
- private String textColor = "#ffffff";
+ @ColumnInfo(defaultValue = "0")
+ private Integer textColor = 0;
@NonNull
@ColumnInfo(defaultValue = "0.6.4")
@@ -53,6 +53,9 @@ public class Account implements Serializable {
@ColumnInfo(defaultValue = "0")
private boolean maintenanceEnabled = false;
+ private String etag;
+ private String boardsEtag;
+
@Ignore
public Account(Long id, @NonNull String name, @NonNull String userName, @NonNull String url) {
this(name, userName, url);
@@ -60,7 +63,7 @@ public class Account implements Serializable {
}
@Ignore
- public Account(String name, String userName, String url) {
+ public Account(@NonNull String name, @NonNull String userName, @NonNull String url) {
this.name = name;
this.userName = userName;
this.url = url;
@@ -74,22 +77,29 @@ public class Account implements Serializable {
public Account() {
}
- public void applyCapabilities(Capabilities capabilities) {
+ public void applyCapabilities(Capabilities capabilities, String eTag) {
+ if (capabilities == null) {
+ maintenanceEnabled = true;
+ return;
+ }
maintenanceEnabled = capabilities.isMaintenanceEnabled();
if (!isMaintenanceEnabled()) {
try {
// Nextcloud might return color format #000 which cannot be parsed by Color.parseColor()
// https://github.com/stefan-niedermann/nextcloud-deck/issues/466
- color = ColorUtil.formatColorToParsableHexString(capabilities.getColor());
- textColor = ColorUtil.formatColorToParsableHexString(capabilities.getTextColor());
+ color = capabilities.getColor();
+ textColor = capabilities.getTextColor();
} catch (Exception e) {
DeckLog.logError(e);
- color = "#0082c9";
- color = "#ffffff";
+ color = 0;
+ color = 0;
}
if (capabilities.getDeckVersion() != null) {
serverDeckVersion = capabilities.getDeckVersion().getOriginalVersion();
}
+ if (eTag != null) {
+ this.etag = eTag;
+ }
}
}
@@ -132,21 +142,23 @@ public class Account implements Serializable {
return serialVersionUID;
}
+ @ColorInt
@NonNull
- public String getColor() {
+ public Integer getColor() {
return color;
}
- public void setColor(@NonNull String color) {
+ public void setColor(@NonNull Integer color) {
this.color = color;
}
@NonNull
- public String getTextColor() {
+ public Integer getTextColor() {
return textColor;
}
- public void setTextColor(@NonNull String textColor) {
+ @Deprecated
+ public void setTextColor(@NonNull Integer textColor) {
this.textColor = textColor;
}
@@ -171,6 +183,31 @@ public class Account implements Serializable {
this.maintenanceEnabled = maintenanceEnabled;
}
+ public String getEtag() {
+ return etag;
+ }
+
+ public void setEtag(String etag) {
+ this.etag = etag;
+ }
+
+ public String getBoardsEtag() {
+ return boardsEtag;
+ }
+
+ public void setBoardsEtag(String boardsEtag) {
+ this.boardsEtag = boardsEtag;
+ }
+
+ /**
+ * 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.
+ */
+ public String getAvatarUrl(@Px int size) {
+ return getUrl() + "/index.php/avatar/" + Uri.encode(getUserName()) + "/" + size;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -185,16 +222,8 @@ public class Account implements Serializable {
if (!url.equals(account.url)) return false;
if (!color.equals(account.color)) return false;
if (!textColor.equals(account.textColor)) return false;
- return serverDeckVersion.equals(account.serverDeckVersion);
- }
-
- /**
- * 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.
- */
- public String getAvatarUrl(@Px int size) {
- return getUrl() + "/index.php/avatar/" + Uri.encode(getUserName()) + "/" + size;
+ if (!serverDeckVersion.equals(account.serverDeckVersion)) return false;
+ return etag != null ? etag.equals(account.etag) : account.etag == null;
}
@Override
@@ -207,6 +236,7 @@ public class Account implements Serializable {
result = 31 * result + textColor.hashCode();
result = 31 * result + serverDeckVersion.hashCode();
result = 31 * result + (maintenanceEnabled ? 1 : 0);
+ result = 31 * result + (etag != null ? etag.hashCode() : 0);
return result;
}
@@ -221,6 +251,7 @@ public class Account implements Serializable {
", textColor='" + textColor + '\'' +
", serverDeckVersion='" + serverDeckVersion + '\'' +
", maintenanceEnabled=" + maintenanceEnabled +
+ ", eTag='" + etag + '\'' +
'}';
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Attachment.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Attachment.java
index 91192f597..7eabcbe70 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Attachment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Attachment.java
@@ -2,32 +2,33 @@ package it.niedermann.nextcloud.deck.model;
import androidx.room.Entity;
import androidx.room.ForeignKey;
+import androidx.room.Ignore;
import androidx.room.Index;
import java.io.Serializable;
-import java.util.Date;
+import java.time.Instant;
import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
@Entity(inheritSuperIndices = true,
indices = {@Index("cardId")},
foreignKeys = {
- @ForeignKey(
- entity = Card.class,
- parentColumns = "localId",
- childColumns = "cardId",
- onDelete = ForeignKey.CASCADE
- )
- }
+ @ForeignKey(
+ entity = Card.class,
+ parentColumns = "localId",
+ childColumns = "cardId",
+ onDelete = ForeignKey.CASCADE
+ )
+ }
)
public class Attachment extends AbstractRemoteEntity implements Comparable<Attachment>, Serializable {
private long cardId;
private String type = "deck_file";
private String data;
- private Date createdAt;
+ private Instant createdAt;
private String createdBy;
- private Date deletedAt;
+ private Instant deletedAt;
private long filesize;
private String mimetype;
private String dirname;
@@ -35,6 +36,9 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac
private String extension;
private String filename;
private String localPath;
+ // TODO should probably be a Long... depends on https://github.com/nextcloud/deck/pull/2638
+ @Ignore
+ private String fileId;
public long getCardId() {
return cardId;
@@ -60,11 +64,11 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac
this.data = data;
}
- public Date getCreatedAt() {
+ public Instant getCreatedAt() {
return createdAt;
}
- public void setCreatedAt(Date createdAt) {
+ public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
@@ -76,11 +80,11 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac
this.createdBy = createdBy;
}
- public Date getDeletedAt() {
+ public Instant getDeletedAt() {
return deletedAt;
}
- public void setDeletedAt(Date deletedAt) {
+ public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
@@ -140,6 +144,22 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac
this.localPath = localPath;
}
+ /**
+ * TODO depends on https://github.com/nextcloud/deck/pull/2638
+ */
+ @Ignore
+ public String getFileId() {
+ return this.fileId;
+ }
+
+ /**
+ * TODO depends on https://github.com/nextcloud/deck/pull/2638
+ */
+ @Ignore
+ public void setFileId(String fileId) {
+ this.fileId = fileId;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -202,7 +222,7 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac
private static int longToComparsionResult(long diff) {
if (diff > 0) {
return 1;
- } else if(diff < 0) {
+ } else if (diff < 0) {
return -1;
}
return 0;
@@ -210,18 +230,18 @@ public class Attachment extends AbstractRemoteEntity implements Comparable<Attac
public long getModificationTimeForComparsion() {
if (lastModifiedLocal != null) {
- return lastModifiedLocal.getTime();
+ return lastModifiedLocal.toEpochMilli();
}
if (lastModified != null) {
- return lastModified.getTime();
+ return lastModified.toEpochMilli();
}
- return new Date().getTime();
+ return Instant.now().toEpochMilli();
}
public long getCreationTimeForComparsion() {
if (createdAt != null) {
- return createdAt.getTime();
+ return createdAt.toEpochMilli();
}
- return new Date().getTime();
+ return Instant.now().toEpochMilli();
}
} \ No newline at end of file
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 29afe844b..27dcd1df8 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
@@ -1,17 +1,23 @@
package it.niedermann.nextcloud.deck.model;
+import android.graphics.Color;
+
+import androidx.annotation.ColorInt;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.Index;
+import com.google.gson.annotations.JsonAdapter;
+
import java.io.Serializable;
-import java.util.Date;
+import java.time.Instant;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.api.json.JsonColorSerializer;
import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
@Entity(
inheritSuperIndices = true,
@@ -31,20 +37,18 @@ public class Board extends AbstractRemoteEntity implements Serializable {
}
@Ignore
- public Board(String title, String color) {
- this.title = title;
+ public Board(String title, @ColorInt int color) {
+ setTitle(title);
setColor(color);
}
private String title;
private long ownerId;
- /**
- * Deck App sends color strings without leading # character
- */
- private String color;
+ @JsonAdapter(JsonColorSerializer.class)
+ private Integer color;
private boolean archived;
private int shared;
- private Date deletedAt;
+ private Instant deletedAt;
private boolean permissionRead = false;
private boolean permissionEdit = false;
private boolean permissionManage = false;
@@ -52,22 +56,22 @@ public class Board extends AbstractRemoteEntity implements Serializable {
@Override
- public Date getLastModified() {
+ public Instant getLastModified() {
return lastModified;
}
@Override
- public void setLastModified(Date lastModified) {
+ public void setLastModified(Instant lastModified) {
this.lastModified = lastModified;
}
@Override
- public Date getLastModifiedLocal() {
+ public Instant getLastModifiedLocal() {
return lastModifiedLocal;
}
@Override
- public void setLastModifiedLocal(Date lastModifiedLocal) {
+ public void setLastModifiedLocal(Instant lastModifiedLocal) {
this.lastModifiedLocal = lastModifiedLocal;
}
@@ -79,21 +83,25 @@ public class Board extends AbstractRemoteEntity implements Serializable {
this.id = id;
}
- public String getColor() {
+ @ColorInt
+ public Integer getColor() {
return color;
}
+
public void setColor(String color) {
try {
- // Nextcloud might return color format #000 which cannot be parsed by Color.parseColor()
- // https://github.com/stefan-niedermann/nextcloud-deck/issues/466
- this.color = ColorUtil.formatColorToParsableHexString(color).substring(1);
+ setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(color)));
} catch (Exception e) {
DeckLog.logError(e);
- this.color = "757575";
+ setColor(Color.GRAY);
}
}
+ public void setColor(@ColorInt Integer color) {
+ this.color = color;
+ }
+
public boolean isArchived() {
return archived;
}
@@ -114,11 +122,11 @@ public class Board extends AbstractRemoteEntity implements Serializable {
this.shared = shared;
}
- public Date getDeletedAt() {
+ public Instant getDeletedAt() {
return deletedAt;
}
- public void setDeletedAt(Date deletedAt) {
+ public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java
index 732ccfc50..6f2b70020 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java
@@ -8,7 +8,7 @@ import androidx.room.Index;
import com.google.gson.annotations.SerializedName;
-import java.util.Date;
+import java.time.Instant;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -16,22 +16,23 @@ import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
@Entity(inheritSuperIndices = true,
- indices = {
- @Index(value = "accountId", name = "card_accID"),
- @Index("stackId")
- },
- foreignKeys = {
- @ForeignKey(
- entity = Stack.class,
- parentColumns = "localId",
- childColumns = "stackId", onDelete = ForeignKey.CASCADE
- )
- }
+ indices = {
+ @Index(value = "accountId", name = "card_accID"),
+ @Index("stackId")
+ },
+ foreignKeys = {
+ @ForeignKey(
+ entity = Stack.class,
+ parentColumns = "localId",
+ childColumns = "stackId", onDelete = ForeignKey.CASCADE
+ )
+ }
)
public class Card extends AbstractRemoteEntity {
private static Pattern PATTERN_MD_TASK = Pattern.compile("\\[([xX ])]");
- public class TaskStatus {
+
+ public static class TaskStatus {
public int taskCount;
public int doneCount;
@@ -49,20 +50,21 @@ public class Card extends AbstractRemoteEntity {
@NonNull
private Long stackId;
private String type;
- private Date createdAt;
- private Date deletedAt;
+ private Instant createdAt;
+ private Instant deletedAt;
private int attachmentCount;
private Long userId;
private int order;
private boolean archived;
@SerializedName("duedate")
- private Date dueDate;
+ private Instant dueDate;
private boolean notified;
private int overdue;
private int commentsUnread;
- public Card() {}
+ public Card() {
+ }
@Ignore
public Card(String title, String description, long stackId) {
@@ -89,15 +91,15 @@ public class Card extends AbstractRemoteEntity {
this.commentsUnread = card.getCommentsUnread();
}
- public TaskStatus getTaskStatus(){
- if (taskStatus == null){
+ public TaskStatus getTaskStatus() {
+ if (taskStatus == null) {
int count = 0, done = 0;
if (description != null) {
Matcher matcher = PATTERN_MD_TASK.matcher(description);
- while (matcher.find()){
+ while (matcher.find()) {
count++;
char c = matcher.group().charAt(1);
- if (c == 'x' || c == 'X'){
+ if (c == 'x' || c == 'X') {
done++;
}
}
@@ -164,19 +166,19 @@ public class Card extends AbstractRemoteEntity {
this.type = type;
}
- public Date getCreatedAt() {
+ public Instant getCreatedAt() {
return createdAt;
}
- public void setCreatedAt(Date createdAt) {
+ public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
- public Date getDeletedAt() {
+ public Instant getDeletedAt() {
return deletedAt;
}
- public void setDeletedAt(Date deletedAt) {
+ public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
@@ -200,12 +202,12 @@ public class Card extends AbstractRemoteEntity {
this.archived = archived;
}
- public Date getDueDate() {
+ public Instant getDueDate() {
return dueDate;
}
- public void setDueDate(Date dueDate) {
- this.dueDate = dueDate;
+ public void setDueDate(Instant dateTime) {
+ this.dueDate = dateTime;
}
public int getOverdue() {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java
index 0d97ce5a0..da24edb5a 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java
@@ -1,27 +1,41 @@
package it.niedermann.nextcloud.deck.model;
+import android.graphics.Color;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
+import com.google.gson.annotations.JsonAdapter;
+
import java.io.Serializable;
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.api.json.JsonColorSerializer;
import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
@Entity(inheritSuperIndices = true,
indices = {@Index("boardId"), @Index(value = {"boardId", "title"}, unique = true, name = "idx_label_title_unique")},
foreignKeys = {
- @ForeignKey(
- entity = Board.class,
- parentColumns = "localId",
- childColumns = "boardId",
- onDelete = ForeignKey.CASCADE
- )
- }
+ @ForeignKey(
+ entity = Board.class,
+ parentColumns = "localId",
+ childColumns = "boardId",
+ onDelete = ForeignKey.CASCADE
+ )
+ }
)
public class Label extends AbstractRemoteEntity implements Serializable {
private String title;
- private String color;
+
+ @JsonAdapter(JsonColorSerializer.class)
+ @NonNull
+ @ColumnInfo(defaultValue = "0")
+ private Integer color;
private long boardId;
public Label() {
@@ -42,14 +56,25 @@ public class Label extends AbstractRemoteEntity implements Serializable {
this.title = title;
}
- public String getColor() {
+ @NonNull
+ @ColorInt
+ public Integer getColor() {
return color;
}
- public void setColor(String color) {
+ public void setColor(@NonNull @ColorInt Integer color) {
this.color = color;
}
+ public void setColor(String color) {
+ try {
+ setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(color)));
+ } catch (Exception e) {
+ DeckLog.logError(e);
+ setColor(Color.GRAY);
+ }
+ }
+
public long getBoardId() {
return boardId;
}
@@ -62,18 +87,20 @@ public class Label extends AbstractRemoteEntity implements Serializable {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
Label label = (Label) o;
if (boardId != label.boardId) return false;
if (title != null ? !title.equals(label.title) : label.title != null) return false;
- return color != null ? color.equals(label.color) : label.color == null;
+ return color.equals(label.color);
}
@Override
public int hashCode() {
- int result = title != null ? title.hashCode() : 0;
- result = 31 * result + (color != null ? color.hashCode() : 0);
+ int result = super.hashCode();
+ result = 31 * result + (title != null ? title.hashCode() : 0);
+ result = 31 * result + color.hashCode();
result = 31 * result + (int) (boardId ^ (boardId >>> 32));
return result;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java
index 330ec16d7..00f054605 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java
@@ -1,12 +1,11 @@
package it.niedermann.nextcloud.deck.model;
-import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.Index;
-import java.util.Date;
+import java.time.Instant;
import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
@@ -35,12 +34,10 @@ public class Stack extends AbstractRemoteEntity {
private String title;
- @NonNull
private long boardId;
- private Date deletedAt;
+ private Instant deletedAt;
- @NonNull
private int order;
//
// @ToMany
@@ -64,11 +61,11 @@ public class Stack extends AbstractRemoteEntity {
this.boardId = boardId;
}
- public Date getDeletedAt() {
+ public Instant getDeletedAt() {
return deletedAt;
}
- public void setDeletedAt(Date deletedAt) {
+ public void setDeletedAt(Instant deletedAt) {
this.deletedAt = deletedAt;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java
index cfb48d543..fd318ce7a 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java
@@ -4,10 +4,12 @@ import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.Index;
+import java.io.Serializable;
+
import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
@Entity(inheritSuperIndices = true, indices = {@Index(value = "uid", name = "user_uid")})
-public class User extends AbstractRemoteEntity {
+public class User extends AbstractRemoteEntity implements Serializable {
private String primaryKey;
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java
new file mode 100644
index 000000000..fb7a74076
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java
@@ -0,0 +1,68 @@
+package it.niedermann.nextcloud.deck.model.appwidgets;
+
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Stack;
+
+@Entity(
+ indices = {
+ @Index("stackId"),
+ @Index("accountId")
+ },
+ foreignKeys = {
+ @ForeignKey(
+ entity = Account.class,
+ parentColumns = "id",
+ childColumns = "accountId", onDelete = ForeignKey.CASCADE
+ ),
+ @ForeignKey(
+ entity = Stack.class,
+ parentColumns = "localId",
+ childColumns = "stackId", onDelete = ForeignKey.CASCADE
+ )
+ }
+)
+public class StackWidgetModel {
+
+ @PrimaryKey()
+ private Integer appWidgetId;
+ private Long accountId;
+ private Long stackId;
+ private boolean darkTheme;
+
+ public Integer getAppWidgetId() {
+ return appWidgetId;
+ }
+
+ public void setAppWidgetId(Integer appWidgetId) {
+ this.appWidgetId = appWidgetId;
+ }
+
+ public Long getAccountId() {
+ return accountId;
+ }
+
+ public void setAccountId(Long accountId) {
+ this.accountId = accountId;
+ }
+
+ public Long getStackId() {
+ return stackId;
+ }
+
+ public void setStackId(Long stackId) {
+ this.stackId = stackId;
+ }
+
+ public boolean getDarkTheme() {
+ return darkTheme;
+ }
+
+ public void setDarkTheme(boolean darkTheme) {
+ this.darkTheme = darkTheme;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java
index 31ad21f16..f9f1643ff 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java
@@ -1,5 +1,6 @@
package it.niedermann.nextcloud.deck.model.full;
+import androidx.annotation.NonNull;
import androidx.room.Embedded;
import androidx.room.Ignore;
import androidx.room.Relation;
@@ -29,6 +30,8 @@ public class FullBoard implements IRemoteEntity {
@Relation(entity = Stack.class, parentColumn = "localId", entityColumn = "boardId")
public List<Stack> stacks;
+ @Ignore
+ public List<User> users;
public User getOwner() {
return owner;
@@ -54,9 +57,17 @@ public class FullBoard implements IRemoteEntity {
this.labels = labels;
}
+ public List<User> getUsers() {
+ return users;
+ }
+
+ public void setUsers(List<User> users) {
+ this.users = users;
+ }
+
@Ignore
@Override
- public IRemoteEntity getEntity() {
+ public Board getEntity() {
return board;
}
@@ -102,6 +113,7 @@ public class FullBoard implements IRemoteEntity {
return result;
}
+ @NonNull
@Override
public String toString() {
return "FullBoard{" +
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java
index 41303cac6..611baf3df 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java
@@ -6,8 +6,6 @@ import androidx.room.Ignore;
import androidx.room.Junction;
import androidx.room.Relation;
-import org.jetbrains.annotations.NotNull;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -25,7 +23,7 @@ import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
public class FullCard implements IRemoteEntity, DragAndDropModel {
@Ignore
- private transient boolean isAttachmentsSorted = false;
+ protected transient boolean isAttachmentsSorted = false;
@Embedded
public Card card;
@@ -48,7 +46,6 @@ public class FullCard implements IRemoteEntity, DragAndDropModel {
@Relation(entity = DeckComment.class, parentColumn = "localId", entityColumn = "objectId", projection = "localId")
public List<Long> commentIDs;
-
public FullCard() {
super();
}
@@ -126,11 +123,11 @@ public class FullCard implements IRemoteEntity, DragAndDropModel {
@Ignore
@Override
- public IRemoteEntity getEntity() {
+ public Card getEntity() {
return card;
}
- @NotNull
+ @NonNull
@Override
public String toString() {
return "FullCard{" +
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java
new file mode 100644
index 000000000..cf43fc86b
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java
@@ -0,0 +1,82 @@
+package it.niedermann.nextcloud.deck.model.full;
+
+import androidx.annotation.NonNull;
+import androidx.room.Junction;
+import androidx.room.Relation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject;
+import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources;
+
+public class FullCardWithProjects extends FullCard {
+
+
+ @NonNull
+ @Relation(entity = OcsProject.class, parentColumn = "localId", entityColumn = "localId",
+ associateBy = @Junction(value = JoinCardWithProject.class, parentColumn = "cardId", entityColumn = "projectId"))
+
+ private List<OcsProjectWithResources> projects = new ArrayList<>();
+
+ public FullCardWithProjects() {
+ super();
+ }
+
+ public FullCardWithProjects(FullCardWithProjects fullCard) {
+ super(fullCard);
+ this.projects = copyList(fullCard.getProjects());
+ }
+
+ @NonNull
+ public List<OcsProjectWithResources> getProjects() {
+ return projects;
+ }
+
+ public void setProjects(@NonNull List<OcsProjectWithResources> projects) {
+ this.projects = projects;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "FullCard{" +
+ "card=" + card +
+ ", labels=" + labels +
+ ", assignedUsers=" + assignedUsers +
+ ", owner=" + owner +
+ ", attachments=" + attachments +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FullCardWithProjects fullCard = (FullCardWithProjects) o;
+
+ if (card != null ? !card.equals(fullCard.card) : fullCard.card != null) return false;
+ if (labels != null ? !labels.equals(fullCard.labels) : fullCard.labels != null)
+ return false;
+ if (assignedUsers != null ? !assignedUsers.equals(fullCard.assignedUsers) : fullCard.assignedUsers != null)
+ return false;
+ if (owner != null ? !owner.equals(fullCard.owner) : fullCard.owner != null) return false;
+ if (attachments != null ? !attachments.equals(fullCard.attachments) : fullCard.attachments != null)
+ return false;
+ return commentIDs != null ? commentIDs.equals(fullCard.commentIDs) : fullCard.commentIDs == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (isAttachmentsSorted ? 1 : 0);
+ result = 31 * result + (card != null ? card.hashCode() : 0);
+ result = 31 * result + (labels != null ? labels.hashCode() : 0);
+ result = 31 * result + (assignedUsers != null ? assignedUsers.hashCode() : 0);
+ result = 31 * result + (owner != null ? owner.hashCode() : 0);
+ result = 31 * result + (attachments != null ? attachments.hashCode() : 0);
+ result = 31 * result + (commentIDs != null ? commentIDs.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java
index eddbafce4..1c9e94899 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java
@@ -14,7 +14,7 @@ public class FullStack implements IRemoteEntity {
@Embedded
public Stack stack;
- @Relation(entity = Card.class, parentColumn = "localId", entityColumn = "stackId")
+ @Relation(entity = Card.class, parentColumn = "localId", entityColumn = "stackId")
public List<Card> cards;
@@ -36,7 +36,7 @@ public class FullStack implements IRemoteEntity {
@Ignore
@Override
- public IRemoteEntity getEntity() {
+ public Stack getEntity() {
return stack;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java
index 50057dfe4..ce14ac6be 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java
@@ -1,21 +1,20 @@
package it.niedermann.nextcloud.deck.model.interfaces;
-import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.Ignore;
+
import it.niedermann.nextcloud.deck.model.enums.DBStatus;
@Entity()
public abstract class AbstractJoinEntity {
- @NonNull
protected int status = DBStatus.UP_TO_DATE.getId();
public int getStatus() {
return status;
}
- public void setStatus(@NonNull int status) {
+ public void setStatus(int status) {
this.status = status;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java
index 328c642ae..2f7771383 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java
@@ -1,31 +1,30 @@
package it.niedermann.nextcloud.deck.model.interfaces;
-import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
-import java.util.Date;
+import java.time.Instant;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.enums.DBStatus;
@Entity(
- indices = {
- @Index("accountId"),
- @Index("id"),
- @Index("lastModifiedLocal"),
- @Index(value = {"accountId", "id"}, unique = true)
- },
- foreignKeys = {
- @ForeignKey(
- entity = Account.class,
- parentColumns = "id",
- childColumns = "accountId", onDelete = ForeignKey.CASCADE
- )
- }
+ indices = {
+ @Index("accountId"),
+ @Index("id"),
+ @Index("lastModifiedLocal"),
+ @Index(value = {"accountId", "id"}, unique = true)
+ },
+ foreignKeys = {
+ @ForeignKey(
+ entity = Account.class,
+ parentColumns = "id",
+ childColumns = "accountId", onDelete = ForeignKey.CASCADE
+ )
+ }
)
public abstract class AbstractRemoteEntity implements IRemoteEntity {
@PrimaryKey(autoGenerate = true)
@@ -35,11 +34,12 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity {
protected Long id;
- @NonNull
protected int status = DBStatus.UP_TO_DATE.getId();
- protected Date lastModified;
- protected Date lastModifiedLocal;
+ protected Instant lastModified;
+ protected Instant lastModifiedLocal;
+
+ protected String etag;
public AbstractRemoteEntity() {
}
@@ -102,35 +102,30 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity {
@Override
- public void setStatus(@NonNull int status) {
+ public void setStatus(int status) {
this.status = status;
}
-
@Override
- public Date getLastModified() {
+ public Instant getLastModified() {
return this.lastModified;
}
-
@Override
- public void setLastModified(Date lastModified) {
+ public void setLastModified(Instant lastModified) {
this.lastModified = lastModified;
}
-
@Override
- public Date getLastModifiedLocal() {
+ public Instant getLastModifiedLocal() {
return this.lastModifiedLocal;
}
-
@Override
- public void setLastModifiedLocal(Date lastModifiedLocal) {
+ public void setLastModifiedLocal(Instant lastModifiedLocal) {
this.lastModifiedLocal = lastModifiedLocal;
}
-
@Ignore
@Override
public DBStatus getStatusEnum() {
@@ -144,6 +139,15 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity {
this.status = status.getId();
}
+ @Override
+ public String getEtag() {
+ return etag;
+ }
+
+ @Override
+ public void setEtag(String etag) {
+ this.etag = etag;
+ }
@Override
public boolean equals(Object o) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java
index 30a478808..f197c0d30 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java
@@ -1,94 +1,87 @@
package it.niedermann.nextcloud.deck.model.interfaces;
-import androidx.annotation.NonNull;
-
+import java.time.Instant;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
import it.niedermann.nextcloud.deck.model.enums.DBStatus;
public interface IRemoteEntity {
- default IRemoteEntity getEntity() {return this;}
+ default IRemoteEntity getEntity() {
+ return this;
+ }
default Long getLocalId() {
return getEntity().getLocalId();
}
-
default void setLocalId(Long localId) {
getEntity().setLocalId(localId);
}
-
default long getAccountId() {
return getEntity().getAccountId();
}
-
default void setAccountId(long accountId) {
getEntity().setAccountId(accountId);
}
-
default Long getId() {
return getEntity().getId();
}
-
default void setId(Long id) {
getEntity().setId(id);
}
-
default int getStatus() {
return getEntity().getStatus();
}
-
- default void setStatus(@NonNull int status) {
+ default void setStatus(int status) {
getEntity().setStatus(status);
}
-
- default Date getLastModified() {
+ default Instant getLastModified() {
return getEntity().getLastModified();
}
-
- default void setLastModified(Date lastModified) {
+ default void setLastModified(Instant lastModified) {
getEntity().setLastModified(lastModified);
}
-
- default Date getLastModifiedLocal() {
+ default Instant getLastModifiedLocal() {
return getEntity().getLastModifiedLocal();
}
-
- default void setLastModifiedLocal(Date lastModifiedLocal) {
+ default void setLastModifiedLocal(Instant lastModifiedLocal) {
getEntity().setLastModifiedLocal(lastModifiedLocal);
}
-
default DBStatus getStatusEnum() {
return getEntity().getStatusEnum();
}
-
default void setStatusEnum(DBStatus status) {
getEntity().setStatusEnum(status);
}
+ default String getEtag() {
+ return getEntity().getEtag();
+ }
+
+ default void setEtag(String etag) {
+ getEntity().setEtag(etag);
+ }
+
default <T> List<T> copyList(List<T> listToCopy) {
if (listToCopy == null) {
return null;
}
List<T> list = new ArrayList<>(listToCopy.size());
- for (T t : listToCopy) {
- list.add(t);
- }
+ list.addAll(listToCopy);
return list;
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java
index f3892dfa5..1b5f0879d 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java
@@ -3,8 +3,6 @@ package it.niedermann.nextcloud.deck.model.internal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
-
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@@ -14,12 +12,21 @@ import it.niedermann.nextcloud.deck.model.User;
import it.niedermann.nextcloud.deck.model.enums.EDueType;
public class FilterInformation implements Serializable {
+
+ public enum EArchiveStatus{
+ ALL, ARCHIVED, NON_ARCHIVED
+ }
+
@NonNull
private EDueType dueType = EDueType.NO_FILTER;
+ private boolean noAssignedLabel = false;
+ private boolean noAssignedUser = false;
@NonNull
private List<User> users = new ArrayList<>();
@NonNull
private List<Label> labels = new ArrayList<>();
+ @NonNull
+ private EArchiveStatus archiveStatus = EArchiveStatus.NON_ARCHIVED;
public FilterInformation() {
// Default constructor
@@ -28,8 +35,12 @@ public class FilterInformation implements Serializable {
public FilterInformation(@Nullable FilterInformation filterInformation) {
if (filterInformation != null) {
this.dueType = filterInformation.getDueType();
+ this.archiveStatus = filterInformation.getArchiveStatus();
this.users.addAll(filterInformation.getUsers());
this.labels.addAll(filterInformation.getLabels());
+ this.noAssignedUser = filterInformation.isNoAssignedUser();
+ this.noAssignedLabel = filterInformation.isNoAssignedLabel();
+ this.archiveStatus = filterInformation.getArchiveStatus();
}
}
@@ -47,7 +58,7 @@ public class FilterInformation implements Serializable {
return users;
}
- @NotNull
+ @NonNull
public List<Label> getLabels() {
return labels;
}
@@ -68,23 +79,55 @@ public class FilterInformation implements Serializable {
users.remove(user);
}
- @NotNull
+ public boolean isNoAssignedUser() {
+ return noAssignedUser;
+ }
+
+ public void setNoAssignedUser(boolean noAssignedUser) {
+ this.noAssignedUser = noAssignedUser;
+ }
+
+ public boolean isNoAssignedLabel() {
+ return noAssignedLabel;
+ }
+
+ public void setNoAssignedLabel(boolean noAssignedLabel) {
+ this.noAssignedLabel = noAssignedLabel;
+ }
+
+ public void setArchiveStatus(@NonNull EArchiveStatus archiveStatus) {
+ this.archiveStatus = archiveStatus;
+ }
+
+ @NonNull
+ public EArchiveStatus getArchiveStatus() {
+ return archiveStatus;
+ }
+
+ @NonNull
@Override
public String toString() {
return "FilterInformation{" +
"dueType=" + dueType +
+ ", noAssignedLabel=" + noAssignedLabel +
+ ", noAssignedUser=" + noAssignedUser +
", users=" + users +
", labels=" + labels +
+ ", archiveStatus=" + archiveStatus +
'}';
}
/**
- * @return whether or not the given filterInformation has any actual filters set
+ * @return whether or not the given {@param filterInformation} has any actual filters set
*/
public static boolean hasActiveFilter(@Nullable FilterInformation filterInformation) {
if (filterInformation == null) {
return false;
}
- return filterInformation.getDueType() != EDueType.NO_FILTER || filterInformation.getUsers().size() > 0 || filterInformation.getLabels().size() > 0;
+ return filterInformation.getDueType() != EDueType.NO_FILTER
+ || filterInformation.getUsers().size() > 0
+ || filterInformation.getLabels().size() > 0
+ || filterInformation.noAssignedUser
+ || filterInformation.noAssignedLabel;
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java
index 9ff4d7945..12e8692d4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java
@@ -5,8 +5,8 @@ public class Capabilities {
private Version deckVersion;
private Version nextcloudVersion;
- private String color = "#0082c9";
- private String textColor = "#ffffff";
+ private int color = 0;
+ private int textColor = 0;
private boolean maintenanceEnabled = false;
public Capabilities() {
@@ -28,19 +28,19 @@ public class Capabilities {
this.nextcloudVersion = nextcloudVersion;
}
- public String getColor() {
+ public int getColor() {
return color;
}
- public void setColor(String color) {
+ public void setColor(int color) {
this.color = color;
}
- public String getTextColor() {
+ public int getTextColor() {
return textColor;
}
- public void setTextColor(String textColor) {
+ public void setTextColor(int textColor) {
this.textColor = textColor;
}
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 7ef97a294..cfd973831 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
@@ -6,18 +6,18 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
-import org.jetbrains.annotations.NotNull;
-
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Attachment;
import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
public class Version implements Comparable<Version> {
private static final Pattern NUMBER_EXTRACTION_PATTERN = Pattern.compile("[0-9]+");
private static final Version VERSION_1_0_0 = new Version("1.0.0", 1, 0, 0);
private static final Version VERSION_1_0_3 = new Version("1.0.3", 1, 0, 3);
+ private static final Version VERSION_1_3_0 = new Version("1.3.0", 1, 3, 0);
@Nullable
private static Version VERSION_MINIMUM_SUPPORTED;
@@ -125,7 +125,7 @@ public class Version implements Comparable<Version> {
return 0;
}
- @NotNull
+ @NonNull
@Override
public String toString() {
return "Version{" +
@@ -157,6 +157,22 @@ public class Version implements Comparable<Version> {
}
/**
+ * Before {@link #VERSION_1_3_0} all {@link Attachment}s have been stored in a special folder at the server.
+ * Starting with {@link #VERSION_1_3_0} {@link Attachment}s can be stored as regular files, allowing for example to make use of server side thumbnail generation.
+ * <p>
+ * Since the migration takes a long time, it does not happen on upgrading the server app but step by step via a cronjob.
+ * Therefore this method is just an indicator, that it is possible that {@link Attachment}s are stored as files, but it is no guarantee that all {@link Attachment}s already have been migrated to files.
+ *
+ * @return whether or not the server supports file attachments
+ * @see <a href="https://github.com/nextcloud/deck/pull/2638">documentation in PR</a>
+ */
+ public boolean supportsFileAttachments() {
+ return false;
+// TODO depends on https://github.com/nextcloud/deck/pull/2638
+// return isGreaterOrEqualTo(VERSION_1_3_0);
+ }
+
+ /**
* Title max length has been increased from 100 to 255 characters beginning with server {@link Version} 1.0.0
*
* @return the number of characters that the title fields of cards allow
@@ -167,6 +183,7 @@ public class Version implements Comparable<Version> {
? 255
: 100;
}
+
/**
* URL to view a card in the web interface has been changed in {@link Version} 1.0.0
*
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/comment/DeckComment.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/comment/DeckComment.java
index 707bae974..8bba9740b 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/comment/DeckComment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/comment/DeckComment.java
@@ -5,12 +5,13 @@ import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.Index;
+import java.time.Instant;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
import it.niedermann.nextcloud.deck.model.Card;
import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
+
@Entity(inheritSuperIndices = true,
indices = {
@Index(value = "accountId", name = "comment_accID"),
@@ -35,7 +36,7 @@ public class DeckComment extends AbstractRemoteEntity {
private Long objectId;
private String actorType;
- private Date creationDateTime;
+ private Instant creationDateTime;
private String actorId;
private String actorDisplayName;
private String message;
@@ -47,7 +48,7 @@ public class DeckComment extends AbstractRemoteEntity {
}
@Ignore
- public DeckComment(String message, String actorDisplayName, Date creationDateTime) {
+ public DeckComment(String message, String actorDisplayName, Instant creationDateTime) {
setMessage(message);
setActorDisplayName(actorDisplayName);
setCreationDateTime(creationDateTime);
@@ -76,11 +77,11 @@ public class DeckComment extends AbstractRemoteEntity {
this.actorType = actorType;
}
- public Date getCreationDateTime() {
+ public Instant getCreationDateTime() {
return creationDateTime;
}
- public void setCreationDateTime(Date creationDateTime) {
+ public void setCreationDateTime(Instant creationDateTime) {
this.creationDateTime = creationDateTime;
}
@@ -121,8 +122,8 @@ public class DeckComment extends AbstractRemoteEntity {
}
public void setMessage(String message) {
- if (message!= null && message.length() > MAX_MESSAGE_LENGTH) {
- throw new IllegalArgumentException("The server won't accept messages longer than "+MAX_MESSAGE_LENGTH+" characters!");
+ if (message != null && message.length() > MAX_MESSAGE_LENGTH) {
+ throw new IllegalArgumentException("The server won't accept messages longer than " + MAX_MESSAGE_LENGTH + " characters!");
}
this.message = message;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java
new file mode 100644
index 000000000..615c7a2a2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java
@@ -0,0 +1,66 @@
+package it.niedermann.nextcloud.deck.model.ocs.projects;
+
+import androidx.annotation.NonNull;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.interfaces.AbstractJoinEntity;
+
+@Entity(
+ primaryKeys = {"projectId", "cardId"},
+ indices = {@Index("cardId"), @Index("projectId")},
+ foreignKeys = {
+ @ForeignKey(entity = OcsProject.class,
+ parentColumns = "localId",
+ childColumns = "projectId",
+ onDelete = ForeignKey.CASCADE
+ ),
+ @ForeignKey(entity = Card.class,
+ parentColumns = "localId",
+ childColumns = "cardId",
+ onDelete = ForeignKey.CASCADE
+ )
+ })
+public class JoinCardWithProject extends AbstractJoinEntity {
+ @NonNull
+ private Long projectId;
+ @NonNull
+ private Long cardId;
+
+ @NonNull
+ public Long getProjectId() {
+ return projectId;
+ }
+
+ public void setProjectId(@NonNull Long projectId) {
+ this.projectId = projectId;
+ }
+
+ public Long getCardId() {
+ return cardId;
+ }
+
+ public void setCardId(Long cardId) {
+ this.cardId = cardId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ JoinCardWithProject that = (JoinCardWithProject) o;
+
+ if (!projectId.equals(that.projectId)) return false;
+ return cardId.equals(that.cardId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = projectId.hashCode();
+ result = 31 * result + cardId.hashCode();
+ return result;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java
new file mode 100644
index 000000000..49bc7296d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java
@@ -0,0 +1,45 @@
+package it.niedermann.nextcloud.deck.model.ocs.projects;
+
+import androidx.annotation.NonNull;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.Index;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
+
+@Entity(inheritSuperIndices = true,
+ indices = {
+ @Index(value = "accountId", name = "index_project_accID"),
+ },
+ foreignKeys = {
+ }
+)
+public class OcsProject extends AbstractRemoteEntity {
+ @NonNull
+ private String name;
+
+ @Ignore
+ @NonNull
+ private ArrayList<OcsProjectResource> resources = new ArrayList<>();
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @NonNull
+ public ArrayList<OcsProjectResource> getResources() {
+ return resources;
+ }
+
+ public void setResources(@NonNull List<OcsProjectResource> resources) {
+ this.resources.clear();
+ this.resources.addAll(resources);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java
new file mode 100644
index 000000000..af42b8432
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java
@@ -0,0 +1,24 @@
+package it.niedermann.nextcloud.deck.model.ocs.projects;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class OcsProjectList {
+ List<OcsProject> projects;
+
+ public OcsProjectList() {
+ projects = new ArrayList<>();
+ }
+
+ public OcsProjectList(List<OcsProject> projects) {
+ this.projects = projects;
+ }
+
+ public List<OcsProject> getProjects() {
+ return projects;
+ }
+
+ public void add(OcsProject project) {
+ projects.add(project);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java
new file mode 100644
index 000000000..cd63ccde4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java
@@ -0,0 +1,135 @@
+package it.niedermann.nextcloud.deck.model.ocs.projects;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+import androidx.room.RoomWarnings;
+
+import java.io.Serializable;
+
+import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity;
+
+@SuppressWarnings(RoomWarnings.INDEX_FROM_PARENT_IS_DROPPED)
+@Entity(
+ indices = {
+ @Index(value = "id", name = "index_OcsProjectResource_id"),
+ @Index(value = "lastModifiedLocal", name = "index_OcsProjectResource_lastModifiedLocal"),
+ @Index(value = {"accountId", "id", "idString", "projectId"}, name = "index_OcsProjectResource_accountId_id", unique = true),
+ @Index(value = "accountId", name = "index_projectResource_accID"),
+ @Index(value = "projectId", name = "index_projectResource_projectId"),
+ },
+ foreignKeys = {
+ @ForeignKey(
+ entity = OcsProject.class,
+ parentColumns = "localId",
+ childColumns = "projectId", onDelete = ForeignKey.CASCADE
+ )
+ }
+)
+public class OcsProjectResource extends AbstractRemoteEntity implements Serializable {
+ @Nullable
+ private String type;
+ @Nullable
+ private String name;
+ @Nullable
+ private String link;
+ @Nullable
+ private String path;
+ @Nullable
+ private String iconUrl;
+ @Nullable
+ private String mimetype;
+ @Nullable
+ private Boolean previewAvailable;
+ @Nullable
+ private String idString;
+
+
+ @NonNull
+ private Long projectId;
+
+ public Long getProjectId() {
+ return projectId;
+ }
+
+ public void setProjectId(Long projectId) {
+ this.projectId = projectId;
+ }
+
+ @Nullable
+ public String getType() {
+ return type;
+ }
+
+ public void setType(@Nullable String type) {
+ this.type = type;
+ }
+
+ @Nullable
+ public String getName() {
+ return name;
+ }
+
+ public void setName(@Nullable String name) {
+ this.name = name;
+ }
+
+ /**
+ * Caution: the Link might be a full url or only the relative path!
+ * @return The link to the Resource
+ */
+ @Nullable
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(@Nullable String link) {
+ this.link = link;
+ }
+
+ @Nullable
+ public String getIconUrl() {
+ return iconUrl;
+ }
+
+ public void setIconUrl(@Nullable String iconUrl) {
+ this.iconUrl = iconUrl;
+ }
+
+ @Nullable
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(@Nullable String path) {
+ this.path = path;
+ }
+
+ @Nullable
+ public String getMimetype() {
+ return mimetype;
+ }
+
+ public void setMimetype(@Nullable String mimetype) {
+ this.mimetype = mimetype;
+ }
+
+ public Boolean getPreviewAvailable() {
+ return Boolean.TRUE.equals(previewAvailable);
+ }
+
+ public void setPreviewAvailable(@Nullable Boolean previewAvailable) {
+ this.previewAvailable = previewAvailable;
+ }
+
+ @Nullable
+ public String getIdString() {
+ return idString;
+ }
+
+ public void setIdString(@Nullable String idString) {
+ this.idString = idString;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java
new file mode 100644
index 000000000..257cf6f60
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java
@@ -0,0 +1,50 @@
+package it.niedermann.nextcloud.deck.model.ocs.projects.full;
+
+import androidx.annotation.NonNull;
+import androidx.room.Embedded;
+import androidx.room.Relation;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+
+public class OcsProjectWithResources implements IRemoteEntity {
+ @Embedded
+ public OcsProject project;
+
+
+ @Relation(entity = OcsProjectResource.class, parentColumn = "localId", entityColumn = "projectId")
+ public List<OcsProjectResource> resources;
+
+ public OcsProject getProject() {
+ return project;
+ }
+
+ public void setProject(OcsProject project) {
+ this.project = project;
+ }
+
+ @NonNull
+ public List<OcsProjectResource> getResources() {
+ return resources;
+ }
+
+ public void setResources(List<OcsProjectResource> resources) {
+ this.resources = resources;
+ }
+
+ public String getName() {
+ return project.getName();
+ }
+
+ public void setName(String name) {
+ project.setName(name);
+ }
+
+ @Override
+ public IRemoteEntity getEntity() {
+ return project;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java
new file mode 100644
index 000000000..085ca786e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java
@@ -0,0 +1,27 @@
+package it.niedermann.nextcloud.deck.model.ocs.user;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GroupMemberUIDs {
+ private List<String> uids = new ArrayList<>();
+
+ public List<String> getUids() {
+ return uids;
+ }
+
+ public void setUids(List<String> uids) {
+ this.uids = uids;
+ }
+
+ @Override
+ public String toString() {
+ return "GroupMemberUIDs{" +
+ "uids=" + uids +
+ '}';
+ }
+
+ public void add(String uid) {
+ uids.add(uid);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/OcsUserList.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/OcsUserList.java
index a10ae005d..818d5b96e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/OcsUserList.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/OcsUserList.java
@@ -1,7 +1,16 @@
package it.niedermann.nextcloud.deck.model.ocs.user;
import java.util.ArrayList;
+import java.util.List;
-public class OcsUserList extends ArrayList<String> {
- // nothing.
+public class OcsUserList {
+ private List<OcsUser> users = new ArrayList<>();
+
+ public List<OcsUser> getUsers() {
+ return users;
+ }
+
+ public void addUser(OcsUser user) {
+ this.users.add(user);
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java
new file mode 100644
index 000000000..1f167bad1
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java
@@ -0,0 +1,71 @@
+package it.niedermann.nextcloud.deck.model.relations;
+
+import androidx.annotation.NonNull;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.User;
+
+@Entity(
+ primaryKeys = {"userId", "boardId"},
+ indices = {@Index("userId"), @Index("boardId"), @Index(name = "unique_idx_user_board", value = {"userId","boardId"}, unique = true)},
+ foreignKeys = {
+ @ForeignKey(entity = User.class,
+ parentColumns = "localId",
+ childColumns = "userId", onDelete = ForeignKey.CASCADE),
+ @ForeignKey(entity = Board.class,
+ parentColumns = "localId",
+ childColumns = "boardId", onDelete = ForeignKey.CASCADE)
+ })
+public class UserInBoard {
+ @NonNull
+ private Long userId;
+ @NonNull
+ private Long boardId;
+
+ @NonNull
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(@NonNull Long userId) {
+ this.userId = userId;
+ }
+
+ @NonNull
+ public Long getBoardId() {
+ return boardId;
+ }
+
+ public void setBoardId(@NonNull Long boardId) {
+ this.boardId = boardId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ UserInBoard that = (UserInBoard) o;
+
+ if (!userId.equals(that.userId)) return false;
+ return boardId.equals(that.boardId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = userId.hashCode();
+ result = 31 * result + boardId.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "UserInGroup{" +
+ "userId=" + userId +
+ ", boardId=" + boardId +
+ '}';
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java
new file mode 100644
index 000000000..48e16cce5
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java
@@ -0,0 +1,70 @@
+package it.niedermann.nextcloud.deck.model.relations;
+
+import androidx.annotation.NonNull;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+
+import it.niedermann.nextcloud.deck.model.User;
+
+@Entity(
+ primaryKeys = {"groupId", "memberId"},
+ indices = {@Index("groupId"), @Index("memberId"), @Index(name = "unique_idx_group_member", value = {"groupId","memberId"}, unique = true)},
+ foreignKeys = {
+ @ForeignKey(entity = User.class,
+ parentColumns = "localId",
+ childColumns = "groupId", onDelete = ForeignKey.CASCADE),
+ @ForeignKey(entity = User.class,
+ parentColumns = "localId",
+ childColumns = "memberId", onDelete = ForeignKey.CASCADE)
+ })
+public class UserInGroup {
+ @NonNull
+ private Long groupId;
+ @NonNull
+ private Long memberId;
+
+ @NonNull
+ public Long getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(@NonNull Long groupId) {
+ this.groupId = groupId;
+ }
+
+ @NonNull
+ public Long getMemberId() {
+ return memberId;
+ }
+
+ public void setMemberId(@NonNull Long memberId) {
+ this.memberId = memberId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ UserInGroup that = (UserInGroup) o;
+
+ if (!groupId.equals(that.groupId)) return false;
+ return memberId.equals(that.memberId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = groupId.hashCode();
+ result = 31 * result + memberId.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "UserInGroup{" +
+ "groupId=" + groupId +
+ ", memberId=" + memberId +
+ '}';
+ }
+}
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 9ed9035cf..d93835423 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
@@ -1,31 +1,43 @@
package it.niedermann.nextcloud.deck.persistence.sync;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.database.sqlite.SQLiteConstraintException;
import androidx.annotation.AnyThread;
+import androidx.annotation.ColorInt;
+import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.Size;
import androidx.annotation.WorkerThread;
import androidx.core.util.Pair;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
+import com.nextcloud.android.sso.api.ParsedResponse;
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.Date;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.api.GsonConfig;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.api.LastSyncUtil;
+import it.niedermann.nextcloud.deck.exceptions.DeckException;
import it.niedermann.nextcloud.deck.exceptions.OfflineException;
import it.niedermann.nextcloud.deck.model.AccessControl;
import it.niedermann.nextcloud.deck.model.Account;
@@ -36,9 +48,11 @@ 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;
@@ -46,12 +60,12 @@ 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.user.OcsUser;
-import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
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.LiveDataHelper;
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;
@@ -64,20 +78,23 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.CardPropa
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.DeckCommentsDataProvider;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.LabelDataProvider;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.StackDataProvider;
-import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWitAclDownSyncDataProvider;
-import it.niedermann.nextcloud.deck.util.DateUtil;
+import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithAclDownSyncDataProvider;
+import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithStacksAndLabelsUpSyncDataProvider;
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
@SuppressWarnings("WeakerAccess")
public class SyncManager {
@NonNull
- private Context appContext;
+ private final Context appContext;
@NonNull
- private DataBaseAdapter dataBaseAdapter;
+ private final DataBaseAdapter dataBaseAdapter;
@NonNull
- private ServerAdapter serverAdapter;
+ private final ServerAdapter serverAdapter;
+
+ private static final Map<Long, List<IResponseCallback<Boolean>>> RUNNING_SYNCS = new ConcurrentHashMap<>();
@AnyThread
public SyncManager(@NonNull Context context) {
@@ -98,62 +115,35 @@ public class SyncManager {
}
@AnyThread
- public MutableLiveData<FullCard> synchronizeCardByRemoteId(long cardRemoteId, @NonNull Account account) {
- MutableLiveData<FullCard> liveData = new MutableLiveData<>();
- doAsync(() -> {
- Long accountId = account.getId();
- Card card = dataBaseAdapter.getCardByRemoteIdDirectly(accountId, cardRemoteId);
- FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId());
- // only sync this one card.
- stack.setCards(Collections.singletonList(card));
- Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId());
- new SyncHelper(serverAdapter, dataBaseAdapter, new Date()).setResponseCallback(new IResponseCallback<Boolean>(account) {
- @Override
- public void onResponse(Boolean response) {
- FullCard fullCard = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, card.getLocalId());
- liveData.postValue(fullCard);
- }
-
- @Override
- public void onError(Throwable throwable) {
- liveData.postValue(null);
- }
- }).doSyncFor(new CardDataProvider(null, board, stack));
- });
- return liveData;
- }
-
- // TODO if the card does not exist yet, try to synchronize it first, instead of directly returning null. If sync failed, return null.
- @AnyThread
public LiveData<Long> getLocalBoardIdByCardRemoteIdAndAccount(long cardRemoteId, @NonNull Account account) {
return dataBaseAdapter.getLocalBoardIdByCardRemoteIdAndAccountId(cardRemoteId, account.getId());
}
- @AnyThread
+ @WorkerThread
public boolean synchronizeEverything() {
List<Account> accounts = dataBaseAdapter.getAllAccountsDirectly();
if (accounts.size() > 0) {
- final BooleanResultHolder success = new BooleanResultHolder();
+ final AtomicBoolean success = new AtomicBoolean();
CountDownLatch latch = new CountDownLatch(accounts.size());
try {
for (Account account : accounts) {
new SyncManager(dataBaseAdapter.getContext(), account.getName()).synchronize(new IResponseCallback<Boolean>(account) {
@Override
public void onResponse(Boolean response) {
- success.result = success.result && Boolean.TRUE.equals(response);
+ success.set(success.get() && Boolean.TRUE.equals(response));
latch.countDown();
}
@Override
public void onError(Throwable throwable) {
- success.result = false;
+ success.set(false);
super.onError(throwable);
latch.countDown();
}
});
}
latch.await();
- return success.result;
+ return success.get();
} catch (InterruptedException e) {
DeckLog.logError(e);
return false;
@@ -164,76 +154,160 @@ public class SyncManager {
@AnyThread
public void synchronize(@NonNull IResponseCallback<Boolean> responseCallback) {
- if(responseCallback.getAccount() == null) {
+ synchronize(Collections.singletonList(responseCallback));
+ }
+
+ @AnyThread
+ public void synchronizeBoard(@NonNull IResponseCallback<Boolean> responseCallback, long localBoadId) {
+ doAsync(() -> {
+ FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(responseCallback.getAccount().getId(), localBoadId);
+ try {
+ new SyncHelper(serverAdapter, dataBaseAdapter, null).setResponseCallback(responseCallback).doSyncFor(new StackDataProvider(null, board));
+ } catch (OfflineException e) {
+ responseCallback.onError(e);
+ }
+ });
+ }
+
+ @AnyThread
+ public void synchronizeCard(@NonNull IResponseCallback<Boolean> responseCallback, Card card) {
+ doAsync(() -> {
+ FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId());
+ Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId());
+ try {
+ new SyncHelper(serverAdapter, dataBaseAdapter, null).setResponseCallback(responseCallback).doSyncFor(new CardDataProvider(null, board, stack));
+ } catch (OfflineException e) {
+ responseCallback.onError(e);
+ }
+ });
+ }
+
+ private void synchronize(@NonNull @Size(min = 1) List<IResponseCallback<Boolean>> responseCallbacks) {
+ if (responseCallbacks == null || responseCallbacks.size() < 1) {
+ return;
+ }
+ IResponseCallback<Boolean> responseCallback = responseCallbacks.get(0);
+ Account callbackAccount = responseCallback.getAccount();
+ if (callbackAccount == null) {
throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + IResponseCallback.class.getSimpleName() + " must not be null.");
}
- if(responseCallback.getAccount().getId() == null) {
+ Long callbackAccountId = callbackAccount.getId();
+ if (callbackAccountId == null) {
throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + IResponseCallback.class.getSimpleName() + " must contain a valid id, but given id was null.");
}
- doAsync(() -> refreshCapabilities(new IResponseCallback<Capabilities>(responseCallback.getAccount()) {
- @Override
- public void onResponse(Capabilities response) {
- if (!response.isMaintenanceEnabled()) {
- if (response.getDeckVersion().isSupported(appContext)) {
- long accountId = responseCallback.getAccount().getId();
- Date lastSyncDate = LastSyncUtil.getLastSyncDate(responseCallback.getAccount().getId());
- Date now = DateUtil.nowInGMT();
-
- final SyncHelper syncHelper = new SyncHelper(serverAdapter, dataBaseAdapter, lastSyncDate);
+ List<IResponseCallback<Boolean>> queuedCallbacks = RUNNING_SYNCS.get(callbackAccountId);
+ if (queuedCallbacks != null) {
+ queuedCallbacks.addAll(responseCallbacks);
+ return;
+ } else {
+ RUNNING_SYNCS.put(callbackAccountId, new ArrayList<>(responseCallbacks));
+ }
+ doAsync(() -> {
+ List<IResponseCallback<Boolean>> existingQueue = RUNNING_SYNCS.get(callbackAccountId);
+ List<IResponseCallback<Boolean>> callbacksQueueForSync = existingQueue == null ? new ArrayList<>() : new ArrayList<>(existingQueue);
+ refreshCapabilities(new IResponseCallback<Capabilities>(responseCallback.getAccount()) {
+ @Override
+ public void onResponse(Capabilities response) {
+ if (response != null && !response.isMaintenanceEnabled()) {
+ if (response.getDeckVersion().isSupported(appContext)) {
+ long accountId = callbackAccountId;
+ Instant lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId);
- IResponseCallback<Boolean> callback = new IResponseCallback<Boolean>(responseCallback.getAccount()) {
- @Override
- public void onResponse(Boolean response) {
- syncHelper.setResponseCallback(new IResponseCallback<Boolean>(account) {
- @Override
- public void onResponse(Boolean response) {
- // TODO deactivate for dev
- LastSyncUtil.setLastSyncDate(accountId, now);
- responseCallback.onResponse(response);
- }
+ final SyncHelper syncHelper = new SyncHelper(serverAdapter, dataBaseAdapter, lastSyncDate);
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- responseCallback.onError(throwable);
- }
- });
- doAsync(() -> {
- try {
- syncHelper.doUpSyncFor(new BoardDataProvider());
- } catch (Throwable e) {
- DeckLog.logError(e);
- responseCallback.onError(e);
- }
- });
+ IResponseCallback<Boolean> callback = new IResponseCallback<Boolean>(callbackAccount) {
+ @Override
+ public void onResponse(Boolean response) {
+ syncHelper.setResponseCallback(new IResponseCallback<Boolean>(account) {
+ @Override
+ public void onResponse(Boolean response) {
+ // TODO deactivate for dev
+ LastSyncUtil.setLastSyncDate(accountId, Instant.now());
+ respondCallbacksAfterSync(callbacksQueueForSync, response, null);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ respondCallbacksAfterSync(callbacksQueueForSync, null, throwable);
+ }
+ });
+ doAsync(() -> {
+ try {
+ syncHelper.doUpSyncFor(new BoardDataProvider());
+ } catch (Throwable e) {
+ DeckLog.logError(e);
+ respondCallbacksAfterSync(callbacksQueueForSync, null, e);
+ }
+ });
- }
+ }
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- responseCallback.onError(throwable);
- }
- };
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ respondCallbacksAfterSync(callbacksQueueForSync, null, throwable);
+ }
+ };
- syncHelper.setResponseCallback(callback);
+ syncHelper.setResponseCallback(callback);
- try {
- syncHelper.doSyncFor(new BoardDataProvider());
- } catch (Throwable e) {
- DeckLog.logError(e);
- responseCallback.onError(e);
+ try {
+ syncHelper.doSyncFor(new BoardDataProvider());
+ } catch (Throwable e) {
+ DeckLog.logError(e);
+ respondCallbacksAfterSync(callbacksQueueForSync, null, e);
+ }
+ } else {
+ respondCallbacksAfterSync(callbacksQueueForSync, Boolean.FALSE, null);
+ DeckLog.warn("No sync. Server version not supported: " + response.getDeckVersion().getOriginalVersion());
}
} else {
- responseCallback.onResponse(false);
- DeckLog.warn("No sync. Server version not supported: " + response.getDeckVersion().getOriginalVersion());
+ respondCallbacksAfterSync(callbacksQueueForSync, Boolean.FALSE, null);
+ if (response != null) {
+ DeckLog.warn("No sync. Status maintenance mode: " + response.isMaintenanceEnabled());
+ }
}
- } else {
- responseCallback.onResponse(false);
- DeckLog.warn("No sync. Status maintenance mode: " + response.isMaintenanceEnabled());
}
+ });
+ });
+ }
+
+ private void respondCallbacksAfterSync(List<IResponseCallback<Boolean>> callbacksQueueForSync, Boolean response, Throwable throwable) {
+ if (callbacksQueueForSync == null || callbacksQueueForSync.isEmpty()) {
+ return;
+ }
+ // notify done callbacks
+ DeckLog.info("SyncQueue: responding sync for " + callbacksQueueForSync.size() + " queued callbacks!");
+ List<IResponseCallback<Boolean>> callbacksQueue = new ArrayList<>(callbacksQueueForSync);
+ if (throwable == null) {
+ //success:
+ for (IResponseCallback<Boolean> callback : callbacksQueue) {
+ if (callback != null) callback.onResponse(response);
}
- }));
+ } else {
+ // failure:
+ for (IResponseCallback<Boolean> callback : callbacksQueue) {
+ if (callback != null) callback.onError(throwable);
+ }
+ }
+ // remove done callbacks from queue
+ IResponseCallback<Boolean> firstCallbackOfAccount = callbacksQueue.iterator().next();
+ List<IResponseCallback<Boolean>> queuedCallbacks = RUNNING_SYNCS.get(firstCallbackOfAccount.getAccount().getId());
+ if (queuedCallbacks == null) {
+ return;
+ }
+ for (IResponseCallback<Boolean> callback : callbacksQueue) {
+ queuedCallbacks.remove(callback);
+ }
+ // cleanup if done, or proceed if not
+ if (queuedCallbacks.isEmpty()) {
+ RUNNING_SYNCS.remove(firstCallbackOfAccount.getAccount().getId());
+ } else {
+ DeckLog.info("SyncQueue: starting sync for " + queuedCallbacks.size() + " queued callbacks!");
+ RUNNING_SYNCS.remove(firstCallbackOfAccount.getAccount().getId());
+ synchronize(queuedCallbacks);
+ }
}
//
@@ -274,14 +348,15 @@ public class SyncManager {
}
@AnyThread
- public WrappedLiveData<Account> createAccount(@NonNull Account accout) {
- return dataBaseAdapter.createAccount(accout);
+ public WrappedLiveData<Account> createAccount(@NonNull Account account) {
+ return dataBaseAdapter.createAccount(account);
}
public boolean hasInternetConnection() {
return serverAdapter.hasInternetConnection();
}
+ @AnyThread
public void deleteAccount(long id) {
doAsync(() -> {
dataBaseAdapter.deleteAccount(id);
@@ -289,10 +364,6 @@ public class SyncManager {
});
}
- public void updateAccount(Account account) {
- dataBaseAdapter.updateAccount(account);
- }
-
@AnyThread
public LiveData<Account> readAccount(long id) {
return dataBaseAdapter.readAccount(id);
@@ -320,7 +391,7 @@ public class SyncManager {
* - located at the given {@param host}
* - and have the permission to read the board with the given {@param boardRemoteId} (aka the {@link Board} is shared with this {@link User}).
*/
- @AnyThread
+ @MainThread
public LiveData<List<Account>> readAccountsForHostWithReadAccessToBoard(String host, long boardRemoteId) {
MediatorLiveData<List<Account>> liveData = new MediatorLiveData<>();
liveData.addSource(dataBaseAdapter.readAccountsForHostWithReadAccessToBoard(host, boardRemoteId), accounts -> {
@@ -333,7 +404,7 @@ public class SyncManager {
public void onResponse(Boolean response) {
liveData.postValue(dataBaseAdapter.readAccountsForHostWithReadAccessToBoardDirectly(host, boardRemoteId));
}
- }).doSyncFor(new BoardWitAclDownSyncDataProvider());
+ }).doSyncFor(new BoardWithAclDownSyncDataProvider());
}
});
});
@@ -345,15 +416,17 @@ public class SyncManager {
public void refreshCapabilities(@NonNull IResponseCallback<Capabilities> callback) {
doAsync(() -> {
try {
- serverAdapter.getCapabilities(new IResponseCallback<Capabilities>(callback.getAccount()) {
+ Account accountForEtag = dataBaseAdapter.getAccountByIdDirectly(callback.getAccount().getId());
+ serverAdapter.getCapabilities(accountForEtag.getEtag(), new IResponseCallback<ParsedResponse<Capabilities>>(callback.getAccount()) {
@Override
- public void onResponse(Capabilities response) {
+ public void onResponse(ParsedResponse<Capabilities> response) {
Account acc = dataBaseAdapter.getAccountByIdDirectly(account.getId());
- acc.applyCapabilities(response);
+ acc.applyCapabilities(response.getResponse(), response.getHeaders().get("ETag"));
dataBaseAdapter.updateAccount(acc);
- callback.onResponse(response);
+ callback.onResponse(response.getResponse());
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
if (throwable instanceof NextcloudHttpRequestFailedException) {
@@ -361,13 +434,28 @@ public class SyncManager {
if (requestFailedException.getStatusCode() == HTTP_UNAVAILABLE && requestFailedException.getCause() != null) {
String errorString = requestFailedException.getCause().getMessage();
Capabilities capabilities = GsonConfig.getGson().fromJson(errorString, Capabilities.class);
+ DeckLog.verbose("HTTP Status " + HTTP_UNAVAILABLE + ": This server seems to be in maintenance mode.");
if (capabilities.isMaintenanceEnabled()) {
- doAsync(() -> {
- onResponse(capabilities);
- });
+ doAsync(() -> onResponse(ParsedResponse.of(capabilities)));
} else {
onError(throwable);
}
+ } else if (requestFailedException.getStatusCode() == HTTP_NOT_MODIFIED) {
+ DeckLog.verbose("HTTP Status " + HTTP_NOT_MODIFIED + ": There haven't been any changes on the server side for this request.");
+ //could be after maintenance. so we have to at least revert the maintenance flag
+ doAsync(() -> {
+ Account acc = dataBaseAdapter.getAccountByIdDirectly(account.getId());
+ if (acc.isMaintenanceEnabled()) {
+ acc.setMaintenanceEnabled(false);
+ dataBaseAdapter.updateAccount(acc);
+ }
+ Capabilities capabilities = new Capabilities();
+ capabilities.setMaintenanceEnabled(false);
+ capabilities.setDeckVersion(acc.getServerDeckVersionAsObject());
+ capabilities.setTextColor(acc.getTextColor());
+ capabilities.setColor(acc.getColor());
+ callback.onResponse(capabilities);
+ });
}
} else {
callback.onError(throwable);
@@ -377,39 +465,30 @@ public class SyncManager {
} catch (OfflineException e) {
callback.onError(e);
}
-
- try {
- serverAdapter.getAllOcsUsers(new IResponseCallback<OcsUserList>(callback.getAccount()) {
- @Override
- public void onResponse(OcsUserList response) {
- Long accountId = callback.getAccount().getId();
- for (String ocsUserName : response) {
- User existingUser = dataBaseAdapter.getUserByUidDirectly(accountId, ocsUserName);
- if (existingUser == null) {
- // we don't know this user, lets get some details...
- serverAdapter.getOcsUserDetails(ocsUserName, new IResponseCallback<OcsUser>(callback.getAccount()) {
- @Override
- public void onResponse(OcsUser response) {
- User newUser = new User();
- newUser.setStatus(DBStatus.UP_TO_DATE.getId());
- newUser.setPrimaryKey(ocsUserName);
- newUser.setUid(ocsUserName);
- newUser.setDisplayname(response.getDisplayName());
- dataBaseAdapter.createUser(accountId, newUser);
- }
- });
- }
- }
- }
- });
- } catch (OfflineException ignored) {
- // Nothing to do here...
- }
});
}
/**
* @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
*/
@@ -445,8 +524,8 @@ public class SyncManager {
}
@AnyThread
- public LiveData<FullBoard> createBoard(long accountId, @NonNull Board board) {
- MutableLiveData<FullBoard> liveData = new MutableLiveData<>();
+ public WrappedLiveData<FullBoard> createBoard(long accountId, @NonNull Board board) {
+ WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>();
doAsync(() -> {
Account account = dataBaseAdapter.getAccountByIdDirectly(accountId);
User owner = dataBaseAdapter.getUserByUidDirectly(accountId, account.getUserName());
@@ -465,6 +544,12 @@ public class SyncManager {
public void onResponse(FullBoard response) {
liveData.postValue(response);
}
+
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void onError(Throwable throwable, FullBoard entity) {
+ liveData.postError(throwable, entity);
+ }
});
}
});
@@ -474,58 +559,145 @@ public class SyncManager {
/**
* Creates a new {@link Board} and adds the same {@link Label} and {@link Stack} as in the origin {@link Board}.
* Owner of the target {@link Board} will be the {@link User} with the {@link Account} of {@param targetAccountId}.
- * Does <strong>not</strong> clone any {@link Card} or {@link AccessControl} from the origin {@link Board}.
+ *
+ * @param cloneCards determines whether or not the cards in this {@link Board} shall be cloned or not
+ * Does <strong>not</strong> clone any {@link Card} or {@link AccessControl} from the origin {@link Board}.
+ * <p>
+ * TODO implement https://github.com/stefan-niedermann/nextcloud-deck/issues/608
*/
@AnyThread
- public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, String targetBoardTitle, String targetBoardColor) {
- WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>();
+ public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards) {
+ final WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>();
doAsync(() -> {
Account originAccount = dataBaseAdapter.getAccountByIdDirectly(originAccountId);
User newOwner = dataBaseAdapter.getUserByUidDirectly(originAccountId, originAccount.getUserName());
+ if (newOwner == null) {
+ liveData.postError(new DeckException(DeckException.Hint.UNKNOWN_ACCOUNT_USER_ID, "User with Account-UID \"" + originAccount.getUserName() + "\" not found."));
+ return;
+ }
FullBoard originalBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(originAccountId, originBoardLocalId);
+ String newBoardTitleBaseName = originalBoard.getBoard().getTitle().trim();
+ int newBoardTitleCopyIndex = 0;
+ //already a copy?
+ String regex = " \\(copy [0-9]+\\)$";
+ Pattern pattern = Pattern.compile(regex);
+ Matcher matcher = pattern.matcher(originalBoard.getBoard().getTitle());
+ if (matcher.find()) {
+ String found = matcher.group();
+ newBoardTitleBaseName = newBoardTitleBaseName.substring(0, newBoardTitleBaseName.length() - found.length());
+ Matcher indexMatcher = Pattern.compile("[0-9]+").matcher(found);
+ //noinspection ResultOfMethodCallIgnored
+ indexMatcher.find();
+ String oldIndexString = indexMatcher.group();
+ newBoardTitleCopyIndex = Integer.parseInt(oldIndexString);
+ }
+
+ String newBoardTitle;
+ do {
+ newBoardTitleCopyIndex++;
+ newBoardTitle = newBoardTitleBaseName + " (copy " + newBoardTitleCopyIndex + ")";
+
+ } while (dataBaseAdapter.getBoardForAccountByNameDirectly(targetAccountId, newBoardTitle) != null);
+
+
originalBoard.setAccountId(targetAccountId);
- originalBoard.getBoard().setTitle(targetBoardTitle);
- originalBoard.getBoard().setColor(targetBoardColor);
- originalBoard.getBoard().setOwnerId(newOwner.getId());
- originalBoard.setStatusEnum(DBStatus.LOCAL_EDITED);
- originalBoard.setOwner(newOwner);
originalBoard.setId(null);
originalBoard.setLocalId(null);
+ originalBoard.getBoard().setTitle(newBoardTitle);
+ originalBoard.getBoard().setColor(String.format("%06X", 0xFFFFFF & targetBoardColor));
+ originalBoard.getBoard().setOwnerId(newOwner.getLocalId());
+ originalBoard.setStatusEnum(DBStatus.LOCAL_EDITED);
+ originalBoard.setOwner(newOwner);
long newBoardId = dataBaseAdapter.createBoardDirectly(originAccountId, originalBoard.getBoard());
originalBoard.setLocalId(newBoardId);
- for (Stack stack : originalBoard.getStacks()) {
- stack.setLocalId(null);
- stack.setId(null);
- stack.setStatusEnum(DBStatus.LOCAL_EDITED);
- stack.setAccountId(targetAccountId);
- stack.setBoardId(newBoardId);
- dataBaseAdapter.createStack(targetAccountId, stack);
+ boolean isSameAccount = targetAccountId == originAccountId;
+
+ if (isSameAccount) {
+ List<AccessControl> aclList = originalBoard.getParticipants();
+ for (AccessControl acl : aclList) {
+ acl.setLocalId(null);
+ acl.setId(null);
+ acl.setBoardId(newBoardId);
+ dataBaseAdapter.createAccessControl(targetAccountId, acl);
+ }
}
+
+ Map<Long, Long> oldToNewLabelIdsDictionary = new HashMap<>();
+
for (Label label : originalBoard.getLabels()) {
+ Long oldLocalId = label.getLocalId();
label.setLocalId(null);
label.setId(null);
label.setAccountId(targetAccountId);
label.setStatusEnum(DBStatus.LOCAL_EDITED);
label.setBoardId(newBoardId);
- dataBaseAdapter.createLabel(targetAccountId, label);
+ long newLocalId = dataBaseAdapter.createLabelDirectly(targetAccountId, label);
+ oldToNewLabelIdsDictionary.put(oldLocalId, newLocalId);
}
- Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId);
- new SyncHelper(serverAdapter, dataBaseAdapter, null)
- .setResponseCallback(new IResponseCallback<Boolean>(targetAccount) {
- @Override
- public void onResponse(Boolean response) {
- liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId));
- }
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- liveData.postError(throwable);
+ List<Stack> oldStacks = originalBoard.getStacks();
+ for (Stack stack : oldStacks) {
+ Long oldStackId = stack.getLocalId();
+ stack.setLocalId(null);
+ stack.setId(null);
+ stack.setStatusEnum(DBStatus.LOCAL_EDITED);
+ stack.setAccountId(targetAccountId);
+ stack.setBoardId(newBoardId);
+ long createdStackId = dataBaseAdapter.createStack(targetAccountId, stack);
+ if (cloneCards) {
+ List<FullCard> oldCards = dataBaseAdapter.getFullCardsForStackDirectly(originAccountId, oldStackId, null);
+ for (FullCard oldCard : oldCards) {
+ Card newCard = oldCard.getCard();
+ newCard.setId(null);
+ newCard.setUserId(newOwner.getLocalId());
+ newCard.setLocalId(null);
+ newCard.setStackId(createdStackId);
+ newCard.setAccountId(targetAccountId);
+ newCard.setStatusEnum(DBStatus.LOCAL_EDITED);
+ long createdCardId = dataBaseAdapter.createCardDirectly(targetAccountId, newCard);
+ if (oldCard.getLabels() != null) {
+ for (Label oldLabel : oldCard.getLabels()) {
+ Long newLabelId = oldToNewLabelIdsDictionary.get(oldLabel.getLocalId());
+ if (newLabelId != null) {
+ dataBaseAdapter.createJoinCardWithLabel(newLabelId, createdCardId, DBStatus.LOCAL_EDITED);
+ } else
+ DeckLog.error("ID of created Label is null! Skipping assignment of \"" + oldLabel.getTitle() + "\"...");
+ }
+ }
+ if (isSameAccount && oldCard.getAssignedUsers() != null) {
+ for (User assignedUser : oldCard.getAssignedUsers()) {
+ dataBaseAdapter.createJoinCardWithUser(assignedUser.getLocalId(), createdCardId, DBStatus.LOCAL_EDITED);
+ }
}
- }).doSyncFor(new BoardDataProvider());
+ }
+ }
+ }
+ // dont trigger concurrent syncs!
+ List<IResponseCallback<Boolean>> queuedSync = RUNNING_SYNCS.get(targetAccountId);
+ if ((queuedSync == null || queuedSync.isEmpty()) && serverAdapter.hasInternetConnection()) {
+ Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId);
+ ServerAdapter serverAdapterToUse = this.serverAdapter;
+ if (originAccountId != targetAccountId) {
+ serverAdapterToUse = new ServerAdapter(appContext, targetAccount.getName());
+ }
+ new SyncHelper(serverAdapterToUse, dataBaseAdapter, null)
+ .setResponseCallback(new IResponseCallback<Boolean>(targetAccount) {
+ @Override
+ public void onResponse(Boolean response) {
+ liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId));
+ }
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ liveData.postError(throwable);
+ }
+ }).doUpSyncFor(new BoardWithStacksAndLabelsUpSyncDataProvider(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)));
+ } else {
+ liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId));
+ }
});
return liveData;
}
@@ -624,6 +796,7 @@ public class SyncManager {
liveData.postValue(response);
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
liveData.postError(throwable);
@@ -633,8 +806,8 @@ public class SyncManager {
return liveData;
}
- public LiveData<List<FullStack>> getStacksForBoard(long accountId, long localBoardId) {
- return dataBaseAdapter.getFullStacksForBoard(accountId, localBoardId);
+ public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
+ return dataBaseAdapter.getStacksForBoard(accountId, localBoardId);
}
public LiveData<FullStack> getStack(long accountId, long localStackId) {
@@ -651,8 +824,7 @@ public class SyncManager {
new AccessControlDataProvider(null, board, Collections.singletonList(entity)), entity, getCallbackToLiveDataConverter(account, liveData), ((entity1, response) -> {
response.setBoardId(entity.getBoardId());
response.setUserId(entity.getUser().getLocalId());
- }
- )
+ })
);
});
return liveData;
@@ -687,6 +859,7 @@ public class SyncManager {
liveData.postValue(response);
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
liveData.postError(throwable);
@@ -711,6 +884,7 @@ public class SyncManager {
liveData.postValue(response);
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
liveData.postError(throwable);
@@ -725,18 +899,30 @@ public class SyncManager {
}
@AnyThread
- public WrappedLiveData<FullStack> createStack(long accountId, @NonNull Stack stack) {
+ public WrappedLiveData<FullStack> createStack(long accountId, @NonNull String title, long boardLocalId) {
WrappedLiveData<FullStack> liveData = new WrappedLiveData<>();
doAsync(() -> {
+ Stack stack = new Stack(title, boardLocalId);
Account account = dataBaseAdapter.getAccountByIdDirectly(accountId);
FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId());
FullStack fullStack = new FullStack();
- // TODO set stack order to (highest stack-order from board) + 1 and remove logic from caller
+ stack.setOrder(dataBaseAdapter.getHighestStackOrderInBoard(stack.getBoardId()) + 1);
stack.setAccountId(accountId);
stack.setBoardId(board.getLocalId());
fullStack.setStack(stack);
fullStack.setAccountId(accountId);
- new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new StackDataProvider(null, board), fullStack, getCallbackToLiveDataConverter(account, liveData));
+ new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new StackDataProvider(null, board), fullStack, new IResponseCallback<FullStack>(account) {
+ @Override
+ public void onResponse(FullStack response) {
+ liveData.postValue(response);
+ }
+
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void onError(Throwable throwable, FullStack entity) {
+ liveData.postError(throwable, entity);
+ }
+ });
});
return liveData;
}
@@ -754,36 +940,36 @@ public class SyncManager {
}
@AnyThread
- public WrappedLiveData<FullStack> updateStack(@NonNull FullStack stack) {
+ public WrappedLiveData<FullStack> updateStackTitle(long localStackId, @NonNull String newTitle) {
WrappedLiveData<FullStack> liveData = new WrappedLiveData<>();
doAsync(() -> {
+ FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(localStackId);
+ FullBoard fullBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(stack.getAccountId(), stack.getStack().getBoardId());
Account account = dataBaseAdapter.getAccountByIdDirectly(stack.getAccountId());
- FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(stack.getAccountId(), stack.getStack().getBoardId());
- updateStack(account, board, stack, liveData);
+ stack.getStack().setTitle(newTitle);
+ updateStack(account, fullBoard, stack, liveData);
});
return liveData;
-
}
@AnyThread
private void updateStack(@NonNull Account account, @NonNull FullBoard board, @NonNull FullStack stack, @Nullable WrappedLiveData<FullStack> liveData) {
- doAsync(() -> {
- new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new StackDataProvider(null, board), stack, new IResponseCallback<FullStack>(account) {
- @Override
- public void onResponse(FullStack response) {
- if (liveData != null) {
- liveData.postValue(response);
- }
+ doAsync(() -> new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new StackDataProvider(null, board), stack, new IResponseCallback<FullStack>(account) {
+ @Override
+ public void onResponse(FullStack response) {
+ if (liveData != null) {
+ liveData.postValue(response);
}
+ }
- @Override
- public void onError(Throwable throwable) {
- if (liveData != null) {
- liveData.postError(throwable);
- }
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void onError(Throwable throwable) {
+ if (liveData != null) {
+ liveData.postError(throwable);
}
- });
- });
+ }
+ }));
}
/**
@@ -813,8 +999,8 @@ public class SyncManager {
});
}
- public LiveData<FullCard> getCardByLocalId(long accountId, long cardLocalId) {
- return dataBaseAdapter.getCardByLocalId(accountId, cardLocalId);
+ 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) {
@@ -863,8 +1049,8 @@ public class SyncManager {
// }
@AnyThread
- public LiveData<FullCard> createFullCard(long accountId, long localBoardId, long localStackId, @NonNull FullCard card) {
- MutableLiveData<FullCard> liveData = new MutableLiveData<>();
+ public WrappedLiveData<FullCard> createFullCard(long accountId, long localBoardId, long localStackId, @NonNull FullCard card) {
+ WrappedLiveData<FullCard> liveData = new WrappedLiveData<>();
doAsync(() -> {
Account account = dataBaseAdapter.getAccountByIdDirectly(accountId);
User owner = dataBaseAdapter.getUserByUidDirectly(accountId, account.getUserName());
@@ -875,7 +1061,7 @@ public class SyncManager {
card.getCard().setAccountId(accountId);
card.getCard().setStatusEnum(DBStatus.LOCAL_EDITED);
card.getCard().setOrder(dataBaseAdapter.getHighestCardOrderInStack(localStackId) + 1);
- long localCardId = dataBaseAdapter.createCard(accountId, card.getCard());
+ long localCardId = dataBaseAdapter.createCardDirectly(accountId, card.getCard());
card.getCard().setLocalId(localCardId);
List<User> assignedUsers = card.getAssignedUsers();
@@ -901,11 +1087,28 @@ public class SyncManager {
}
}
- liveData.postValue(card);
+
if (serverAdapter.hasInternetConnection()) {
new SyncHelper(serverAdapter, dataBaseAdapter, null)
- .setResponseCallback(IResponseCallback.getDefaultResponseCallback(account))
+ .setResponseCallback(new IResponseCallback<Boolean>(account) {
+ @Override
+ public void onResponse(Boolean response) {
+ liveData.postValue(card);
+ }
+
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void onError(Throwable throwable) {
+ if (throwable.getClass() == DeckException.class && ((DeckException)throwable).getHint().equals(DeckException.Hint.DEPENDENCY_NOT_SYNCED_YET)) {
+ liveData.postValue(card);
+ } else {
+ liveData.postError(throwable);
+ }
+ }
+ })
.doUpSyncFor(new CardDataProvider(null, board, stack));
+ } else {
+ liveData.postValue(card);
}
});
return liveData;
@@ -917,7 +1120,7 @@ public class SyncManager {
doAsync(() -> {
FullCard fullCard = dataBaseAdapter.getFullCardByLocalIdDirectly(card.getAccountId(), card.getLocalId());
if (fullCard == null) {
- throw new IllegalArgumentException("card to delete does not exist.");
+ throw new IllegalArgumentException("card with id " + card.getLocalId() + " to delete does not exist.");
}
Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId());
FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId());
@@ -935,12 +1138,12 @@ public class SyncManager {
FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId());
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId());
card.getCard().setArchived(true);
- updateCardForArchive(account, stack, board, card, getCallbackToLiveDataConverter(account, liveData));
+ updateCardForArchive(stack, board, card, getCallbackToLiveDataConverter(account, liveData));
});
return liveData;
}
- private void updateCardForArchive(Account account, FullStack stack, Board board, FullCard card, @NonNull IResponseCallback<FullCard> callback) {
+ private void updateCardForArchive(FullStack stack, Board board, FullCard card, @NonNull IResponseCallback<FullCard> callback) {
new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new CardDataProvider(null, board, stack), card, callback);
}
@@ -952,29 +1155,34 @@ public class SyncManager {
FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId());
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId());
card.getCard().setArchived(false);
- updateCardForArchive(account, stack, board, card, getCallbackToLiveDataConverter(account, liveData));
+ updateCardForArchive(stack, board, card, getCallbackToLiveDataConverter(account, liveData));
});
return liveData;
}
@AnyThread
- public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId) {
+ public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation) {
WrappedLiveData<Void> liveData = new WrappedLiveData<>();
doAsync(() -> {
Account account = dataBaseAdapter.getAccountByIdDirectly(accountId);
FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackLocalId);
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId());
- List<FullCard> cards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stackLocalId);
+ List<FullCard> cards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stackLocalId, filterInformation);
if (cards.size() > 0) {
CountDownLatch latch = new CountDownLatch(cards.size());
for (FullCard card : cards) {
+ if (card.getCard().isArchived()) {
+ latch.countDown();
+ continue;
+ }
card.getCard().setArchived(true);
- updateCardForArchive(account, stack, board, card, new IResponseCallback<FullCard>(account) {
+ updateCardForArchive(stack, board, card, new IResponseCallback<FullCard>(account) {
@Override
public void onResponse(FullCard response) {
latch.countDown();
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
latch.countDown();
@@ -996,21 +1204,35 @@ public class SyncManager {
}
@AnyThread
- public void archiveBoard(@NonNull Board board) {
+ public WrappedLiveData<FullBoard> archiveBoard(@NonNull Board board) {
+ WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>();
doAsync(() -> {
- FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId());
- b.getBoard().setArchived(true);
- updateBoard(b);
+ try {
+ FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId());
+ b.getBoard().setArchived(true);
+ updateBoard(b);
+ liveData.postValue(b);
+ } catch (Throwable e) {
+ liveData.postError(e);
+ }
});
+ return liveData;
}
@AnyThread
- public void dearchiveBoard(@NonNull Board board) {
+ public WrappedLiveData<FullBoard> dearchiveBoard(@NonNull Board board) {
+ WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>();
doAsync(() -> {
- FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId());
- b.getBoard().setArchived(false);
- updateBoard(b);
+ try {
+ FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId());
+ b.getBoard().setArchived(false);
+ updateBoard(b);
+ liveData.postValue(b);
+ } catch (Throwable e) {
+ liveData.postError(e);
+ }
});
+ return liveData;
}
@AnyThread
@@ -1055,6 +1277,7 @@ public class SyncManager {
liveData.postValue(dataBaseAdapter.getFullCardByLocalIdDirectly(card.getAccountId(), card.getLocalId()));
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
liveData.postError(throwable);
@@ -1092,39 +1315,44 @@ public class SyncManager {
public WrappedLiveData<Void> moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) {
return LiveDataHelper.wrapInLiveData(() -> {
- FullCard originalCard = dataBaseAdapter.getFullCardByLocalIdDirectly(originAccountId, originCardLocalId);
+ final FullCard originalCard = dataBaseAdapter.getFullCardByLocalIdDirectly(originAccountId, originCardLocalId);
int newIndex = dataBaseAdapter.getHighestCardOrderInStack(targetStackLocalId) + 1;
- FullBoard originalBoard = dataBaseAdapter.getFullBoardByLocalCardIdDirectly(originCardLocalId);
+ final FullBoard originalBoard = dataBaseAdapter.getFullBoardByLocalCardIdDirectly(originCardLocalId);
// ### maybe shortcut possible? (just moved to another stack)
if (targetBoardLocalId == originalBoard.getLocalId()) {
reorder(originAccountId, originalCard, targetStackLocalId, newIndex);
return null;
}
// ### get rid of original card where it is now.
- Card originalInnerCard = originalCard.getCard();
- deleteCard(originalInnerCard);
+ final Card originalInnerCard = originalCard.getCard();
+ deleteCard(new Card(originalInnerCard));
// ### clone card itself
- Card targetCard = originalInnerCard;
- targetCard.setAccountId(targetAccountId);
- targetCard.setId(null);
- targetCard.setLocalId(null);
- targetCard.setStatusEnum(DBStatus.LOCAL_EDITED);
- targetCard.setStackId(targetStackLocalId);
- targetCard.setOrder(newIndex);
- //TODO: this needs to propagate to server as well, since anything else propagates as well (otherwise card isn't known on server)
- FullCard fullCardForServerPropagation = new FullCard();
- fullCardForServerPropagation.setCard(targetCard);
-
- Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId);
- FullBoard targetBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, targetBoardLocalId);
- FullStack targetFullStack = dataBaseAdapter.getFullStackByLocalIdDirectly(targetStackLocalId);
- User userOfTargetAccount = dataBaseAdapter.getUserByUidDirectly(targetAccountId, targetAccount.getUserName());
- CountDownLatch latch = new CountDownLatch(1);
- new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new CardPropagationDataProvider(null, targetBoard.getBoard(), targetFullStack), fullCardForServerPropagation, new IResponseCallback<FullCard>(targetAccount) {
+ originalInnerCard.setAccountId(targetAccountId);
+ originalInnerCard.setId(null);
+ originalInnerCard.setLocalId(null);
+ originalInnerCard.setStatusEnum(DBStatus.LOCAL_EDITED);
+ originalInnerCard.setStackId(targetStackLocalId);
+ originalInnerCard.setOrder(newIndex);
+ originalInnerCard.setArchived(false);
+ originalInnerCard.setAttachmentCount(0);
+ originalInnerCard.setCommentsUnread(0);
+ final FullCard fullCardForServerPropagation = new FullCard();
+ fullCardForServerPropagation.setCard(originalInnerCard);
+
+ final Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId);
+ final FullBoard targetBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, targetBoardLocalId);
+ final FullStack targetFullStack = dataBaseAdapter.getFullStackByLocalIdDirectly(targetStackLocalId);
+ final User userOfTargetAccount = dataBaseAdapter.getUserByUidDirectly(targetAccountId, targetAccount.getUserName());
+ final CountDownLatch latch = new CountDownLatch(1);
+ ServerAdapter serverToUse = serverAdapter;
+ if (originAccountId != targetAccountId) {
+ serverToUse = new ServerAdapter(appContext, targetAccount.getName());
+ }
+ new DataPropagationHelper(serverToUse, dataBaseAdapter).createEntity(new CardPropagationDataProvider(null, targetBoard.getBoard(), targetFullStack), fullCardForServerPropagation, new IResponseCallback<FullCard>(targetAccount) {
@Override
public void onResponse(FullCard response) {
- targetCard.setId(response.getId());
- targetCard.setLocalId(response.getLocalId());
+ originalInnerCard.setId(response.getId());
+ originalInnerCard.setLocalId(response.getLocalId());
latch.countDown();
}
@@ -1134,8 +1362,10 @@ public class SyncManager {
throw new RuntimeException("unable to create card in moveCard target", throwable);
}
}, (FullCard entity, FullCard response) -> {
- response.getCard().setUserId(entity.getCard().getUserId());
+ response.getCard().setUserId(userOfTargetAccount.getLocalId());
response.getCard().setStackId(targetFullStack.getLocalId());
+ entity.getCard().setUserId(userOfTargetAccount.getLocalId());
+ entity.getCard().setStackId(targetFullStack.getLocalId());
});
try {
@@ -1145,7 +1375,7 @@ public class SyncManager {
throw new RuntimeException("error fulfilling countDownLatch", e);
}
- long newCardId = targetCard.getLocalId();
+ long newCardId = originalInnerCard.getLocalId();
// ### clone labels, assign them
// prepare
@@ -1154,7 +1384,7 @@ public class SyncManager {
List<AccessControl> aclOfTargetBoard = dataBaseAdapter.getAccessControlByLocalBoardIdDirectly(targetAccountId, targetBoard.getLocalId());
if (!hasManagePermission) {
for (AccessControl accessControl : aclOfTargetBoard) {
- if (accessControl.getUserId() == userOfTargetAccount.getLocalId() && accessControl.isPermissionManage()) {
+ if (accessControl.getUserId().equals(userOfTargetAccount.getLocalId()) && accessControl.isPermissionManage()) {
hasManagePermission = true;
break;
}
@@ -1178,10 +1408,10 @@ public class SyncManager {
originalLabel.setLocalId(null);
originalLabel.setStatusEnum(DBStatus.LOCAL_EDITED);
originalLabel.setAccountId(targetBoard.getAccountId());
- createAndAssignLabelToCard(originalBoard.getAccountId(), originalLabel, newCardId);
+ createAndAssignLabelToCard(targetBoard.getAccountId(), originalLabel, newCardId, serverToUse);
}
} else {
- assignLabelToCard(existingMatch, targetCard);
+ assignLabelToCard(existingMatch, originalInnerCard, serverToUse);
}
}
@@ -1194,7 +1424,7 @@ public class SyncManager {
boolean hasViewPermission = targetBoard.getBoard().getOwnerId() == assignedUser.getLocalId();
if (!hasViewPermission) {
for (AccessControl accessControl : aclOfTargetBoard) {
- if (accessControl.getUserId() == userOfTargetAccount.getLocalId()) {
+ if (accessControl.getUserId().equals(userOfTargetAccount.getLocalId())) {
// ACL exists, so viewing is granted
hasViewPermission = true;
break;
@@ -1202,7 +1432,7 @@ public class SyncManager {
}
}
if (hasViewPermission) {
- assignUserToCard(assignedUser, targetCard);
+ assignUserToCard(assignedUser, originalInnerCard);
}
}
}
@@ -1229,25 +1459,28 @@ public class SyncManager {
liveData.postValue(response);
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
liveData.postError(throwable);
}
- }, (entity, response) -> {
- response.setBoardId(board.getLocalId());
- });
+ }, (entity, response) -> response.setBoardId(board.getLocalId()));
});
return liveData;
}
- @AnyThread
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<>();
doAsync(() -> {
Account account = dataBaseAdapter.getAccountByIdDirectly(accountId);
Board board = dataBaseAdapter.getBoardByLocalCardIdDirectly(localCardId);
label.setAccountId(accountId);
- new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new LabelDataProvider(null, board, null), label, new IResponseCallback<Label>(account) {
+ new DataPropagationHelper(serverAdapterToUse, dataBaseAdapter).createEntity(new LabelDataProvider(null, board, null), label, new IResponseCallback<Label>(account) {
@Override
public void onResponse(Label response) {
assignLabelToCard(response, dataBaseAdapter.getCardByLocalIdDirectly(accountId, localCardId));
@@ -1317,6 +1550,11 @@ public class SyncManager {
@AnyThread
public void assignLabelToCard(@NonNull Label label, @NonNull Card card) {
+ assignLabelToCard(label, card, serverAdapter);
+ }
+
+ @AnyThread
+ public void assignLabelToCard(@NonNull Label label, @NonNull Card card, ServerAdapter serverAdapterToUse) {
doAsync(() -> {
final long localLabelId = label.getLocalId();
final long localCardId = card.getLocalId();
@@ -1327,8 +1565,8 @@ public class SyncManager {
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId());
Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId());
Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId());
- if (serverAdapter.hasInternetConnection()) {
- serverAdapter.assignLabelToCard(board.getId(), stack.getId(), card.getId(), label.getId(), new IResponseCallback<Void>(account) {
+ if (serverAdapterToUse.hasInternetConnection()) {
+ serverAdapterToUse.assignLabelToCard(board.getId(), stack.getId(), card.getId(), label.getId(), new IResponseCallback<Void>(account) {
@Override
public void onResponse(Void response) {
@@ -1395,11 +1633,6 @@ public class SyncManager {
return findProposalsForLabelsToAssign(accountId, boardId, -1L);
}
- // TODO Difference to getFullBoardByid() ??? I think those methods are equal, we should drop one of them.
- public LiveData<FullBoard> getFullBoard(Long accountId, Long localId) {
- return dataBaseAdapter.getFullBoardById(accountId, localId);
- }
-
public LiveData<User> getUserByLocalId(long accountId, long localId) {
return dataBaseAdapter.getUserByLocalId(accountId, localId);
}
@@ -1417,12 +1650,12 @@ public class SyncManager {
return dataBaseAdapter.searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, searchTerm);
}
- public LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedInACL, final String searchTerm) {
- return dataBaseAdapter.searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedInACL, searchTerm);
+ public UserSearchLiveData searchUserByUidOrDisplayNameForACL() {
+ return new UserSearchLiveData(dataBaseAdapter, serverAdapter);
}
- public LiveData<Board> getBoard(long accountId, long remoteId) {
- return dataBaseAdapter.getBoard(accountId, remoteId);
+ public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
+ return dataBaseAdapter.getBoardByRemoteId(accountId, remoteId);
}
public LiveData<Stack> getStackByRemoteId(long accountId, long localBoardId, long remoteId) {
@@ -1445,10 +1678,6 @@ public class SyncManager {
return dataBaseAdapter.searchNotYetAssignedLabelsByTitle(accountId, boardId, notYetAssignedToLocalCardId, searchTerm);
}
- public String getServerUrl() {
- return serverAdapter.getServerUrl();
- }
-
/**
* @see <a href="https://github.com/stefan-niedermann/nextcloud-deck/issues/360">reenable reorder</a>
*/
@@ -1456,7 +1685,7 @@ public class SyncManager {
public void reorder(long accountId, @NonNull FullCard movedCard, long newStackId, int newIndex) {
doAsync(() -> {
// read cards of new stack
- List<FullCard> cardsOfNewStack = dataBaseAdapter.getFullCardsForStackDirectly(accountId, newStackId);
+ List<FullCard> cardsOfNewStack = dataBaseAdapter.getFullCardsForStackDirectly(accountId, newStackId, null);
int newOrder = newIndex;
if (cardsOfNewStack.size() > newIndex) {
newOrder = cardsOfNewStack.get(newIndex).getCard().getOrder();
@@ -1526,7 +1755,7 @@ public class SyncManager {
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(movedCard.getCard().getStackId());
FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId());
Account account = dataBaseAdapter.getAccountByIdDirectly(movedCard.getCard().getAccountId());
- new SyncHelper(serverAdapter, dataBaseAdapter, new Date()).setResponseCallback(new IResponseCallback<Boolean>(account) {
+ new SyncHelper(serverAdapter, dataBaseAdapter, Instant.now()).setResponseCallback(new IResponseCallback<Boolean>(account) {
@Override
public void onResponse(Boolean response) {
// doNothing();
@@ -1597,7 +1826,7 @@ public class SyncManager {
}
private void reorderAscending(@NonNull Card movedCard, @NonNull List<Card> cardsToReorganize, int startingAtOrder) {
- Date now = new Date();
+ final Instant now = Instant.now();
for (Card card : cardsToReorganize) {
card.setOrder(startingAtOrder);
if (card.getStatus() == DBStatus.UP_TO_DATE.getId()) {
@@ -1625,7 +1854,7 @@ public class SyncManager {
WrappedLiveData<Attachment> liveData = new WrappedLiveData<>();
doAsync(() -> {
Attachment attachment = populateAttachmentEntityForFile(new Attachment(), localCardId, mimeType, file);
- Date now = new Date();
+ final Instant now = Instant.now();
attachment.setLastModifiedLocal(now);
attachment.setCreatedAt(now);
FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, localCardId);
@@ -1645,7 +1874,7 @@ public class SyncManager {
WrappedLiveData<Attachment> liveData = new WrappedLiveData<>();
doAsync(() -> {
Attachment attachment = populateAttachmentEntityForFile(existing, existing.getCardId(), mimeType, file);
- attachment.setLastModifiedLocal(new Date());
+ attachment.setLastModifiedLocal(Instant.now());
if (serverAdapter.hasInternetConnection()) {
FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, existing.getCardId());
Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getCard().getStackId());
@@ -1658,6 +1887,7 @@ public class SyncManager {
liveData.postValue(response);
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
liveData.postError(throwable);
@@ -1670,15 +1900,14 @@ public class SyncManager {
@AnyThread
private static Attachment populateAttachmentEntityForFile(@NonNull Attachment target, long localCardId, @NonNull String mimeType, @NonNull File file) {
- Attachment attachment = target;
- attachment.setCardId(localCardId);
- attachment.setMimetype(mimeType);
- attachment.setData(file.getName());
- attachment.setFilename(file.getName());
- attachment.setBasename(file.getName());
- attachment.setLocalPath(file.getAbsolutePath());
- attachment.setFilesize(file.length());
- return attachment;
+ target.setCardId(localCardId);
+ target.setMimetype(mimeType);
+ target.setData(file.getName());
+ target.setFilename(file.getName());
+ target.setBasename(file.getName());
+ target.setLocalPath(file.getAbsolutePath());
+ target.setFilesize(file.length());
+ return target;
}
@AnyThread
@@ -1726,7 +1955,27 @@ public class SyncManager {
doAsync(() -> dataBaseAdapter.deleteSingleCardWidget(widgetId));
}
- private static class BooleanResultHolder {
- public boolean result = true;
+ public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) {
+ doAsync(() -> 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) {
+ doAsync(() -> 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());
}
}
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 4f41173e9..bff8ddfdb 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
@@ -13,18 +13,14 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
+import com.nextcloud.android.sso.api.ParsedResponse;
+
import java.io.File;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
import java.util.List;
-import java.util.Locale;
-import java.util.TimeZone;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.ApiProvider;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
-import it.niedermann.nextcloud.deck.api.LastSyncUtil;
import it.niedermann.nextcloud.deck.api.RequestHelper;
import it.niedermann.nextcloud.deck.exceptions.OfflineException;
import it.niedermann.nextcloud.deck.model.AccessControl;
@@ -39,11 +35,12 @@ import it.niedermann.nextcloud.deck.model.full.FullStack;
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.projects.OcsProjectList;
+import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs;
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.util.DateUtil;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
@@ -53,18 +50,11 @@ import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN;
public class ServerAdapter {
- private String prefKeyWifiOnly;
-
- private static final DateFormat API_FORMAT =
- new SimpleDateFormat("E, dd MMM yyyy hh:mm:ss z", Locale.US);
-
- static {
- API_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
- }
+ private final String prefKeyWifiOnly;
@NonNull
- private Context applicationContext;
- private ApiProvider provider;
+ private final Context applicationContext;
+ private final ApiProvider provider;
public ServerAdapter(@NonNull Context applicationContext) {
this(applicationContext, null);
@@ -135,32 +125,35 @@ public class ServerAdapter {
// return lastSyncHeader;
}
- // TODO not used
- private Date getLastSync(long accountId) {
- Date lastSync = DateUtil.nowInGMT();
- lastSync.setTime(LastSyncUtil.getLastSync(accountId));
+ public void getBoards(IResponseCallback<ParsedResponse<List<FullBoard>>> responseCallback) {
+ RequestHelper.request(provider, () ->
+ provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId()), responseCallback.getAccount().getBoardsEtag()),
+ responseCallback);
+ }
- return lastSync;
+ public void getCapabilities(String eTag, IResponseCallback<ParsedResponse<Capabilities>> responseCallback) {
+ ensureInternetConnection();
+ RequestHelper.request(provider, () -> provider.getNextcloudAPI().getCapabilities(eTag), responseCallback);
}
- public void getBoards(IResponseCallback<List<FullBoard>> responseCallback) {
- RequestHelper.request(provider, () ->
- provider.getDeckAPI().getBoards(true, getLastSyncDateFormatted(responseCallback.getAccount().getId())),
- responseCallback);
+ public void getProjectsForCard(long remoteCardId, IResponseCallback<OcsProjectList> responseCallback) {
+ ensureInternetConnection();
+ RequestHelper.request(provider, () -> provider.getNextcloudAPI().getProjectsForCard(remoteCardId), responseCallback);
}
- public void getCapabilities(IResponseCallback<Capabilities> responseCallback) {
+ public void searchUser(String searchTerm, IResponseCallback<OcsUserList> responseCallback) {
ensureInternetConnection();
- RequestHelper.request(provider, () -> provider.getNextcloudAPI().getCapabilities(), responseCallback);
+ RequestHelper.request(provider, () -> provider.getNextcloudAPI().searchUser(searchTerm), responseCallback);
}
- public void getAllOcsUsers(IResponseCallback<OcsUserList> responseCallback) {
+
+ public void getSingleUserData(String userUid, IResponseCallback<OcsUser> responseCallback) {
ensureInternetConnection();
- RequestHelper.request(provider, () -> provider.getNextcloudAPI().getAllUsers(), responseCallback);
+ RequestHelper.request(provider, () -> provider.getNextcloudAPI().getSingleUserData(userUid), responseCallback);
}
- public void getOcsUserDetails(String ocsUserName, IResponseCallback<OcsUser> responseCallback) {
+ public void searchGroupMembers(String groupUID, IResponseCallback<GroupMemberUIDs> responseCallback) {
ensureInternetConnection();
- RequestHelper.request(provider, () -> provider.getNextcloudAPI().getUserDetails(ocsUserName), responseCallback);
+ RequestHelper.request(provider, () -> provider.getNextcloudAPI().searchGroupMembers(groupUID), responseCallback);
}
public void getActivitiesForCard(long cardId, IResponseCallback<List<it.niedermann.nextcloud.deck.model.ocs.Activity>> responseCallback) {
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 c643289bd..982844f06 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
@@ -2,16 +2,15 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db;
import android.content.Context;
+import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import androidx.sqlite.db.SimpleSQLiteQuery;
-import org.jetbrains.annotations.NotNull;
-
+import java.time.Instant;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
import it.niedermann.nextcloud.deck.DeckLog;
@@ -26,10 +25,12 @@ 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.enums.EDueType;
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.interfaces.AbstractRemoteEntity;
@@ -39,10 +40,16 @@ import it.niedermann.nextcloud.deck.model.ocs.Activity;
import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
import it.niedermann.nextcloud.deck.model.ocs.comment.Mention;
import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
+import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.model.relations.UserInBoard;
+import it.niedermann.nextcloud.deck.model.relations.UserInGroup;
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.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.widget.singlecard.SingleCardWidget;
+import it.niedermann.nextcloud.deck.ui.widget.stack.StackWidget;
import static androidx.lifecycle.Transformations.distinctUntilChanged;
@@ -57,28 +64,28 @@ public class DataBaseAdapter {
this.db = DeckDatabase.getInstance(applicationContext);
}
- @NotNull
+ @NonNull
public Context getContext() {
return context;
}
private <T extends AbstractRemoteEntity> void markAsEditedIfNeeded(T entity, boolean setStatus) {
if (!setStatus) return;
- entity.setLastModifiedLocal(new Date()); // now.
+ entity.setLastModifiedLocal(Instant.now());
entity.setStatusEnum(DBStatus.LOCAL_EDITED);
}
private <T extends AbstractRemoteEntity> void markAsDeletedIfNeeded(T entity, boolean setStatus) {
if (!setStatus) return;
entity.setStatusEnum(DBStatus.LOCAL_DELETED);
- entity.setLastModifiedLocal(new Date()); // now.
+ entity.setLastModifiedLocal(Instant.now());
}
public LiveData<Boolean> hasAccounts() {
return LiveDataHelper.postCustomValue(db.getAccountDao().countAccounts(), data -> data != null && data > 0);
}
- public LiveData<Board> getBoard(long accountId, long remoteId) {
+ public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
return distinctUntilChanged(db.getBoardDao().getBoardByRemoteId(accountId, remoteId));
}
@@ -180,24 +187,66 @@ public class DataBaseAdapter {
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);
+
+ }
+
+ private void fillSqlWithListValues(StringBuilder query, List<Object> args, @NonNull List<? extends IRemoteEntity> entities) {
+ for (int i = 0; i < entities.size(); i++) {
+ if (i > 0) {
+ query.append(", ");
+ }
+ query.append("?");
+ args.add(entities.get(i).getLocalId());
+ }
+ }
+
+ @WorkerThread
+ public List<FullCard> getFullCardsForStackDirectly(long accountId, long localStackId, FilterInformation filter) {
+ if (filter == null) {
+ return db.getCardDao().getFullCardsForStackDirectly(accountId, localStackId);
+ }
+ List<Object> args = new ArrayList<>();
+ args.add(accountId);
+ args.add(localStackId);
+
+ return db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, accountId, localStackId));
+ }
+ @AnyThread
+ private SimpleSQLiteQuery getQueryForFilter(FilterInformation filter, long accountId, long localStackId) {
List<Object> args = new ArrayList<>();
- StringBuilder query = new StringBuilder("SELECT * FROM card c " +
- "WHERE accountId = ? AND stackId = ? ");
args.add(accountId);
args.add(localStackId);
+ StringBuilder query = new StringBuilder("SELECT * FROM card c " +
+ "WHERE accountId = ? AND stackId = ? ");
if (!filter.getLabels().isEmpty()) {
- query.append("and exists(select 1 from joincardwithlabel j where c.localId = cardId and labelId in (");
+ query.append("and (exists(select 1 from joincardwithlabel j where c.localId = cardId and labelId in (");
fillSqlWithListValues(query, args, filter.getLabels());
query.append(") and j.status<>3) ");
+ if (filter.isNoAssignedLabel()) {
+ query.append("or not exists(select 1 from joincardwithlabel j where c.localId = cardId and j.status<>3)) ");
+ } else {
+ query.append(") ");
+ }
+ } else if (filter.isNoAssignedLabel()) {
+ query.append("and not exists(select 1 from joincardwithlabel j where c.localId = cardId and j.status<>3) ");
}
if (!filter.getUsers().isEmpty()) {
- query.append("and exists(select 1 from joincardwithuser j where c.localId = cardId and userId in (");
+ query.append("and (exists(select 1 from joincardwithuser j where c.localId = cardId and userId in (");
fillSqlWithListValues(query, args, filter.getUsers());
query.append(") and j.status<>3) ");
+ if (filter.isNoAssignedUser()) {
+ query.append("or not exists(select 1 from joincardwithuser j where c.localId = cardId and j.status<>3)) ");
+ } else {
+ query.append(") ");
+ }
+ } else if (filter.isNoAssignedUser()) {
+ query.append("and not exists(select 1 from joincardwithuser j where c.localId = cardId and j.status<>3) ");
}
+
if (filter.getDueType() != EDueType.NO_FILTER) {
switch (filter.getDueType()) {
case NO_DUE:
@@ -219,24 +268,11 @@ public class DataBaseAdapter {
throw new IllegalArgumentException("Xou need to add your new EDueType value\"" + filter.getDueType() + "\" here!");
}
}
- query.append(" and status<>3 order by `order`, createdAt asc;");
- return LiveDataHelper.interceptLiveData(db.getCardDao().getFilteredFullCardsForStack(new SimpleSQLiteQuery(query.toString(), args.toArray())), this::filterRelationsForCard);
-
- }
-
- private void fillSqlWithListValues(StringBuilder query, List<Object> args, @NonNull List<? extends IRemoteEntity> entities) {
- for (int i = 0; i < entities.size(); i++) {
- if (i > 0) {
- query.append(", ");
- }
- query.append("?");
- args.add(entities.get(i).getLocalId());
+ if (filter.getArchiveStatus() != FilterInformation.EArchiveStatus.ALL) {
+ query.append(" and c.archived = " + (filter.getArchiveStatus() == FilterInformation.EArchiveStatus.ARCHIVED ? 1 : 0));
}
- }
-
- @WorkerThread
- public List<FullCard> getFullCardsForStackDirectly(long accountId, long localStackId) {
- return db.getCardDao().getFullCardsForStackDirectly(accountId, localStackId);
+ query.append(" and status<>3 order by `order`, createdAt asc;");
+ return new SimpleSQLiteQuery(query.toString(), args.toArray());
}
@WorkerThread
@@ -244,17 +280,20 @@ public class DataBaseAdapter {
return db.getUserDao().getUserByUidDirectly(accountId, uid);
}
+ @WorkerThread
public long createUser(long accountId, User user) {
user.setAccountId(accountId);
return db.getUserDao().insert(user);
}
+ @WorkerThread
public void updateUser(long accountId, User user, boolean setStatus) {
markAsEditedIfNeeded(user, setStatus);
user.setAccountId(accountId);
db.getUserDao().update(user);
}
+ @AnyThread
public LiveData<Label> getLabelByRemoteId(long accountId, long remoteId) {
return distinctUntilChanged(db.getLabelDao().getLabelByRemoteId(accountId, remoteId));
}
@@ -264,7 +303,8 @@ public class DataBaseAdapter {
return db.getLabelDao().getLabelByRemoteIdDirectly(accountId, remoteId);
}
- public long createLabel(long accountId, @NonNull Label label) {
+ @WorkerThread
+ public long createLabelDirectly(long accountId, @NonNull Label label) {
label.setAccountId(accountId);
return db.getLabelDao().insert(label);
}
@@ -320,6 +360,8 @@ public class DataBaseAdapter {
// readded!
existing.setStatusEnum(DBStatus.LOCAL_EDITED);
db.getJoinCardWithUserDao().update(existing);
+ } else if (existing != null) {
+ return;
} else {
JoinCardWithUser join = new JoinCardWithUser();
join.setCardId(localCardId);
@@ -344,6 +386,28 @@ public class DataBaseAdapter {
db.getJoinBoardWithLabelDao().deleteByBoardId(localBoardId);
}
+ public void deleteGroupMembershipsOfGroup(Long localGroupUserId) {
+ db.getUserInGroupDao().deleteByGroupId(localGroupUserId);
+ }
+
+ public void deleteBoardMembershipsOfBoard(Long localBoardId) {
+ db.getUserInBoardDao().deleteByBoardId(localBoardId);
+ }
+
+ public void addUserToGroup(Long localGroupUserId, Long localGroupMemberId) {
+ UserInGroup relation = new UserInGroup();
+ relation.setGroupId(localGroupUserId);
+ relation.setMemberId(localGroupMemberId);
+ db.getUserInGroupDao().insert(relation);
+ }
+
+ public void addUserToBoard(Long localUserId, Long localBoardId) {
+ UserInBoard relation = new UserInBoard();
+ relation.setBoardId(localBoardId);
+ relation.setUserId(localUserId);
+ db.getUserInBoardDao().insert(relation);
+ }
+
public void updateLabel(Label label, boolean setStatus) {
markAsEditedIfNeeded(label, setStatus);
db.getLabelDao().update(label);
@@ -391,6 +455,10 @@ public class DataBaseAdapter {
return distinctUntilChanged(db.getAccountDao().getAllAccounts());
}
+ 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
@@ -431,8 +499,8 @@ public class DataBaseAdapter {
db.getBoardDao().update(board);
}
- public LiveData<List<FullStack>> getFullStacksForBoard(long accountId, long localBoardId) {
- return distinctUntilChanged(db.getStackDao().getFullStacksForBoard(accountId, localBoardId));
+ public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
+ return distinctUntilChanged(db.getStackDao().getStacksForBoard(accountId, localBoardId));
}
@WorkerThread
@@ -440,27 +508,36 @@ public class DataBaseAdapter {
return db.getStackDao().getFullStacksForBoardDirectly(accountId, localBoardId);
}
+ @AnyThread
public LiveData<FullStack> getStack(long accountId, long localStackId) {
return distinctUntilChanged(db.getStackDao().getFullStack(accountId, localStackId));
}
+ @WorkerThread
public long createStack(long accountId, Stack stack) {
stack.setAccountId(accountId);
return db.getStackDao().insert(stack);
}
+ @WorkerThread
public void deleteStack(Stack stack, boolean setStatus) {
markAsDeletedIfNeeded(stack, setStatus);
db.getStackDao().update(stack);
}
+ @WorkerThread
public void deleteStackPhysically(Stack stack) {
db.getStackDao().delete(stack);
}
+ @WorkerThread
public void updateStack(Stack stack, boolean setStatus) {
markAsEditedIfNeeded(stack, setStatus);
db.getStackDao().update(stack);
+ if (db.getStackWidgetModelDao().containsStackLocalId(stack.getLocalId())) {
+ DeckLog.info("Notifying " + StackWidget.class.getSimpleName() + " about card changes for \"" + stack.getTitle() + "\"");
+ StackWidget.notifyDatasetChanged(context);
+ }
}
@WorkerThread
@@ -468,10 +545,16 @@ public class DataBaseAdapter {
return db.getCardDao().getCardByLocalIdDirectly(accountId, localCardId);
}
+ @AnyThread
public LiveData<FullCard> getCardByLocalId(long accountId, long localCardId) {
return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardByLocalId(accountId, localCardId), this::filterRelationsForCard);
}
+ @AnyThread
+ public LiveData<FullCardWithProjects> getCardWithProjectsByLocalId(long accountId, long localCardId) {
+ return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardWithProjectsByLocalId(accountId, localCardId), this::filterRelationsForCard);
+ }
+
@WorkerThread
public List<FullCard> getLocallyChangedCardsDirectly(long accountId) {
return db.getCardDao().getLocallyChangedCardsDirectly(accountId);
@@ -482,15 +565,27 @@ public class DataBaseAdapter {
return db.getCardDao().getLocallyChangedCardsByLocalStackIdDirectly(accountId, localStackId);
}
- public long createCard(long accountId, Card card) {
+ @WorkerThread
+ public long createCardDirectly(long accountId, Card card) {
card.setAccountId(accountId);
- return db.getCardDao().insert(card);
+ long newCardId = db.getCardDao().insert(card);
+
+ notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId());
+
+ return newCardId;
}
- public int getHighestCardOrderInStack(long localStackId){
+ @WorkerThread
+ public int getHighestCardOrderInStack(long localStackId) {
return db.getCardDao().getHighestOrderInStack(localStackId);
}
+ @WorkerThread
+ public int getHighestStackOrderInBoard(long localBoardId) {
+ return db.getStackDao().getHighestStackOrderInBoard(localBoardId);
+ }
+
+ @WorkerThread
public void deleteCard(Card card, boolean setStatus) {
markAsDeletedIfNeeded(card, setStatus);
if (setStatus) {
@@ -498,21 +593,35 @@ public class DataBaseAdapter {
} else {
deleteCardPhysically(card);
}
+
+ notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId());
}
+ @WorkerThread
public void deleteCardPhysically(Card card) {
db.getCardDao().delete(card);
}
+ @WorkerThread
public void updateCard(@NonNull Card card, boolean setStatus) {
markAsEditedIfNeeded(card, setStatus);
+ Long originalStackLocalId = db.getCardDao().getLocalStackIdByLocalCardId(card.getLocalId());
db.getCardDao().update(card);
if (db.getSingleCardWidgetModelDao().containsCardLocalId(card.getLocalId())) {
- DeckLog.info("Notifying widget about card changes for \"" + card.getTitle() + "\"");
+ DeckLog.info("Notifying " + SingleCardWidget.class.getSimpleName() + " about card changes for \"" + card.getTitle() + "\"");
SingleCardWidget.notifyDatasetChanged(context);
}
+ notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId(), originalStackLocalId);
+ }
+
+ private void notifyStackWidgetsIfNeeded(String cardTitle, long... affectedStackIds) {
+ if (db.getStackWidgetModelDao().containsStackLocalId(affectedStackIds)) {
+ DeckLog.info("Notifying " + StackWidget.class.getSimpleName() + " about card changes for \"" + cardTitle + "\"");
+ StackWidget.notifyDatasetChanged(context);
+ }
}
+ @WorkerThread
public long createAccessControl(long accountId, @NonNull AccessControl entity) {
entity.setAccountId(accountId);
return db.getAccessControlDao().insert(entity);
@@ -571,9 +680,9 @@ public class DataBaseAdapter {
return db.getUserDao().searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%");
}
- public LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedToACL, final String searchTerm) {
+ public List<User> searchUserByUidOrDisplayNameForACLDirectly(final long accountId, final long notYetAssignedToACL, final String searchTerm) {
validateSearchTerm(searchTerm);
- return db.getUserDao().searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%");
+ return db.getUserDao().searchUserByUidOrDisplayNameForACLDirectly(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%");
}
public LiveData<List<Label>> searchNotYetAssignedLabelsByTitle(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, String searchTerm) {
@@ -618,9 +727,14 @@ public class DataBaseAdapter {
return db.getAttachmentDao().getLocallyChangedAttachmentsDirectly(accountId);
}
+ @WorkerThread
+ public List<Attachment> getLocallyChangedAttachmentsForStackDirectly(long localStackId) {
+ return db.getAttachmentDao().getLocallyChangedAttachmentsForStackDirectly(localStackId);
+ }
+
public long createAttachment(long accountId, @NonNull Attachment attachment) {
attachment.setAccountId(accountId);
- attachment.setCreatedAt(new Date());
+ attachment.setCreatedAt(Instant.now());
return db.getAttachmentDao().insert(attachment);
}
@@ -720,16 +834,24 @@ public class DataBaseAdapter {
return db.getJoinCardWithLabelDao().getAllDeletedJoinsWithRemoteIDs();
}
- public List<JoinCardWithLabel> getAllChangedJoins() {
+ public List<JoinCardWithLabel> getAllChangedLabelJoins() {
return db.getJoinCardWithLabelDao().getAllChangedJoins();
}
- public JoinCardWithLabel getRemoteIdsForJoin(Long localCardId, Long localLabelId) {
+ public List<JoinCardWithLabel> getAllChangedLabelJoinsForStack(Long localStackId) {
+ return db.getJoinCardWithLabelDao().getAllChangedJoinsForStack(localStackId);
+ }
+
+ public JoinCardWithLabel getAllChangedLabelJoinsWithRemoteIDs(Long localCardId, Long localLabelId) {
return db.getJoinCardWithLabelDao().getRemoteIdsForJoin(localCardId, localLabelId);
}
- public List<JoinCardWithUser> getAllDeletedUserJoinsWithRemoteIDs() {
- return db.getJoinCardWithUserDao().getDeletedJoinsWithRemoteIDs();
+ public List<JoinCardWithUser> getAllChangedUserJoinsWithRemoteIDs() {
+ return db.getJoinCardWithUserDao().getChangedJoinsWithRemoteIDs();
+ }
+
+ public List<JoinCardWithUser> getAllChangedUserJoinsWithRemoteIDsForStack(Long localStackId) {
+ return db.getJoinCardWithUserDao().getChangedJoinsWithRemoteIDsForStack(localStackId);
}
public void deleteJoinedLabelForCardPhysicallyByRemoteIDs(Long accountId, Long remoteCardId, Long remoteLabelId) {
@@ -837,11 +959,17 @@ public class DataBaseAdapter {
return db.getCommentDao().getCommentByLocalCardIdDirectly(localCardId);
}
+ @WorkerThread
public List<Card> getCardsWithLocallyChangedCommentsDirectly(Long accountId) {
return db.getCardDao().getCardsWithLocallyChangedCommentsDirectly(accountId);
}
@WorkerThread
+ public List<Card> getCardsWithLocallyChangedCommentsForStackDirectly(Long localStackId) {
+ return db.getCardDao().getCardsWithLocallyChangedCommentsForStackDirectly(localStackId);
+ }
+
+ @WorkerThread
public Long getLocalStackIdByRemoteStackIdDirectly(long accountId, Long stackId) {
return db.getStackDao().getLocalStackIdByRemoteStackIdDirectly(accountId, stackId);
}
@@ -910,11 +1038,89 @@ public class DataBaseAdapter {
db.getSingleCardWidgetModelDao().delete(model);
}
+ public long createStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) {
+ StackWidgetModel model = new StackWidgetModel();
+ model.setAppWidgetId(appWidgetId);
+ model.setAccountId(accountId);
+ model.setStackId(stackId);
+ model.setDarkTheme(darkTheme);
+
+ return db.getStackWidgetModelDao().insert(model);
+ }
+
+ public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) {
+ return db.getStackWidgetModelDao().getStackWidgetByAppWidgetIdDirectly(appWidgetId);
+ }
+
+ public void deleteStackWidget(int appWidgetId) {
+ StackWidgetModel model = new StackWidgetModel();
+ model.setAppWidgetId(appWidgetId);
+ db.getStackWidgetModelDao().delete(model);
+ }
+
public LiveData<List<Account>> readAccountsForHostWithReadAccessToBoard(String host, long boardRemoteId) {
- return db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%"+host+"%", boardRemoteId);
+ return db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%" + host + "%", boardRemoteId);
}
public List<Account> readAccountsForHostWithReadAccessToBoardDirectly(String host, long boardRemoteId) {
- return db.getAccountDao().readAccountsForHostWithReadAccessToBoardDirectly("%"+host+"%", boardRemoteId);
+ return db.getAccountDao().readAccountsForHostWithReadAccessToBoardDirectly("%" + host + "%", boardRemoteId);
+ }
+
+ public Board getBoardForAccountByNameDirectly(long account, String title) {
+ return db.getBoardDao().getBoardForAccountByNameDirectly(account, title);
+ }
+
+ public OcsProject getProjectByRemoteIdDirectly(long accountId, Long remoteId) {
+ return db.getOcsProjectDao().getProjectByRemoteIdDirectly(accountId, remoteId);
+ }
+
+ public Long createProjectDirectly(long accountId, OcsProject entity) {
+ entity.setAccountId(accountId);
+ return db.getOcsProjectDao().insert(entity);
+ }
+
+ public void deleteProjectResourcesForProjectIdDirectly(Long localProjectId) {
+ db.getOcsProjectResourceDao().deleteByProjectId(localProjectId);
+ }
+
+ public void updateProjectDirectly(long accountId, OcsProject entity) {
+ entity.setAccountId(accountId);
+ db.getOcsProjectDao().update(entity);
+ }
+
+ public void deleteProjectDirectly(OcsProject ocsProject) {
+ db.getOcsProjectResourceDao().deleteByProjectId(ocsProject.getLocalId());
+ db.getOcsProjectDao().delete(ocsProject);
+ }
+
+ public Long createProjectResourceDirectly(Long accountId, OcsProjectResource resource) {
+ resource.setAccountId(accountId);
+ return db.getOcsProjectResourceDao().insert(resource);
+ }
+
+ public int countProjectResourcesInProjectDirectly(Long projectLocalId) {
+ return db.getOcsProjectResourceDao().countProjectResourcesInProjectDirectly(projectLocalId);
+ }
+
+ public LiveData<Integer> countProjectResourcesInProject(Long projectLocalId) {
+ return db.getOcsProjectResourceDao().countProjectResourcesInProject(projectLocalId);
+ }
+
+ public LiveData<List<OcsProjectResource>> getResourcesByLocalProjectId(Long projectLocalId) {
+ return db.getOcsProjectResourceDao().getResourcesByLocalProjectId(projectLocalId);
+ }
+
+ public void assignCardToProjectIfMissng(Long accountId, Long localProjectId, Long remoteCardId) {
+ Card card = db.getCardDao().getCardByRemoteIdDirectly(accountId, remoteCardId);
+ if (card != null) {
+ JoinCardWithProject existing = db.getJoinCardWithOcsProjectDao().getAssignmentByCardIdAndProjectIdDirectly(card.getLocalId(), localProjectId);
+ if (existing == null) {
+ JoinCardWithProject assignment = new JoinCardWithProject();
+ assignment.setStatus(DBStatus.UP_TO_DATE.getId());
+ assignment.setCardId(card.getLocalId());
+ assignment.setProjectId(localProjectId);
+ db.getJoinCardWithOcsProjectDao().insert(assignment);
+ }
+ }
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DateTypeConverter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DateTypeConverter.java
index 6b198a502..f6811e033 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DateTypeConverter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DateTypeConverter.java
@@ -2,17 +2,17 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db;
import androidx.room.TypeConverter;
-import java.util.Date;
+import java.time.Instant;
public class DateTypeConverter {
@TypeConverter
- public static Date toDate(Long value) {
- return value == null ? null : new Date(value);
+ public static Instant toInstant(Long value) {
+ return value == null ? null : Instant.ofEpochMilli(value);
}
@TypeConverter
- public static Long toLong(Date value) {
- return value == null ? null : value.getTime();
+ public static Long fromInstant(Instant value) {
+ return value == null ? null : value.toEpochMilli();
}
} \ No newline at end of file
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 0e86557fa..a491946ed 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
@@ -1,8 +1,11 @@
package it.niedermann.nextcloud.deck.persistence.sync.adapters.db;
import android.content.Context;
+import android.content.SharedPreferences;
import android.database.Cursor;
+import android.graphics.Color;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.room.Database;
@@ -12,6 +15,7 @@ import androidx.room.TypeConverters;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.api.LastSyncUtil;
import it.niedermann.nextcloud.deck.model.AccessControl;
@@ -28,10 +32,16 @@ import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.model.Permission;
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.ocs.Activity;
import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
import it.niedermann.nextcloud.deck.model.ocs.comment.Mention;
+import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.model.relations.UserInBoard;
+import it.niedermann.nextcloud.deck.model.relations.UserInGroup;
import it.niedermann.nextcloud.deck.model.widget.singlecard.SingleCardWidgetModel;
import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.AccessControlDao;
@@ -49,9 +59,15 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.JoinCardWit
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.LabelDao;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.MentionDao;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.PermissionDao;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.SingleCardWidgetModelDao;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.StackDao;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserDao;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserInBoardDao;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserInGroupDao;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.JoinCardWithOcsProjectDao;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.OcsProjectDao;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.OcsProjectResourceDao;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.SingleCardWidgetModelDao;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.StackWidgetModelDao;
@Database(
entities = {
@@ -73,9 +89,15 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserDao;
DeckComment.class,
Mention.class,
SingleCardWidgetModel.class,
+ StackWidgetModel.class,
+ OcsProject.class,
+ OcsProjectResource.class,
+ JoinCardWithProject.class,
+ UserInGroup.class,
+ UserInBoard.class,
},
exportSchema = false,
- version = 15
+ version = 23
)
@TypeConverters({DateTypeConverter.class})
public abstract class DeckDatabase extends RoomDatabase {
@@ -175,8 +197,204 @@ public abstract class DeckDatabase extends RoomDatabase {
}
};
- public static final RoomDatabase.Callback ON_CREATE_CALLBACK = new RoomDatabase.Callback() {
+ private static final Migration MIGRATION_15_16 = new Migration(15, 16) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE `StackWidgetModel` (`appWidgetId` INTEGER PRIMARY KEY, `accountId` INTEGER, `stackId` INTEGER, `darkTheme` INTEGER CHECK (`darkTheme` IN (0,1)) NOT NULL, " +
+ "FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE, " +
+ "FOREIGN KEY(`stackId`) REFERENCES `Stack`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )");
+ database.execSQL("CREATE INDEX `index_StackWidgetModel_stackId` ON `StackWidgetModel` (`stackId`)");
+ database.execSQL("CREATE INDEX `index_StackWidgetModel_accountId` ON `StackWidgetModel` (`accountId`)");
+ }
+ };
+
+ private static final Migration MIGRATION_16_17 = new Migration(16, 17) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE `OcsProject` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `name` TEXT NOT NULL, `status` INTEGER NOT NULL, `lastModified` INTEGER, `lastModifiedLocal` INTEGER)");
+ database.execSQL("CREATE UNIQUE INDEX `index_OcsProject_accountId_id` ON `OcsProject` (`accountId`, `id`)");
+ database.execSQL("CREATE INDEX `index_project_accID` ON `OcsProject` (`accountId`)");
+ database.execSQL("CREATE INDEX `index_OcsProject_id` ON `OcsProject` (`id`)");
+ database.execSQL("CREATE INDEX `index_OcsProject_lastModifiedLocal` ON `OcsProject` (`lastModifiedLocal`)");
+
+ database.execSQL("CREATE TABLE `OcsProjectResource` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `name` TEXT, `status` INTEGER NOT NULL, `lastModified` INTEGER, `lastModifiedLocal` INTEGER, `projectId` INTEGER NOT NULL, `type` TEXT , `link` TEXT , `path` TEXT, `iconUrl` TEXT , `previewAvailable` INTEGER, `mimetype` TEXT, FOREIGN KEY(`projectId`) REFERENCES `OcsProject`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)");
+ database.execSQL("CREATE INDEX `index_projectResource_accID` ON `OcsProjectResource` (`accountId`)");
+ database.execSQL("CREATE INDEX `index_projectResource_projectId` ON `OcsProjectResource` (`projectId`)");
+ database.execSQL("CREATE UNIQUE INDEX `index_OcsProjectResource_accountId_id` ON `OcsProjectResource` (`accountId`, `id`, `projectId`)");
+ database.execSQL("CREATE INDEX `index_OcsProjectResource_id` ON `OcsProjectResource` (`id`)");
+ database.execSQL("CREATE INDEX `index_OcsProjectResource_lastModifiedLocal` ON `OcsProjectResource` (`lastModifiedLocal`)");
+
+ database.execSQL("CREATE TABLE `JoinCardWithProject` (`status` INTEGER NOT NULL, `projectId` INTEGER NOT NULL, `cardId` INTEGER NOT NULL, PRIMARY KEY (`projectId`, `cardId`), FOREIGN KEY(`cardId`) REFERENCES `Card`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`projectId`) REFERENCES `OcsProject`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)");
+ database.execSQL("CREATE INDEX `index_JoinCardWithProject_projectId` ON `JoinCardWithProject` (`projectId`)");
+ database.execSQL("CREATE INDEX `index_JoinCardWithProject_cardId` ON `JoinCardWithProject` (`cardId`)");
+ }
+ };
+ private static final Migration MIGRATION_17_18 = new Migration(17, 18) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/435
+ database.execSQL("ALTER TABLE `Account` ADD `etag` TEXT");
+ }
+ };
+ private static final Migration MIGRATION_18_19 = new Migration(18, 19) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/619
+ database.execSQL("DROP INDEX `index_OcsProjectResource_accountId_id`");
+ database.execSQL("ALTER TABLE `OcsProjectResource` ADD `idString` TEXT");
+ database.execSQL("CREATE UNIQUE INDEX `index_OcsProjectResource_accountId_id` ON `OcsProjectResource` (`accountId`, `id`, `idString`, `projectId`)");
+ }
+ };
+ private static final Migration MIGRATION_19_20 = new Migration(19, 20) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/492
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/631
+ database.execSQL("CREATE TABLE `UserInGroup` (`groupId` INTEGER NOT NULL, `memberId` INTEGER NOT NULL, " +
+ "primary KEY(`groupId`, `memberId`), " +
+ "FOREIGN KEY(`groupId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, " +
+ "FOREIGN KEY(`memberId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)");
+ database.execSQL("CREATE UNIQUE INDEX `unique_idx_group_member` ON `UserInGroup` (`groupId`, `memberId`)");
+ database.execSQL("CREATE INDEX `index_UserInGroup_groupId` ON `UserInGroup` (`groupId`)");
+ database.execSQL("CREATE INDEX `index_UserInGroup_memberId` ON `UserInGroup` (`memberId`)");
+
+ database.execSQL("CREATE TABLE `UserInBoard` (`userId` INTEGER NOT NULL, `boardId` INTEGER NOT NULL, " +
+ "primary KEY(`userId`, `boardId`), " +
+ "FOREIGN KEY(`userId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, " +
+ "FOREIGN KEY(`boardId`) REFERENCES `Board`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)");
+ database.execSQL("CREATE UNIQUE INDEX `unique_idx_user_board` ON `UserInBoard` (`userId`, `boardId`)");
+ database.execSQL("CREATE INDEX `index_UserInBoard_userId` ON `UserInBoard` (`userId`)");
+ database.execSQL("CREATE INDEX `index_UserInBoard_boardId` ON `UserInBoard` (`boardId`)");
+ }
+ };
+
+ private static final Migration MIGRATION_20_21 = new Migration(20, 21) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/556
+ String suffix = "_new";
+ {
+ String tableName = "Account";
+ database.execSQL("CREATE TABLE `" + tableName + suffix + "` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `userName` TEXT NOT NULL, `url` TEXT NOT NULL, " +
+ "`color` INTEGER NOT NULL DEFAULT 0, `textColor` INTEGER NOT NULL DEFAULT 0, `serverDeckVersion` TEXT NOT NULL DEFAULT '0.6.4', `maintenanceEnabled` INTEGER NOT NULL DEFAULT 0, `etag` TEXT)");
+ Cursor cursor = database.query("select * from `" + tableName + "`");
+ while (cursor.moveToNext()) {
+ String colorAsString1 = cursor.getString(4); // color
+ String colorAsString2 = cursor.getString(5); // textColor
+
+ @ColorInt Integer color1 = null;
+ @ColorInt Integer color2 = null;
+ try {
+ color1 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString1));
+ color2 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString2));
+ } catch (Exception e) {
+ color1 = Color.GRAY;
+ color2 = Color.GRAY;
+ }
+ database.execSQL("Insert into `" + tableName + suffix + "` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{
+ cursor.getLong(0), cursor.getString(1), cursor.getString(2), cursor.getString(3),
+ color1, color2, cursor.getString(6), cursor.getInt(7), cursor.getString(8)});
+
+ }
+
+
+ database.execSQL("DROP TABLE `" + tableName + "`");
+ database.execSQL("ALTER TABLE `" + tableName + suffix + "` RENAME TO `" + tableName + "`");
+ database.execSQL("CREATE UNIQUE INDEX `index_Account_name` ON `" + tableName + "` (`name`)");
+ database.execSQL("UPDATE SQLITE_SEQUENCE SET seq = (select max(id) from " + tableName + ") WHERE name = ?", new Object[]{tableName});
+ }
+ {
+ String tableName = "Board";
+ database.execSQL("CREATE TABLE `" + tableName + suffix + "` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `status` INTEGER NOT NULL, " +
+ "`lastModified` INTEGER, `lastModifiedLocal` INTEGER, `title` TEXT, `ownerId` INTEGER NOT NULL, `color` INTEGER, " +
+ "`archived` INTEGER NOT NULL, `shared` INTEGER NOT NULL, `deletedAt` INTEGER, `permissionRead` INTEGER NOT NULL, " +
+ "`permissionEdit` INTEGER NOT NULL, `permissionManage` INTEGER NOT NULL, `permissionShare` INTEGER NOT NULL, " +
+ "FOREIGN KEY(`ownerId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE SET NULL )");
+ Cursor cursor = database.query("select * from `" + tableName + "`");
+ while (cursor.moveToNext()) {
+ String colorAsString1 = cursor.getString(8); // color
+
+ @ColorInt Integer color1 = null;
+ try {
+ color1 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString1));
+ } catch (Exception e) {
+ color1 = Color.GRAY;
+ }
+ database.execSQL("Insert into `" + tableName + suffix + "` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{
+ cursor.getLong(0), cursor.getLong(1), cursor.getLong(2), cursor.getInt(3),
+ cursor.getLong(4), cursor.getLong(5), cursor.getString(6), cursor.getLong(7), color1,
+ cursor.getInt(9), cursor.getInt(10), cursor.getInt(11), cursor.getInt(12),
+ cursor.getInt(13), cursor.getInt(14), cursor.getInt(15)
+ });
+
+ }
+
+
+ database.execSQL("DROP TABLE `" + tableName + "`");
+ database.execSQL("ALTER TABLE `" + tableName + suffix + "` RENAME TO `" + tableName + "`");
+ database.execSQL("CREATE INDEX `index_Board_accountId` ON `" + tableName + "` (`accountId`)");
+ database.execSQL("CREATE UNIQUE INDEX `index_Board_accountId_id` ON `" + tableName + "` (`accountId`, `id`)");
+ database.execSQL("CREATE INDEX `index_Board_id` ON `" + tableName + "` (`id`)");
+ database.execSQL("CREATE INDEX `index_Board_ownerId` ON `" + tableName + "` (`ownerId`)");
+ database.execSQL("CREATE INDEX `index_Board_lastModifiedLocal` ON `" + tableName + "` (`lastModifiedLocal`)");
+ database.execSQL("UPDATE SQLITE_SEQUENCE SET seq = (select max(id) from " + tableName + ") WHERE name = ?", new Object[]{tableName});
+ }
+ {
+ String tableName = "Label";
+ database.execSQL("CREATE TABLE `" + tableName + suffix + "` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `status` INTEGER NOT NULL, " +
+ "`lastModified` INTEGER, `lastModifiedLocal` INTEGER, `title` TEXT, `color` INTEGER NOT NULL DEFAULT 0, `boardId` INTEGER NOT NULL, " +
+ "FOREIGN KEY(`boardId`) REFERENCES `Board`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )");
+ Cursor cursor = database.query("select * from `" + tableName + "`");
+ while (cursor.moveToNext()) {
+ String colorAsString1 = cursor.getString(7); // color
+
+ @ColorInt Integer color1 = null;
+ try {
+ color1 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString1));
+ } catch (Exception e) {
+ color1 = Color.GRAY;
+ }
+ database.execSQL("Insert into `" + tableName + suffix + "` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{
+ cursor.getLong(0), cursor.getLong(1), cursor.getLong(2), cursor.getInt(3),
+ cursor.getLong(4), cursor.getLong(5), cursor.getString(6), color1, cursor.getLong(8)});
+
+ }
+
+
+ database.execSQL("DROP TABLE `" + tableName + "`");
+ database.execSQL("ALTER TABLE `" + tableName + suffix + "` RENAME TO `" + tableName + "`");
+ database.execSQL("CREATE UNIQUE INDEX `index_Label_accountId_id` ON `" + tableName + "` (`accountId`, `id`)");
+ database.execSQL("CREATE INDEX `index_Label_boardId` ON `" + tableName + "` (`boardId`)");
+ database.execSQL("CREATE INDEX `index_Label_accountId` ON `" + tableName + "` (`accountId`)");
+ database.execSQL("CREATE UNIQUE INDEX `idx_label_title_unique` ON `" + tableName + "` (`boardId`, `title`)");
+ database.execSQL("CREATE INDEX `index_Label_id` ON `" + tableName + "` (`id`)");
+ database.execSQL("CREATE INDEX `index_Label_lastModifiedLocal` ON `" + tableName + "` (`lastModifiedLocal`)");
+ database.execSQL("UPDATE SQLITE_SEQUENCE SET seq = (select max(id) from " + tableName + ") WHERE name = ?", new Object[]{tableName});
+ }
+ }
+ };
+
+ private static final Migration MIGRATION_22_23 = new Migration(22, 23) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/359
+ database.execSQL("ALTER TABLE `Account` ADD `boardsEtag` TEXT");
+ database.execSQL("ALTER TABLE `Board` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `Stack` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `Card` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `Label` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `AccessControl` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `Attachment` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `User` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `DeckComment` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `Activity` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `OcsProject` ADD `etag` TEXT");
+ database.execSQL("ALTER TABLE `OcsProjectResource` ADD `etag` TEXT");
+ }
+ };
+
+ public static final RoomDatabase.Callback ON_CREATE_CALLBACK = new RoomDatabase.Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
@@ -216,6 +434,26 @@ public abstract class DeckDatabase extends RoomDatabase {
.apply();
}
})
+ .addMigrations(MIGRATION_15_16)
+ .addMigrations(MIGRATION_16_17)
+ .addMigrations(MIGRATION_17_18)
+ .addMigrations(MIGRATION_18_19)
+ .addMigrations(MIGRATION_19_20)
+ .addMigrations(MIGRATION_20_21)
+ .addMigrations(new Migration(21, 22) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/715
+ final SharedPreferences.Editor lastSyncPref = context.getApplicationContext().getSharedPreferences("it.niedermann.nextcloud.deck.last_sync", Context.MODE_PRIVATE).edit();
+ Cursor cursor = database.query("select id from `Account`");
+ while (cursor.moveToNext()) {
+ lastSyncPref.remove("lS_" + cursor.getLong(0));
+ }
+ cursor.close();
+ lastSyncPref.apply();
+ }
+ })
+ .addMigrations(MIGRATION_22_23)
.fallbackToDestructiveMigration()
.addCallback(ON_CREATE_CALLBACK)
.build();
@@ -256,4 +494,16 @@ public abstract class DeckDatabase extends RoomDatabase {
public abstract MentionDao getMentionDao();
public abstract SingleCardWidgetModelDao getSingleCardWidgetModelDao();
+
+ public abstract StackWidgetModelDao getStackWidgetModelDao();
+
+ public abstract OcsProjectDao getOcsProjectDao();
+
+ public abstract OcsProjectResourceDao getOcsProjectResourceDao();
+
+ public abstract JoinCardWithOcsProjectDao getJoinCardWithOcsProjectDao();
+
+ public abstract UserInGroupDao getUserInGroupDao();
+
+ public abstract UserInBoardDao getUserInBoardDao();
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java
index 2d0887903..8f7ee1cba 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java
@@ -25,6 +25,10 @@ public interface AttachmentDao extends GenericDao<Attachment> {
@Query("SELECT * FROM attachment WHERE accountId = :accountId and (status<>1 or id is null or lastModified <> lastModifiedLocal)")
List<Attachment> getLocallyChangedAttachmentsDirectly(long accountId);
+ @Query("SELECT a.* FROM attachment a inner join card c on c.localId = a.cardId " +
+ "WHERE c.stackId = :localStackId and (a.status<>1 or a.id is null or a.lastModified <> a.lastModifiedLocal)")
+ List<Attachment> getLocallyChangedAttachmentsForStackDirectly(long localStackId);
+
@Query("SELECT * FROM attachment WHERE accountId = :accountId and cardId = :localCardId")
List<Attachment> getAttachmentsForLocalCardIdDirectly(long accountId, Long localCardId);
} \ 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 bd189e846..6cb322aa3 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,6 +13,9 @@ 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);
@@ -52,7 +55,8 @@ public interface BoardDao extends GenericDao<Board> {
@Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId")
Board getBoardByLocalCardIdDirectly(long localCardId);
- @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId")
+ @Transaction
+ @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId and c.stackId = s.localId")
FullBoard getFullBoardByLocalCardIdDirectly(long localCardId);
@Transaction
@@ -72,4 +76,7 @@ public interface BoardDao extends GenericDao<Board> {
@Query("SELECT count(*) FROM board WHERE accountId = :accountId and archived = 1 and (deletedAt = 0 or deletedAt is null) and status <> 3")
LiveData<Integer> countArchivedBoards(long accountId);
+
+ @Query("SELECT * FROM board WHERE accountId = :accountId and title = :title")
+ Board getBoardForAccountByNameDirectly(long accountId, String title);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java
index 57163112a..82bdf1b8d 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java
@@ -11,6 +11,7 @@ import java.util.List;
import it.niedermann.nextcloud.deck.model.Card;
import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects;
@Dao
public interface CardDao extends GenericDao<Card> {
@@ -36,17 +37,24 @@ public interface CardDao extends GenericDao<Card> {
@Query("SELECT * FROM card WHERE accountId = :accountId AND archived = 0 AND stackId = :localStackId and status<>3 order by `order`, createdAt asc")
LiveData<List<FullCard>> getFullCardsForStack(final long accountId, final long localStackId);
- @Transaction // v not deleted!
+ @Transaction
@RawQuery(observedEntities = Card.class)
LiveData<List<FullCard>> getFilteredFullCardsForStack(SupportSQLiteQuery query);
@Transaction
+ @RawQuery(observedEntities = Card.class)
+ List<FullCard> getFilteredFullCardsForStackDirectly(SupportSQLiteQuery query);
+
+ @Transaction
@Query("SELECT * FROM card WHERE accountId = :accountId AND stackId = :localStackId order by `order`, createdAt asc")
List<FullCard> getFullCardsForStackDirectly(final long accountId, final long localStackId);
@Transaction
@Query("SELECT * FROM card WHERE accountId = :accountId and localId = :localCardId")
LiveData<FullCard> getFullCardByLocalId(final long accountId, final long localCardId);
+ @Transaction
+ @Query("SELECT * FROM card WHERE accountId = :accountId and localId = :localCardId")
+ LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(final long accountId, final long localCardId);
@Transaction
@Query("SELECT * FROM card WHERE accountId = :accountId and id = :remoteId")
@@ -66,9 +74,15 @@ public interface CardDao extends GenericDao<Card> {
@Query("SELECT * FROM card c WHERE accountId = :accountId and exists ( select 1 from DeckComment dc where dc.objectId = c.localId and dc.status<>1)")
List<Card> getCardsWithLocallyChangedCommentsDirectly(Long accountId);
+ @Query("SELECT * FROM card c WHERE stackId = :localStackId and exists ( select 1 from DeckComment dc where dc.objectId = c.localId and dc.status<>1)")
+ List<Card> getCardsWithLocallyChangedCommentsForStackDirectly(Long localStackId);
+
@Query("SELECT count(*) FROM card c WHERE accountId = :accountId and stackId = :localStackId and status <> 3")
LiveData<Integer> countCardsInStack(long accountId, long localStackId);
@Query("SELECT coalesce(MAX(`order`), -1) FROM card c WHERE stackId = :localStackId and status <> 3")
Integer getHighestOrderInStack(Long localStackId);
+
+ @Query("SELECT c.stackId FROM card c WHERE localId = :localCardId")
+ Long getLocalStackIdByLocalCardId(Long localCardId);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java
index c96c64e7d..26e6c65a8 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java
@@ -40,6 +40,9 @@ public interface JoinCardWithLabelDao extends GenericDao<JoinCardWithLabel> {
@Query("select * from joincardwithlabel WHERE status <> 1") // not UP_TO_DATE
List<JoinCardWithLabel> getAllChangedJoins();
+ @Query("select j.* from joincardwithlabel j inner join card c on j.cardId = c.localId WHERE c.stackId = :localStackId and j.status <> 1") // not UP_TO_DATE
+ List<JoinCardWithLabel> getAllChangedJoinsForStack(Long localStackId);
+
@Query("delete from joincardwithlabel " +
"where cardId = (select c.localId from card c where c.accountId = :accountId and c.id = :remoteCardId) " +
"and labelId = (select l.localId from label l where l.accountId = :accountId and l.id = :remoteLabelId)")
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java
index 46554685f..416d52eed 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java
@@ -25,7 +25,14 @@ public interface JoinCardWithUserDao extends GenericDao<JoinCardWithUser> {
"inner join card c on j.cardId = c.localId " +
"inner join user u on j.userId = u.localId " +
"WHERE j.status <> 1") // not UP_TO_DATE
- List<JoinCardWithUser> getDeletedJoinsWithRemoteIDs();
+ List<JoinCardWithUser> getChangedJoinsWithRemoteIDs();
+
+ @Query("select u.localId as userId, c.id as cardId, j.status from joincardwithuser j " +
+ "inner join card c on j.cardId = c.localId " +
+ "inner join user u on j.userId = u.localId " +
+ "WHERE c.stackId = :localStackId " +
+ "AND j.status <> 1") // not UP_TO_DATE
+ List<JoinCardWithUser> getChangedJoinsWithRemoteIDsForStack(Long localStackId);
@Query("delete from joincardwithuser " +
"where cardId = (select c.localId from card c where c.accountId = :accountId and c.id = :remoteCardId) " +
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java
index 0fbccbe08..feb7e453b 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java
@@ -13,7 +13,7 @@ import it.niedermann.nextcloud.deck.model.full.FullStack;
@Dao
public interface StackDao extends GenericDao<Stack> {
- @Query("SELECT * FROM stack WHERE accountId = :accountId AND boardId = :localBoardId order by `order` asc")
+ @Query("SELECT * FROM stack WHERE accountId = :accountId AND boardId = :localBoardId and status<>3 and (deletedAt is null or deletedAt = 0) order by `order` asc")
LiveData<List<Stack>> getStacksForBoard(final long accountId, final long localBoardId);
@Query("SELECT * FROM stack WHERE accountId = :accountId and boardId = :localBoardId and id = :remoteId")
@@ -31,10 +31,6 @@ public interface StackDao extends GenericDao<Stack> {
FullStack getFullStackByRemoteIdDirectly(final long accountId, final long localBoardId, final long remoteId);
@Transaction
- @Query("SELECT * FROM stack WHERE accountId = :accountId AND boardId = :localBoardId and status<>3 and (deletedAt is null or deletedAt = 0) order by `order` asc")
- LiveData<List<FullStack>> getFullStacksForBoard(final long accountId, final long localBoardId);
-
- @Transaction
@Query("SELECT * FROM stack WHERE accountId = :accountId and boardId = :localBoardId and id = :remoteId")
LiveData<FullStack> getFullStackByRemoteId(final long accountId, final long localBoardId, final long remoteId);
@@ -56,4 +52,7 @@ public interface StackDao extends GenericDao<Stack> {
@Query("SELECT localId FROM stack s WHERE accountId = :accountId and id = :stackId")
Long getLocalStackIdByRemoteStackIdDirectly(long accountId, Long stackId);
+
+ @Query("SELECT coalesce(MAX(`order`), -1) FROM stack s WHERE boardId = :localBoardId")
+ Integer getHighestStackOrderInBoard(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 6f712d07f..6e42cdaae 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
@@ -26,12 +26,16 @@ public interface UserDao extends GenericDao<User> {
" where ju.userId = u.localId" +
" and ju.cardId = :notYetAssignedToLocalCardId AND status <> 3" + // not LOCAL_DELETED
" )" +
- " AND" +
- " (" +
- " EXISTS (" +
- " select 1 from accesscontrol" +
- " where userId = u.localId and boardId = :boardId" +
- " )" +
+ " AND ( " +
+ " EXISTS (" +
+ " select 1 from userinboard where boardId = :boardId AND userId = u.localId" +
+ " )" +
+ " OR" +
+ " EXISTS (" +
+ " select 1 from accesscontrol" + // v GROUP!
+ " where (userId = u.localId OR (type = 1 and exists(select 1 from UserInGroup uig where uig.memberId = u.localId and uig.groupId = userId))) " +
+ " and boardId = :boardId and status <> 3" +
+ " )" +
" OR" +
" EXISTS (" +
" select 1 from board where localId = :boardId AND ownerId = u.localId" +
@@ -48,7 +52,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")
- LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long boardId, final String searchTerm);
+ List<User> searchUserByUidOrDisplayNameForACLDirectly(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);
@@ -66,12 +70,16 @@ public interface UserDao extends GenericDao<User> {
" where ju.userId = u.localId" +
" and ju.cardId = :notAssignedToLocalCardId AND status <> 3" + // not LOCAL_DELETED
" )" +
- " AND" +
- " (" +
- " EXISTS (" +
- " select 1 from accesscontrol" +
- " where userId = u.localId and boardId = :boardId" +
- " )" +
+ " AND ( " +
+ " EXISTS (" +
+ " select 1 from userinboard where boardId = :boardId AND userId = u.localId" +
+ " )" +
+ " OR" +
+ " EXISTS (" +
+ " select 1 from accesscontrol" + // v GROUP!
+ " where (userId = u.localId OR (type = 1 and exists(select 1 from UserInGroup uig where uig.memberId = u.localId and uig.groupId = userId))) " +
+ " and boardId = :boardId and status <> 3" +
+ " )" +
" OR" +
" EXISTS (" +
" select 1 from board where localId = :boardId AND ownerId = u.localId" +
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java
new file mode 100644
index 000000000..7a0476c53
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java
@@ -0,0 +1,12 @@
+package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao;
+
+import androidx.room.Dao;
+import androidx.room.Query;
+
+import it.niedermann.nextcloud.deck.model.relations.UserInBoard;
+
+@Dao
+public interface UserInBoardDao extends GenericDao<UserInBoard> {
+ @Query("DELETE FROM userinboard WHERE boardId = :localId")
+ void deleteByBoardId(long localId);
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java
new file mode 100644
index 000000000..fb3f5b26e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java
@@ -0,0 +1,12 @@
+package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao;
+
+import androidx.room.Dao;
+import androidx.room.Query;
+
+import it.niedermann.nextcloud.deck.model.relations.UserInGroup;
+
+@Dao
+public interface UserInGroupDao extends GenericDao<UserInGroup> {
+ @Query("DELETE FROM useringroup WHERE groupId = :localId")
+ void deleteByGroupId(long localId);
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java
new file mode 100644
index 000000000..d132796e4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java
@@ -0,0 +1,13 @@
+package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects;
+
+import androidx.room.Dao;
+import androidx.room.Query;
+
+import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao;
+
+@Dao
+public interface JoinCardWithOcsProjectDao extends GenericDao<JoinCardWithProject> {
+ @Query("select * from JoinCardWithProject where projectId = :localProjectId and cardId = :localCardId")
+ JoinCardWithProject getAssignmentByCardIdAndProjectIdDirectly(Long localCardId, Long localProjectId);
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java
new file mode 100644
index 000000000..fb0e3f836
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java
@@ -0,0 +1,13 @@
+package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects;
+
+import androidx.room.Dao;
+import androidx.room.Query;
+
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao;
+
+@Dao
+public interface OcsProjectDao extends GenericDao<OcsProject> {
+ @Query("select * from OcsProject where accountId = :accountId and id = :remoteId")
+ OcsProject getProjectByRemoteIdDirectly(long accountId, Long remoteId);
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java
new file mode 100644
index 000000000..8a544667a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java
@@ -0,0 +1,25 @@
+package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Query;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao;
+
+@Dao
+public interface OcsProjectResourceDao extends GenericDao<OcsProjectResource> {
+ @Query("delete from OcsProjectResource where projectId = :localProjectId")
+ void deleteByProjectId(Long localProjectId);
+
+ @Query("select * from OcsProjectResource where projectId = :localProjectId")
+ LiveData<List<OcsProjectResource>> getResourcesByLocalProjectId(Long localProjectId);
+
+ @Query("select count(id) from OcsProjectResource where projectId = :localProjectId")
+ int countProjectResourcesInProjectDirectly(Long localProjectId);
+
+ @Query("select count(id) from OcsProjectResource where projectId = :localProjectId")
+ LiveData<Integer> countProjectResourcesInProject(Long localProjectId);
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/SingleCardWidgetModelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/SingleCardWidgetModelDao.java
index 0c2a485c1..a26269c30 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/SingleCardWidgetModelDao.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/SingleCardWidgetModelDao.java
@@ -1,4 +1,4 @@
-package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao;
+package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets;
import androidx.room.Dao;
import androidx.room.Query;
@@ -6,6 +6,7 @@ import androidx.room.Transaction;
import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel;
import it.niedermann.nextcloud.deck.model.widget.singlecard.SingleCardWidgetModel;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao;
@Dao
public interface SingleCardWidgetModelDao extends GenericDao<SingleCardWidgetModel> {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java
new file mode 100644
index 000000000..9b370e32b
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java
@@ -0,0 +1,19 @@
+package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets;
+
+import androidx.room.Dao;
+import androidx.room.Query;
+import androidx.room.Transaction;
+
+import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao;
+
+@Dao
+public interface StackWidgetModelDao extends GenericDao<StackWidgetModel> {
+
+ @Query("SELECT * FROM stackwidgetmodel WHERE appwidgetid = :appWidgetId")
+ StackWidgetModel getStackWidgetByAppWidgetIdDirectly(final int appWidgetId);
+
+ @Transaction
+ @Query("SELECT EXISTS (SELECT 1 FROM stackwidgetmodel WHERE stackId in (:stackLocalIds))")
+ boolean containsStackLocalId(final long... stackLocalIds);
+}
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
index 038fcbad8..003bceb26 100644
--- 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
@@ -1,5 +1,6 @@
package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.MutableLiveData;
@@ -28,10 +29,13 @@ public class WrappedLiveData<T> extends MutableLiveData<T> {
}
public void postError(@Nullable Throwable error) {
+ postError(error, null);
+ }
+ public void postError(@Nullable Throwable error, @NonNull T locallyCreatedEntity) {
if (error == null) {
DeckLog.warn("Given error is null");
}
setError(error);
- postValue(null);
+ postValue(locallyCreatedEntity);
}
}
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
new file mode 100644
index 000000000..fca4ca369
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java
@@ -0,0 +1,75 @@
+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<T, TimerTask>();
+ 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
new file mode 100644
index 000000000..179d816eb
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java
@@ -0,0 +1,93 @@
+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.IResponseCallback;
+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 DataBaseAdapter db;
+ private ServerAdapter server;
+ long accountId;
+ String searchTerm;
+ long notYetAssignedInACL;
+ private 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 IResponseCallback<OcsUserList>(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/DataPropagationHelper.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/DataPropagationHelper.java
index 9590f5abd..782b6d951 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/DataPropagationHelper.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/DataPropagationHelper.java
@@ -34,27 +34,31 @@ public class DataPropagationHelper {
entity.setLocalId(newID);
boolean connected = serverAdapter.hasInternetConnection();
if (connected) {
- provider.createOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(new Account(accountId)) {
- @Override
- public void onResponse(T response) {
- new Thread(() -> {
- response.setAccountId(accountId);
- response.setLocalId(newID);
- if (actionOnResponse!= null) {
- actionOnResponse.onResponse(entity, response);
- }
- response.setStatus(DBStatus.UP_TO_DATE.getId());
- provider.updateInDB(dataBaseAdapter, accountId, response, false);
- callback.onResponse(response);
- }).start();
- }
+ try {
+ provider.createOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(new Account(accountId)) {
+ @Override
+ public void onResponse(T response) {
+ new Thread(() -> {
+ response.setAccountId(accountId);
+ response.setLocalId(newID);
+ if (actionOnResponse != null) {
+ actionOnResponse.onResponse(entity, response);
+ }
+ response.setStatus(DBStatus.UP_TO_DATE.getId());
+ provider.updateInDB(dataBaseAdapter, accountId, response, false);
+ callback.onResponse(response);
+ }).start();
+ }
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- new Thread(() -> callback.onError(throwable)).start();
- }
- }, entity);
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ new Thread(() -> callback.onError(throwable, entity)).start();
+ }
+ }, entity);
+ } catch (Throwable t) {
+ callback.onError(t, entity);
+ }
} else {
callback.onResponse(entity);
}
@@ -71,24 +75,28 @@ public class DataPropagationHelper {
}
boolean connected = serverAdapter.hasInternetConnection();
if (entity.getId() != null && connected) {
- provider.updateOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(new Account(accountId)) {
- @Override
- public void onResponse(T response) {
- new Thread(() -> {
- entity.setStatus(DBStatus.UP_TO_DATE.getId());
- provider.updateInDB(dataBaseAdapter, accountId, entity, false);
- callback.onResponse(entity);
- }).start();
- }
+ try {
+ provider.updateOnServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<T>(new Account(accountId)) {
+ @Override
+ public void onResponse(T response) {
+ new Thread(() -> {
+ entity.setStatus(DBStatus.UP_TO_DATE.getId());
+ provider.updateInDB(dataBaseAdapter, accountId, entity, false);
+ callback.onResponse(entity);
+ }).start();
+ }
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- new Thread(() -> {
- callback.onError(throwable);
- }).start();
- }
- }, entity);
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ new Thread(() -> {
+ callback.onError(throwable, entity);
+ }).start();
+ }
+ }, entity);
+ } catch (Throwable t) {
+ callback.onError(t, entity);
+ }
} else {
callback.onResponse(entity);
}
@@ -98,23 +106,28 @@ public class DataPropagationHelper {
provider.deleteInDB(dataBaseAdapter, accountId, entity);
boolean connected = serverAdapter.hasInternetConnection();
if (entity.getId() != null && connected) {
- provider.deleteOnServer(serverAdapter, accountId, new IResponseCallback<Void>(new Account(accountId)) {
- @Override
- public void onResponse(Void response) {
- new Thread(() -> {
- provider.deletePhysicallyInDB(dataBaseAdapter, accountId, entity);
- callback.onResponse(null);
- }).start();
- }
+ try {
+ provider.deleteOnServer(serverAdapter, accountId, new IResponseCallback<Void>(new Account(accountId)) {
+ @Override
+ public void onResponse(Void response) {
+ new Thread(() -> {
+ provider.deletePhysicallyInDB(dataBaseAdapter, accountId, entity);
+ callback.onResponse(null);
+ }).start();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ new Thread(() -> {
+ callback.onError(throwable);
+ }).start();
+ }
+ }, entity, dataBaseAdapter);
+ } catch (Throwable t) {
+ callback.onError(t);
+ }
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- new Thread(() -> {
- callback.onError(throwable);
- }).start();
- }
- }, entity, dataBaseAdapter);
} else {
callback.onResponse(null);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java
index 18d8c3672..17c0de2cf 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java
@@ -1,6 +1,12 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers;
-import java.util.Date;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+
+import java.net.HttpURLConnection;
+import java.time.Instant;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -15,28 +21,33 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AbstractS
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.IRelationshipProvider;
public class SyncHelper {
- private ServerAdapter serverAdapter;
- private DataBaseAdapter dataBaseAdapter;
+ private final ServerAdapter serverAdapter;
+ private final DataBaseAdapter dataBaseAdapter;
private Account account;
private long accountId;
private IResponseCallback<Boolean> responseCallback;
- private Date lastSync;
+ private final Instant lastSync;
- public SyncHelper(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, Date lastSync) {
+ public SyncHelper(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, Instant lastSync) {
this.serverAdapter = serverAdapter;
this.dataBaseAdapter = dataBaseAdapter;
this.lastSync = lastSync;
}
// Sync Server -> App
- public <T extends IRemoteEntity> void doSyncFor(final AbstractSyncDataProvider<T> provider){
+ public <T extends IRemoteEntity> void doSyncFor(@NonNull final AbstractSyncDataProvider<T> provider) {
provider.registerChildInParent(provider);
- provider.getAllFromServer(serverAdapter, accountId, new IResponseCallback<List<T>>(account) {
+ provider.getAllFromServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<List<T>>(account) {
@Override
public void onResponse(List<T> response) {
if (response != null) {
provider.goingDeeper();
for (T entityFromServer : response) {
+ if (entityFromServer == null) {
+ // see https://github.com/stefan-niedermann/nextcloud-deck/issues/574
+ DeckLog.error("Skipped null value from server for DataProvider: " + provider.getClass().getSimpleName());
+ continue;
+ }
entityFromServer.setAccountId(accountId);
T existingEntity = provider.getSingleFromDB(dataBaseAdapter, accountId, entityFromServer);
@@ -44,13 +55,14 @@ public class SyncHelper {
provider.createInDB(dataBaseAdapter, accountId, entityFromServer);
} else {
//TODO: how to handle deletes? what about archived?
- if (existingEntity.getStatus() != DBStatus.UP_TO_DATE.getId()){
- DeckLog.log("Conflicting changes on entity: "+existingEntity);
+ if (existingEntity.getStatus() != DBStatus.UP_TO_DATE.getId()) {
+ DeckLog.warn("Conflicting changes on entity: " + existingEntity);
// TODO: what to do?
} else {
-// if (existingEntity.getLastModified().getTime() == entityFromServer.getLastModified().getTime()) {
-// continue; // TODO: is this is ok for sure? -> isn`t! NPE
-// }
+ if (entityFromServer.getEtag() != null && entityFromServer.getEtag().equals(existingEntity.getEtag())) {
+ DeckLog.log("[" + provider.getClass().getSimpleName() + "] ETags do match! skipping " + existingEntity.getClass().getSimpleName() + " with localId: " + existingEntity.getLocalId());
+ continue;
+ }
provider.updateInDB(dataBaseAdapter, accountId, applyUpdatesFromRemote(provider, existingEntity, entityFromServer, accountId), false);
}
}
@@ -58,7 +70,7 @@ public class SyncHelper {
provider.goDeeper(SyncHelper.this, existingEntity, entityFromServer, responseCallback);
}
- provider.handleDeletes(serverAdapter, dataBaseAdapter, accountId, response);
+ provider.handleDeletes(serverAdapter, dataBaseAdapter, accountId, response);
provider.doneGoingDeeper(responseCallback, true);
} else {
@@ -68,25 +80,35 @@ public class SyncHelper {
@Override
public void onError(Throwable throwable) {
+ super.onError(throwable);
+ if (throwable.getClass() == NextcloudHttpRequestFailedException.class) {
+ NextcloudHttpRequestFailedException requestFailedException = (NextcloudHttpRequestFailedException) throwable;
+ if (HttpURLConnection.HTTP_NOT_MODIFIED == requestFailedException.getStatusCode()){
+ DeckLog.log("[" + provider.getClass().getSimpleName() + "] ETags do match! skipping this one.");
+ // well, etags say we're fine here. no need to go deeper.
+ provider.childDone(provider, responseCallback, false);
+ return;
+ }
+ }
provider.onError(throwable, responseCallback);
- DeckLog.logError(throwable);
responseCallback.onError(throwable);
}
}, lastSync);
}
// Sync App -> Server
- public <T extends IRemoteEntity> void doUpSyncFor(AbstractSyncDataProvider<T> provider){
+ public <T extends IRemoteEntity> void doUpSyncFor(@NonNull AbstractSyncDataProvider<T> provider) {
doUpSyncFor(provider, null);
}
- public <T extends IRemoteEntity> void doUpSyncFor(AbstractSyncDataProvider<T> provider, CountDownLatch countDownLatch){
- List<T> allFromDB = provider.getAllChangedFromDB(dataBaseAdapter, accountId, lastSync);
+
+ public <T extends IRemoteEntity> void doUpSyncFor(@NonNull AbstractSyncDataProvider<T> provider, @Nullable CountDownLatch countDownLatch) {
+ final List<T> allFromDB = provider.getAllChangedFromDB(dataBaseAdapter, accountId, lastSync);
if (allFromDB != null && !allFromDB.isEmpty()) {
for (T entity : allFromDB) {
- if (entity.getId()!=null) {
+ if (entity.getId() != null) {
if (entity.getStatusEnum() == DBStatus.LOCAL_DELETED) {
provider.deleteOnServer(serverAdapter, accountId, getDeleteCallback(provider, entity), entity, dataBaseAdapter);
- if (countDownLatch != null){
+ if (countDownLatch != null) {
countDownLatch.countDown();
}
} else {
@@ -98,13 +120,13 @@ public class SyncHelper {
}
} else {
provider.goDeeperForUpSync(this, serverAdapter, dataBaseAdapter, responseCallback);
- if (countDownLatch != null){
+ if (countDownLatch != null) {
countDownLatch.countDown();
}
}
}
- private <T extends IRemoteEntity> IResponseCallback<Void> getDeleteCallback(AbstractSyncDataProvider<T> provider, T entity) {
+ private <T extends IRemoteEntity> IResponseCallback<Void> getDeleteCallback(@NonNull AbstractSyncDataProvider<T> provider, T entity) {
return new IResponseCallback<Void>(account) {
@Override
public void onResponse(Void response) {
@@ -120,16 +142,17 @@ public class SyncHelper {
};
}
- private <T extends IRemoteEntity> IResponseCallback<T> getUpdateCallback(AbstractSyncDataProvider<T> provider, T entity, CountDownLatch countDownLatch) {
+ private <T extends IRemoteEntity> IResponseCallback<T> getUpdateCallback(@NonNull AbstractSyncDataProvider<T> provider, @NonNull T entity, @Nullable CountDownLatch countDownLatch) {
return new IResponseCallback<T>(account) {
@Override
public void onResponse(T response) {
response.setAccountId(this.account.getId());
T update = applyUpdatesFromRemote(provider, entity, response, accountId);
+ update.setId(response.getId());
update.setStatus(DBStatus.UP_TO_DATE.getId());
provider.updateInDB(dataBaseAdapter, accountId, update, false);
provider.goDeeperForUpSync(SyncHelper.this, serverAdapter, dataBaseAdapter, responseCallback);
- if (countDownLatch != null){
+ if (countDownLatch != null) {
countDownLatch.countDown();
}
}
@@ -138,20 +161,20 @@ public class SyncHelper {
public void onError(Throwable throwable) {
super.onError(throwable);
responseCallback.onError(throwable);
- if (countDownLatch != null){
+ if (countDownLatch != null) {
countDownLatch.countDown();
}
}
};
}
- public void fixRelations(IRelationshipProvider relationshipProvider) {
+ public void fixRelations(@NonNull IRelationshipProvider relationshipProvider) {
// this is OK, since the delete only affects records with status UP_TO_DATE
relationshipProvider.deleteAllExisting(dataBaseAdapter, accountId);
relationshipProvider.insertAllNecessary(dataBaseAdapter, accountId);
}
- private <T extends IRemoteEntity> T applyUpdatesFromRemote(AbstractSyncDataProvider<T> provider, T localEntity, T remoteEntity, Long accountId) {
+ private <T extends IRemoteEntity> T applyUpdatesFromRemote(@NonNull AbstractSyncDataProvider<T> provider, @NonNull T localEntity, @NonNull T remoteEntity, @NonNull Long accountId) {
if (!accountId.equals(localEntity.getAccountId())) {
throw new IllegalArgumentException("IDs of Accounts are not matching! WTF are you doin?!");
}
@@ -160,7 +183,7 @@ public class SyncHelper {
return remoteEntity;
}
- public SyncHelper setResponseCallback(IResponseCallback<Boolean> callback) {
+ public SyncHelper setResponseCallback(@NonNull IResponseCallback<Boolean> callback) {
this.responseCallback = callback;
this.account = responseCallback.getAccount();
accountId = account.getId();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java
index 166d6c519..11fb38a66 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java
@@ -1,9 +1,10 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
+import java.time.Instant;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
+import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter;
@@ -26,28 +27,37 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> {
}
}
- public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<T> entitiesFromServer){
+ public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<T> entitiesFromServer) {
// do nothing as a default.
}
/**
* Searches each entry of <code>listB</code> in list <code>listA</code> and returns the missing ones
+ *
* @param listA List
* @param listB List
* @return all entries of <code>listB</code> missing in <code>listA</code>
*/
- public static <T extends IRemoteEntity> List<T> findDelta(List<T> listA, List<T> listB){
+ public static <T extends IRemoteEntity> List<T> findDelta(List<T> listA, List<T> listB) {
List<T> delta = new ArrayList<>();
for (T b : listB) {
+ if (b == null) {
+ DeckLog.error("Entry in listB is null! skipping...");
+ continue;
+ }
boolean found = false;
for (T a : listA) {
- if ((a.getLocalId()!= null && b.getLocalId()!= null ? (a.getLocalId().equals(b.getLocalId()))
+ if (a == null) {
+ DeckLog.error("Entry in listA is null! skipping...");
+ continue;
+ }
+ if ((a.getLocalId() != null && b.getLocalId() != null ? (a.getLocalId().equals(b.getLocalId()))
: a.getId().equals(b.getId())) && b.getAccountId() == a.getAccountId()) {
found = true;
break;
}
}
- if (!found){
+ if (!found) {
delta.add(b);
}
}
@@ -58,7 +68,14 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> {
children.add(child);
}
- public abstract void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<T>> responder, Date lastSync);
+ public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<T>> responder, Instant lastSync) {
+ return;
+ }
+
+ public void getAllFromServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<List<T>> responder, Instant lastSync) {
+ // Overridden, because we also need the DB-Adapter at some points here (see ACL data provider)
+ getAllFromServer(serverAdapter, accountId, responder, lastSync);
+ }
public abstract T getSingleFromDB(DataBaseAdapter dataBaseAdapter, long accountId, T entity);
@@ -72,7 +89,7 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> {
public abstract void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, T t);
- public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, T t){
+ public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, T t) {
deleteInDB(dataBaseAdapter, accountId, t);
}
@@ -106,7 +123,7 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> {
stillGoingDeeper = true;
}
- public abstract List<T> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync);
+ public abstract List<T> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync);
public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) {
//do nothing
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java
index 3242ab7c1..6dde6b45c 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java
@@ -1,17 +1,24 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
-import java.util.Date;
+import java.time.Instant;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import it.niedermann.nextcloud.deck.DeckLog;
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.User;
import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs;
+import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser;
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.helpers.util.AsyncUtil;
public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessControl> {
+ private static final Long TYPE_GROUP = 1L;
private List<AccessControl> acl;
private FullBoard board;
@@ -22,10 +29,66 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo
}
@Override
- public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<AccessControl>> responder, Date lastSync) {
+ public void getAllFromServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<List<AccessControl>> responder, Instant lastSync) {
+ AsyncUtil.awaitAsyncWork(acl.size(), latch -> {
+ for (AccessControl accessControl : acl) {
+ if (accessControl.getType() == TYPE_GROUP) {
+ serverAdapter.searchGroupMembers(accessControl.getUser().getUid(), new IResponseCallback<GroupMemberUIDs>(responder.getAccount()) {
+ @Override
+ public void onResponse(GroupMemberUIDs response) {
+ accessControl.setGroupMemberUIDs(response);
+ if (response.getUids().size() > 0) {
+ ensureGroupMembersInDB(getAccount(), dataBaseAdapter, serverAdapter, response);
+ }
+ latch.countDown();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ latch.countDown();
+ }
+ });
+ } else latch.countDown();
+ }
+ });
+
responder.onResponse(acl);
}
+ private void ensureGroupMembersInDB(Account account, DataBaseAdapter dataBaseAdapter, ServerAdapter serverAdapter, GroupMemberUIDs response) {
+ CountDownLatch memberLatch = new CountDownLatch(response.getUids().size());
+ for (String uid : response.getUids()) {
+ User user = dataBaseAdapter.getUserByUidDirectly(account.getId(), uid);
+ if (user == null) {
+ // unknown user. fetch!
+ serverAdapter.getSingleUserData(uid, new IResponseCallback<OcsUser>(account) {
+ @Override
+ public void onResponse(OcsUser response) {
+ DeckLog.log(response.toString());
+ User user = new User();
+ user.setUid(response.getId());
+ user.setPrimaryKey(response.getId());
+ user.setDisplayname(response.getDisplayName());
+ dataBaseAdapter.createUser(account.getId(), user);
+ memberLatch.countDown();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ memberLatch.countDown();
+ }
+ });
+ } else memberLatch.countDown();
+ }
+ try {
+ memberLatch.await();
+ } catch (InterruptedException e) {
+ DeckLog.logError(e);
+ }
+ }
+
@Override
public AccessControl getSingleFromDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity) {
return dataBaseAdapter.getAccessControlByRemoteIdDirectly(accountId, entity.getEntity().getId());
@@ -34,7 +97,26 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo
@Override
public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity) {
prepareUser(dataBaseAdapter, accountId, entity);
- return dataBaseAdapter.createAccessControl(accountId, entity);
+ long newId = dataBaseAdapter.createAccessControl(accountId, entity);
+ entity.setLocalId(newId);
+ handleGroupMemberships(dataBaseAdapter, entity);
+ return newId;
+ }
+
+ private void handleGroupMemberships(DataBaseAdapter dataBaseAdapter, AccessControl entity) {
+ if (entity.getType() != TYPE_GROUP) {
+ return;
+ }
+ dataBaseAdapter.deleteGroupMembershipsOfGroup(entity.getUser().getLocalId());
+ if (entity.getGroupMemberUIDs() == null) {
+ return;
+ }
+ for (String groupMemberUID : entity.getGroupMemberUIDs().getUids()) {
+ User member = dataBaseAdapter.getUserByUidDirectly(entity.getAccountId(), groupMemberUID);
+ if (member != null) {
+ dataBaseAdapter.addUserToGroup(entity.getUserId(), member.getLocalId());
+ }
+ }
}
private void prepareUser(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity) {
@@ -42,6 +124,7 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo
if (user == null) {
long userId = dataBaseAdapter.createUser(accountId, entity.getUser());
entity.setUserId(userId);
+ entity.getUser().setLocalId(userId);
} else {
entity.setUserId(user.getLocalId());
entity.getUser().setLocalId(user.getLocalId());
@@ -52,7 +135,9 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo
@Override
public void updateInDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity, boolean setStatus) {
prepareUser(dataBaseAdapter, accountId, entity);
+ entity.setBoardId(board.getLocalId());
dataBaseAdapter.updateAccessControl(entity, setStatus);
+ handleGroupMemberships(dataBaseAdapter, entity);
}
@Override
@@ -64,6 +149,9 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo
public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<AccessControl> responder, AccessControl entity) {
AccessControl acl = new AccessControl(entity);
acl.setBoardId(board.getBoard().getId());
+ if (acl.getUser() == null && acl.getUserId() != null) {
+ acl.setUser(dataBaseAdapter.getUserByLocalIdDirectly(acl.getUserId()));
+ }
serverAdapter.createAccessControl(board.getBoard().getId(), acl, responder);
}
@@ -79,6 +167,7 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo
@Override
public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl accessControl) {
+ dataBaseAdapter.deleteGroupMembershipsOfGroup(accessControl.getUser().getLocalId());
dataBaseAdapter.deleteAccessControl(accessControl, false);
}
@@ -88,7 +177,7 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo
}
@Override
- public List<AccessControl> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) {
+ public List<AccessControl> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
return dataBaseAdapter.getLocallyChangedAccessControl(accountId, board.getLocalId());
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java
index a3785ca78..5a7abf732 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java
@@ -1,7 +1,9 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
-import java.util.ArrayList;
-import java.util.Date;
+import androidx.annotation.NonNull;
+
+import java.time.Instant;
+import java.util.Collections;
import java.util.List;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
@@ -12,15 +14,16 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter
public class ActivityDataProvider extends AbstractSyncDataProvider<Activity> {
- protected Card card;
+ @NonNull
+ private final Card card;
- public ActivityDataProvider(AbstractSyncDataProvider<?> parent, Card card) {
+ public ActivityDataProvider(AbstractSyncDataProvider<?> parent, @NonNull Card card) {
super(parent);
this.card = card;
}
@Override
- public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Activity>> responder, Date lastSync) {
+ public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Activity>> responder, Instant lastSync) {
serverAdapter.getActivitiesForCard(card.getId(), responder);
}
@@ -65,7 +68,7 @@ public class ActivityDataProvider extends AbstractSyncDataProvider<Activity> {
}
@Override
- public List<Activity> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) {
- return new ArrayList<>();
+ public List<Activity> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
+ return Collections.emptyList();
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AttachmentDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AttachmentDataProvider.java
index 37e00dada..781f7b1f4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AttachmentDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AttachmentDataProvider.java
@@ -4,7 +4,7 @@ import android.net.Uri;
import java.io.File;
import java.io.IOException;
-import java.util.Date;
+import java.time.Instant;
import java.util.List;
import it.niedermann.nextcloud.deck.DeckLog;
@@ -32,7 +32,7 @@ public class AttachmentDataProvider extends AbstractSyncDataProvider<Attachment>
}
@Override
- public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Attachment>> responder, Date lastSync) {
+ public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Attachment>> responder, Instant lastSync) {
responder.onResponse(attachments);
}
@@ -104,7 +104,7 @@ public class AttachmentDataProvider extends AbstractSyncDataProvider<Attachment>
}
@Override
- public List<Attachment> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) {
+ public List<Attachment> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
return dataBaseAdapter.getLocallyChangedAttachmentsByLocalCardIdDirectly(accountId, card.getLocalId());
}
@@ -120,7 +120,7 @@ public class AttachmentDataProvider extends AbstractSyncDataProvider<Attachment>
dataBaseAdapter.deleteAttachment(accountId, attachment, false);
}
for (Attachment attachment : entitiesFromServer) {
- if (attachment.getDeletedAt() != null && attachment.getDeletedAt().getTime() != 0) {
+ if (attachment.getDeletedAt() != null && attachment.getDeletedAt().toEpochMilli() != 0) {
Attachment toDelete = dataBaseAdapter.getAttachmentByRemoteIdDirectly(accountId, attachment.getId());
if (toDelete != null) {
dataBaseAdapter.deleteAttachment(accountId, toDelete, false);
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 98163029f..f5e071adb 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
@@ -1,14 +1,16 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
+import android.annotation.SuppressLint;
+
+import com.nextcloud.android.sso.api.ParsedResponse;
+
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.model.AccessControl;
import it.niedermann.nextcloud.deck.model.Board;
@@ -19,16 +21,33 @@ import it.niedermann.nextcloud.deck.model.full.FullStack;
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.helpers.SyncHelper;
+import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.AsyncUtil;
public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> {
- public BoardDataProvider(){
+ public BoardDataProvider() {
super(null);
}
@Override
- public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullBoard>> responder, Date lastSync) {
- serverAdapter.getBoards(responder);
+ public void getAllFromServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<List<FullBoard>> responder, Instant lastSync) {
+ serverAdapter.getBoards(new IResponseCallback<ParsedResponse<List<FullBoard>>>(responder.getAccount()) {
+ @Override
+ public void onResponse(ParsedResponse<List<FullBoard>> response) {
+ String etag = response.getHeaders().get("ETag");
+ if (etag != null && !etag.equals(account.getBoardsEtag())) {
+ account.setBoardsEtag(etag);
+ dataBaseAdapter.updateAccount(account);
+ }
+ responder.onResponse(response.getResponse());
+ }
+
+ @SuppressLint("MissingSuperCall")
+ @Override
+ public void onError(Throwable throwable) {
+ responder.onError(throwable);
+ }
+ });
}
@Override
@@ -39,27 +58,65 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> {
@Override
public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity) {
handleOwner(dataBaseAdapter, accountId, entity);
- return dataBaseAdapter.createBoardDirectly(accountId, entity.getBoard());
+ Long localId = dataBaseAdapter.createBoardDirectly(accountId, entity.getBoard());
+ entity.getBoard().setLocalId(localId);
+ handleUsers(dataBaseAdapter, accountId, entity);
+ return localId;
}
private void handleOwner(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity) {
- if (entity.getOwner()!=null) {
- User remoteOwner = entity.getOwner();
- User owner = dataBaseAdapter.getUserByUidDirectly(accountId, remoteOwner.getUid());
- if (owner == null){
- dataBaseAdapter.createUser(accountId, remoteOwner);
- } else {
- dataBaseAdapter.updateUser(accountId, remoteOwner, false);
- }
- owner = dataBaseAdapter.getUserByUidDirectly(accountId, remoteOwner.getUid());
+ if (entity.getOwner() != null) {
+ User owner = createOrUpdateUser(dataBaseAdapter, accountId, entity.getOwner());
entity.getBoard().setOwnerId(owner.getLocalId());
}
}
+ private void handleUsers(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity) {
+ dataBaseAdapter.deleteBoardMembershipsOfBoard(entity.getLocalId());
+ if (entity.getUsers() != null && !entity.getUsers().isEmpty()) {
+ for (User user : entity.getUsers()) {
+ if (user == null) {
+ continue;
+ }
+ User existing = createOrUpdateUser(dataBaseAdapter, accountId, user);
+ dataBaseAdapter.addUserToBoard(existing.getLocalId(), entity.getLocalId());
+ }
+ }
+ }
+
+ private User createOrUpdateUser(DataBaseAdapter dataBaseAdapter, long accountId, User remoteUser) {
+ User owner = dataBaseAdapter.getUserByUidDirectly(accountId, remoteUser.getUid());
+ if (owner == null) {
+ dataBaseAdapter.createUser(accountId, remoteUser);
+ } else {
+ dataBaseAdapter.updateUser(accountId, remoteUser, false);
+ }
+ return dataBaseAdapter.getUserByUidDirectly(accountId, remoteUser.getUid());
+ }
+
@Override
public void updateInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity, boolean setStatus) {
+ handleDefaultLabels(dataBaseAdapter, entity);
handleOwner(dataBaseAdapter, accountId, entity);
dataBaseAdapter.updateBoard(entity.getBoard(), setStatus);
+ handleUsers(dataBaseAdapter, accountId, entity);
+ }
+
+ private void handleDefaultLabels(DataBaseAdapter dataBaseAdapter, FullBoard entity) {
+ // ## merge labels (created at board creation):
+ // the server creates four default labels. if a board is copied, they will also be copied. At sync, after creating the board, the labels are already there.
+ // this merges the created default ones with the ones i already have.
+ if (entity != null && entity.getLabels() != null) {
+ for (Label label : entity.getLabels()) {
+ // does this label exist and unknown to server yet?
+ Label existing = dataBaseAdapter.getLabelByBoardIdAndTitleDirectly(entity.getLocalId(), label.getTitle());
+ if (existing != null && existing.getId() == null) {
+ // take our label and lets say it IS the same as on server (but use the local color, no matter what the server says)
+ existing.setId(label.getId());
+ dataBaseAdapter.updateLabel(existing, false);
+ }
+ }
+ }
}
@Override
@@ -71,19 +128,19 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> {
@Override
public void goDeeper(SyncHelper syncHelper, FullBoard existingEntity, FullBoard entityFromServer, IResponseCallback<Boolean> callback) {
List<Label> labels = entityFromServer.getLabels();
- if (labels != null && !labels.isEmpty()){
+ if (labels != null && !labels.isEmpty()) {
syncHelper.doSyncFor(new LabelDataProvider(this, existingEntity.getBoard(), labels));
}
List<AccessControl> acl = entityFromServer.getParticipants();
- if (acl != null && !acl.isEmpty()){
- for (AccessControl ac : acl){
+ if (acl != null && !acl.isEmpty()) {
+ for (AccessControl ac : acl) {
ac.setBoardId(existingEntity.getLocalId());
}
syncHelper.doSyncFor(new AccessControlDataProvider(this, existingEntity, acl));
}
- if (entityFromServer.getStacks() != null && !entityFromServer.getStacks().isEmpty()){
+ if (entityFromServer.getStacks() != null && !entityFromServer.getStacks().isEmpty()) {
syncHelper.doSyncFor(new StackDataProvider(this, existingEntity));
}
}
@@ -94,7 +151,7 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> {
}
@Override
- public List<FullBoard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) {
+ public List<FullBoard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
return dataBaseAdapter.getLocallyChangedBoards(accountId);
}
@@ -102,21 +159,17 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> {
public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) {
Long accountId = callback.getAccount().getId();
List<Label> locallyChangedLabels = dataBaseAdapter.getLocallyChangedLabels(accountId);
- CountDownLatch countDownLatch = new CountDownLatch(locallyChangedLabels.size());
- for (Label label : locallyChangedLabels) {
- Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId());
- label.setBoardId(board.getId());
- syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), countDownLatch);
- }
- try {
- countDownLatch.await();
- } catch (InterruptedException e) {
- DeckLog.logError(e);
- }
+ AsyncUtil.awaitAsyncWork(locallyChangedLabels.size(), (countDownLatch) -> {
+ for (Label label : locallyChangedLabels) {
+ Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId());
+ label.setBoardId(board.getId());
+ syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), countDownLatch);
+ }
+ });
List<Long> localBoardIDsWithChangedACL = dataBaseAdapter.getBoardIDsOfLocallyChangedAccessControl(accountId);
for (Long boardId : localBoardIDsWithChangedACL) {
- syncHelper.doUpSyncFor(new AccessControlDataProvider(this, dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, boardId) ,new ArrayList<>()));
+ syncHelper.doUpSyncFor(new AccessControlDataProvider(this, dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, boardId), new ArrayList<>()));
}
Set<Long> syncedBoards = new HashSet<>();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java
index 03e02bd70..0ceac2cd8 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java
@@ -1,12 +1,15 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
+import android.annotation.SuppressLint;
+
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Date;
import java.util.List;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.exceptions.DeckException;
import it.niedermann.nextcloud.deck.exceptions.OfflineException;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Attachment;
@@ -37,7 +40,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
}
@Override
- public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullCard>> responder, Date lastSync) {
+ public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullCard>> responder, Instant lastSync) {
List<FullCard> result = new ArrayList<>();
if (stack.getCards() == null || stack.getCards().isEmpty()) {
@@ -54,6 +57,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
}
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
responder.onError(throwable);
@@ -70,7 +74,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
@Override
public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullCard entity) {
fixRelations(dataBaseAdapter, accountId, entity);
- return dataBaseAdapter.createCard(accountId, entity.getCard());
+ return dataBaseAdapter.createCardDirectly(accountId, entity.getCard());
}
protected CardUpdate toCardUpdate(FullCard card) {
@@ -144,10 +148,17 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
} else {
DeckLog.verbose("Comments - Version is too low, DONT SYNC");
}
+ syncHelper.doSyncFor(new OcsProjectDataProvider(this, existingEntity.getCard()));
}
@Override
public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<FullCard> responder, FullCard entity) {
+ if (stack.getId() == null) {
+ responder.onError(new DeckException(DeckException.Hint.DEPENDENCY_NOT_SYNCED_YET, "Stack \"" +
+ stack.getStack().getTitle() + "\" for Card \"" + entity.getCard().getTitle() +
+ "\" is not synced yet. Perform a full sync (pull to refresh) as soon as you are online again."));
+ return;
+ }
entity.getCard().setStackId(stack.getId());
serverAdapter.createCard(board.getId(), stack.getId(), entity.getCard(), responder);
}
@@ -170,7 +181,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
}
@Override
- public List<FullCard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) {
+ public List<FullCard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
if (board == null || stack == null) {
// no cards changed!
// (see call from StackDataProvider: goDeeperForUpSync called with null for board.)
@@ -184,8 +195,13 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) {
FullStack stack;
Board board;
+ List<JoinCardWithLabel> changedLabels;
+ if (this.stack == null) {
+ changedLabels = dataBaseAdapter.getAllChangedLabelJoins();
+ } else {
+ changedLabels = dataBaseAdapter.getAllChangedLabelJoinsForStack(this.stack.getLocalId());
+ }
- List<JoinCardWithLabel> changedLabels = dataBaseAdapter.getAllChangedJoins();
Account account = callback.getAccount();
for (JoinCardWithLabel changedLabelLocal : changedLabels) {
Card card = dataBaseAdapter.getCardByLocalIdDirectly(account.getId(), changedLabelLocal.getCardId());
@@ -201,7 +217,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
board = this.board;
}
- JoinCardWithLabel changedLabel = dataBaseAdapter.getRemoteIdsForJoin(changedLabelLocal.getCardId(), changedLabelLocal.getLabelId());
+ JoinCardWithLabel changedLabel = dataBaseAdapter.getAllChangedLabelJoinsWithRemoteIDs(changedLabelLocal.getCardId(), changedLabelLocal.getLabelId());
if (changedLabel.getStatusEnum() == DBStatus.LOCAL_DELETED) {
if (changedLabel.getLabelId() == null || changedLabel.getCardId() == null) {
dataBaseAdapter.deleteJoinedLabelForCardPhysicallyByRemoteIDs(account.getId(), changedLabel.getCardId(), changedLabel.getLabelId());
@@ -229,11 +245,22 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
}
}
- List<JoinCardWithUser> deletedUsers = dataBaseAdapter.getAllDeletedUserJoinsWithRemoteIDs();
- for (JoinCardWithUser deletedUser : deletedUsers) {
- Card card = dataBaseAdapter.getCardByRemoteIdDirectly(account.getId(), deletedUser.getCardId());
+
+ List<JoinCardWithUser> changedUsers;
+ if (this.stack == null) {
+ changedUsers = dataBaseAdapter.getAllChangedUserJoinsWithRemoteIDs();
+ } else {
+ changedUsers = dataBaseAdapter.getAllChangedUserJoinsWithRemoteIDsForStack(this.stack.getLocalId());
+ }
+ for (JoinCardWithUser changedUser : changedUsers) {
+ // not already known to server?
+ if (changedUser.getCardId() == null) {
+ //skip for now
+ continue;
+ }
+ Card card = dataBaseAdapter.getCardByRemoteIdDirectly(account.getId(), changedUser.getCardId());
if (this.stack == null) {
- stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getLocalId());
+ stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId());
} else {
stack = this.stack;
}
@@ -243,16 +270,16 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
} else {
board = this.board;
}
- User user = dataBaseAdapter.getUserByLocalIdDirectly(deletedUser.getUserId());
- if (deletedUser.getStatusEnum() == DBStatus.LOCAL_DELETED) {
- serverAdapter.unassignUserFromCard(board.getId(), stack.getId(), deletedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) {
+ User user = dataBaseAdapter.getUserByLocalIdDirectly(changedUser.getUserId());
+ if (changedUser.getStatusEnum() == DBStatus.LOCAL_DELETED) {
+ serverAdapter.unassignUserFromCard(board.getId(), stack.getId(), changedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) {
@Override
public void onResponse(Void response) {
- dataBaseAdapter.deleteJoinedUserForCardPhysicallyByRemoteIDs(account.getId(), deletedUser.getCardId(), user.getUid());
+ dataBaseAdapter.deleteJoinedUserForCardPhysicallyByRemoteIDs(account.getId(), changedUser.getCardId(), user.getUid());
}
});
- } else if (deletedUser.getStatusEnum() == DBStatus.LOCAL_EDITED) {
- serverAdapter.assignUserToCard(board.getId(), stack.getId(), deletedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) {
+ } else if (changedUser.getStatusEnum() == DBStatus.LOCAL_EDITED) {
+ serverAdapter.assignUserToCard(board.getId(), stack.getId(), changedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) {
@Override
public void onResponse(Void response) {
dataBaseAdapter.setStatusForJoinCardWithUser(card.getLocalId(), user.getLocalId(), DBStatus.UP_TO_DATE.getId());
@@ -261,7 +288,12 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
}
}
- List<Attachment> attachments = dataBaseAdapter.getLocallyChangedAttachmentsDirectly(account.getId());
+ List<Attachment> attachments;
+ if (this.stack == null) {
+ attachments = dataBaseAdapter.getLocallyChangedAttachmentsDirectly(account.getId());
+ } else {
+ attachments = dataBaseAdapter.getLocallyChangedAttachmentsForStackDirectly(this.stack.getLocalId());
+ }
for (Attachment attachment : attachments) {
FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(account.getId(), attachment.getCardId());
stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId());
@@ -269,7 +301,12 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
syncHelper.doUpSyncFor(new AttachmentDataProvider(this, board, stack.getStack(), card, Collections.singletonList(attachment)));
}
- List<Card> cardsWithChangedComments = dataBaseAdapter.getCardsWithLocallyChangedCommentsDirectly(account.getId());
+ List<Card> cardsWithChangedComments;
+ if (this.stack == null) {
+ cardsWithChangedComments = dataBaseAdapter.getCardsWithLocallyChangedCommentsDirectly(account.getId());
+ } else {
+ cardsWithChangedComments = dataBaseAdapter.getCardsWithLocallyChangedCommentsForStackDirectly(this.stack.getLocalId());
+ }
for (Card card : cardsWithChangedComments) {
syncHelper.doUpSyncFor(new DeckCommentsDataProvider(this, card));
}
@@ -279,7 +316,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
@Override
public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<FullCard> entitiesFromServer) {
- List<FullCard> localCards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stack.getLocalId());
+ List<FullCard> localCards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stack.getLocalId(), null);
List<FullCard> delta = findDelta(entitiesFromServer, localCards);
for (FullCard cardToDelete : delta) {
if (cardToDelete.getId() == null) {
@@ -294,6 +331,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> {
// do not delete, it's still there and was just moved!
}
+ @SuppressLint("MissingSuperCall")
@Override
public void onError(Throwable throwable) {
if (!(throwable instanceof OfflineException)) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java
index 5b9be120f..06ef3030d 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java
@@ -1,8 +1,8 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Date;
import java.util.List;
import it.niedermann.nextcloud.deck.DeckLog;
@@ -24,10 +24,13 @@ public class DeckCommentsDataProvider extends AbstractSyncDataProvider<OcsCommen
}
@Override
- public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<OcsComment>> responder, Date lastSync) {
+ public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<OcsComment>> responder, Instant lastSync) {
serverAdapter.getCommentsForRemoteCardId(card.getId(), new IResponseCallback<OcsComment>(responder.getAccount()) {
@Override
public void onResponse(OcsComment response) {
+ if (response == null) {
+ response = new OcsComment();
+ }
List<OcsComment> comments = response.split();
Collections.sort(comments, (o1, o2) -> o1.getSingle().getCreationDateTime().compareTo(o2.getSingle().getCreationDateTime()));
verifyCommentListIntegrity(comments);
@@ -131,7 +134,7 @@ public class DeckCommentsDataProvider extends AbstractSyncDataProvider<OcsCommen
}
@Override
- public List<OcsComment> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) {
+ public List<OcsComment> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
return new OcsComment(dataBaseAdapter.getLocallyChangedCommentsByLocalCardIdDirectly(accountId, card.getLocalId())).split();
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java
index 403d71f87..caa7c68e6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java
@@ -1,8 +1,9 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
-import java.util.Date;
+import java.time.Instant;
import java.util.List;
+import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.exceptions.HandledServerErrors;
import it.niedermann.nextcloud.deck.model.Board;
@@ -12,14 +13,14 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter
public class LabelDataProvider extends AbstractSyncDataProvider<Label> {
- private List<Label> labels;
- private Board board;
+ private final List<Label> labels;
+ private final Board board;
public LabelDataProvider(AbstractSyncDataProvider<?> parent, Board board, List<Label> labels) {
super(parent);
this.board = board;
this.labels = labels;
- if (this.labels!= null && board != null){
+ if (this.labels != null && board != null) {
for (Label label : labels) {
label.setBoardId(board.getLocalId());
}
@@ -27,7 +28,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> {
}
@Override
- public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Label>> responder, Date lastSync) {
+ public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<Label>> responder, Instant lastSync) {
responder.onResponse(labels);
}
@@ -44,7 +45,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> {
updateInDB(dataBaseAdapter, accountId, entity, false);
return entity.getLocalId();
} else {
- return dataBaseAdapter.createLabel(accountId, entity);
+ return dataBaseAdapter.createLabelDirectly(accountId, entity);
}
}
@@ -53,7 +54,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> {
dataBaseAdapter.updateLabel(entity, setStatus);
}
- private IResponseCallback<Label> getLabelUniqueHandler(DataBaseAdapter dataBaseAdapter, Label entitiy, IResponseCallback<Label> responder){
+ private IResponseCallback<Label> getLabelUniqueHandler(DataBaseAdapter dataBaseAdapter, Label entitiy, IResponseCallback<Label> responder) {
return new IResponseCallback<Label>(responder.getAccount()) {
@Override
public void onResponse(Label response) {
@@ -62,10 +63,13 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> {
@Override
public void onError(Throwable throwable) {
- if (HandledServerErrors.LABELS_TITLE_MUST_BE_UNIQUE == HandledServerErrors.fromThrowable(throwable)){
+ if (HandledServerErrors.LABELS_TITLE_MUST_BE_UNIQUE == HandledServerErrors.fromThrowable(throwable)) {
+ DeckLog.log(throwable.getCause().getMessage() + ": " + entitiy.toString());
dataBaseAdapter.deleteLabelPhysically(entitiy);
+ responder.onResponse(entitiy);
+ } else {
+ responder.onError(throwable);
}
- responder.onError(throwable);
}
};
}
@@ -92,7 +96,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> {
}
@Override
- public List<Label> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) {
+ public List<Label> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
return labels;
}
@@ -105,7 +109,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> {
public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<Label> entitiesFromServer) {
List<Label> deletedLabels = findDelta(labels, dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, board.getLocalId()).getLabels());
for (Label deletedLabel : deletedLabels) {
- if (deletedLabel.getId()!=null){
+ if (deletedLabel.getId() != null) {
// preserve new, unsynced card.
dataBaseAdapter.deleteLabelPhysically(deletedLabel);
}
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
new file mode 100644
index 000000000..b9bfb8b31
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java
@@ -0,0 +1,100 @@
+package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectList;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter;
+
+public class OcsProjectDataProvider extends AbstractSyncDataProvider<OcsProject> {
+ private Card card;
+
+ public OcsProjectDataProvider(AbstractSyncDataProvider<?> parent, Card card) {
+ super(parent);
+ this.card = card;
+ }
+
+ @Override
+ public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<OcsProject>> responder, Instant lastSync) {
+ serverAdapter.getProjectsForCard(card.getId(), new IResponseCallback<OcsProjectList>(responder.getAccount()) {
+ @Override
+ public void onResponse(OcsProjectList response) {
+ responder.onResponse(response.getProjects());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ // dont break the sync!
+ DeckLog.logError(throwable);
+ responder.onResponse(Collections.emptyList());
+ }
+ });
+ }
+
+ @Override
+ public OcsProject getSingleFromDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject entity) {
+ return dataBaseAdapter.getProjectByRemoteIdDirectly(accountId, entity.getId());
+ }
+
+ @Override
+ public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject entity) {
+ Long newId = dataBaseAdapter.createProjectDirectly(accountId, entity);
+ entity.setLocalId(newId);
+ updateResources(dataBaseAdapter, accountId, entity);
+ return newId;
+ }
+
+ @Override
+ public void updateInDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject entity, boolean setStatus) {
+ dataBaseAdapter.updateProjectDirectly(accountId, entity);
+ dataBaseAdapter.deleteProjectResourcesForProjectIdDirectly(entity.getLocalId());
+ updateResources(dataBaseAdapter, accountId, entity);
+ }
+
+ @Override
+ public void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject ocsProject) {
+ if (ocsProject != null && ocsProject.getLocalId() != null) {
+ dataBaseAdapter.deleteProjectDirectly(ocsProject);
+ }
+ }
+
+ private void updateResources(DataBaseAdapter dataBaseAdapter, Long accountId, OcsProject entity) {
+ if (entity.getResources() != null) {
+ for (OcsProjectResource resource : entity.getResources()) {
+ resource.setProjectId(entity.getLocalId());
+ resource.setLocalId(dataBaseAdapter.createProjectResourceDirectly(accountId, resource));
+ if ("deck-card".equals(resource.getType())) {
+ dataBaseAdapter.assignCardToProjectIfMissng(accountId, entity.getLocalId(), resource.getId());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<OcsProject> responder, OcsProject entity) {
+ // Do Nothing
+ }
+
+ @Override
+ public void updateOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<OcsProject> callback, OcsProject entity) {
+ // Do Nothing
+ }
+
+ @Override
+ public void deleteOnServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<Void> callback, OcsProject entity, DataBaseAdapter dataBaseAdapter) {
+ // Do Nothing
+ }
+
+ @Override
+ public List<OcsProject> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
+ return Collections.emptyList();
+ }
+}
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 a3c123afd..93761d616 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
@@ -1,12 +1,13 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
+import java.time.Instant;
import java.util.Collections;
-import java.util.Date;
-import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.exceptions.DeckException;
import it.niedermann.nextcloud.deck.model.Board;
import it.niedermann.nextcloud.deck.model.Card;
import it.niedermann.nextcloud.deck.model.full.FullBoard;
@@ -19,13 +20,15 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper;
public class StackDataProvider extends AbstractSyncDataProvider<FullStack> {
private FullBoard board;
+ private Set<Long> syncedStacks = new ConcurrentSkipListSet<>();
+
public StackDataProvider(AbstractSyncDataProvider<?> parent, FullBoard board) {
super(parent);
this.board = board;
}
@Override
- public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullStack>> responder, Date lastSync) {
+ public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<FullStack>> responder, Instant lastSync) {
serverAdapter.getStacks(board.getId(), responder);
}
@@ -54,12 +57,12 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> {
@Override
public void goDeeper(SyncHelper syncHelper, FullStack existingEntity, FullStack entityFromServer, IResponseCallback<Boolean> callback) {
- boolean serverHasCards = entityFromServer.getCards() != null && !entityFromServer.getCards().isEmpty();
- boolean weHaveCards = existingEntity.getCards() != null && !existingEntity.getCards().isEmpty();
- if (serverHasCards || weHaveCards){
+ boolean serverHasCards = entityFromServer.getCards() != null && !entityFromServer.getCards().isEmpty();
+ boolean weHaveCards = existingEntity.getCards() != null && !existingEntity.getCards().isEmpty();
+ if (serverHasCards || weHaveCards) {
existingEntity.setCards(entityFromServer.getCards());
List<Card> cards = existingEntity.getCards();
- if (cards != null ){
+ if (cards != null) {
for (Card card : cards) {
card.setStackId(existingEntity.getLocalId());
}
@@ -72,6 +75,9 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> {
@Override
public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<FullStack> responder, FullStack entity) {
+ if (board.getId() == null) {
+ throw new DeckException(DeckException.Hint.DEPENDENCY_NOT_SYNCED_YET, "Board for this stack is not synced yet. Perform a full sync (pull to referesh) as soon as you are online again.");
+ }
entity.getStack().setBoardId(board.getId());
serverAdapter.createStack(board.getBoard(), entity.getStack(), responder);
}
@@ -89,8 +95,8 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> {
}
@Override
- public List<FullStack> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) {
- if (board == null){
+ public List<FullStack> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
+ if (board == null) {
// no stacks changed!
// (see call from BoardDataProvider: goDeeperForUpSync called with null for board.)
// so we can just skip this one and proceed with cards.
@@ -103,16 +109,19 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> {
@Override
public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) {
List<FullCard> changedCards = dataBaseAdapter.getLocallyChangedCardsDirectly(callback.getAccount().getId());
- Set<Long> syncedStacks = new HashSet<>();
- if (changedCards != null && changedCards.size() > 0){
+ if (changedCards != null && !changedCards.isEmpty()) {
for (FullCard changedCard : changedCards) {
long stackId = changedCard.getCard().getStackId();
- boolean added = syncedStacks.add(stackId);
- if (added) {
+ boolean alreadySynced = syncedStacks.contains(stackId);
+ if (!alreadySynced) {
FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackId);
- Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId());
- changedCard.getCard().setStackId(stack.getId());
- syncHelper.doUpSyncFor(new CardDataProvider(this, board, stack));
+ // already synced and known to server?
+ if (stack.getStack().getId() != null) {
+ syncedStacks.add(stackId);
+ Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId());
+ changedCard.getCard().setStackId(stack.getId());
+ syncHelper.doUpSyncFor(new CardDataProvider(this, board, stack));
+ }
}
}
} else {
@@ -132,7 +141,7 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> {
List<FullStack> localStacks = dataBaseAdapter.getFullStacksForBoardDirectly(accountId, board.getLocalId());
List<FullStack> delta = findDelta(entitiesFromServer, localStacks);
for (FullStack stackToDelete : delta) {
- if (stackToDelete.getId() == null){
+ if (stackToDelete.getId() == null) {
// not pushed up yet so:
continue;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/UserDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/UserDataProvider.java
index 60c906bda..279ce9e55 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/UserDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/UserDataProvider.java
@@ -1,6 +1,6 @@
package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers;
-import java.util.Date;
+import java.time.Instant;
import java.util.List;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
@@ -27,7 +27,7 @@ public class UserDataProvider extends AbstractSyncDataProvider<User> {
}
@Override
- public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<User>> responder, Date lastSync) {
+ public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<User>> responder, Instant lastSync) {
responder.onResponse(users);
}
@@ -67,7 +67,7 @@ public class UserDataProvider extends AbstractSyncDataProvider<User> {
}
@Override
- public List<User> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) {
+ public List<User> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
return null;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWitAclDownSyncDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithAclDownSyncDataProvider.java
index 326d257ab..8516f0fc0 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWitAclDownSyncDataProvider.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithAclDownSyncDataProvider.java
@@ -11,7 +11,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AccessControlDataProvider;
import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.BoardDataProvider;
-public class BoardWitAclDownSyncDataProvider extends BoardDataProvider {
+public class BoardWithAclDownSyncDataProvider extends BoardDataProvider {
@Override
public void goDeeper(SyncHelper syncHelper, FullBoard existingEntity, FullBoard entityFromServer, IResponseCallback<Boolean> callback) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java
new file mode 100644
index 000000000..5f2c79ad3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java
@@ -0,0 +1,37 @@
+package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+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.helpers.SyncHelper;
+import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.BoardDataProvider;
+
+public class BoardWithStacksAndLabelsUpSyncDataProvider extends BoardDataProvider {
+
+ private FullBoard board;
+
+ public BoardWithStacksAndLabelsUpSyncDataProvider(FullBoard boardToSync) {
+ board = boardToSync;
+ }
+
+ @Override
+ public List<FullBoard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Instant lastSync) {
+ return Collections.singletonList(board);
+ }
+
+ @Override
+ public void goDeeper(SyncHelper syncHelper, FullBoard existingEntity, FullBoard entityFromServer, IResponseCallback<Boolean> callback) {
+ // do nothing!
+
+ }
+
+ @Override
+ public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<FullBoard> entitiesFromServer) {
+ // do nothing!
+ }
+}
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
new file mode 100644
index 000000000..faabda163
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java
@@ -0,0 +1,21 @@
+package it.niedermann.nextcloud.deck.persistence.sync.helpers.util;
+
+import java.util.concurrent.CountDownLatch;
+
+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);
+ try {
+ countDownLatch.await();
+ } catch (InterruptedException e) {
+ DeckLog.logError(e);
+ }
+ }
+}
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 854b99a6a..3ded31bb7 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
@@ -23,6 +23,7 @@ import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGrant
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 it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
@@ -36,7 +37,6 @@ import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
-import it.niedermann.nextcloud.deck.util.ExceptionUtil;
import static com.nextcloud.android.sso.AccountImporter.REQUEST_AUTH_TOKEN_SSO;
@@ -79,7 +79,11 @@ public class ImportAccountActivity extends AppCompatActivity {
try {
AccountImporter.pickNewAccount(this);
} catch (NextcloudFilesAppNotInstalledException e) {
- ExceptionUtil.handleNextcloudFilesAppNotInstalledException(this, e);
+ UiExceptionManager.showDialogForException(this, e);
+ DeckLog.warn("=============================================================");
+ DeckLog.warn("Nextcloud app is not installed. Cannot choose account");
+ DeckLog.logError(e);
+ binding.addButton.setEnabled(true);
} catch (AndroidGetAccountsPermissionNotGranted e) {
binding.addButton.setEnabled(true);
AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this);
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
index b5713909b..d7e726a7d 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java
@@ -1,5 +1,6 @@
package it.niedermann.nextcloud.deck.ui;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -53,6 +54,7 @@ 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.nextcloud.deck.DeckApplication;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
@@ -65,6 +67,7 @@ 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;
@@ -86,6 +89,7 @@ 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;
@@ -112,8 +116,8 @@ import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandTo
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.clearBrandColors;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.saveBrandColors;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficient;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficient;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas;
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;
@@ -126,6 +130,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
protected MainViewModel mainViewModel;
private FilterViewModel filterViewModel;
+ private PickStackViewModel pickStackViewModel;
protected static final int ACTIVITY_ABOUT = 1;
protected static final int ACTIVITY_SETTINGS = 2;
@@ -133,7 +138,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@NonNull
protected List<Account> accountsList = new ArrayList<>();
- protected SyncManager syncManager;
protected SharedPreferences sharedPreferences;
private StackAdapter stackAdapter;
long lastBoardId;
@@ -143,7 +147,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
private Observer<List<Board>> boardsLiveDataObserver;
private Menu listMenu;
- private LiveData<List<FullStack>> stacksLiveData;
+ private LiveData<List<Stack>> stacksLiveData;
private LiveData<Boolean> hasArchivedBoardsLiveData;
private Observer<Boolean> hasArchivedBoardsLiveDataObserver;
@@ -178,6 +182,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
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);
@@ -191,12 +196,11 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
toggle.syncState();
binding.navigationView.setNavigationItemSelectedListener(this);
- syncManager = new SyncManager(this);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- switchMap(syncManager.hasAccounts(), hasAccounts -> {
+ switchMap(mainViewModel.hasAccounts(), hasAccounts -> {
if (hasAccounts) {
- return syncManager.readAccounts();
+ return mainViewModel.readAccounts();
} else {
startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
return null;
@@ -220,7 +224,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
registerAutoSyncOnNetworkAvailable();
} else {
- syncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
+ mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
@Override
public void onResponse(Boolean response) {
}
@@ -240,7 +244,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
mainViewModel.getCurrentAccountLiveData().observe(this, (currentAccount) -> {
SingleAccountHelper.setCurrentAccount(getApplicationContext(), mainViewModel.getCurrentAccount().getName());
- syncManager = new SyncManager(this);
+ mainViewModel.recreateSyncManager();
saveCurrentAccountId(this, mainViewModel.getCurrentAccount().getId());
if (mainViewModel.getCurrentAccount().isMaintenanceEnabled()) {
@@ -253,7 +257,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
boardsLiveData.removeObserver(boardsLiveDataObserver);
}
- boardsLiveData = syncManager.getBoards(currentAccount.getId(), false);
+ boardsLiveData = mainViewModel.getBoards(currentAccount.getId(), false);
boardsLiveDataObserver = (boards) -> {
if (boards == null) {
throw new IllegalStateException("List<Board> boards must not be null.");
@@ -273,15 +277,19 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
if (!currentBoardIdWasInList) {
setCurrentBoard(boardsList.get(0));
}
+
+ binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
} else {
clearBrandColors(this);
clearCurrentBoard();
+
+ binding.filter.setOnClickListener(null);
}
if (hasArchivedBoardsLiveData != null && hasArchivedBoardsLiveDataObserver != null) {
hasArchivedBoardsLiveData.removeObserver(hasArchivedBoardsLiveDataObserver);
}
- hasArchivedBoardsLiveData = syncManager.hasArchivedBoards(currentAccount.getId());
+ hasArchivedBoardsLiveData = mainViewModel.hasArchivedBoards(currentAccount.getId());
hasArchivedBoardsLiveDataObserver = (hasArchivedBoards) -> {
mainViewModel.setCurrentAccountHasArchivedBoards(Boolean.TRUE.equals(hasArchivedBoards));
inflateBoardMenu();
@@ -320,7 +328,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
CrossTabDragAndDrop<StackFragment, CardAdapter, FullCard> dragAndDrop = new CrossTabDragAndDrop<>(getResources(), ViewCompat.getLayoutDirection(binding.getRoot()) == ViewCompat.LAYOUT_DIRECTION_LTR);
dragAndDrop.register(binding.viewPager, binding.stackTitles, getSupportFragmentManager());
dragAndDrop.addItemMovedByDragListener((movedCard, stackId, position) -> {
- syncManager.reorder(mainViewModel.getCurrentAccount().getId(), 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);
});
@@ -374,8 +382,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
});
filterViewModel.getFilterInformation().observe(this, (info) ->
binding.filterIndicator.setVisibility(filterViewModel.getFilterInformation().getValue() == null ? View.GONE : View.VISIBLE));
-
- binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
binding.archivedCards.setOnClickListener((v) -> startActivity(ArchivedCardsActvitiy.createIntent(this, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), mainViewModel.currentBoardHasEditPermission())));
@@ -395,7 +401,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
}
} else DeckLog.warn("ConnectivityManager is null");
refreshCapabilities(mainViewModel.getCurrentAccount());
- syncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
+ mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
@Override
public void onResponse(Boolean response) {
runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false));
@@ -422,7 +428,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
applyBrandToPrimaryTabLayout(mainColor, binding.stackTitles);
applyBrandToFAB(mainColor, binding.fab);
// TODO We assume, that the background of the spinner is always white
- binding.swipeRefreshLayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : colorAccent);
+ binding.swipeRefreshLayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : DeckApplication.isDarkTheme(this) ? Color.DKGRAY : colorAccent);
headerBinding.headerView.setBackgroundColor(mainColor);
@ColorInt final int headerTextColor = contrastRatioIsSufficientBigAreas(mainColor, Color.WHITE) ? Color.WHITE : Color.BLACK;
DrawableCompat.setTint(headerBinding.logo.getDrawable(), headerTextColor);
@@ -440,62 +446,49 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onCreateStack(String stackName) {
- // TODO this outer call is only necessary to get the highest order. Move logic to SyncManager.
- observeOnce(syncManager.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), MainActivity.this, fullStacks -> {
- final Stack s = new Stack(stackName, mainViewModel.getCurrentBoardLocalId());
- int heighestOrder = 0;
- for (FullStack fullStack : fullStacks) {
- int currentStackOrder = fullStack.stack.getOrder();
- if (currentStackOrder >= heighestOrder) {
- heighestOrder = currentStackOrder + 1;
- }
+ DeckLog.info("Create Stack in account " + mainViewModel.getCurrentAccount().getName() + " on board " + mainViewModel.getCurrentBoardLocalId());
+ WrappedLiveData<FullStack> createLiveData = mainViewModel.createStack(mainViewModel.getCurrentAccount().getId(), stackName, mainViewModel.getCurrentBoardLocalId());
+ observeOnce(createLiveData, this, (fullStack) -> {
+ if (createLiveData.hasError()) {
+ final Throwable error = createLiveData.getError();
+ assert error != null;
+ BrandedSnackbar.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()))
+ .show();
+ } else {
+ binding.viewPager.setCurrentItem(stackAdapter.getItemCount());
}
- s.setOrder(heighestOrder);
- DeckLog.info("Create Stack in account " + mainViewModel.getCurrentAccount().getName() + " on board " + mainViewModel.getCurrentBoardLocalId());
- WrappedLiveData<FullStack> createLiveData = syncManager.createStack(mainViewModel.getCurrentAccount().getId(), s);
- observeOnce(createLiveData, this, (fullStack) -> {
- if (createLiveData.hasError()) {
- final Throwable error = createLiveData.getError();
- assert error != null;
- BrandedSnackbar.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()))
- .show();
- } else {
- binding.viewPager.setCurrentItem(stackAdapter.getItemCount());
- }
- });
});
}
@Override
public void onUpdateStack(long localStackId, String stackName) {
- observeOnce(syncManager.getStack(mainViewModel.getCurrentAccount().getId(), localStackId), MainActivity.this, fullStack -> {
- fullStack.getStack().setTitle(stackName);
- final WrappedLiveData<FullStack> archiveLiveData = syncManager.updateStack(fullStack);
- observeOnce(archiveLiveData, this, (v) -> {
- if (archiveLiveData.hasError()) {
- ExceptionDialogFragment.newInstance(archiveLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- });
+ final WrappedLiveData<FullStack> liveData = mainViewModel.updateStackTitle(localStackId, stackName);
+ observeOnce(liveData, this, (v) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
});
}
@Override
- public void onCreateBoard(String title, String color) {
+ public void onCreateBoard(String title, @ColorInt int color) {
if (boardsLiveData == null || boardsLiveDataObserver == null) {
throw new IllegalStateException("Cannot create board when noone observe boards yet. boardsLiveData or observer is null.");
}
boardsLiveData.removeObserver(boardsLiveDataObserver);
- final Board boardToCreate = new Board(title, color.startsWith("#") ? color.substring(1) : color);
+ final Board boardToCreate = new Board(title, color);
boardToCreate.setPermissionEdit(true);
boardToCreate.setPermissionManage(true);
- observeOnce(syncManager.createBoard(mainViewModel.getCurrentAccount().getId(), boardToCreate), this, createdBoard -> {
- if (createdBoard == null) {
- BrandedSnackbar.make(binding.coordinatorLayout, "Open Deck in web interface first!", Snackbar.LENGTH_LONG)
- // TODO implement action!
- // .setAction(R.string.simple_open, v -> ExceptionDialogFragment.newInstance(throwable).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+
+ final WrappedLiveData<FullBoard> createLiveData = mainViewModel.createBoard(mainViewModel.getCurrentAccount().getId(), boardToCreate);
+ observeOnce(createLiveData, this, (createdBoard) -> {
+ if (createLiveData.hasError()) {
+ BrandedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(createLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
.show();
- } else {
+ }
+ if (createdBoard != null && !createLiveData.hasError()) {
boardsList.add(createdBoard.getBoard());
setCurrentBoard(createdBoard.getBoard());
@@ -508,11 +501,16 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onUpdateBoard(FullBoard fullBoard) {
- syncManager.updateBoard(fullBoard);
+ final WrappedLiveData<FullBoard> updateLiveData = mainViewModel.updateBoard(fullBoard);
+ observeOnce(updateLiveData, this, (next) -> {
+ if (updateLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(updateLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
private void refreshCapabilities(final Account account) {
- syncManager.refreshCapabilities(new IResponseCallback<Capabilities>(account) {
+ mainViewModel.refreshCapabilities(new IResponseCallback<Capabilities>(account) {
@Override
public void onResponse(Capabilities response) {
if (response.isMaintenanceEnabled()) {
@@ -548,7 +546,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
if (stacksLiveData != null) {
stacksLiveData.removeObservers(this);
}
- saveBrandColors(this, Color.parseColor('#' + board.getColor()));
+ saveBrandColors(this, board.getColor());
mainViewModel.setCurrentBoard(board);
filterViewModel.clearFilterInformation();
@@ -569,12 +567,12 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
binding.emptyContentViewBoards.setVisibility(View.GONE);
binding.swipeRefreshLayout.setVisibility(View.VISIBLE);
- stacksLiveData = syncManager.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), board.getLocalId());
- stacksLiveData.observe(this, (List<FullStack> fullStacks) -> {
- if (fullStacks == null) {
+ 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 = fullStacks.size();
+ currentBoardStacksCount = stacks.size();
if (currentBoardStacksCount == 0) {
binding.emptyContentViewStacks.setVisibility(View.VISIBLE);
@@ -586,19 +584,19 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
listMenu.findItem(R.id.archive_cards).setVisible(currentBoardHasStacks);
int stackPositionInAdapter = 0;
- stackAdapter.setStacks(fullStacks);
+ stackAdapter.setStacks(stacks);
long currentStackId = readCurrentStackId(this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId());
for (int i = 0; i < currentBoardStacksCount; i++) {
- if (fullStacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) {
+ if (stacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) {
stackPositionInAdapter = i;
break;
}
}
final int stackPositionInAdapterClone = stackPositionInAdapter;
final TabTitleGenerator tabTitleGenerator = position -> {
- if (fullStacks.size() > position) {
- return fullStacks.get(position).getStack().getTitle();
+ if (stacks.size() > position) {
+ return stacks.get(position).getTitle();
} else {
DeckLog.logError(new IllegalStateException("Could not generate tab title for position " + position + " because list size is only " + currentBoardStacksCount));
return "ERROR";
@@ -671,71 +669,67 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.archive_cards: {
- final FullStack fullStack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
- final long stackLocalId = fullStack.getLocalId();
- observeOnce(syncManager.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId), MainActivity.this, (numberOfCards) -> {
- new BrandedAlertDialogBuilder(this)
- .setTitle(R.string.archive_cards)
- .setMessage(getString(R.string.do_you_want_to_archive_all_cards_of_the_list, fullStack.getStack().getTitle()))
- .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> {
- final WrappedLiveData<Void> archiveStackLiveData = syncManager.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId);
- observeOnce(archiveStackLiveData, this, (result) -> {
- if (archiveStackLiveData.hasError()) {
- ExceptionDialogFragment.newInstance(archiveStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- });
- })
- .setNeutralButton(android.R.string.cancel, null)
- .create()
- .show();
- });
- return true;
- }
- case R.id.add_list: {
- EditStackDialogFragment.newInstance(NO_STACK_ID).show(getSupportFragmentManager(), addList);
- return true;
- }
- case R.id.rename_list: {
- final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- observeOnce(syncManager.getStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, fullStack ->
- EditStackDialogFragment.newInstance(fullStack.getLocalId(), fullStack.getStack().getTitle())
- .show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
- return true;
- }
- case R.id.move_list_left: {
- final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- // TODO error handling
- final int stackLeftPosition = binding.viewPager.getCurrentItem() - 1;
- final long stackLeftId = stackAdapter.getItem(stackLeftPosition).getLocalId();
- syncManager.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackLeftId));
- stackMoved = true;
- return true;
- }
- case R.id.move_list_right: {
- final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- // TODO error handling
- final int stackRightPosition = binding.viewPager.getCurrentItem() + 1;
- final long stackRightId = stackAdapter.getItem(stackRightPosition).getLocalId();
- syncManager.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackRightId));
- stackMoved = true;
- return true;
- }
- case R.id.delete_list: {
- final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- observeOnce(syncManager.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, (numberOfCards) -> {
- if (numberOfCards != null && numberOfCards > 0) {
- DeleteStackDialogFragment.newInstance(stackId, numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName());
- } else {
- onStackDeleted(stackId);
- }
- });
- return true;
- }
- default:
- return super.onOptionsItemSelected(item);
+ int itemId = item.getItemId();
+ if (itemId == R.id.archive_cards) {
+ final Stack stack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
+ final long stackLocalId = stack.getLocalId();
+ observeOnce(mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId), MainActivity.this, (numberOfCards) -> {
+ new BrandedAlertDialogBuilder(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 FilterInformation filterInformation = filterViewModel.getFilterInformation().getValue();
+ final WrappedLiveData<Void> archiveStackLiveData = mainViewModel.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, filterInformation == null ? new FilterInformation() : filterInformation);
+ observeOnce(archiveStackLiveData, this, (result) -> {
+ if (archiveStackLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(archiveStackLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(archiveStackLiveData.getError(), 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(NO_STACK_ID).show(getSupportFragmentManager(), addList);
+ return true;
+ } else if (itemId == R.id.rename_list) {
+ final long 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) {
+ final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
+ // TODO error handling
+ final int stackLeftPosition = binding.viewPager.getCurrentItem() - 1;
+ final long stackLeftId = stackAdapter.getItem(stackLeftPosition).getLocalId();
+ mainViewModel.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackLeftId));
+ stackMoved = true;
+ return true;
+ } else if (itemId == R.id.move_list_right) {
+ final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
+ // TODO error handling
+ final int stackRightPosition = binding.viewPager.getCurrentItem() + 1;
+ final long stackRightId = stackAdapter.getItem(stackRightPosition).getLocalId();
+ mainViewModel.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackRightId));
+ stackMoved = true;
+ return true;
+ } else if (itemId == R.id.delete_list) {
+ final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
+ observeOnce(mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, (numberOfCards) -> {
+ if (numberOfCards != null && numberOfCards > 0) {
+ DeleteStackDialogFragment.newInstance(stackId, numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName());
+ } else {
+ onStackDeleted(stackId);
+ }
+ });
+ return true;
}
+ return super.onOptionsItemSelected(item);
}
protected void showFabIfEditPermissionGranted() {
@@ -780,7 +774,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
default:
try {
AccountImporter.onActivityResult(requestCode, resultCode, data, this, (account) -> {
- final WrappedLiveData<Account> accountLiveData = this.syncManager.createAccount(new Account(account.name, account.userId, account.url));
+ final WrappedLiveData<Account> accountLiveData = mainViewModel.createAccount(new Account(account.name, account.userId, account.url));
accountLiveData.observe(this, (createdAccount) -> {
if (!accountLiveData.hasError()) {
if (createdAccount == null) {
@@ -789,12 +783,13 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
final SyncManager importSyncManager = new SyncManager(this, account.name);
importSyncManager.refreshCapabilities(new IResponseCallback<Capabilities>(createdAccount) {
+ @SuppressLint("StringFormatInvalid")
@Override
public void onResponse(Capabilities response) {
if (!response.isMaintenanceEnabled()) {
if (response.getDeckVersion().isSupported(getApplicationContext())) {
runOnUiThread(() -> {
- syncManager = importSyncManager;
+ mainViewModel.setSyncManager(importSyncManager);
mainViewModel.setCurrentAccount(account);
final Snackbar importSnackbar = BrandedSnackbar.make(binding.coordinatorLayout, R.string.account_is_getting_imported, Snackbar.LENGTH_INDEFINITE);
@@ -827,7 +822,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
startActivity(openURL);
finish();
}).show());
- syncManager.deleteAccount(createdAccount.getId());
+ mainViewModel.deleteAccount(createdAccount.getId());
}
} else {
DeckLog.warn("Cannot import account because server version is currently in maintenance mode.");
@@ -836,14 +831,14 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
.setMessage(getString(R.string.maintenance_mode_explanation, createdAccount.getUrl()))
.setPositiveButton(R.string.simple_close, null)
.show());
- syncManager.deleteAccount(createdAccount.getId());
+ mainViewModel.deleteAccount(createdAccount.getId());
}
}
@Override
public void onError(Throwable throwable) {
super.onError(throwable);
- syncManager.deleteAccount(createdAccount.getId());
+ mainViewModel.deleteAccount(createdAccount.getId());
if (throwable instanceof OfflineException) {
DeckLog.warn("Cannot import account because device is currently offline.");
runOnUiThread(() -> new BrandedAlertDialogBuilder(MainActivity.this)
@@ -893,7 +888,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onAvailable(@NonNull Network network) {
DeckLog.log("Got Network connection");
- syncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
+ mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
@Override
public void onResponse(Boolean response) {
DeckLog.log("Auto-Sync after connection available successful");
@@ -924,9 +919,9 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onStackDeleted(Long stackLocalId) {
long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- final WrappedLiveData<Void> deleteStackLiveData = syncManager.deleteStack(mainViewModel.getCurrentAccount().getId(), stackId, mainViewModel.getCurrentBoardLocalId());
+ final WrappedLiveData<Void> deleteStackLiveData = mainViewModel.deleteStack(mainViewModel.getCurrentAccount().getId(), stackId, mainViewModel.getCurrentBoardLocalId());
observeOnce(deleteStackLiveData, this, (v) -> {
- if (deleteStackLiveData.hasError()) {
+ if (deleteStackLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteStackLiveData.getError())) {
ExceptionDialogFragment.newInstance(deleteStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
});
@@ -946,7 +941,14 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard);
}
}
- syncManager.deleteBoard(board);
+
+ final WrappedLiveData<Void> deleteLiveData = mainViewModel.deleteBoard(board);
+ observeOnce(deleteLiveData, this, (next) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+
binding.drawerLayout.closeDrawer(GravityCompat.START);
}
@@ -966,6 +968,39 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onArchive(@NonNull Board board) {
- syncManager.archiveBoard(board);
+ final WrappedLiveData<FullBoard> liveData = mainViewModel.archiveBoard(board);
+ observeOnce(liveData, this, (fullBoard) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), 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 BrandedAlertDialogBuilder(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 Snackbar snackbar = BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.cloning_board, board.getTitle()), Snackbar.LENGTH_INDEFINITE);
+ snackbar.show();
+ final WrappedLiveData<FullBoard> liveData = mainViewModel.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor(), checkedItems[0]);
+ observeOnce(liveData, this, (fullBoard -> {
+ snackbar.dismiss();
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ } else {
+ setCurrentBoard(fullBoard.getBoard());
+ BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.successfully_cloned_board, fullBoard.getBoard().getTitle()), Snackbar.LENGTH_LONG)
+ .setAction(R.string.edit, v -> EditBoardDialogFragment.newInstance(fullBoard.getLocalId()).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName()))
+ .show();
+ }
+ }));
+ })
+ .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
index e5bd4f482..ac244b88e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java
@@ -2,19 +2,42 @@ package it.niedermann.nextcloud.deck.ui;
import android.app.Application;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
+import java.io.File;
+import java.util.List;
+
+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.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;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
@SuppressWarnings("WeakerAccess")
public class MainViewModel extends AndroidViewModel {
- private MutableLiveData<Account> currentAccount = new MutableLiveData<>();
+ private SyncManager syncManager;
+
+ private final MutableLiveData<Account> currentAccount = new MutableLiveData<>();
+ @Nullable
private Board currentBoard;
private boolean currentAccountHasArchivedBoards = false;
@@ -22,6 +45,7 @@ public class MainViewModel extends AndroidViewModel {
public MainViewModel(@NonNull Application application) {
super(application);
+ this.syncManager = new SyncManager(application);
}
public Account getCurrentAccount() {
@@ -37,16 +61,21 @@ public class MainViewModel extends AndroidViewModel {
this.currentAccountIsSupportedVersion = currentAccount.getServerDeckVersionAsObject().isSupported(getApplication().getApplicationContext());
}
- public void setCurrentBoard(Board currentBoard) {
+ 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();
}
- @Nullable
public Long getCurrentBoardRemoteId() {
+ if (currentBoard == null) {
+ throw new IllegalStateException("getCurrentBoardRemoteId() called before setCurrentBoard()");
+ }
return this.currentBoard.getId();
}
@@ -65,4 +94,192 @@ public class MainViewModel extends AndroidViewModel {
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 IResponseCallback<Boolean> responseCallback) {
+ syncManager.synchronize(responseCallback);
+ }
+
+ public void refreshCapabilities(@NonNull IResponseCallback<Capabilities> callback) {
+ syncManager.refreshCapabilities(callback);
+ }
+
+ public LiveData<Boolean> hasAccounts() {
+ return syncManager.hasAccounts();
+ }
+
+ public WrappedLiveData<Account> createAccount(@NonNull Account accout) {
+ return syncManager.createAccount(accout);
+ }
+
+ public void deleteAccount(long id) {
+ syncManager.deleteAccount(id);
+ }
+
+ public LiveData<List<Account>> readAccounts() {
+ return syncManager.readAccounts();
+ }
+
+ public WrappedLiveData<FullBoard> createBoard(long accountId, @NonNull Board board) {
+ return syncManager.createBoard(accountId, board);
+ }
+
+ public WrappedLiveData<FullBoard> updateBoard(@NonNull FullBoard board) {
+ return syncManager.updateBoard(board);
+ }
+
+ 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 WrappedLiveData<FullBoard> archiveBoard(@NonNull Board board) {
+ return syncManager.archiveBoard(board);
+ }
+
+ public WrappedLiveData<FullBoard> dearchiveBoard(@NonNull Board board) {
+ return syncManager.dearchiveBoard(board);
+ }
+
+ public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards) {
+ return syncManager.cloneBoard(originAccountId, originBoardLocalId, targetAccountId, targetBoardColor, cloneCards);
+ }
+
+ public WrappedLiveData<Void> deleteBoard(@NonNull Board board) {
+ return syncManager.deleteBoard(board);
+ }
+
+ public LiveData<Boolean> hasArchivedBoards(long accountId) {
+ return syncManager.hasArchivedBoards(accountId);
+ }
+
+ public WrappedLiveData<AccessControl> createAccessControl(long accountId, AccessControl entity) {
+ return syncManager.createAccessControl(accountId, entity);
+ }
+
+ public WrappedLiveData<AccessControl> updateAccessControl(@NonNull AccessControl entity) {
+ return syncManager.updateAccessControl(entity);
+ }
+
+ public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long id) {
+ return syncManager.getAccessControlByLocalBoardId(accountId, id);
+ }
+
+ public WrappedLiveData<Void> deleteAccessControl(@NonNull AccessControl entity) {
+ return syncManager.deleteAccessControl(entity);
+ }
+
+ public WrappedLiveData<Label> createLabel(long accountId, Label label, long localBoardId) {
+ return syncManager.createLabel(accountId, label, localBoardId);
+ }
+
+ public LiveData<Integer> countCardsWithLabel(long localLabelId) {
+ return syncManager.countCardsWithLabel(localLabelId);
+ }
+
+ public WrappedLiveData<Label> updateLabel(@NonNull Label label) {
+ return syncManager.updateLabel(label);
+ }
+
+ public WrappedLiveData<Void> deleteLabel(@NonNull Label label) {
+ return syncManager.deleteLabel(label);
+ }
+
+ public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
+ return syncManager.getStacksForBoard(accountId, localBoardId);
+ }
+
+ public WrappedLiveData<FullStack> createStack(long accountId, @NonNull String title, long boardLocalId) {
+ return syncManager.createStack(accountId, title, boardLocalId);
+ }
+
+ public LiveData<FullStack> getStack(long accountId, long localStackId) {
+ return syncManager.getStack(accountId, localStackId);
+ }
+
+ public void swapStackOrder(long accountId, long boardLocalId, @NonNull Pair<Long, Long> stackLocalIds) {
+ syncManager.swapStackOrder(accountId, boardLocalId, stackLocalIds);
+ }
+
+ public WrappedLiveData<FullStack> updateStackTitle(long localStackId, @NonNull String newTitle) {
+ return syncManager.updateStackTitle(localStackId, newTitle);
+ }
+
+ public WrappedLiveData<Void> deleteStack(long accountId, long stackLocalId, long boardLocalId) {
+ return syncManager.deleteStack(accountId, stackLocalId, boardLocalId);
+ }
+
+ public void reorder(long accountId, @NonNull FullCard movedCard, long newStackId, int newIndex) {
+ syncManager.reorder(accountId, movedCard, newStackId, newIndex);
+ }
+
+ public LiveData<Integer> countCardsInStack(long accountId, long localStackId) {
+ return syncManager.countCardsInStack(accountId, localStackId);
+ }
+
+ public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation) {
+ return syncManager.archiveCardsInStack(accountId, stackLocalId, filterInformation);
+ }
+
+ public WrappedLiveData<FullCard> updateCard(@NonNull FullCard fullCard) {
+ return syncManager.updateCard(fullCard);
+ }
+
+ public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) {
+ syncManager.addCommentToCard(accountId, cardId, comment);
+ }
+
+ public WrappedLiveData<Attachment> addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file) {
+ return syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file);
+ }
+
+ 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 WrappedLiveData<Void> moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) {
+ return syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId);
+ }
+
+ 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 WrappedLiveData<FullCard> archiveCard(@NonNull FullCard card) {
+ return syncManager.archiveCard(card);
+ }
+
+ public WrappedLiveData<FullCard> dearchiveCard(@NonNull FullCard card) {
+ return syncManager.dearchiveCard(card);
+ }
+
+ public WrappedLiveData<Void> deleteCard(@NonNull Card card) {
+ return syncManager.deleteCard(card);
+ }
}
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
new file mode 100644
index 000000000..2339a8783
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java
@@ -0,0 +1,115 @@
+package it.niedermann.nextcloud.deck.ui;
+
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.os.Bundle;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.List;
+
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ActivityPickStackBinding;
+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.branding.Branded;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+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 static androidx.lifecycle.Transformations.switchMap;
+import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas;
+
+public abstract class PickStackActivity extends AppCompatActivity implements Branded, PickStackListener {
+
+ protected ActivityPickStackBinding binding;
+ protected PickStackViewModel viewModel;
+
+ private boolean brandingEnabled;
+
+ private Account selectedAccount;
+ private Board selectedBoard;
+ private Stack selectedStack;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this));
+
+ brandingEnabled = isBrandingEnabled(this);
+
+ binding = ActivityPickStackBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(PickStackViewModel.class);
+
+ setContentView(binding.getRoot());
+ setSupportActionBar(binding.toolbar);
+
+ switchMap(viewModel.hasAccounts(), hasAccounts -> {
+ if (hasAccounts) {
+ return viewModel.readAccounts();
+ } else {
+ startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
+ 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();
+ });
+ binding.cancel.setOnClickListener((v) -> finish());
+ binding.submit.setOnClickListener((v) -> onSubmit(selectedAccount, selectedBoard.getLocalId(), selectedStack.getLocalId()));
+ }
+
+ @Override
+ public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) {
+ this.selectedAccount = account;
+ this.selectedBoard = board;
+ this.selectedStack = stack;
+ if (board == null) {
+ binding.submit.setEnabled(false);
+ } else {
+ applyBrand(board.getColor());
+ binding.submit.setEnabled(stack != null);
+ }
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ try {
+ if (brandingEnabled) {
+ @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(this, R.color.primary))
+ ? mainColor
+ : isDarkTheme(this) ? Color.WHITE : Color.BLACK;
+ DrawableCompat.setTintList(binding.submit.getBackground(), ColorStateList.valueOf(finalMainColor));
+ binding.submit.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor));
+ binding.cancel.setTextColor(getSecondaryForegroundColorDependingOnTheme(this, mainColor));
+ }
+ } catch (Throwable t) {
+ DeckLog.logError(t);
+ }
+ }
+
+ abstract protected void onSubmit(Account account, long boardId, long stackId);
+
+ abstract protected boolean showBoardsWithoutEditPermission();
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java
index b0b0d68ae..cdc20ed50 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java
@@ -5,26 +5,28 @@ import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProvider;
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
-
+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.databinding.ActivityPushNotificationBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.util.ProjectUtil;
-import static android.graphics.Color.parseColor;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
public class PushNotificationActivity extends AppCompatActivity {
private ActivityPushNotificationBinding binding;
+ private PushNotificationViewModel viewModel;
// Provided by Files app NotificationJob
private static final String KEY_SUBJECT = "subject";
@@ -44,6 +46,8 @@ public class PushNotificationActivity extends AppCompatActivity {
}
binding = ActivityPushNotificationBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(PushNotificationViewModel.class);
+
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
@@ -56,62 +60,105 @@ public class PushNotificationActivity extends AppCompatActivity {
}
final String link = getIntent().getStringExtra(KEY_LINK);
+ long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link);
binding.cancel.setOnClickListener((v) -> finish());
- final SyncManager accountReadingSyncManager = new SyncManager(this);
final String cardRemoteIdString = getIntent().getStringExtra(KEY_CARD_REMOTE_ID);
final String accountString = getIntent().getStringExtra(KEY_ACCOUNT);
DeckLog.verbose("cardRemoteIdString = " + cardRemoteIdString);
- if (cardRemoteIdString != null) {
- try {
- final int cardRemoteId = Integer.parseInt(cardRemoteIdString);
- observeOnce(accountReadingSyncManager.readAccount(accountString), this, (account -> {
- if (account != null) {
- SingleAccountHelper.setCurrentAccount(this, account.getName());
- final SyncManager syncManager = new SyncManager(this);
- DeckLog.verbose("account: " + account);
- observeOnce(syncManager.getLocalBoardIdByCardRemoteIdAndAccount(cardRemoteId, account), PushNotificationActivity.this, (boardLocalId -> {
- DeckLog.verbose("BoardLocalId " + boardLocalId);
- if (boardLocalId != null) {
- observeOnce(syncManager.synchronizeCardByRemoteId(cardRemoteId, account), PushNotificationActivity.this, (fullCard -> {
- DeckLog.verbose("FullCard: " + fullCard);
- if (fullCard != null) {
- runOnUiThread(() -> {
- binding.submit.setOnClickListener((v) -> launchEditActivity(account, boardLocalId, fullCard.getLocalId()));
- binding.submit.setText(R.string.simple_open);
- applyBrandToSubmitButton(account);
- binding.submit.setEnabled(true);
- binding.progress.setVisibility(View.INVISIBLE);
- });
- } else {
- DeckLog.warn("Something went wrong while synchronizing the card " + cardRemoteId + " (cardRemoteId). Given fullCard is null.");
- applyBrandToSubmitButton(account);
- fallbackToBrowser(link);
- }
- }));
- } else {
- DeckLog.warn("Given localBoardId for cardRemoteId " + cardRemoteId + " is null.");
- applyBrandToSubmitButton(account);
- fallbackToBrowser(link);
- }
- }));
- } else {
- DeckLog.warn("Given account for " + accountString + " is null.");
- fallbackToBrowser(link);
- }
- }));
- } catch (NumberFormatException e) {
- DeckLog.logError(e);
+ if (ids.length == 2) {
+ if (cardRemoteIdString != null) {
+ try {
+ final int cardRemoteId = Integer.parseInt(cardRemoteIdString);
+ observeOnce(viewModel.readAccount(accountString), this, (account -> {
+ if (account != null) {
+ viewModel.setAccount(account.getName());
+ DeckLog.verbose("account: " + account);
+ observeOnce(viewModel.getBoardByRemoteId(account.getId(), ids[0]), PushNotificationActivity.this, (board -> {
+ DeckLog.verbose("BoardLocalId " + board);
+ if (board != null) {
+ observeOnce(viewModel.getCardByRemoteID(account.getId(), cardRemoteId), PushNotificationActivity.this, (card -> {
+ DeckLog.verbose("Card: " + card);
+ if (card != null) {
+ viewModel.synchronizeCard(new IResponseCallback<Boolean>(account) {
+ @Override
+ public void onResponse(Boolean response) {
+ openCardOnSubmit(account, board.getLocalId(), card.getLocalId());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ openCardOnSubmit(account, board.getLocalId(), card.getLocalId());
+ }
+ }, card);
+ } else {
+ DeckLog.info("Card is not yet available locally. Synchronize board with localId " + board);
+
+ viewModel.synchronizeBoard(new IResponseCallback<Boolean>(account) {
+ @Override
+ public void onResponse(Boolean response) {
+ runOnUiThread(() -> {
+ observeOnce(viewModel.getCardByRemoteID(account.getId(), cardRemoteId), PushNotificationActivity.this, (card -> {
+ DeckLog.verbose("Card: " + card);
+ if (card != null) {
+ openCardOnSubmit(account, board.getLocalId(), card.getLocalId());
+ } else {
+ DeckLog.warn("Something went wrong while synchronizing the card " + cardRemoteId + " (cardRemoteId). Given fullCard is null.");
+ applyBrandToSubmitButton(account);
+ fallbackToBrowser(link);
+ }
+ }));
+ });
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ DeckLog.warn("Something went wrong while synchronizing the board with localId " + board + ".");
+ applyBrandToSubmitButton(account);
+ fallbackToBrowser(link);
+ }
+ }, board.getLocalId());
+ }
+ }));
+ } else {
+ DeckLog.warn("Given localBoardId for cardRemoteId " + cardRemoteId + " is null.");
+ applyBrandToSubmitButton(account);
+ fallbackToBrowser(link);
+ }
+ }));
+ } else {
+ DeckLog.warn("Given account for " + accountString + " is null.");
+ fallbackToBrowser(link);
+ }
+ }));
+ } catch (NumberFormatException e) {
+ DeckLog.logError(e);
+ fallbackToBrowser(link);
+ }
+ } else {
+ DeckLog.warn(KEY_CARD_REMOTE_ID + " is null.");
fallbackToBrowser(link);
}
} else {
- DeckLog.warn(KEY_CARD_REMOTE_ID + " is null.");
+ DeckLog.warn("Link does not contain two IDs (expected one board id and one card id): " + link);
fallbackToBrowser(link);
}
}
+ private void openCardOnSubmit(@NonNull Account account, long boardLocalId, long cardlocalId) {
+ runOnUiThread(() -> {
+ binding.submit.setOnClickListener((v) -> launchEditActivity(account, boardLocalId, cardlocalId));
+ binding.submit.setText(R.string.simple_open);
+ applyBrandToSubmitButton(account);
+ binding.submit.setEnabled(true);
+ binding.progress.setVisibility(View.INVISIBLE);
+ });
+ }
+
/**
* If anything goes wrong and we cannot open the card directly, we fall back to open the given link in the webbrowser
*/
@@ -146,10 +193,13 @@ public class PushNotificationActivity extends AppCompatActivity {
return true;
}
+ // TODO implement Branded interface
+ // TODO apply branding based on board color
public void applyBrandToSubmitButton(@NonNull Account account) {
+ @ColorInt final int mainColor = account.getColor();
try {
- binding.submit.setBackgroundColor(parseColor(account.getColor()));
- binding.submit.setTextColor(parseColor(account.getTextColor()));
+ binding.submit.setBackgroundColor(mainColor);
+ binding.submit.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(mainColor));
} catch (Throwable t) {
DeckLog.logError(t);
}
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
new file mode 100644
index 000000000..d15b412f4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java
@@ -0,0 +1,52 @@
+package it.niedermann.nextcloud.deck.ui;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+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.Card;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+
+public class PushNotificationViewModel extends AndroidViewModel {
+
+ private final SyncManager readAccountSyncManager;
+ private SyncManager accountSpecificSyncManager;
+
+ public PushNotificationViewModel(@NonNull Application application) {
+ super(application);
+ this.readAccountSyncManager = new SyncManager(application);
+ }
+
+ public LiveData<Account> readAccount(@Nullable String name) {
+ return readAccountSyncManager.readAccount(name);
+ }
+
+ public void setAccount(@NonNull String accountName) {
+ SingleAccountHelper.setCurrentAccount(getApplication(), accountName);
+ accountSpecificSyncManager = new SyncManager(getApplication());
+ }
+
+ public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
+ return accountSpecificSyncManager.getBoardByRemoteId(accountId, remoteId);
+ }
+
+ public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) {
+ return accountSpecificSyncManager.getCardByRemoteID(accountId, remoteId);
+ }
+
+ public void synchronizeCard(@NonNull IResponseCallback<Boolean> responseCallback, Card card) {
+ accountSpecificSyncManager.synchronizeCard(responseCallback, card);
+ }
+
+ public void synchronizeBoard(@NonNull IResponseCallback<Boolean> responseCallback, long localBoadId) {
+ accountSpecificSyncManager.synchronizeBoard(responseCallback, localBoadId);
+ }
+}
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 c00ff212a..0bd92bc78 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
@@ -14,13 +14,13 @@ import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.FragmentAboutLicenseTabBinding;
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas;
import static it.niedermann.nextcloud.deck.util.SpannableUtil.setTextWithURL;
public class AboutFragmentLicenseTab extends BrandedFragment {
@@ -42,6 +42,6 @@ public class AboutFragmentLicenseTab extends BrandedFragment {
? mainColor
: isDarkTheme(requireContext()) ? Color.WHITE : Color.BLACK;
DrawableCompat.setTintList(binding.aboutAppLicenseButton.getBackground(), ColorStateList.valueOf(finalMainColor));
- binding.aboutAppLicenseButton.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor));
+ binding.aboutAppLicenseButton.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor));
}
} \ 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 592f2e8cc..744498c4a 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,7 +1,6 @@
package it.niedermann.nextcloud.deck.ui.accountswitcher;
import android.app.Dialog;
-import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -16,43 +15,37 @@ import com.bumptech.glide.request.RequestOptions;
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 it.niedermann.android.util.DimensionUtil;
+import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogAccountSwitcherBinding;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
import it.niedermann.nextcloud.deck.ui.manageaccounts.ManageAccountsActivity;
-import it.niedermann.nextcloud.deck.util.ExceptionUtil;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.MainActivity.ACTIVITY_MANAGE_ACCOUNTS;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
public class AccountSwitcherDialog extends BrandedDialogFragment {
private AccountSwitcherAdapter adapter;
- private SyncManager syncManager;
private DialogAccountSwitcherBinding binding;
private MainViewModel viewModel;
- @Override
- public void onAttach(@NonNull Context context) {
- super.onAttach(context);
- viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
- syncManager = new SyncManager(requireActivity());
- }
-
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
binding = DialogAccountSwitcherBinding.inflate(requireActivity().getLayoutInflater());
+ viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
+
binding.accountName.setText(viewModel.getCurrentAccount().getUserName());
binding.accountHost.setText(Uri.parse(viewModel.getCurrentAccount().getUrl()).getHost());
binding.check.setSelected(true);
Glide.with(requireContext())
- .load(viewModel.getCurrentAccount().getAvatarUrl(dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size)))
+ .load(viewModel.getCurrentAccount().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())
@@ -65,7 +58,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
dismiss();
}));
- observeOnce(syncManager.readAccounts(), requireActivity(), (accounts) -> {
+ observeOnce(viewModel.readAccounts(), requireActivity(), (accounts) -> {
accounts.remove(viewModel.getCurrentAccount());
adapter.setAccounts(accounts);
});
@@ -76,7 +69,10 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
try {
AccountImporter.pickNewAccount(requireActivity());
} catch (NextcloudFilesAppNotInstalledException e) {
- ExceptionUtil.handleNextcloudFilesAppNotInstalledException(requireContext(), 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());
}
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 9c93c422e..a60b6a0ea 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
@@ -10,12 +10,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
-import it.niedermann.android.glidesso.SingleSignOnUrl;
+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 static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
@@ -30,7 +29,7 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
binding.accountName.setText(account.getUserName());
binding.accountHost.setText(Uri.parse(account.getUrl()).getHost());
Glide.with(itemView.getContext())
- .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))))
+ .load(new SingleSignOnUrl(account.getName(), 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())
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 5ab94b4f6..30f1e5d49 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
@@ -6,6 +6,7 @@ import android.view.MenuItem;
import android.view.View;
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;
@@ -30,14 +31,13 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder {
void bind(boolean isSupportedVersion, Board board, FragmentManager fragmentManager, Consumer<Board> dearchiveBoardListener) {
final Context context = itemView.getContext();
- binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, "#" + board.getColor()));
+ binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, board.getColor()));
binding.boardMenu.setVisibility(View.GONE);
binding.boardTitle.setText(board.getTitle());
if (isSupportedVersion) {
if (board.isPermissionManage()) {
binding.boardMenu.setVisibility(View.VISIBLE);
- binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, R.color.grey600));
-
+ binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, ContextCompat.getColor(context, R.color.grey600)));
binding.boardMenu.setOnClickListener((v) -> {
PopupMenu popup = new PopupMenu(context, binding.boardMenu);
popup.getMenuInflater().inflate(R.menu.archived_board_menu, popup.getMenu());
@@ -47,28 +47,27 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder {
}
popup.setOnMenuItemClickListener((MenuItem item) -> {
final String editBoard = context.getString(R.string.edit_board);
- switch (item.getItemId()) {
- case SHARE_BOARD_ID:
- AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName());
- return true;
- case R.id.edit_board:
- EditBoardDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, editBoard);
- return true;
- case R.id.dearchive_board:
- dearchiveBoardListener.accept(board);
- return true;
- case R.id.delete_board:
- DeleteBoardDialogFragment.newInstance(board).show(fragmentManager, DeleteBoardDialogFragment.class.getSimpleName());
- return true;
- default:
- return false;
+ int itemId = item.getItemId();
+ if (itemId == SHARE_BOARD_ID) {
+ AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName());
+ return true;
+ } else if (itemId == R.id.edit_board) {
+ EditBoardDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, editBoard);
+ return true;
+ } else if (itemId == R.id.dearchive_board) {
+ dearchiveBoardListener.accept(board);
+ return true;
+ } else if (itemId == R.id.delete_board) {
+ DeleteBoardDialogFragment.newInstance(board).show(fragmentManager, DeleteBoardDialogFragment.class.getSimpleName());
+ return true;
}
+ return false;
});
popup.show();
});
} else if (board.isPermissionShare()) {
binding.boardMenu.setVisibility(View.VISIBLE);
- binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, R.color.grey600));
+ 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.setVisibility(View.VISIBLE);
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/ArchivedBoardsActvitiy.java
index 7c3a2d23c..d6d9cac1e 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/ArchivedBoardsActvitiy.java
@@ -15,13 +15,17 @@ 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.persistence.sync.adapters.db.util.WrappedLiveData;
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.branding.BrandedActivity;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
+
public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoardListener, EditBoardListener, ArchiveBoardListener {
private static final String BUNDLE_KEY_ACCOUNT = "accountId";
@@ -29,7 +33,6 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa
private MainViewModel viewModel;
private ActivityArchivedBinding binding;
private ArchivedBoardsAdapter adapter;
- private SyncManager syncManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -54,12 +57,18 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
viewModel.setCurrentAccount(account);
- syncManager = new SyncManager(this);
- adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), (board) -> syncManager.dearchiveBoard(board));
+ adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), (board) -> {
+ final WrappedLiveData<FullBoard> liveData = viewModel.dearchiveBoard(board);
+ observeOnce(liveData, this, (fullBoard) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ });
binding.recyclerView.setAdapter(adapter);
- syncManager.getBoards(account.getId(), true).observe(this, (boards) -> {
+ viewModel.getBoards(account.getId(), true).observe(this, (boards) -> {
viewModel.setCurrentAccountHasArchivedBoards(boards != null && boards.size() > 0);
adapter.setBoards(boards == null ? Collections.emptyList() : boards);
});
@@ -80,16 +89,36 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa
@Override
public void onBoardDeleted(Board board) {
- syncManager.deleteBoard(board);
+ final WrappedLiveData<Void> deleteLiveData = viewModel.deleteBoard(board);
+ observeOnce(deleteLiveData, this, (next) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
@Override
public void onUpdateBoard(FullBoard fullBoard) {
- syncManager.updateBoard(fullBoard);
+ final WrappedLiveData<FullBoard> updateLiveData = viewModel.updateBoard(fullBoard);
+ observeOnce(updateLiveData, this, (next) -> {
+ if (updateLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(updateLiveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
@Override
public void onArchive(Board board) {
- syncManager.dearchiveBoard(board);
+ final WrappedLiveData<FullBoard> liveData = viewModel.dearchiveBoard(board);
+ observeOnce(liveData, this, (fullBoard) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ }
+
+ @Override
+ public void onClone(Board board) {
+
}
}
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
index ed2ee7097..b3533528e 100644
--- 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
@@ -6,12 +6,15 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+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.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper;
+import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel;
public class ArchivedCardsActvitiy extends BrandedActivity {
@@ -21,7 +24,8 @@ public class ArchivedCardsActvitiy extends BrandedActivity {
private ActivityArchivedBinding binding;
private ArchivedCardsAdapter adapter;
- private SyncManager syncManager;
+ private MainViewModel viewModel;
+ private PickStackViewModel pickStackViewModel;
private Account account;
private long boardId;
@@ -50,16 +54,20 @@ public class ArchivedCardsActvitiy extends BrandedActivity {
}
binding = ActivityArchivedBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(MainViewModel.class);
+ pickStackViewModel = new ViewModelProvider(this).get(PickStackViewModel.class);
+
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
- syncManager = new SyncManager(this);
+ viewModel.setCurrentAccount(account);
+ LiveDataHelper.observeOnce(viewModel.getFullBoardById(account.getId(), boardId), this, (fullBoard) -> {
+ viewModel.setCurrentBoard(fullBoard.getBoard());
- adapter = new ArchivedCardsAdapter(this, getSupportFragmentManager(), account, boardId, false, syncManager, this);
- binding.recyclerView.setAdapter(adapter);
+ adapter = new ArchivedCardsAdapter(this, getSupportFragmentManager(), viewModel, this);
+ binding.recyclerView.setAdapter(adapter);
- syncManager.getArchivedFullCardsForBoard(account.getId(), boardId).observe(this, (fullCards) -> {
- adapter.setCardList(fullCards);
+ viewModel.getArchivedFullCardsForBoard(account.getId(), boardId).observe(this, (fullCards) -> adapter.setCardList(fullCards));
});
}
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
index e6abf0ccc..b5034ebfa 100644
--- 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
@@ -1,67 +1,55 @@
package it.niedermann.nextcloud.deck.ui.archivedcards;
import android.content.Context;
-import android.view.Menu;
import android.view.MenuItem;
-import android.view.View;
-import android.widget.PopupMenu;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
-import org.jetbrains.annotations.NotNull;
-
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
+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.card.ItemCardViewHolder;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+
+import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
public class ArchivedCardsAdapter extends CardAdapter {
@SuppressWarnings("WeakerAccess")
- public ArchivedCardsAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull Account account, long boardId, boolean canEdit, @NonNull SyncManager syncManager, @NonNull LifecycleOwner lifecycleOwner) {
- super(context, fragmentManager, account, boardId, 0L, 0L, canEdit, syncManager, lifecycleOwner, null);
+ public ArchivedCardsAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull MainViewModel viewModel, @NonNull LifecycleOwner lifecycleOwner) {
+ super(context, fragmentManager, 0L, viewModel, lifecycleOwner, null);
}
@Override
- public void onBindViewHolder(@NonNull ItemCardViewHolder viewHolder, int position) {
- super.onBindViewHolder(viewHolder, position);
- viewHolder.binding.card.setOnClickListener(null);
- viewHolder.binding.card.setOnLongClickListener(null);
- }
-
- protected void onOverflowIconClicked(@NotNull View view, FullCard card) {
- final Context context = view.getContext();
- final PopupMenu popup = new PopupMenu(context, view);
- popup.inflate(R.menu.card_menu);
- prepareOptionsMenu(popup.getMenu(), card);
-
- popup.setOnMenuItemClickListener(item -> optionsItemSelected(context, item, card));
- popup.show();
- }
-
- protected void prepareOptionsMenu(Menu menu, @NotNull FullCard card) {
- // Nothing to do
+ 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, mainColor);
}
- protected boolean optionsItemSelected(@NonNull Context context, @NotNull MenuItem item, FullCard fullCard) {
- switch (item.getItemId()) {
- case R.id.action_card_dearchive: {
- // TODO error handling
- new Thread(() -> syncManager.dearchiveCard(fullCard)).start();
- return true;
- }
- case R.id.action_card_delete: {
- // TODO error handling
- syncManager.deleteCard(fullCard.getCard());
- return true;
- }
- default: {
- return false;
- }
+ @Override
+ public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) {
+ int itemId = menuItem.getItemId();
+ if (itemId == R.id.action_card_dearchive) {
+ final WrappedLiveData<FullCard> liveData = mainViewModel.dearchiveCard(fullCard);
+ observeOnce(liveData, lifecycleOwner, (next) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ return true;
+ } else if (itemId == R.id.action_card_delete) {
+ final WrappedLiveData<Void> liveData = mainViewModel.deleteCard(fullCard.getCard());
+ observeOnce(liveData, lifecycleOwner, (next) -> {
+ if (liveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(liveData.getError())) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), 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/AttachmentAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java
index 0794323ec..c7d32bd37 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java
@@ -1,43 +1,31 @@
package it.niedermann.nextcloud.deck.ui.attachments;
import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.load.DataSource;
-import com.bumptech.glide.load.engine.GlideException;
-import com.bumptech.glide.request.RequestListener;
-import com.bumptech.glide.request.target.Target;
-
+import java.util.ArrayList;
import java.util.List;
-import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Attachment;
-import it.niedermann.nextcloud.deck.util.AttachmentUtil;
-import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
public class AttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> {
private final Account account;
private final long cardRemoteId;
@NonNull
- private List<Attachment> attachments;
- private Context context;
+ private final List<Attachment> attachments = new ArrayList<>();
@SuppressWarnings("WeakerAccess")
public AttachmentAdapter(@NonNull Account account, long cardRemoteId, @NonNull List<Attachment> attachments) {
super();
- this.attachments = attachments;
+ this.attachments.clear();
+ this.attachments.addAll(attachments);
this.account = account;
this.cardRemoteId = cardRemoteId;
}
@@ -45,43 +33,13 @@ public class AttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder
@NonNull
@Override
public AttachmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- this.context = parent.getContext();
- return new AttachmentViewHolder(ItemAttachmentBinding.inflate(LayoutInflater.from(context), parent, false));
+ final Context context = parent.getContext();
+ return new AttachmentViewHolder(context, ItemAttachmentBinding.inflate(LayoutInflater.from(context), parent, false));
}
@Override
public void onBindViewHolder(@NonNull AttachmentViewHolder holder, int position) {
- final Attachment attachment = attachments.get(position);
- final String uri = AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId());
- if (MimeTypeUtil.isImage(attachment.getMimetype())) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- holder.binding.preview.setTransitionName(context.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId())));
- }
- holder.binding.preview.setImageResource(R.drawable.ic_image_grey600_24dp);
- Glide.with(context)
- .load(uri)
- .listener(new RequestListener<Drawable>() {
- @Override
- public boolean onLoadFailed(@Nullable GlideException e, Object model,
- Target<Drawable> target, boolean isFirstResource) {
- if (context instanceof FragmentActivity) {
- ((FragmentActivity) context).supportStartPostponedEnterTransition();
- }
- return false;
- }
-
- @Override
- public boolean onResourceReady(Drawable resource, Object model,
- Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
- if (context instanceof FragmentActivity) {
- ((FragmentActivity) context).supportStartPostponedEnterTransition();
- }
- return false;
- }
- })
- .error(R.drawable.ic_image_grey600_24dp)
- .into(holder.binding.preview);
- }
+ holder.bind(account, attachments.get(position), cardRemoteId);
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java
index 584a57d1d..6f4fe3c74 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java
@@ -1,15 +1,72 @@
package it.niedermann.nextcloud.deck.ui.attachments;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.target.Target;
+
+import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Attachment;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
+import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
public class AttachmentViewHolder extends RecyclerView.ViewHolder {
- public ItemAttachmentBinding binding;
+ @NonNull
+ private final Context parentContext;
+ @NonNull
+ private final ItemAttachmentBinding binding;
@SuppressWarnings("WeakerAccess")
- public AttachmentViewHolder(ItemAttachmentBinding binding) {
+ public AttachmentViewHolder(@NonNull Context parentContext, @NonNull ItemAttachmentBinding binding) {
super(binding.getRoot());
+ this.parentContext = parentContext;
this.binding = binding;
}
+
+ public void bind(@NonNull Account account, @NonNull Attachment attachment, long cardRemoteId) {
+ if (MimeTypeUtil.isImage(attachment.getMimetype())) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ binding.preview.setTransitionName(parentContext.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId())));
+ }
+ binding.preview.setImageResource(R.drawable.ic_image_grey600_24dp);
+ binding.preview.post(() -> {
+ final String uri = AttachmentUtil.getThumbnailUrl(account.getServerDeckVersionAsObject(), account.getUrl(), cardRemoteId, attachment, binding.preview.getWidth());
+ Glide.with(parentContext)
+ .load(uri)
+ .listener(new RequestListener<Drawable>() {
+ @Override
+ public boolean onLoadFailed(@Nullable GlideException e, Object model,
+ Target<Drawable> target, boolean isFirstResource) {
+ if (parentContext instanceof FragmentActivity) {
+ ((FragmentActivity) parentContext).supportStartPostponedEnterTransition();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onResourceReady(Drawable resource, Object model,
+ Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
+ if (parentContext instanceof FragmentActivity) {
+ ((FragmentActivity) parentContext).supportStartPostponedEnterTransition();
+ }
+ return false;
+ }
+ })
+ .error(R.drawable.ic_image_grey600_24dp)
+ .into(binding.preview);
+ });
+ }
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java
index 98cfd4440..6618f7f72 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java
@@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.ui.attachments;
import android.content.Context;
import android.content.Intent;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
@@ -9,6 +10,9 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.SharedElementCallback;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
@@ -21,7 +25,6 @@ import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ActivityAttachmentsBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Attachment;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
@@ -32,6 +35,7 @@ public class AttachmentsActivity extends AppCompatActivity {
private static final String BUNDLE_KEY_CURRENT_ATTACHMENT_LOCAL_ID = "currentAttachmenLocaltId";
private ActivityAttachmentsBinding binding;
+ private AttachmentsViewModel viewModel;
private ViewPager2.OnPageChangeCallback onPageChangeCallback;
@Override
@@ -40,10 +44,15 @@ public class AttachmentsActivity extends AppCompatActivity {
Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
binding = ActivityAttachmentsBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(AttachmentsViewModel.class);
+
setContentView(binding.getRoot());
supportPostponeEnterTransition();
setSupportActionBar(binding.toolbar);
+ final Drawable navigationIcon = getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp);
+ DrawableCompat.setTint(navigationIcon, ContextCompat.getColor(this, android.R.color.white));
+ binding.toolbar.setNavigationIcon(navigationIcon);
final Bundle args = getIntent().getExtras();
if (args == null || !args.containsKey(BUNDLE_KEY_ACCOUNT) || !args.containsKey(BUNDLE_KEY_CARD_ID)) {
@@ -58,8 +67,7 @@ public class AttachmentsActivity extends AppCompatActivity {
long cardId = args.getLong(BUNDLE_KEY_CARD_ID);
- final SyncManager syncManager = new SyncManager(this);
- syncManager.getCardByLocalId(account.getId(), cardId).observe(this, fullCard -> {
+ viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardId).observe(this, fullCard -> {
final List<Attachment> attachments = new ArrayList<>();
for (Attachment a : fullCard.getAttachments()) {
if (MimeTypeUtil.isImage(a.getMimetype())) {
@@ -67,7 +75,7 @@ public class AttachmentsActivity extends AppCompatActivity {
}
}
if (fullCard.getAttachments().size() == 0) {
- DeckLog.logError(new IllegalStateException(AttachmentsActivity.class.getSimpleName() + " called, but card " + fullCard.getLocalId() + "has no attachments"));
+ DeckLog.logError(new IllegalStateException(AttachmentsActivity.class.getSimpleName() + " called, but card " + fullCard.getCard().getTitle() + " has no attachments"));
supportFinishAfterTransition();
return;
}
@@ -79,7 +87,7 @@ public class AttachmentsActivity extends AppCompatActivity {
binding.toolbar.setTitle(attachments.get(position).getBasename());
}
};
- RecyclerView.Adapter adapter = new AttachmentAdapter(account, fullCard.getId(), attachments);
+ RecyclerView.Adapter<AttachmentViewHolder> adapter = new AttachmentAdapter(account, fullCard.getId(), attachments);
binding.viewPager.setAdapter(adapter);
binding.viewPager.registerOnPageChangeCallback(onPageChangeCallback);
@@ -104,7 +112,7 @@ public class AttachmentsActivity extends AppCompatActivity {
long currentAttachmentLocalId = attachments.get(binding.viewPager.getCurrentItem()).getLocalId();
String transitionKey = getString(R.string.transition_attachment_preview, String.valueOf(currentAttachmentLocalId));
if (transitionKey.equals(names.get(0))) {
- sharedElements.put(transitionKey, binding.viewPager.getRootView().findViewById(R.id.preview)
+ sharedElements.put(transitionKey, binding.viewPager.getRootView().findViewById(R.id.avatar)
);
}
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
new file mode 100644
index 000000000..87a17470c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java
@@ -0,0 +1,25 @@
+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;
+
+@SuppressWarnings("WeakerAccess")
+public class AttachmentsViewModel extends AndroidViewModel {
+
+ private final SyncManager syncManager;
+
+ 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);
+ }
+}
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 b7e27aa97..ff0d3e941 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
@@ -4,4 +4,5 @@ import it.niedermann.nextcloud.deck.model.Board;
public interface ArchiveBoardListener {
void onArchive(Board board);
+ void onClone(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
index 7049cc16c..c9501ffa9 100644
--- 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
@@ -37,7 +37,7 @@ public class BoardAdapter extends ArrayAdapter<Board> {
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);
+ 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"));
}
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/EditBoardDialogFragment.java
index e5d0a482b..9da836c4c 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/EditBoardDialogFragment.java
@@ -7,13 +7,13 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogTextColorInputBinding;
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.branding.BrandedAlertDialogBuilder;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
@@ -53,24 +53,24 @@ public class EditBoardDialogFragment extends BrandedDialogFragment {
if (args != null && args.containsKey(KEY_BOARD_ID)) {
dialogBuilder.setTitle(R.string.edit_board);
dialogBuilder.setPositiveButton(R.string.simple_save, (dialog, which) -> {
- this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor().substring(1));
+ this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor());
this.fullBoard.board.setTitle(binding.input.getText().toString());
- editBoardListener.onUpdateBoard(fullBoard);
+ this.editBoardListener.onUpdateBoard(fullBoard);
});
final MainViewModel viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
- new SyncManager(requireActivity()).getFullBoardById(viewModel.getCurrentAccount().getId(), args.getLong(KEY_BOARD_ID)).observe(EditBoardDialogFragment.this, (FullBoard fb) -> {
+ viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), args.getLong(KEY_BOARD_ID)).observe(EditBoardDialogFragment.this, (FullBoard fb) -> {
if (fb.board != null) {
this.fullBoard = fb;
String title = this.fullBoard.getBoard().getTitle();
binding.input.setText(title);
binding.input.setSelection(title.length());
- binding.colorChooser.selectColor("#" + fullBoard.getBoard().getColor());
+ binding.colorChooser.selectColor(fullBoard.getBoard().getColor());
}
});
} else {
dialogBuilder.setTitle(R.string.add_board);
dialogBuilder.setPositiveButton(R.string.simple_add, (dialog, which) -> editBoardListener.onCreateBoard(binding.input.getText().toString(), binding.colorChooser.getSelectedColor()));
- binding.colorChooser.selectColor(String.format("#%06X", 0xFFFFFF & getResources().getColor(R.color.board_default_color)));
+ binding.colorChooser.selectColor(ContextCompat.getColor(requireContext(), R.color.board_default_color));
}
return dialogBuilder
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
index ee9ba9b9d..9d8fcdbde 100644
--- 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
@@ -1,11 +1,13 @@
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, String color) {
+ 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 e5a50d9f4..0a1281fae 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
@@ -10,6 +10,7 @@ import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SwitchCompat;
+import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.recyclerview.widget.RecyclerView;
@@ -51,7 +52,7 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View
this.account = account;
this.accessControlChangedListener = accessControlChangedListener;
this.context = context;
- this.mainColor = context.getResources().getColor(R.color.primary);
+ this.mainColor = ContextCompat.getColor(context, R.color.primary);
setHasStableIds(true);
}
@@ -172,9 +173,9 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View
final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor);
DrawableCompat.setTintList(switchCompat.getThumbDrawable(), new ColorStateList(
new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}},
- new int[]{finalMainColor, context.getResources().getColor(R.color.fg_secondary)}
+ new int[]{finalMainColor, ContextCompat.getColor(context, R.color.fg_secondary)}
));
- final int trackColor = context.getResources().getColor(R.color.fg_secondary);
+ final int trackColor = ContextCompat.getColor(context, R.color.fg_secondary);
final int lightTrackColor = Color.argb(77, Color.red(trackColor), Color.green(trackColor), Color.blue(trackColor));
final int lightTrackColorChecked = Color.argb(77, Color.red(finalMainColor), Color.green(finalMainColor), Color.blue(finalMainColor));
DrawableCompat.setTintList(switchCompat.getTrackDrawable(), new ColorStateList(
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 33c0fe5b1..78ccd5333 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
@@ -43,7 +43,6 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement
private static final String KEY_BOARD_ID = "board_id";
private long boardId;
- private SyncManager syncManager;
private UserAutoCompleteAdapter userAutoCompleteAdapter;
private AccessControlAdapter adapter;
@@ -75,10 +74,9 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement
adapter = new AccessControlAdapter(viewModel.getCurrentAccount(), this, requireContext());
binding.peopleList.setAdapter(adapter);
- syncManager = new SyncManager(requireActivity());
- syncManager.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (FullBoard fullBoard) -> {
+ viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (FullBoard fullBoard) -> {
if (fullBoard != null) {
- syncManager.getAccessControlByLocalBoardId(viewModel.getCurrentAccount().getId(), boardId).observe(this, (List<AccessControl> accessControlList) -> {
+ viewModel.getAccessControlByLocalBoardId(viewModel.getCurrentAccount().getId(), boardId).observe(this, (List<AccessControl> accessControlList) -> {
final AccessControl ownerControl = new AccessControl();
ownerControl.setLocalId(HEADER_ITEM_LOCAL_ID);
ownerControl.setUser(fullBoard.getOwner());
@@ -103,15 +101,20 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement
@Override
public void updateAccessControl(AccessControl accessControl) {
- syncManager.updateAccessControl(accessControl);
+ WrappedLiveData<AccessControl> updateLiveData = viewModel.updateAccessControl(accessControl);
+ observeOnce(updateLiveData, requireActivity(), (next) -> {
+ if (updateLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(updateLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
@Override
public void deleteAccessControl(AccessControl ac) {
- final WrappedLiveData<Void> wrappedDeleteLiveData = syncManager.deleteAccessControl(ac);
+ final WrappedLiveData<Void> wrappedDeleteLiveData = viewModel.deleteAccessControl(ac);
adapter.remove(ac);
observeOnce(wrappedDeleteLiveData, this, (ignored) -> {
- if (wrappedDeleteLiveData.hasError()) {
+ if (wrappedDeleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(wrappedDeleteLiveData.getError())) {
DeckLog.logError(wrappedDeleteLiveData.getError());
BrandedSnackbar.make(requireView(), getString(R.string.error_revoking_ac, ac.getUser().getDisplayname()), Snackbar.LENGTH_LONG)
.setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(wrappedDeleteLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
@@ -129,7 +132,12 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement
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);
- syncManager.createAccessControl(viewModel.getCurrentAccount().getId(), ac);
+ final WrappedLiveData<AccessControl> createLiveData = viewModel.createAccessControl(viewModel.getCurrentAccount().getId(), ac);
+ observeOnce(createLiveData, this, (next) -> {
+ if (createLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(createLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
binding.people.setText("");
userAutoCompleteAdapter.exclude(user);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java
index d460d1590..2dacfe6ac 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java
@@ -58,14 +58,14 @@ public class EditLabelDialogFragment extends BrandedDialogFragment {
dialogBuilder.setTitle(getString(R.string.edit_tag, label.getTitle()));
dialogBuilder.setPositiveButton(R.string.simple_save, (dialog, which) -> {
- this.label.setColor(binding.colorChooser.getSelectedColor().substring(1));
+ this.label.setColor(binding.colorChooser.getSelectedColor());
this.label.setTitle(binding.input.getText().toString());
listener.onLabelUpdated(this.label);
});
String title = this.label.getTitle();
binding.input.setText(title);
binding.input.setSelection(title.length());
- binding.colorChooser.selectColor("#" + this.label.getColor());
+ binding.colorChooser.selectColor(this.label.getColor());
return dialogBuilder
.setView(binding.getRoot())
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 3391c7a99..bc0d98ca3 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
@@ -38,7 +38,6 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
private static final String KEY_BOARD_ID = "board_id";
private long boardId;
- private SyncManager syncManager;
@Override
public void onAttach(@NonNull Context context) {
@@ -67,8 +66,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
colors = getResources().getStringArray(R.array.board_default_colors);
adapter = new ManageLabelsAdapter(this, requireContext());
binding.labels.setAdapter(adapter);
- syncManager = new SyncManager(requireActivity());
- syncManager.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (fullBoard) -> {
+ viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (fullBoard) -> {
if (fullBoard == null) {
throw new IllegalStateException("FullBoard should not be null");
}
@@ -80,9 +78,9 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
final Label label = new Label();
label.setBoardId(boardId);
label.setTitle(binding.addLabelTitle.getText().toString());
- label.setColor(colors[new Random().nextInt(colors.length)].substring(1));
+ label.setColor(colors[new Random().nextInt(colors.length)]);
- WrappedLiveData<Label> createLiveData = syncManager.createLabel(viewModel.getCurrentAccount().getId(), label, boardId);
+ WrappedLiveData<Label> createLiveData = viewModel.createLabel(viewModel.getCurrentAccount().getId(), label, boardId);
observeOnce(createLiveData, this, (createdLabel) -> {
if (createLiveData.hasError()) {
final Throwable error = createLiveData.getError();
@@ -126,7 +124,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
@Override
public void requestDelete(@NonNull Label label) {
- observeOnce(syncManager.countCardsWithLabel(label.getLocalId()), this, (count) -> {
+ observeOnce(viewModel.countCardsWithLabel(label.getLocalId()), this, (count) -> {
if (count > 0) {
new BrandedDeleteAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete_something, label.getTitle()))
@@ -141,9 +139,9 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
}
private void deleteLabel(@NonNull Label label) {
- final WrappedLiveData<Void> deleteLiveData = syncManager.deleteLabel(label);
+ final WrappedLiveData<Void> deleteLiveData = viewModel.deleteLabel(label);
observeOnce(deleteLiveData, this, (v) -> {
- if (deleteLiveData.hasError()) {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
final Throwable error = deleteLiveData.getError();
assert error != null;
Toast.makeText(requireContext(), error.getLocalizedMessage(), Toast.LENGTH_LONG).show();
@@ -159,7 +157,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
@Override
public void onLabelUpdated(@NonNull Label label) {
- WrappedLiveData<Label> updateLiveData = syncManager.updateLabel(label);
+ WrappedLiveData<Label> updateLiveData = viewModel.updateLabel(label);
observeOnce(updateLiveData, this, (updatedLabel) -> {
if (updateLiveData.hasError()) {
final Throwable error = updateLiveData.getError();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java
index 7fa3abd89..381a290e6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java
@@ -1,14 +1,13 @@
package it.niedermann.nextcloud.deck.ui.board.managelabels;
import android.content.res.ColorStateList;
-import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.databinding.ItemManageLabelBinding;
import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
public class ManageLabelsViewHolder extends RecyclerView.ViewHolder {
private ItemManageLabelBinding binding;
@@ -17,13 +16,14 @@ public class ManageLabelsViewHolder extends RecyclerView.ViewHolder {
public ManageLabelsViewHolder(ItemManageLabelBinding binding) {
super(binding.getRoot());
this.binding = binding;
+ this.binding.label.setClickable(false);
}
public void bind(@NonNull Label label, @NonNull ManageLabelListener listener) {
binding.label.setText(label.getTitle());
- final int labelColor = Color.parseColor("#" + label.getColor());
+ final int labelColor = label.getColor();
binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
- final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor);
+ final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
binding.label.setTextColor(color);
binding.delete.setOnClickListener((v) -> listener.requestDelete(label));
binding.editText.setOnClickListener((v) -> listener.requestEdit(label));
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java
index cfeffe7dc..880e21073 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java
@@ -9,8 +9,6 @@ import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
-import org.jetbrains.annotations.NotNull;
-
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor;
@@ -22,7 +20,7 @@ public class BrandedAlertDialogBuilder extends AlertDialog.Builder implements Br
super(context);
}
- @NotNull
+ @NonNull
@Override
public AlertDialog create() {
this.dialog = super.create();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java
index 5bef66f2c..319df7f79 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java
@@ -17,7 +17,7 @@ import com.wdullaer.materialdatetimepicker.date.DatePickerDialog;
import java.util.Calendar;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
+import it.niedermann.nextcloud.deck.util.DeckColorUtil;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
@@ -44,7 +44,7 @@ public class BrandedDatePickerDialog extends DatePickerDialog implements Branded
setOkColor(buttonTextColor);
setCancelColor(buttonTextColor);
// Text in picker title is always white
- setAccentColor(ColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent));
+ setAccentColor(DeckColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent));
}
/**
@@ -52,13 +52,13 @@ public class BrandedDatePickerDialog extends DatePickerDialog implements Branded
*
* @param callBack How the parent is notified that the date is set.
* @param year The initial year of the dialog.
- * @param monthOfYear The initial month of the dialog.
+ * @param monthOfYear The initial month of the dialog. [0 - 11]
* @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) {
DatePickerDialog ret = new BrandedDatePickerDialog();
- ret.initialize(callBack, year, monthOfYear, dayOfMonth);
+ ret.initialize(callBack, year, monthOfYear - 1, dayOfMonth);
return ret;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java
index ec3cef553..d88fdd6cc 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java
@@ -5,6 +5,7 @@ import android.content.DialogInterface;
import android.widget.Button;
import androidx.annotation.CallSuper;
+import androidx.core.content.ContextCompat;
import it.niedermann.nextcloud.deck.R;
@@ -20,7 +21,7 @@ public class BrandedDeleteAlertDialogBuilder extends BrandedAlertDialogBuilder {
super.applyBrand(mainColor);
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
if (positiveButton != null) {
- positiveButton.setTextColor(getContext().getResources().getColor(R.color.danger));
+ positiveButton.setTextColor(ContextCompat.getColor(getContext(), R.color.danger));
}
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java
index 20e6f8dc8..0159a59dc 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java
@@ -11,8 +11,8 @@ import androidx.core.content.ContextCompat;
import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor;
@@ -25,9 +25,9 @@ public class BrandedSnackbar {
final Snackbar snackbar = Snackbar.make(view, text, duration);
if (isBrandingEnabled(view.getContext())) {
@ColorInt final int color = readBrandMainColor(view.getContext());
- snackbar.setActionTextColor(ColorUtil.isColorDark(color) ? Color.WHITE : color);
+ snackbar.setActionTextColor(ColorUtil.INSTANCE.isColorDark(color) ? Color.WHITE : color);
} else {
- snackbar.setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.primary));
+ snackbar.setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.defaultBrand));
}
return snackbar;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java
index a1963aa18..2e0c4d3b8 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java
@@ -14,10 +14,10 @@ import androidx.core.content.ContextCompat;
import com.wdullaer.materialdatetimepicker.time.TimePickerDialog;
-import java.util.Calendar;
+import java.time.LocalTime;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
+import it.niedermann.nextcloud.deck.util.DeckColorUtil;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
@@ -44,7 +44,7 @@ public class BrandedTimePickerDialog extends TimePickerDialog implements Branded
setOkColor(buttonTextColor);
setCancelColor(buttonTextColor);
// Text in picker title is always white
- setAccentColor(ColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent));
+ setAccentColor(DeckColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent));
}
/**
@@ -86,9 +86,9 @@ public class BrandedTimePickerDialog extends TimePickerDialog implements Branded
* @param is24HourMode True to render 24 hour mode, false to render AM / PM selectors.
* @return a new TimePickerDialog instance.
*/
- @SuppressWarnings({"unused", "SameParameterValue"})
+ @SuppressWarnings({"SameParameterValue"})
public static TimePickerDialog newInstance(OnTimeSetListener callback, boolean is24HourMode) {
- Calendar now = Calendar.getInstance();
- return newInstance(callback, now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), is24HourMode);
+ LocalTime now = LocalTime.now();
+ return newInstance(callback, now.getHour(), now.getMinute(), is24HourMode);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java
index 02ad6b309..b780efec5 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java
@@ -17,14 +17,13 @@ import androidx.preference.PreferenceManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficient;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.getContrastRatio;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.getForegroundColorForBackgroundColor;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficient;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas;
public abstract class BrandingUtil {
@@ -44,7 +43,7 @@ public abstract class BrandingUtil {
DeckLog.log("--- Read: shared_preference_theme_main");
return sharedPreferences.getInt(context.getString(R.string.shared_preference_theme_main), context.getApplicationContext().getResources().getColor(R.color.defaultBrand));
} else {
- return context.getResources().getColor(R.color.defaultBrand);
+ return ContextCompat.getColor(context, R.color.defaultBrand);
}
}
@@ -87,13 +86,13 @@ public abstract class BrandingUtil {
fab.setSupportBackgroundTintList(ColorStateList.valueOf(contrastRatioIsSufficient
? mainColor
: ContextCompat.getColor(fab.getContext(), R.color.accent)));
- fab.setColorFilter(contrastRatioIsSufficient ? getForegroundColorForBackgroundColor(mainColor) : mainColor);
+ fab.setColorFilter(contrastRatioIsSufficient ? ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(mainColor) : mainColor);
}
public static void applyBrandToPrimaryTabLayout(@ColorInt int mainColor, @NonNull TabLayout tabLayout) {
- @ColorInt int finalMainColor = getSecondaryForegroundColorDependingOnTheme(tabLayout.getContext(), mainColor);
+ @ColorInt final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(tabLayout.getContext(), mainColor);
tabLayout.setBackgroundColor(ContextCompat.getColor(tabLayout.getContext(), R.color.primary));
- final boolean contrastRatioIsSufficient = getContrastRatio(mainColor, ContextCompat.getColor(tabLayout.getContext(), R.color.primary)) > 1.7d;
+ final boolean contrastRatioIsSufficient = ColorUtil.INSTANCE.getContrastRatio(mainColor, ContextCompat.getColor(tabLayout.getContext(), R.color.primary)) > 1.7d;
tabLayout.setSelectedTabIndicatorColor(contrastRatioIsSufficient ? mainColor : finalMainColor);
}
@@ -112,7 +111,7 @@ public abstract class BrandingUtil {
finalMainColor,
finalMainColor,
finalMainColor,
- editText.getContext().getResources().getColor(R.color.fg_secondary)
+ ContextCompat.getColor(editText.getContext(), R.color.fg_secondary)
}
));
}
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
new file mode 100644
index 000000000..4d3b8bb35
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java
@@ -0,0 +1,122 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.content.Context;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.ColorInt;
+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.google.android.material.card.MaterialCardView;
+
+import org.jetbrains.annotations.Contract;
+
+import java.time.ZoneId;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.R;
+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.enums.DBStatus;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.util.DateUtil;
+import it.niedermann.nextcloud.deck.util.ViewUtil;
+
+public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder {
+
+ public AbstractCardViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ /**
+ * 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, @ColorInt int mainColor) {
+ final Context context = itemView.getContext();
+
+ bindCardClickListener(null);
+ bindCardLongClickListener(null);
+
+ getCardMenu().setVisibility(hasEditPermission ? View.VISIBLE : View.GONE);
+ getCardTitle().setText(fullCard.getCard().getTitle().trim());
+
+ DrawableCompat.setTint(getNotSyncedYet().getDrawable(), mainColor);
+ getNotSyncedYet().setVisibility(DBStatus.LOCAL_EDITED.equals(fullCard.getStatusEnum()) ? View.VISIBLE : View.GONE);
+
+ if (fullCard.getCard().getDueDate() != null) {
+ setupDueDate(getCardDueDate(), fullCard.getCard());
+ getCardDueDate().setVisibility(View.VISIBLE);
+ } else {
+ getCardDueDate().setVisibility(View.GONE);
+ }
+
+ getCardMenu().setOnClickListener(view -> {
+ final PopupMenu popup = new PopupMenu(context, view);
+ popup.inflate(optionsMenu);
+ final Menu menu = popup.getMenu();
+ if (containsUser(fullCard.getAssignedUsers(), account.getUserName())) {
+ menu.removeItem(menu.findItem(R.id.action_card_assign).getItemId());
+ } else {
+ menu.removeItem(menu.findItem(R.id.action_card_unassign).getItemId());
+ }
+ if (boardRemoteId == null || fullCard.getCard().getId() == null) {
+ menu.removeItem(R.id.share_link);
+ }
+
+ popup.setOnMenuItemClickListener(item -> optionsItemsSelectedListener.onCardOptionsItemSelected(item, fullCard));
+ popup.show();
+ });
+ }
+
+ protected abstract TextView getCardDueDate();
+
+ protected abstract ImageView getNotSyncedYet();
+
+ protected abstract TextView getCardTitle();
+
+ protected abstract View getCardMenu();
+
+ protected abstract MaterialCardView getCard();
+
+ public void bindCardClickListener(@Nullable OnClickListener l) {
+ getCard().setOnClickListener(l);
+ }
+
+ public void bindCardLongClickListener(@Nullable OnLongClickListener l) {
+ getCard().setOnLongClickListener(l);
+ }
+
+ public MaterialCardView getDraggable() {
+ return getCard();
+ }
+
+ private static void setupDueDate(@NonNull TextView cardDueDate, @NonNull Card card) {
+ final Context context = cardDueDate.getContext();
+ cardDueDate.setText(DateUtil.getRelativeDateTimeString(context, card.getDueDate().toEpochMilli()));
+ ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate().atZone(ZoneId.systemDefault()).toLocalDate());
+ }
+
+ @Contract("null, _ -> false")
+ private static boolean containsUser(List<User> userList, String username) {
+ if (userList != null) {
+ for (User user : userList) {
+ if (user.getPrimaryKey().equals(username)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+} \ No newline at end of file
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 986a66cad..87140e544 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,99 +1,82 @@
package it.niedermann.nextcloud.deck.ui.card;
-import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
-import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.PopupMenu;
-import android.widget.TextView;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
-import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.RecyclerView;
-import org.jetbrains.annotations.Contract;
-import org.jetbrains.annotations.NotNull;
-
import java.util.ArrayList;
-import java.util.LinkedList;
import java.util.List;
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.databinding.ItemCardBinding;
+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.Account;
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.enums.DBStatus;
import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.model.full.FullStack;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
+import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.branding.Branded;
-import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-import it.niedermann.nextcloud.deck.util.DateUtil;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
+import it.niedermann.nextcloud.deck.ui.movecard.MoveCardDialogFragment;
+import static androidx.preference.PreferenceManager.getDefaultSharedPreferences;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN;
-public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implements DragAndDropAdapter<FullCard>, Branded {
-
- protected final SyncManager syncManager;
+public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> implements DragAndDropAdapter<FullCard>, CardOptionsItemSelectedListener, Branded {
- private final FragmentManager fragmentManager;
- private final Account account;
- @Nullable
- private final Long currentBoardRemoteId;
- private final long boardId;
+ private final boolean compactMode;
+ @NonNull
+ protected final MainViewModel mainViewModel;
+ @NonNull
+ protected final FragmentManager fragmentManager;
private final long stackId;
- private final boolean canEdit;
@NonNull
private final Context context;
@Nullable
private final SelectCardListener selectCardListener;
- private List<FullCard> cardList = new LinkedList<>();
- private LifecycleOwner lifecycleOwner;
- private List<FullStack> availableStacks = new ArrayList<>();
- private String counterMaxValue;
-
- private int mainColor;
+ @NonNull
+ protected List<FullCard> cardList = new ArrayList<>();
+ @NonNull
+ protected LifecycleOwner lifecycleOwner;
+ @NonNull
+ protected String counterMaxValue;
+ @ColorInt
+ protected int mainColor;
@StringRes
- private int shareLinkRes;
+ private final int shareLinkRes;
- public CardAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull Account account, long boardId, @Nullable Long currentBoardRemoteId, long stackId, boolean canEdit, @NonNull SyncManager syncManager, @NonNull LifecycleOwner lifecycleOwner, @Nullable SelectCardListener selectCardListener) {
+ public CardAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, long stackId, @NonNull MainViewModel mainViewModel, @NonNull LifecycleOwner lifecycleOwner, @Nullable SelectCardListener selectCardListener) {
this.context = context;
+ this.counterMaxValue = context.getString(R.string.counter_max_value);
this.fragmentManager = fragmentManager;
this.lifecycleOwner = lifecycleOwner;
- this.account = account;
- this.shareLinkRes = account.getServerDeckVersionAsObject().getShareLinkResource();
- this.boardId = boardId;
- this.currentBoardRemoteId = currentBoardRemoteId;
+ this.shareLinkRes = mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().getShareLinkResource();
this.stackId = stackId;
- this.canEdit = canEdit;
- this.syncManager = syncManager;
+ this.mainViewModel = mainViewModel;
this.selectCardListener = selectCardListener;
- this.mainColor = context.getResources().getColor(R.color.primary);
- syncManager.getStacksForBoard(account.getId(), boardId).observe(this.lifecycleOwner, (stacks) -> {
- availableStacks.clear();
- availableStacks.addAll(stacks);
- });
+ this.mainColor = ContextCompat.getColor(context, R.color.defaultBrand);
+ this.compactMode = getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.pref_key_compact), false);
setHasStableIds(true);
}
@@ -104,118 +87,62 @@ public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implem
@NonNull
@Override
- public ItemCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
- final Context context = viewGroup.getContext();
- counterMaxValue = context.getString(R.string.counter_max_value);
+ public AbstractCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+ if (viewType == R.layout.item_card_compact) {
+ return new CompactCardViewHolder(ItemCardCompactBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false));
+ } else if (viewType == R.layout.item_card_default_only_title) {
+ return new DefaultCardOnlyTitleViewHolder(ItemCardDefaultOnlyTitleBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false));
+ }
+ return new DefaultCardViewHolder(ItemCardDefaultBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false));
+ }
- LayoutInflater layoutInflater = LayoutInflater.from(context);
- ItemCardBinding binding = ItemCardBinding.inflate(layoutInflater, viewGroup, false);
- return new ItemCardViewHolder(binding);
+ @Override
+ public int getItemViewType(int position) {
+ if (compactMode) {
+ return R.layout.item_card_compact;
+ } else {
+ final FullCard fullCard = cardList.get(position);
+ if (fullCard.getAttachments().size() == 0
+ && fullCard.getAssignedUsers().size() == 0
+ && fullCard.getLabels().size() == 0
+ && fullCard.getCommentCount() == 0) {
+ return R.layout.item_card_default_only_title;
+ }
+ return R.layout.item_card_default;
+ }
}
- @SuppressLint("SetTextI18n")
@Override
- public void onBindViewHolder(@NonNull ItemCardViewHolder viewHolder, int position) {
- final Context context = viewHolder.itemView.getContext();
- final FullCard card = cardList.get(position);
+ public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) {
+ @NonNull FullCard fullCard = cardList.get(position);
+ viewHolder.bind(fullCard, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardRemoteId(), mainViewModel.currentBoardHasEditPermission(), R.menu.card_menu, this, counterMaxValue, mainColor);
- viewHolder.binding.card.setOnClickListener((v) -> {
+ // Only enable details view if there is no one waiting for selecting a card.
+ viewHolder.bindCardClickListener((v) -> {
if (selectCardListener == null) {
- context.startActivity(EditActivity.createEditCardIntent(context, account, boardId, card.getLocalId()));
+ context.startActivity(EditActivity.createEditCardIntent(context, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId()));
} else {
- selectCardListener.onCardSelected(card);
+ selectCardListener.onCardSelected(fullCard);
}
});
- if (canEdit && selectCardListener == null) {
- viewHolder.binding.card.setOnLongClickListener((v) -> {
+
+ // Only enable Drag and Drop if there is no one waiting for selecting a card.
+ if (selectCardListener == null) {
+ viewHolder.bindCardLongClickListener((v) -> {
DeckLog.log("Starting drag and drop");
- v.startDrag(ClipData.newPlainText("cardid", String.valueOf(card.getLocalId())),
+ v.startDrag(ClipData.newPlainText("cardid", String.valueOf(fullCard.getLocalId())),
new View.DragShadowBuilder(v),
- new DraggedItemLocalState<>(card, viewHolder.binding.card, this, position),
+ new DraggedItemLocalState<>(fullCard, viewHolder.getDraggable(), this, position),
0
);
return true;
});
- } else {
- viewHolder.binding.cardMenu.setVisibility(View.GONE);
- }
- viewHolder.binding.cardTitle.setText(card.getCard().getTitle().trim());
-
- if (card.getAssignedUsers() != null && card.getAssignedUsers().size() > 0) {
- viewHolder.binding.overlappingAvatars.setAvatars(account, card.getAssignedUsers());
- viewHolder.binding.overlappingAvatars.setVisibility(View.VISIBLE);
- } else {
- viewHolder.binding.overlappingAvatars.setVisibility(View.GONE);
}
-
- DrawableCompat.setTint(viewHolder.binding.notSyncedYet.getDrawable(), mainColor);
- viewHolder.binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(card.getStatusEnum()) ? View.VISIBLE : View.GONE);
-
- if (card.getCard().getDueDate() != null) {
- setupDueDate(viewHolder.binding.cardDueDate, card.getCard());
- viewHolder.binding.cardDueDate.setVisibility(View.VISIBLE);
- } else {
- viewHolder.binding.cardDueDate.setVisibility(View.GONE);
- }
-
- final int attachmentsCount = card.getAttachments().size();
-
- if (attachmentsCount == 0) {
- viewHolder.binding.cardCountAttachments.setVisibility(View.GONE);
- } else {
- setupCounter(viewHolder.binding.cardCountAttachments, attachmentsCount);
- viewHolder.binding.cardCountAttachments.setVisibility(View.VISIBLE);
- }
-
- final int commentsCount = card.getCommentCount();
-
- if (commentsCount == 0) {
- viewHolder.binding.cardCountComments.setVisibility(View.GONE);
- } else {
- setupCounter(viewHolder.binding.cardCountComments, commentsCount);
-
- viewHolder.binding.cardCountComments.setVisibility(View.VISIBLE);
- }
-
- List<Label> labels = card.getLabels();
- if (labels != null && labels.size() > 0) {
- viewHolder.binding.labels.updateLabels(labels);
- viewHolder.binding.labels.setVisibility(View.VISIBLE);
- } else {
- viewHolder.binding.labels.removeAllViews();
- viewHolder.binding.labels.setVisibility(View.GONE);
- }
-
- Card.TaskStatus taskStatus = card.getCard().getTaskStatus();
- if (taskStatus.taskCount > 0) {
- viewHolder.binding.cardCountTasks.setText(context.getResources().getString(R.string.task_count, String.valueOf(taskStatus.doneCount), String.valueOf(taskStatus.taskCount)));
- viewHolder.binding.cardCountTasks.setVisibility(View.VISIBLE);
- } else {
- viewHolder.binding.cardCountTasks.setVisibility(View.GONE);
- }
-
- viewHolder.binding.cardMenu.setOnClickListener(v -> onOverflowIconClicked(v, card));
- }
-
- private void setupCounter(@NonNull TextView textView, int count) {
- if (count > 99) {
- textView.setText(counterMaxValue);
- } else if (count > 1) {
- textView.setText(String.valueOf(count));
- } else if (count == 1) {
- textView.setText("");
- }
- }
-
- private void setupDueDate(@NonNull TextView cardDueDate, @NotNull Card card) {
- final Context context = cardDueDate.getContext();
- cardDueDate.setText(DateUtil.getRelativeDateTimeString(context, card.getDueDate().getTime()));
- ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate());
}
@Override
public int getItemCount() {
- return cardList == null ? 0 : cardList.size();
+ return cardList.size();
}
public void insertItem(FullCard fullCard, int position) {
@@ -223,6 +150,7 @@ public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implem
notifyItemInserted(position);
}
+ @NonNull
@Override
public List<FullCard> getItemList() {
return this.cardList;
@@ -240,114 +168,59 @@ public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implem
notifyItemRemoved(position);
}
- protected void onOverflowIconClicked(@NotNull View view, FullCard card) {
- final Context context = view.getContext();
- final PopupMenu popup = new PopupMenu(context, view);
- popup.inflate(R.menu.card_menu);
- prepareOptionsMenu(popup.getMenu(), card);
-
- popup.setOnMenuItemClickListener(item -> optionsItemSelected(context, item, card));
- popup.show();
- }
-
- protected void prepareOptionsMenu(Menu menu, @NotNull FullCard card) {
- if (containsUser(card.getAssignedUsers(), account.getUserName())) {
- menu.removeItem(menu.findItem(R.id.action_card_assign).getItemId());
- } else {
- menu.removeItem(menu.findItem(R.id.action_card_unassign).getItemId());
- }
- if (currentBoardRemoteId == null || card.getCard().getId() == null) {
- menu.removeItem(R.id.share_link);
- }
- }
-
public void setCardList(@NonNull List<FullCard> cardList) {
this.cardList.clear();
this.cardList.addAll(cardList);
notifyDataSetChanged();
}
- @Contract("null, _ -> false")
- private boolean containsUser(List<User> userList, String username) {
- if (userList != null) {
- for (User user : userList) {
- if (user.getPrimaryKey().equals(username)) {
- return true;
- }
- }
- }
- return false;
+ @Override
+ public void applyBrand(int mainColor) {
+ this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor);
+ notifyDataSetChanged();
}
- protected boolean optionsItemSelected(@NonNull Context context, @NotNull MenuItem item, FullCard fullCard) {
- switch (item.getItemId()) {
- case R.id.share_link: {
- Intent 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() + context.getString(shareLinkRes, currentBoardRemoteId, fullCard.getCard().getId()));
- context.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle()));
- }
- case R.id.action_card_assign: {
- new Thread(() -> syncManager.assignUserToCard(syncManager.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
- return true;
- }
- case R.id.action_card_unassign: {
- new Thread(() -> syncManager.unassignUserFromCard(syncManager.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
- return true;
- }
- case R.id.action_card_move: {
- int currentStackItem = 0;
- CharSequence[] items = new CharSequence[availableStacks.size()];
- for (int i = 0; i < availableStacks.size(); i++) {
- final Stack stack = availableStacks.get(i).getStack();
- items[i] = stack.getTitle();
- if (stack.getLocalId().equals(stackId)) {
- currentStackItem = i;
- }
+ @Override
+ public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) {
+ int itemId = menuItem.getItemId();
+ final Account account = mainViewModel.getCurrentAccount();
+ if (itemId == R.id.share_link) {
+ Intent 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() + context.getString(shareLinkRes, mainViewModel.getCurrentBoardRemoteId(), fullCard.getCard().getId()));
+ context.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle()));
+ new Thread(() -> mainViewModel.assignUserToCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
+ return true;
+ } else if (itemId == R.id.action_card_assign) {
+ new Thread(() -> mainViewModel.assignUserToCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
+ return true;
+ } else if (itemId == R.id.action_card_unassign) {
+ new Thread(() -> mainViewModel.unassignUserFromCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
+ 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()).show(fragmentManager, MoveCardDialogFragment.class.getSimpleName());
+ return true;
+ } else if (itemId == R.id.action_card_archive) {
+ final WrappedLiveData<FullCard> archiveLiveData = mainViewModel.archiveCard(fullCard);
+ observeOnce(archiveLiveData, lifecycleOwner, (v) -> {
+ if (archiveLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(archiveLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
}
- final FullCard newCard = fullCard;
- new BrandedAlertDialogBuilder(context)
- .setSingleChoiceItems(items, currentStackItem, (dialog, which) -> {
- dialog.cancel();
- newCard.getCard().setStackId(availableStacks.get(which).getStack().getLocalId());
- LiveDataHelper.observeOnce(syncManager.updateCard(newCard), lifecycleOwner, (c) -> {
- // Nothing to do here...
- });
- DeckLog.log("Moved card \"" + fullCard.getCard().getTitle() + "\" to \"" + availableStacks.get(which).getStack().getTitle() + "\"");
- })
- .setNeutralButton(android.R.string.cancel, null)
- .setTitle(context.getString(R.string.action_card_move_title, fullCard.getCard().getTitle()))
- .show();
- return true;
- }
- case R.id.action_card_archive: {
- final WrappedLiveData<FullCard> archiveLiveData = syncManager.archiveCard(fullCard);
- observeOnce(archiveLiveData, lifecycleOwner, (v) -> {
- if (archiveLiveData.hasError()) {
- ExceptionDialogFragment.newInstance(archiveLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
- }
- });
- return true;
- }
- case R.id.action_card_delete: {
- final WrappedLiveData<Void> deleteLiveData = syncManager.deleteCard(fullCard.getCard());
- observeOnce(deleteLiveData, lifecycleOwner, (v) -> {
- if (deleteLiveData.hasError()) {
- ExceptionDialogFragment.newInstance(deleteLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
- }
- });
- return true;
- }
+ });
+ return true;
+ } else if (itemId == R.id.action_card_delete) {
+ final WrappedLiveData<Void> deleteLiveData = mainViewModel.deleteCard(fullCard.getCard());
+ observeOnce(deleteLiveData, lifecycleOwner, (v) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ return true;
}
return true;
}
-
- @Override
- public void applyBrand(int mainColor) {
- this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor);
- notifyDataSetChanged();
- }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java
new file mode 100644
index 000000000..d3050b732
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java
@@ -0,0 +1,11 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+
+public interface CardOptionsItemSelectedListener {
+ boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard);
+} \ No newline at end of file
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
new file mode 100644
index 000000000..e9f366d99
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java
@@ -0,0 +1,84 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.MenuRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.material.card.MaterialCardView;
+
+import java.util.List;
+
+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;
+
+public class CompactCardViewHolder extends AbstractCardViewHolder {
+ private ItemCardCompactBinding binding;
+
+ @SuppressWarnings("WeakerAccess")
+ public CompactCardViewHolder(@NonNull ItemCardCompactBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ /**
+ * Removes all {@link OnClickListener} and {@link OnLongClickListener}
+ */
+ public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @ColorInt int mainColor) {
+ super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, mainColor);
+
+ List<Label> labels = fullCard.getLabels();
+ if (labels != null && labels.size() > 0) {
+ binding.labels.updateLabels(labels);
+ binding.labels.setVisibility(View.VISIBLE);
+ } else {
+ binding.labels.removeAllViews();
+ binding.labels.setVisibility(View.GONE);
+ }
+ }
+
+ public void bindCardClickListener(@Nullable OnClickListener l) {
+ binding.card.setOnClickListener(l);
+ }
+
+ public void bindCardLongClickListener(@Nullable OnLongClickListener l) {
+ binding.card.setOnLongClickListener(l);
+ }
+
+ public MaterialCardView getDraggable() {
+ return binding.card;
+ }
+
+ @Override
+ protected TextView getCardDueDate() {
+ return binding.cardDueDate;
+ }
+
+ @Override
+ protected ImageView getNotSyncedYet() {
+ return binding.notSyncedYet;
+ }
+
+ @Override
+ protected TextView getCardTitle() {
+ return binding.cardTitle;
+ }
+
+ @Override
+ protected View getCardMenu() {
+ return binding.cardMenu;
+ }
+
+ @Override
+ protected MaterialCardView getCard() {
+ return binding.card;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java
new file mode 100644
index 000000000..2f9e132c9
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java
@@ -0,0 +1,61 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.material.card.MaterialCardView;
+
+import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultOnlyTitleBinding;
+
+public class DefaultCardOnlyTitleViewHolder extends AbstractCardViewHolder {
+ private ItemCardDefaultOnlyTitleBinding binding;
+
+ @SuppressWarnings("WeakerAccess")
+ public DefaultCardOnlyTitleViewHolder(@NonNull ItemCardDefaultOnlyTitleBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bindCardClickListener(@Nullable OnClickListener l) {
+ binding.card.setOnClickListener(l);
+ }
+
+ public void bindCardLongClickListener(@Nullable OnLongClickListener l) {
+ binding.card.setOnLongClickListener(l);
+ }
+
+ public MaterialCardView getDraggable() {
+ return binding.card;
+ }
+
+ @Override
+ protected TextView getCardDueDate() {
+ return binding.cardDueDate;
+ }
+
+ @Override
+ protected ImageView getNotSyncedYet() {
+ return binding.notSyncedYet;
+ }
+
+ @Override
+ protected TextView getCardTitle() {
+ return binding.cardTitle;
+ }
+
+ @Override
+ protected View getCardMenu() {
+ return binding.cardMenu;
+ }
+
+ @Override
+ protected MaterialCardView getCard() {
+ return binding.card;
+ }
+} \ No newline at end of file
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
new file mode 100644
index 000000000..6025cdada
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java
@@ -0,0 +1,157 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.MenuRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.card.MaterialCardView;
+
+import org.jetbrains.annotations.Contract;
+
+import java.util.List;
+
+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.Card.TaskStatus;
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+
+public class DefaultCardViewHolder extends AbstractCardViewHolder {
+ private ItemCardDefaultBinding binding;
+
+ @SuppressWarnings("WeakerAccess")
+ public DefaultCardViewHolder(@NonNull ItemCardDefaultBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ /**
+ * Removes all {@link OnClickListener} and {@link OnLongClickListener}
+ */
+ public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @ColorInt int mainColor) {
+ super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, mainColor);
+
+ final Context context = itemView.getContext();
+
+ if (fullCard.getAssignedUsers() != null && fullCard.getAssignedUsers().size() > 0) {
+ binding.overlappingAvatars.setAvatars(account, fullCard.getAssignedUsers());
+ binding.overlappingAvatars.setVisibility(View.VISIBLE);
+ } else {
+ binding.overlappingAvatars.setVisibility(View.GONE);
+ }
+
+ final int attachmentsCount = fullCard.getAttachments().size();
+ if (attachmentsCount == 0) {
+ binding.cardCountAttachments.setVisibility(View.GONE);
+ } else {
+ setupCounter(binding.cardCountAttachments, counterMaxValue, attachmentsCount);
+ binding.cardCountAttachments.setVisibility(View.VISIBLE);
+ }
+
+ final int commentsCount = fullCard.getCommentCount();
+ if (commentsCount == 0) {
+ binding.cardCountComments.setVisibility(View.GONE);
+ } else {
+ setupCounter(binding.cardCountComments, counterMaxValue, commentsCount);
+
+ binding.cardCountComments.setVisibility(View.VISIBLE);
+ }
+
+ final List<Label> labels = fullCard.getLabels();
+ if (labels != null && labels.size() > 0) {
+ binding.labels.updateLabels(labels);
+ binding.labels.setVisibility(View.VISIBLE);
+ } else {
+ binding.labels.removeAllViews();
+ binding.labels.setVisibility(View.GONE);
+ }
+
+ final TaskStatus taskStatus = fullCard.getCard().getTaskStatus();
+ if (taskStatus.taskCount > 0) {
+ binding.cardCountTasks.setText(context.getResources().getString(R.string.task_count, String.valueOf(taskStatus.doneCount), String.valueOf(taskStatus.taskCount)));
+ binding.cardCountTasks.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.ic_check_grey600_24dp), null, null, null);
+ binding.cardCountTasks.setVisibility(View.VISIBLE);
+ } else {
+ final String description = fullCard.getCard().getDescription();
+ if (!TextUtils.isEmpty(description)) {
+ binding.cardCountTasks.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.ic_baseline_subject_24), null, null, null);
+ binding.cardCountTasks.setText(null);
+ binding.cardCountTasks.setVisibility(View.VISIBLE);
+ } else {
+ binding.cardCountTasks.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ protected TextView getCardDueDate() {
+ return binding.cardDueDate;
+ }
+
+ @Override
+ protected ImageView getNotSyncedYet() {
+ return binding.notSyncedYet;
+ }
+
+ @Override
+ protected TextView getCardTitle() {
+ return binding.cardTitle;
+ }
+
+ @Override
+ protected View getCardMenu() {
+ return binding.cardMenu;
+ }
+
+ @Override
+ protected MaterialCardView getCard() {
+ return binding.card;
+ }
+
+ public void bindCardClickListener(@Nullable OnClickListener l) {
+ binding.card.setOnClickListener(l);
+ }
+
+ public void bindCardLongClickListener(@Nullable OnLongClickListener l) {
+ binding.card.setOnLongClickListener(l);
+ }
+
+ public MaterialCardView getDraggable() {
+ return binding.card;
+ }
+
+
+ private static void setupCounter(@NonNull TextView textView, @NonNull String counterMaxValue, int count) {
+ if (count > 99) {
+ textView.setText(counterMaxValue);
+ } else if (count > 1) {
+ textView.setText(String.valueOf(count));
+ } else if (count == 1) {
+ textView.setText("");
+ }
+ }
+
+ @Contract("null, _ -> false")
+ private static boolean containsUser(List<User> userList, String username) {
+ if (userList != null) {
+ for (User user : userList) {
+ if (user.getPrimaryKey().equals(username)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+} \ No newline at end of file
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 0710b7d7c..2e1ff5e06 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
@@ -25,13 +25,12 @@ 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.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;
import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
import it.niedermann.nextcloud.deck.util.CardUtil;
-import static android.graphics.Color.parseColor;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToPrimaryTabLayout;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
@@ -45,7 +44,6 @@ public class EditActivity extends BrandedActivity {
private ActivityEditBinding binding;
private EditCardViewModel viewModel;
- private SyncManager syncManager;
private static final int[] tabTitles = new int[]{
R.string.card_edit_details,
@@ -83,7 +81,6 @@ public class EditActivity extends BrandedActivity {
setSupportActionBar(binding.toolbar);
viewModel = new ViewModelProvider(this).get(EditCardViewModel.class);
- syncManager = new SyncManager(this);
loadDataFromIntent();
}
@@ -120,8 +117,8 @@ public class EditActivity extends BrandedActivity {
final long boardId = args.getLong(BUNDLE_KEY_BOARD_ID);
- observeOnce(syncManager.getFullBoardById(account.getId(), boardId), EditActivity.this, (fullBoard -> {
- applyBrand(parseColor('#' + fullBoard.getBoard().getColor()));
+ observeOnce(viewModel.getFullBoardById(account.getId(), boardId), EditActivity.this, (fullBoard -> {
+ applyBrand(fullBoard.getBoard().getColor());
viewModel.setCanEdit(fullBoard.getBoard().isPermissionEdit());
invalidateOptionsMenu();
if (viewModel.isCreateMode()) {
@@ -138,7 +135,7 @@ public class EditActivity extends BrandedActivity {
setupViewPager();
setupTitle();
} else {
- observeOnce(syncManager.getCardByLocalId(account.getId(), cardId), EditActivity.this, (fullCard) -> {
+ observeOnce(viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardId), EditActivity.this, (fullCard) -> {
if (fullCard == null) {
new BrandedAlertDialogBuilder(this)
.setTitle(R.string.card_not_found)
@@ -154,6 +151,8 @@ public class EditActivity extends BrandedActivity {
});
}
}));
+
+ DeckLog.verbose("Finished loading intent data: { accountId = " + viewModel.getAccount().getId() + " , cardId = " + cardId + " }");
}
@Override
@@ -170,12 +169,16 @@ public class EditActivity extends BrandedActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_card_save) {
- saveAndFinish();
+ saveAndRun(super::finish);
}
return super.onOptionsItemSelected(item);
}
- private void saveAndFinish() {
+ /**
+ * Tries to save the current {@link FullCard} from the {@link EditCardViewModel} and then runs the given {@link Runnable}
+ * @param runnable
+ */
+ private void saveAndRun(@NonNull Runnable runnable) {
if (!viewModel.isPendingCreation()) {
viewModel.setPendingCreation(true);
final String title = viewModel.getFullCard().getCard().getTitle();
@@ -193,9 +196,9 @@ public class EditActivity extends BrandedActivity {
.show();
} else {
if (viewModel.isCreateMode()) {
- observeOnce(syncManager.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> super.finish());
+ observeOnce(viewModel.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run());
} else {
- observeOnce(syncManager.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> super.finish());
+ observeOnce(viewModel.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run());
}
}
}
@@ -264,26 +267,15 @@ public class EditActivity extends BrandedActivity {
}
@Override
- public boolean onSupportNavigateUp() {
- finish(); // close this activity as oppose to navigating up
- return true;
- }
-
- @Override
- public void onBackPressed() {
- finish();
- }
-
- @Override
public void finish() {
if (!viewModel.hasChanges() && viewModel.canEdit()) {
new BrandedAlertDialogBuilder(this)
.setTitle(R.string.simple_save)
.setMessage(R.string.do_you_want_to_save_your_changes)
- .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndFinish())
+ .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndRun(super::finish))
.setNegativeButton(R.string.simple_discard, (dialog, whichButton) -> super.finish()).show();
} else {
- directFinish();
+ super.finish();
}
}
@@ -296,10 +288,10 @@ public class EditActivity extends BrandedActivity {
@Override
public void applyBrand(int mainColor) {
- if(isBrandingEnabled(this)) {
+ if (isBrandingEnabled(this)) {
final Drawable navigationIcon = binding.toolbar.getNavigationIcon();
if (navigationIcon == null) {
- DeckLog.error("Excpected navigationIcon to be present.");
+ DeckLog.error("Expected navigationIcon to be present.");
} else {
DrawableCompat.setTint(binding.toolbar.getNavigationIcon(), colorAccent);
}
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 c8c93c838..c754d0800 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,38 +1,57 @@
package it.niedermann.nextcloud.deck.ui.card;
+import android.app.Application;
+
import androidx.annotation.NonNull;
-import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import java.io.File;
import java.util.ArrayList;
+import java.util.List;
import it.niedermann.nextcloud.deck.DeckLog;
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.full.FullBoard;
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.persistence.sync.adapters.db.util.WrappedLiveData;
@SuppressWarnings("WeakerAccess")
-public class EditCardViewModel extends ViewModel {
+public class EditCardViewModel extends AndroidViewModel {
+ private SyncManager syncManager;
private Account account;
private long boardId;
- private FullCard originalCard;
- private FullCard fullCard;
+ private FullCardWithProjects originalCard;
+ private FullCardWithProjects fullCard;
private boolean isSupportedVersion = false;
private boolean hasCommentsAbility = false;
private boolean pendingCreation = false;
private boolean canEdit = false;
private boolean createMode = false;
+ public EditCardViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
+
/**
* Stores a deep copy of the given fullCard to be able to compare the state at every time in #{@link EditCardViewModel#hasChanges()}
*
* @param boardId Local ID, expecting a positive long value
* @param fullCard The card that is currently edited
*/
- public void initializeExistingCard(long boardId, @NonNull FullCard fullCard, boolean isSupportedVersion) {
+ public void initializeExistingCard(long boardId, @NonNull FullCardWithProjects fullCard, boolean isSupportedVersion) {
this.boardId = boardId;
this.fullCard = fullCard;
- this.originalCard = new FullCard(this.fullCard);
+ this.originalCard = new FullCardWithProjects(this.fullCard);
this.isSupportedVersion = isSupportedVersion;
}
@@ -43,7 +62,7 @@ public class EditCardViewModel extends ViewModel {
* @param stackId Local ID, expecting a positive long value where the card should be created
*/
public void initializeNewCard(long boardId, long stackId, boolean isSupportedVersion) {
- final FullCard fullCard = new FullCard();
+ final FullCardWithProjects fullCard = new FullCardWithProjects();
fullCard.setLabels(new ArrayList<>());
fullCard.setAssignedUsers(new ArrayList<>());
fullCard.setAttachments(new ArrayList<>());
@@ -55,11 +74,12 @@ public class EditCardViewModel extends ViewModel {
public void setAccount(@NonNull Account account) {
this.account = account;
+ this.syncManager = new SyncManager(getApplication(), account.getName());
hasCommentsAbility = account.getServerDeckVersionAsObject().supportsComments();
}
public boolean hasChanges() {
- if(fullCard == null) {
+ if (fullCard == null) {
DeckLog.info("Can not check for changes because fullCard is null → assuming no changes have been made yet.");
return false;
}
@@ -74,7 +94,7 @@ public class EditCardViewModel extends ViewModel {
return account;
}
- public FullCard getFullCard() {
+ public FullCardWithProjects getFullCard() {
return fullCard;
}
@@ -105,4 +125,44 @@ public class EditCardViewModel extends ViewModel {
public long getBoardId() {
return boardId;
}
+
+ public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) {
+ return syncManager.getFullBoardById(accountId, localId);
+ }
+
+ public WrappedLiveData<Label> createLabel(long accountId, Label label, long localBoardId) {
+ return syncManager.createLabel(accountId, label, localBoardId);
+ }
+
+ public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) {
+ return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId);
+ }
+
+ public WrappedLiveData<FullCard> createFullCard(long accountId, long localBoardId, long localStackId, @NonNull FullCard card) {
+ return syncManager.createFullCard(accountId, localBoardId, localStackId, card);
+ }
+
+ public WrappedLiveData<FullCard> updateCard(@NonNull FullCard card) {
+ return syncManager.updateCard(card);
+ }
+
+ public LiveData<List<Activity>> syncActivitiesForCard(@NonNull Card card) {
+ return syncManager.syncActivitiesForCard(card);
+ }
+
+ public WrappedLiveData<Attachment> addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file) {
+ return syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file);
+ }
+
+ public WrappedLiveData<Void> deleteAttachmentOfCard(long accountId, long localCardId, long localAttachmentId) {
+ return syncManager.deleteAttachmentOfCard(accountId, localCardId, localAttachmentId);
+ }
+
+ public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) {
+ return syncManager.getCardByRemoteID(accountId, remoteId);
+ }
+
+ public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
+ return syncManager.getBoardByRemoteId(accountId, remoteId);
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java
deleted file mode 100644
index c9e41c37f..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package it.niedermann.nextcloud.deck.ui.card;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import it.niedermann.nextcloud.deck.databinding.ItemCardBinding;
-
-public class ItemCardViewHolder extends RecyclerView.ViewHolder {
- public ItemCardBinding binding;
-
- @SuppressWarnings("WeakerAccess")
- public ItemCardViewHolder(ItemCardBinding binding) {
- super(binding.getRoot());
- this.binding = binding;
- }
-} \ No newline at end of file
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 f77282e11..fe974f2a0 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
@@ -9,6 +9,7 @@ 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.graphics.drawable.DrawableCompat;
@@ -18,11 +19,11 @@ import java.util.Collection;
import java.util.List;
import java.util.Random;
+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.Label;
import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
@@ -35,7 +36,7 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> {
public LabelAutoCompleteAdapter(@NonNull ComponentActivity activity, long accountId, long boardId, long cardId) {
super(activity, accountId, boardId, cardId);
final String[] colors = activity.getResources().getStringArray(R.array.board_default_colors);
- final String createLabelColor = colors[new Random().nextInt(colors.length)].substring(1);
+ @ColorInt int createLabelColor = Color.parseColor(colors[new Random().nextInt(colors.length)]);
observeOnce(syncManager.getFullBoardById(accountId, boardId), activity, (fullBoard) -> {
if (fullBoard.getBoard().isPermissionManage()) {
canManage = true;
@@ -59,8 +60,8 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> {
}
final Label label = getItem(position);
- final int labelColor = Color.parseColor("#" + label.getColor());
- final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor);
+ final int labelColor = label.getColor();
+ final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
binding.label.setText(label.getTitle());
binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
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 d2fd4d40d..473ad06c1 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
@@ -8,6 +8,7 @@ import android.widget.Filter;
import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
import java.util.List;
@@ -15,14 +16,16 @@ 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;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
-
public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> {
@NonNull
private Account account;
+ private UserSearchLiveData liveSearchForACL;
+ private LiveData<List<User>> liveData;
+ private Observer<List<User>> observer;
public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId) {
this(activity, account, boardId, NO_CARD);
@@ -31,6 +34,7 @@ public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> {
public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId, long cardId) {
super(activity, account.getId(), boardId, cardId);
this.account = account;
+ this.liveSearchForACL = syncManager.searchUserByUidOrDisplayNameForACL();
}
@Override
@@ -56,23 +60,24 @@ public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> {
protected FilterResults performFiltering(CharSequence constraint) {
if (constraint != null) {
activity.runOnUiThread(() -> {
- LiveData<List<User>> liveData;
final int constraintLength = constraint.toString().trim().length();
if (cardId == NO_CARD) {
liveData = constraintLength > 0
- ? syncManager.searchUserByUidOrDisplayNameForACL(accountId, boardId, constraint.toString())
+ ? 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));
}
- observeOnce(liveData, activity, users -> {
+ 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/CardActivityFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java
index 08d960257..f95eea89f 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java
@@ -8,11 +8,9 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.RecyclerView;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabActivitiesBinding;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
public class CardActivityFragment extends Fragment {
@@ -39,17 +37,14 @@ public class CardActivityFragment extends Fragment {
}
if (!viewModel.isCreateMode()) {
- final SyncManager syncManager = new SyncManager(requireContext());
-
- syncManager.syncActivitiesForCard(viewModel.getFullCard().getCard()).observe(getViewLifecycleOwner(), (activities -> {
+ viewModel.syncActivitiesForCard(viewModel.getFullCard().getCard()).observe(getViewLifecycleOwner(), (activities -> {
if (activities == null || activities.size() == 0) {
binding.emptyContentView.setVisibility(View.VISIBLE);
binding.activitiesList.setVisibility(View.GONE);
} else {
binding.emptyContentView.setVisibility(View.GONE);
binding.activitiesList.setVisibility(View.VISIBLE);
- RecyclerView.Adapter adapter = new CardActivityAdapter(activities, requireActivity().getMenuInflater());
- binding.activitiesList.setAdapter(adapter);
+ binding.activitiesList.setAdapter(new CardActivityAdapter(activities, requireActivity().getMenuInflater()));
}
}));
} else {
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 7d49b932d..6362f90dd 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
@@ -3,17 +3,18 @@ package it.niedermann.nextcloud.deck.ui.card.activities;
import android.content.Context;
import android.view.MenuInflater;
import android.view.View;
+import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+import it.niedermann.android.util.ClipboardUtil;
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.util.DateUtil;
-
-import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard;
+import it.niedermann.nextcloud.deck.util.ViewUtil;
public class CardActivityViewHolder extends RecyclerView.ViewHolder {
public ItemActivityBinding binding;
@@ -26,38 +27,61 @@ public class CardActivityViewHolder extends RecyclerView.ViewHolder {
public void bind(@NonNull Activity activity, @NonNull MenuInflater inflater) {
final Context context = itemView.getContext();
- binding.date.setText(DateUtil.getRelativeDateTimeString(context, activity.getLastModified().getTime()));
+ binding.date.setText(DateUtil.getRelativeDateTimeString(context, activity.getLastModified().toEpochMilli()));
binding.subject.setText(activity.getSubject());
itemView.setOnClickListener(View::showContextMenu);
itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
inflater.inflate(R.menu.activity_menu, menu);
- menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> copyToClipboard(context, activity.getSubject()));
+ menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(context, activity.getSubject()));
});
- switch (ActivityType.findById(activity.getType())) {
+ final ActivityType type = ActivityType.findById(activity.getType());
+ setImageResource(binding.type, type);
+ setImageColor(context, binding.type, type);
+ }
+
+ private static void setImageResource(@NonNull ImageView imageView, @NonNull ActivityType type) {
+ switch (type) {
case CHANGE:
- binding.type.setImageResource(R.drawable.type_change_36dp);
+ imageView.setImageResource(R.drawable.type_change_36dp);
break;
case ADD:
- binding.type.setImageResource(R.drawable.type_add_color_36dp);
+ imageView.setImageResource(R.drawable.type_add_color_36dp);
break;
case DELETE:
- binding.type.setImageResource(R.drawable.type_delete_color_36dp);
+ imageView.setImageResource(R.drawable.type_delete_color_36dp);
break;
case ARCHIVE:
- binding.type.setImageResource(R.drawable.type_archive_grey600_36dp);
+ imageView.setImageResource(R.drawable.type_archive_grey600_36dp);
break;
case TAGGED_WITH_LABEL:
- binding.type.setImageResource(R.drawable.type_label_grey600_36dp);
+ imageView.setImageResource(R.drawable.type_label_grey600_36dp);
break;
case COMMENT:
- binding.type.setImageResource(R.drawable.type_comment_grey600_36dp);
+ imageView.setImageResource(R.drawable.type_comment_grey600_36dp);
break;
case FILES:
- binding.type.setImageResource(R.drawable.type_file_36dp);
+ imageView.setImageResource(R.drawable.type_file_36dp);
+ break;
case HISTORY:
- binding.type.setImageResource(R.drawable.type_file_36dp);
+ imageView.setImageResource(R.drawable.type_history_36dp);
+ break;
case DECK:
default:
+ imageView.setImageResource(R.drawable.ic_app_logo);
+ break;
+ }
+ }
+
+ 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);
+ break;
+ case DELETE:
+ ViewUtil.setImageColor(context, imageView, R.color.activity_delete);
+ break;
+ default:
+ ViewUtil.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
new file mode 100644
index 000000000..34d2eb3f3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
@@ -0,0 +1,113 @@
+package it.niedermann.nextcloud.deck.ui.card.assignee;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
+
+import com.bumptech.glide.Glide;
+
+import java.io.Serializable;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.DialogPreviewBinding;
+import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedDeleteAlertDialogBuilder;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
+import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+
+import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+
+@Deprecated
+public class CardAssigneeDialog extends BrandedDialogFragment {
+
+ private static final String KEY_USER = "user";
+ private DialogPreviewBinding binding;
+ private EditCardViewModel viewModel;
+
+ @Nullable
+ private CardAssigneeListener cardAssigneeListener = null;
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ @NonNull
+ private User user;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+
+ if (getParentFragment() instanceof CardAssigneeListener) {
+ this.cardAssigneeListener = (CardAssigneeListener) getParentFragment();
+ } else if (context instanceof CardAssigneeListener) {
+ this.cardAssigneeListener = (CardAssigneeListener) context;
+ }
+
+ final Bundle args = requireArguments();
+ if (!args.containsKey(KEY_USER)) {
+ throw new IllegalArgumentException("Provide at least " + KEY_USER);
+ }
+ final Serializable user = args.getSerializable(KEY_USER);
+ if (user == null) {
+ throw new IllegalArgumentException(KEY_USER + " must not be null.");
+ }
+ this.user = (User) user;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ binding = DialogPreviewBinding.inflate(LayoutInflater.from(requireContext()));
+ viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+
+ AlertDialog.Builder dialogBuilder = new BrandedDeleteAlertDialogBuilder(requireContext());
+
+ if (viewModel.canEdit() && cardAssigneeListener != null) {
+ dialogBuilder.setPositiveButton(R.string.simple_unassign, (d, w) -> cardAssigneeListener.onUnassignUser(user));
+ }
+
+ return dialogBuilder
+ .setView(binding.getRoot())
+ .setNeutralButton(R.string.simple_close, null)
+ .create();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ final Context context = requireContext();
+
+ final CircularProgressDrawable circularProgressDrawable = new CircularProgressDrawable(context);
+ circularProgressDrawable.setStrokeWidth(5f);
+ circularProgressDrawable.setCenterRadius(30f);
+ circularProgressDrawable.setColorSchemeColors(isDarkTheme(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())
+ .placeholder(circularProgressDrawable)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .into(binding.avatar));
+ binding.title.setText(user.getDisplayname());
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ }
+
+ public static DialogFragment newInstance(@NonNull User user) {
+ final DialogFragment fragment = new CardAssigneeDialog();
+ final Bundle args = new Bundle();
+ args.putSerializable(KEY_USER, user);
+ fragment.setArguments(args);
+ return fragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java
new file mode 100644
index 000000000..259a8b57c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java
@@ -0,0 +1,11 @@
+package it.niedermann.nextcloud.deck.ui.card.assignee;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.User;
+
+public interface CardAssigneeListener {
+
+ void onUnassignUser(@NonNull User user);
+
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java
index c236fa4c5..2d3ece255 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java
@@ -3,5 +3,5 @@ package it.niedermann.nextcloud.deck.ui.card.attachments;
import it.niedermann.nextcloud.deck.model.Attachment;
public interface AttachmentDeletedListener {
- void onAttachmentDeleted(Attachment attachment);
- } \ No newline at end of file
+ void onAttachmentDeleted(Attachment attachment);
+} \ No newline at end of file
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 ed3031b7c..533bbe322 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
@@ -1,18 +1,59 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
+import android.view.MenuInflater;
import android.view.View;
import android.widget.ImageView;
+import androidx.annotation.CallSuper;
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 it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Attachment;
+import it.niedermann.nextcloud.deck.model.enums.DBStatus;
+import it.niedermann.nextcloud.deck.ui.branding.BrandingUtil;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
+
public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder {
AttachmentViewHolder(@NonNull View itemView) {
super(itemView);
}
+ public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) {
+ bind(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, AttachmentUtil.getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment));
+ }
+
+ @CallSuper
+ public void bind(@NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor, @Nullable String attachmentUri) {
+ setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), mainColor);
+ itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
+ menuInflater.inflate(R.menu.attachment_menu, menu);
+ menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> {
+ DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName());
+ return false;
+ });
+ if (attachmentUri == null || attachment.getId() == null || cardRemoteId == null) {
+ menu.findItem(android.R.id.copyUrl).setVisible(false);
+ } else {
+ menu.findItem(android.R.id.copyUrl).setVisible(true);
+ menu.findItem(android.R.id.copyUrl).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), attachment.getFilename(), attachmentUri));
+ }
+ });
+ }
+
abstract protected ImageView getPreview();
- abstract protected void setNotSyncedYetStatus(boolean synced, @ColorInt int color);
+ protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) {
+ final ImageView notSyncedYet = getNotSyncedYetStatusIcon();
+ DrawableCompat.setTint(notSyncedYet.getDrawable(), BrandingUtil.getSecondaryForegroundColorDependingOnTheme(notSyncedYet.getContext(), mainColor));
+ notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE);
+ }
+
+ abstract protected ImageView getNotSyncedYetStatusIcon();
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java
index a72ed8ef2..d601f6bbd 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java
@@ -3,9 +3,7 @@ package it.niedermann.nextcloud.deck.ui.card.attachments;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
-import android.net.Uri;
import android.os.Build;
-import android.text.format.Formatter;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.View;
@@ -16,10 +14,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityOptionsCompat;
import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.recyclerview.widget.RecyclerView;
-import com.bumptech.glide.Glide;
-
import java.util.ArrayList;
import java.util.List;
@@ -28,21 +26,23 @@ import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Attachment;
-import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.ui.attachments.AttachmentsActivity;
-import it.niedermann.nextcloud.deck.util.AttachmentUtil;
-import it.niedermann.nextcloud.deck.util.DateUtil;
+import it.niedermann.nextcloud.deck.ui.branding.Branded;
import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
import static androidx.recyclerview.widget.RecyclerView.NO_ID;
-import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard;
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser;
@SuppressWarnings("WeakerAccess")
-public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> {
+public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> implements Branded {
public static final int VIEW_TYPE_DEFAULT = 2;
public static final int VIEW_TYPE_IMAGE = 1;
+ @NonNull
+ private final MutableLiveData<Boolean> isEmpty = new MutableLiveData<>(true);
+ @NonNull
private final MenuInflater menuInflater;
@ColorInt
private int mainColor;
@@ -51,17 +51,16 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
private Long cardRemoteId = null;
private final long cardLocalId;
@NonNull
- FragmentManager fragmentManager;
+ private final FragmentManager fragmentManager;
+ @NonNull
+ private final List<Attachment> attachments = new ArrayList<>();
@NonNull
- private List<Attachment> attachments = new ArrayList<>();
- @Nullable
private final AttachmentClickedListener attachmentClickedListener;
CardAttachmentAdapter(
- @NonNull Context context,
@NonNull FragmentManager fragmentManager,
@NonNull MenuInflater menuInflater,
- @Nullable AttachmentClickedListener attachmentClickedListener,
+ @NonNull AttachmentClickedListener attachmentClickedListener,
@NonNull Account account,
@Nullable Long cardLocalId
) {
@@ -95,39 +94,14 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
@Override
public void onBindViewHolder(@NonNull AttachmentViewHolder holder, int position) {
- final Context context = holder.itemView.getContext();
final Attachment attachment = attachments.get(position);
- final int viewType = getItemViewType(position);
-
- @Nullable final String uri = (attachment.getId() == null || cardRemoteId == null)
- ? attachment.getLocalPath() :
- AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId());
- holder.setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), mainColor);
- holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
- menuInflater.inflate(R.menu.attachment_menu, menu);
- menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> {
- DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName());
- return false;
- });
- if (uri == null) {
- menu.findItem(android.R.id.copyUrl).setVisible(false);
- } else {
- menu.findItem(android.R.id.copyUrl).setOnMenuItemClickListener(item -> copyToClipboard(context, attachment.getFilename(), uri));
- }
- });
+ final Context context = holder.itemView.getContext();
+ final View.OnClickListener onClickListener;
- switch (viewType) {
+ switch (getItemViewType(position)) {
case VIEW_TYPE_IMAGE: {
- holder.getPreview().setImageResource(R.drawable.ic_image_grey600_24dp);
- Glide.with(context)
- .load(uri)
- .placeholder(R.drawable.ic_image_grey600_24dp)
- .error(R.drawable.ic_image_grey600_24dp)
- .into(holder.getPreview());
- holder.itemView.setOnClickListener((v) -> {
- if (attachmentClickedListener != null) {
- attachmentClickedListener.onAttachmentClicked(position);
- }
+ onClickListener = (event) -> {
+ attachmentClickedListener.onAttachmentClicked(position);
final Intent intent = AttachmentsActivity.createIntent(context, account, cardLocalId, attachment.getLocalId());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && context instanceof Activity) {
String transitionName = context.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId()));
@@ -136,46 +110,16 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
} else {
context.startActivity(intent);
}
- });
+ };
break;
}
case VIEW_TYPE_DEFAULT:
default: {
- DefaultAttachmentViewHolder defaultHolder = (DefaultAttachmentViewHolder) holder;
-
- if (MimeTypeUtil.isAudio(attachment.getMimetype())) {
- holder.getPreview().setImageResource(R.drawable.ic_music_note_grey600_24dp);
- } else if (MimeTypeUtil.isVideo(attachment.getMimetype())) {
- holder.getPreview().setImageResource(R.drawable.ic_local_movies_grey600_24dp);
- } else if (MimeTypeUtil.isPdf(attachment.getMimetype())) {
- holder.getPreview().setImageResource(R.drawable.ic_baseline_picture_as_pdf_24);
- } else if (MimeTypeUtil.isContact(attachment.getMimetype())) {
- holder.getPreview().setImageResource(R.drawable.ic_baseline_contact_mail_24);
- } else {
- holder.getPreview().setImageResource(R.drawable.ic_attach_file_grey600_24dp);
- }
-
- if (cardRemoteId != null) {
- defaultHolder.itemView.setOnClickListener((event) -> {
- Intent openURL = new Intent(Intent.ACTION_VIEW);
- openURL.setData(Uri.parse(AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId())));
- context.startActivity(openURL);
- });
- }
- defaultHolder.binding.filename.setText(attachment.getBasename());
- defaultHolder.binding.filesize.setText(Formatter.formatFileSize(context, attachment.getFilesize()));
- if (attachment.getLastModifiedLocal() != null) {
- defaultHolder.binding.modified.setText(DateUtil.getRelativeDateTimeString(context, attachment.getLastModifiedLocal().getTime()));
- defaultHolder.binding.modified.setVisibility(View.VISIBLE);
- } else if (attachment.getLastModified() != null) {
- defaultHolder.binding.modified.setText(DateUtil.getRelativeDateTimeString(context, attachment.getLastModified().getTime()));
- defaultHolder.binding.modified.setVisibility(View.VISIBLE);
- } else {
- defaultHolder.binding.modified.setVisibility(View.GONE);
- }
+ onClickListener = (event) -> openAttachmentInBrowser(context, account.getUrl(), cardRemoteId, attachment.getId());
break;
}
}
+ holder.bind(account, menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor);
}
@Override
@@ -188,21 +132,46 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
return attachments.size();
}
+ private void updateIsEmpty() {
+ this.isEmpty.postValue(getItemCount() <= 0);
+ }
+
+ @NonNull
+ public LiveData<Boolean> isEmpty() {
+ return distinctUntilChanged(this.isEmpty);
+ }
+
public void setAttachments(@NonNull List<Attachment> attachments, @Nullable Long cardRemoteId) {
this.cardRemoteId = cardRemoteId;
this.attachments.clear();
this.attachments.addAll(attachments);
notifyDataSetChanged();
+ this.updateIsEmpty();
}
public void addAttachment(Attachment a) {
- this.attachments.add(a);
+ this.attachments.add(0, a);
notifyItemInserted(this.attachments.size());
+ this.updateIsEmpty();
}
public void removeAttachment(Attachment a) {
final int index = this.attachments.indexOf(a);
this.attachments.remove(a);
notifyItemRemoved(index);
+ this.updateIsEmpty();
+ }
+
+ public void replaceAttachment(Attachment toReplace, Attachment with) {
+ final int index = this.attachments.indexOf(toReplace);
+ this.attachments.remove(toReplace);
+ this.attachments.add(index, with);
+ notifyItemChanged(index);
+ }
+
+ @Override
+ public void applyBrand(@ColorInt int mainColor) {
+ this.mainColor = mainColor;
+ notifyDataSetChanged();
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java
new file mode 100644
index 000000000..6b60bbffd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java
@@ -0,0 +1,91 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DimenRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.animation.ArgbEvaluatorCompat;
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import it.niedermann.android.util.DimensionUtil;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN;
+
+public class CardAttachmentsBottomsheetBehaviorCallback extends BottomSheetBehavior.BottomSheetCallback {
+ @NonNull
+ private final OnBackPressedCallback backPressedCallback;
+ @NonNull
+ private final FloatingActionButton fab;
+ @NonNull
+ private final View pickerBackdrop;
+ @NonNull
+ private final BottomNavigationView bottomNavigation;
+ @ColorInt
+ private final int backdropColorExpanded;
+ @ColorInt
+ private final int backdropColorCollapsed;
+ @Px
+ private final int bottomNavigationHeight;
+
+ private float lastOffset = -1;
+
+ public CardAttachmentsBottomsheetBehaviorCallback(@NonNull Context context,
+ @NonNull OnBackPressedCallback backPressedCallback,
+ @NonNull FloatingActionButton fab,
+ @NonNull View pickerBackdrop,
+ @NonNull BottomNavigationView bottomNavigation,
+ @ColorRes int backdropColorExpanded,
+ @ColorRes int backdropColorCollapsed,
+ @DimenRes int bottomNavigationHeight
+ ) {
+ this.backPressedCallback = backPressedCallback;
+ this.fab = fab;
+ this.pickerBackdrop = pickerBackdrop;
+ this.bottomNavigation = bottomNavigation;
+ this.backdropColorExpanded = ContextCompat.getColor(context, backdropColorExpanded);
+ this.backdropColorCollapsed = ContextCompat.getColor(context, backdropColorCollapsed);
+ this.bottomNavigationHeight = DimensionUtil.INSTANCE.dpToPx(context, bottomNavigationHeight);
+ }
+
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ if (newState == STATE_HIDDEN) {
+ backPressedCallback.setEnabled(false);
+ if (pickerBackdrop.getVisibility() != GONE) {
+ pickerBackdrop.setVisibility(GONE);
+ }
+ } else if (pickerBackdrop.getVisibility() != VISIBLE) {
+ pickerBackdrop.setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {
+ if (slideOffset <= 0) {
+ final float bottomSheetPercentageShown = slideOffset * -1;
+ pickerBackdrop.setBackgroundColor(ArgbEvaluatorCompat.getInstance().evaluate(bottomSheetPercentageShown, backdropColorExpanded, backdropColorCollapsed));
+ bottomNavigation.setTranslationY(bottomSheetPercentageShown * bottomNavigationHeight);
+ if (slideOffset <= lastOffset && slideOffset != 0) {
+ if (fab.getVisibility() == GONE) {
+ fab.show();
+ }
+ } else {
+ if (fab.getVisibility() == VISIBLE) {
+ fab.hide();
+ }
+ }
+ }
+ lastOffset = slideOffset;
+ }
+}
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 c291c5bdf..07e8d39dd 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
@@ -1,35 +1,42 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
-import android.Manifest;
-import android.app.Activity;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
+import android.content.res.ColorStateList;
import android.net.Uri;
-import android.os.Build;
import android.os.Bundle;
+import android.provider.ContactsContract;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.core.app.SharedElementCallback;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import java.io.File;
import java.io.IOException;
-import java.util.Date;
+import java.time.Instant;
import java.util.List;
import java.util.Map;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabAttachmentsBinding;
@@ -41,10 +48,36 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiv
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
import it.niedermann.nextcloud.deck.ui.branding.BrandedSnackbar;
import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.AbstractPickerAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.ContactAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.FileAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.FileAdapterLegacy;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.GalleryAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.GalleryItemDecoration;
+import it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialog;
+import it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialogViewModel;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-
+import it.niedermann.nextcloud.deck.ui.takephoto.TakePhotoActivity;
+import it.niedermann.nextcloud.deck.util.DeckColorUtil;
+import it.niedermann.nextcloud.deck.util.VCardUtil;
+
+import static android.Manifest.permission.CAMERA;
+import static android.Manifest.permission.READ_CONTACTS;
+import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.app.Activity.RESULT_OK;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.M;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
+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 it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToFAB;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor;
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.AttachmentUtil.copyContentUriToTempFile;
@@ -53,14 +86,35 @@ import static java.net.HttpURLConnection.HTTP_CONFLICT;
public class CardAttachmentsFragment extends BrandedFragment implements AttachmentDeletedListener, AttachmentClickedListener {
private FragmentCardEditTabAttachmentsBinding binding;
- private EditCardViewModel viewModel;
+ private EditCardViewModel editViewModel;
+ private PreviewDialogViewModel previewViewModel;
+ private BottomSheetBehavior<LinearLayout> mBottomSheetBehaviour;
+
+ private RecyclerView.ItemDecoration galleryItemDecoration;
+
+ private static final int REQUEST_CODE_PICK_FILE = 1;
+ private static final int REQUEST_CODE_PICK_FILE_PERMISSION = 2;
+ private static final int REQUEST_CODE_PICK_CAMERA = 3;
+ private static final int REQUEST_CODE_PICK_GALLERY_PERMISSION = 4;
+ private static final int REQUEST_CODE_PICK_CONTACT = 5;
+ private static final int REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION = 6;
- private static final int REQUEST_CODE_ADD_ATTACHMENT = 1;
- private static final int REQUEST_PERMISSION = 2;
+ @ColorInt
+ private int accentColor;
+ @ColorInt
+ private int primaryColor;
- private SyncManager syncManager;
private CardAttachmentAdapter adapter;
+ private AbstractPickerAdapter<?> pickerAdapter;
+
+ private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ mBottomSheetBehaviour.setState(STATE_HIDDEN);
+ }
+ };
+
private int clickedItemPosition;
@Override
@@ -69,30 +123,58 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
Bundle savedInstanceState) {
binding = FragmentCardEditTabAttachmentsBinding.inflate(inflater, container, false);
- viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+ editViewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+ previewViewModel = new ViewModelProvider(requireActivity()).get(PreviewDialogViewModel.class);
+ binding.bottomNavigation.setOnNavigationItemSelectedListener(item -> {
+ if (item.getItemId() == R.id.gallery) {
+ showGalleryPicker();
+ } else if (item.getItemId() == R.id.contacts) {
+ showContactPicker();
+ } else if (item.getItemId() == R.id.files) {
+ showFilePicker();
+ }
+ 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
- if (viewModel.getFullCard() == null) {
+ if (editViewModel.getFullCard() == null) {
DeckLog.logError(new IllegalStateException("Cannot populate " + CardAttachmentsFragment.class.getSimpleName() + " because viewModel.getFullCard() is null"));
return binding.getRoot();
}
- syncManager = new SyncManager(requireContext());
adapter = new CardAttachmentAdapter(
- requireContext(),
getChildFragmentManager(),
requireActivity().getMenuInflater(),
this,
- viewModel.getAccount(),
- viewModel.getFullCard().getLocalId());
+ editViewModel.getAccount(),
+ editViewModel.getFullCard().getLocalId());
binding.attachmentsList.setAdapter(adapter);
- updateEmptyContentView();
+ adapter.isEmpty().observe(getViewLifecycleOwner(), (isEmpty) -> {
+ if (isEmpty) {
+ this.binding.emptyContentView.setVisibility(VISIBLE);
+ this.binding.attachmentsList.setVisibility(GONE);
+ } else {
+ this.binding.emptyContentView.setVisibility(GONE);
+ this.binding.attachmentsList.setVisibility(VISIBLE);
+ }
+ });
+ galleryItemDecoration = new GalleryItemDecoration(DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1qx));
+ mBottomSheetBehaviour = BottomSheetBehavior.from(binding.bottomSheetParent);
+ mBottomSheetBehaviour.setDraggable(true);
+ mBottomSheetBehaviour.setHideable(true);
+ mBottomSheetBehaviour.setState(STATE_HIDDEN);
+ mBottomSheetBehaviour.addBottomSheetCallback(new CardAttachmentsBottomsheetBehaviorCallback(
+ requireContext(), backPressedCallback, binding.fab, binding.pickerBackdrop, binding.bottomNavigation,
+ R.color.mdtp_transparent_black, android.R.color.transparent, R.dimen.attachments_bottom_navigation_height));
+ binding.pickerBackdrop.setOnClickListener(v -> mBottomSheetBehaviour.setState(STATE_HIDDEN));
final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
- int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column));
- GridLayoutManager glm = new GridLayoutManager(getContext(), spanCount);
+ final int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column));
+ final GridLayoutManager glm = new GridLayoutManager(getContext(), spanCount);
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
@@ -106,7 +188,7 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
}
});
binding.attachmentsList.setLayoutManager(glm);
- if (!viewModel.isCreateMode()) {
+ if (!editViewModel.isCreateMode()) {
// https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html?m=1
// https://github.com/android/animation-samples/blob/master/GridToPager/app/src/main/java/com/google/samples/gridtopager/fragment/ImagePagerFragment.java
setExitSharedElementCallback(new SharedElementCallback() {
@@ -119,17 +201,19 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
}
}
});
- adapter.setAttachments(viewModel.getFullCard().getAttachments(), viewModel.getFullCard().getId());
- updateEmptyContentView();
+ adapter.setAttachments(editViewModel.getFullCard().getAttachments(), editViewModel.getFullCard().getId());
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && viewModel.canEdit()) {
+ if (editViewModel.canEdit()) {
binding.fab.setOnClickListener(v -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
- REQUEST_PERMISSION);
+ if (SDK_INT < LOLLIPOP) {
+ openNativeFilePicker();
} else {
- startFilePickerIntent();
+ binding.bottomNavigation.setSelectedItemId(R.id.gallery);
+ showGalleryPicker();
+ mBottomSheetBehaviour.setState(STATE_COLLAPSED);
+ backPressedCallback.setEnabled(true);
+ requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback);
}
});
binding.fab.show();
@@ -146,116 +230,281 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
binding.fab.hide();
binding.emptyContentView.hideDescription();
}
+ @Nullable Context context = requireContext();
+ applyBrand(isBrandingEnabled(context)
+ ? readBrandMainColor(context)
+ : ContextCompat.getColor(context, R.color.defaultBrand));
return binding.getRoot();
}
- @RequiresApi(api = Build.VERSION_CODES.KITKAT)
- private void startFilePickerIntent() {
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("*/*");
- startActivityForResult(intent, REQUEST_CODE_ADD_ATTACHMENT);
+ @Override
+ public void onPause() {
+ super.onPause();
+ backPressedCallback.setEnabled(false);
}
@Override
- public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == REQUEST_CODE_ADD_ATTACHMENT && resultCode == Activity.RESULT_OK) {
- if (data == null) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Intent data is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
+ public void onResume() {
+ super.onResume();
+ backPressedCallback.setEnabled(binding.bottomNavigation.getTranslationY() == 0);
+ }
+
+ private void showGalleryPicker() {
+ if (!(pickerAdapter instanceof GalleryAdapter)) {
+ if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE) || isPermissionRequestNeeded(CAMERA)) {
+ requestPermissions(new String[]{READ_EXTERNAL_STORAGE, CAMERA}, REQUEST_CODE_PICK_GALLERY_PERMISSION);
+ } else {
+ unbindPickerAdapter();
+ 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));
+ }
+ });
+ }, this::openNativeCameraPicker, getViewLifecycleOwner());
+ if (binding.pickerRecyclerView.getItemDecorationCount() == 0) {
+ binding.pickerRecyclerView.addItemDecoration(galleryItemDecoration);
+ }
+ binding.pickerRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3));
+ binding.pickerRecyclerView.setAdapter(pickerAdapter);
}
- final Uri sourceUri = data.getData();
- if (sourceUri == null) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("sourceUri is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
+ }
+ }
+
+ private void showContactPicker() {
+ if (!(pickerAdapter instanceof ContactAdapter)) {
+ if (isPermissionRequestNeeded(READ_CONTACTS)) {
+ requestPermissions(new String[]{READ_CONTACTS}, REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION);
+ } else {
+ unbindPickerAdapter();
+ 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));
+ }
+ });
+ }, this::openNativeContactPicker);
+ removeGalleryItemDecoration();
+ binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
+ binding.pickerRecyclerView.setAdapter(pickerAdapter);
}
- if (!ContentResolver.SCHEME_CONTENT.equals(sourceUri.getScheme())) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme()), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
+ }
+ }
+
+ private void showFilePicker() {
+ if (!(pickerAdapter instanceof FileAdapter) && !(pickerAdapter instanceof FileAdapterLegacy)) {
+ if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE)) {
+ requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, REQUEST_CODE_PICK_FILE_PERMISSION);
+ } else {
+ unbindPickerAdapter();
+ if (SDK_INT >= LOLLIPOP) {
+// if (SDK_INT >= Build.VERSION_CODES.Q) {
+// // TODO Only usable with Scoped Storage
+// pickerAdapter = new FileAdapter(requireContext(), uri -> onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)), this::openNativeFilePicker);
+// } else {
+ pickerAdapter = new FileAdapterLegacy((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));
+ }
+ });
+ }, this::openNativeFilePicker);
+// }
+ removeGalleryItemDecoration();
+ binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
+ binding.pickerRecyclerView.setAdapter(pickerAdapter);
+ }
}
+ }
+ }
- DeckLog.verbose("--- found content URL " + sourceUri.getPath());
- File fileToUpload;
+ private void openNativeCameraPicker() {
+ if (SDK_INT >= LOLLIPOP) {
+ startActivityForResult(TakePhotoActivity.createIntent(requireContext()), REQUEST_CODE_PICK_CAMERA);
+ } else {
+ ExceptionDialogFragment.newInstance(new UnsupportedOperationException("This feature requires Android 5"), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ }
- try {
- DeckLog.verbose("---- so, now copy & upload: " + sourceUri.getPath());
- fileToUpload = copyContentUriToTempFile(requireContext(), sourceUri, viewModel.getAccount().getId(), viewModel.getFullCard().getCard().getLocalId());
- } catch (IllegalArgumentException | IOException e) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Could not copy content URI to temporary file", e), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
- }
+ private void openNativeContactPicker() {
+ final Intent intent = new Intent(Intent.ACTION_PICK).setType(ContactsContract.Contacts.CONTENT_TYPE);
+ if (intent.resolveActivity(requireContext().getPackageManager()) != null) {
+ startActivityForResult(intent, REQUEST_CODE_PICK_CONTACT);
+ }
+ }
+
+ private void openNativeFilePicker() {
+ startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("*/*"), REQUEST_CODE_PICK_FILE);
+ }
+
+ /**
+ * Checks the current Android version and whether the permission has already been granted.
+ *
+ * @param permission see {@link android.Manifest.permission}
+ * @return whether or not requesting permission is needed
+ */
+ private boolean isPermissionRequestNeeded(@NonNull String permission) {
+ return SDK_INT >= M && checkSelfPermission(requireActivity(), permission) != PERMISSION_GRANTED;
+ }
+
+ private void unbindPickerAdapter() {
+ if (pickerAdapter != null) {
+ pickerAdapter.onDestroy();
+ }
+ }
+
+ private void removeGalleryItemDecoration() {
+ if (binding.pickerRecyclerView.getItemDecorationCount() > 0) {
+ binding.pickerRecyclerView.removeItemDecoration(galleryItemDecoration);
+ }
+ }
- for (Attachment existingAttachment : viewModel.getFullCard().getAttachments()) {
- final String existingPath = existingAttachment.getLocalPath();
- if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) {
- BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
- return;
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ switch (requestCode) {
+ case REQUEST_CODE_PICK_CONTACT:
+ case REQUEST_CODE_PICK_CAMERA:
+ case REQUEST_CODE_PICK_FILE: {
+ if (resultCode == RESULT_OK) {
+ final Uri sourceUri = requestCode == REQUEST_CODE_PICK_CONTACT
+ ? VCardUtil.getVCardContentUri(requireContext(), Uri.parse(data.getDataString()))
+ : data.getData();
+ try {
+ uploadNewAttachmentFromUri(sourceUri, requestCode == REQUEST_CODE_PICK_CAMERA
+ ? data.getType()
+ : requireContext().getContentResolver().getType(sourceUri));
+ mBottomSheetBehaviour.setState(STATE_HIDDEN);
+ } catch (Exception e) {
+ ExceptionDialogFragment.newInstance(e, editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
}
+ break;
+ }
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
}
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (this.pickerAdapter != null) {
+ this.pickerAdapter.onDestroy();
+ this.binding.pickerRecyclerView.setAdapter(null);
+ }
+ super.onDestroy();
+ }
- final Date now = new Date();
- final Attachment a = new Attachment();
- a.setMimetype(requireContext().getContentResolver().getType(sourceUri));
- a.setData(fileToUpload.getName());
- a.setFilename(fileToUpload.getName());
- a.setBasename(fileToUpload.getName());
- a.setFilesize(fileToUpload.length());
- a.setLocalPath(fileToUpload.getAbsolutePath());
- a.setLastModifiedLocal(now);
- a.setStatusEnum(DBStatus.LOCAL_EDITED);
- a.setCreatedAt(now);
- viewModel.getFullCard().getAttachments().add(a);
- adapter.addAttachment(a);
- if (!viewModel.isCreateMode()) {
- WrappedLiveData<Attachment> liveData = syncManager.addAttachmentToCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload);
- observeOnce(liveData, getViewLifecycleOwner(), (next) -> {
- if (liveData.hasError()) {
- Throwable t = liveData.getError();
- if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_CONFLICT) {
- // https://github.com/stefan-niedermann/nextcloud-deck/issues/534
- viewModel.getFullCard().getAttachments().remove(a);
- adapter.removeAttachment(a);
- BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
+ private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) throws UploadAttachmentFailedException, IOException {
+ if (sourceUri == null) {
+ throw new UploadAttachmentFailedException("sourceUri is null");
+ }
+ switch (sourceUri.getScheme()) {
+ case ContentResolver.SCHEME_CONTENT:
+ case ContentResolver.SCHEME_FILE: {
+ DeckLog.verbose("--- found content URL " + sourceUri.getPath());
+ final File fileToUpload = copyContentUriToTempFile(requireContext(), sourceUri, editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId());
+ for (Attachment existingAttachment : editViewModel.getFullCard().getAttachments()) {
+ final String existingPath = existingAttachment.getLocalPath();
+ if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) {
+ BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
+ return;
+ }
+ }
+ final Instant now = Instant.now();
+ final Attachment a = new Attachment();
+ a.setMimetype(mimeType);
+ a.setData(fileToUpload.getName());
+ a.setFilename(fileToUpload.getName());
+ a.setBasename(fileToUpload.getName());
+ a.setFilesize(fileToUpload.length());
+ a.setLocalPath(fileToUpload.getAbsolutePath());
+ a.setLastModifiedLocal(now);
+ a.setCreatedAt(now);
+ a.setStatusEnum(DBStatus.LOCAL_EDITED);
+ editViewModel.getFullCard().getAttachments().add(0, a);
+ adapter.addAttachment(a);
+ if (!editViewModel.isCreateMode()) {
+ WrappedLiveData<Attachment> liveData = editViewModel.addAttachmentToCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload);
+ observeOnce(liveData, getViewLifecycleOwner(), (next) -> {
+ if (liveData.hasError()) {
+ Throwable t = liveData.getError();
+ if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_CONFLICT) {
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/534
+ editViewModel.getFullCard().getAttachments().remove(a);
+ adapter.removeAttachment(a);
+ BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
+ } else {
+ ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
} else {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ editViewModel.getFullCard().getAttachments().remove(a);
+ editViewModel.getFullCard().getAttachments().add(0, next);
+ adapter.replaceAttachment(a, next);
}
- } else {
- viewModel.getFullCard().getAttachments().remove(a);
- adapter.removeAttachment(a);
- viewModel.getFullCard().getAttachments().add(next);
- adapter.addAttachment(next);
- }
- });
+ });
+ }
+ break;
+ }
+ default: {
+ throw new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme());
}
- updateEmptyContentView();
}
-
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- if (requestCode == REQUEST_PERMISSION) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- startFilePickerIntent();
+ switch (requestCode) {
+ case REQUEST_CODE_PICK_FILE_PERMISSION: {
+ if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
+ showFilePicker();
+ } else {
+ Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
+ }
+ break;
}
- } else {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ case REQUEST_CODE_PICK_GALLERY_PERMISSION: {
+ if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED && checkSelfPermission(requireActivity(), CAMERA) == PERMISSION_GRANTED) {
+ showGalleryPicker();
+ } else {
+ Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
+ }
+ break;
+ }
+ case REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION: {
+ if (checkSelfPermission(requireActivity(), READ_CONTACTS) == PERMISSION_GRANTED) {
+ showContactPicker();
+ } else {
+ Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
+ }
+ break;
+ }
+ default:
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
- public static Fragment newInstance() {
- return new CardAttachmentsFragment();
- }
-
@Override
public void onAttachmentDeleted(Attachment attachment) {
adapter.removeAttachment(attachment);
- viewModel.getFullCard().getAttachments().remove(attachment);
- if (!viewModel.isCreateMode() && attachment.getLocalId() != null) {
- syncManager.deleteAttachmentOfCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), attachment.getLocalId());
+ editViewModel.getFullCard().getAttachments().remove(attachment);
+ if (!editViewModel.isCreateMode() && attachment.getLocalId() != null) {
+ final WrappedLiveData<Void> deleteLiveData = editViewModel.deleteAttachmentOfCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), attachment.getLocalId());
+ observeOnce(deleteLiveData, this, (next) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
- updateEmptyContentView();
}
@Override
@@ -263,19 +512,28 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
this.clickedItemPosition = position;
}
-
- private void updateEmptyContentView() {
- if (this.adapter == null || this.adapter.getItemCount() == 0) {
- this.binding.emptyContentView.setVisibility(View.VISIBLE);
- this.binding.attachmentsList.setVisibility(View.GONE);
- } else {
- this.binding.emptyContentView.setVisibility(View.GONE);
- this.binding.attachmentsList.setVisibility(View.VISIBLE);
- }
- }
-
@Override
public void applyBrand(int mainColor) {
applyBrandToFAB(mainColor, binding.fab);
+ adapter.applyBrand(mainColor);
+ @ColorInt final int finalMainColor = DeckColorUtil.contrastRatioIsSufficient(mainColor, primaryColor)
+ ? mainColor
+ : accentColor;
+ final ColorStateList list = new ColorStateList(
+ new int[][]{
+ new int[]{android.R.attr.state_checked},
+ new int[]{}
+ },
+ new int[]{
+ finalMainColor,
+ accentColor
+ }
+ );
+ binding.bottomNavigation.setItemIconTintList(list);
+ binding.bottomNavigation.setItemTextColor(list);
+ }
+
+ public static Fragment newInstance() {
+ return new CardAttachmentsFragment();
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java
index 7acdd390e..2b5358eb9 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java
@@ -1,15 +1,25 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
+import android.text.format.Formatter;
+import android.view.MenuInflater;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.ColorInt;
-import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentManager;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Attachment;
+import it.niedermann.nextcloud.deck.util.DateUtil;
+
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.getIconForMimeType;
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser;
public class DefaultAttachmentViewHolder extends AttachmentViewHolder {
- ItemAttachmentDefaultBinding binding;
+ private final ItemAttachmentDefaultBinding binding;
@SuppressWarnings("WeakerAccess")
public DefaultAttachmentViewHolder(ItemAttachmentDefaultBinding binding) {
@@ -23,8 +33,24 @@ public class DefaultAttachmentViewHolder extends AttachmentViewHolder {
}
@Override
- protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) {
- DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor);
- binding.notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE);
+ protected ImageView getNotSyncedYetStatusIcon() {
+ return binding.notSyncedYet;
+ }
+
+ public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) {
+ super.bind(account, menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor);
+ getPreview().setImageResource(getIconForMimeType(attachment.getMimetype()));
+ itemView.setOnClickListener((event) -> openAttachmentInBrowser(itemView.getContext(), account.getUrl(), cardRemoteId, attachment.getId()));
+ binding.filename.setText(attachment.getBasename());
+ binding.filesize.setText(Formatter.formatFileSize(binding.filesize.getContext(), attachment.getFilesize()));
+ if (attachment.getLastModifiedLocal() != null) {
+ binding.modified.setText(DateUtil.getRelativeDateTimeString(binding.modified.getContext(), attachment.getLastModifiedLocal().toEpochMilli()));
+ binding.modified.setVisibility(View.VISIBLE);
+ } else if (attachment.getLastModified() != null) {
+ binding.modified.setText(DateUtil.getRelativeDateTimeString(binding.modified.getContext(), attachment.getLastModified().toEpochMilli()));
+ binding.modified.setVisibility(View.VISIBLE);
+ } else {
+ binding.modified.setVisibility(View.GONE);
+ }
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java
index d13675a30..3c95da1b7 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java
@@ -1,15 +1,24 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
+import android.view.MenuInflater;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.ColorInt;
-import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentManager;
+import com.bumptech.glide.Glide;
+
+import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Attachment;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
public class ImageAttachmentViewHolder extends AttachmentViewHolder {
- private ItemAttachmentImageBinding binding;
+ private final ItemAttachmentImageBinding binding;
@SuppressWarnings("WeakerAccess")
public ImageAttachmentViewHolder(ItemAttachmentImageBinding binding) {
@@ -23,8 +32,22 @@ public class ImageAttachmentViewHolder extends AttachmentViewHolder {
}
@Override
- protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) {
- DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor);
- binding.notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE);
+ protected ImageView getNotSyncedYetStatusIcon() {
+ return binding.notSyncedYet;
+ }
+
+ public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) {
+ super.bind(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, AttachmentUtil.getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment));
+
+ getPreview().post(() -> {
+ @Nullable final String uri = AttachmentUtil.getThumbnailUrl(account.getServerDeckVersionAsObject(), account.getUrl(), cardRemoteId, attachment, getPreview().getWidth());
+ Glide.with(getPreview().getContext())
+ .load(uri)
+ .placeholder(R.drawable.ic_image_grey600_24dp)
+ .error(R.drawable.ic_image_grey600_24dp)
+ .into(getPreview());
+ });
+
+ itemView.setOnClickListener(onClickListener);
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java
new file mode 100644
index 000000000..a2ea6dd37
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java
@@ -0,0 +1,100 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.BiConsumer;
+
+import static android.database.Cursor.FIELD_TYPE_INTEGER;
+import static android.database.Cursor.FIELD_TYPE_NULL;
+import static androidx.recyclerview.widget.RecyclerView.NO_ID;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * An {@link RecyclerView.Adapter} which provides previews of one type of files and also an option to open a native dialog.
+ * <p>
+ * Example: Previews for images of the gallery as well a one option to take a photo
+ */
+public abstract class AbstractCursorPickerAdapter<T extends RecyclerView.ViewHolder> extends AbstractPickerAdapter<T> {
+
+ private final int count;
+ protected final int columnIndex;
+ private final int columnIndexType;
+ @NonNull
+ protected final BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect;
+ @NonNull
+ protected final Runnable openNativePicker;
+ @NonNull
+ protected final Cursor cursor;
+ @NonNull
+ protected final ContentResolver contentResolver;
+
+ /**
+ * Should be used to bind heavy operations like when dealing with {@link Bitmap}.
+ * This must only be one {@link Thread} because otherwise the cursor might change while fetching data from it.
+ */
+ @NonNull
+ protected final ExecutorService bindExecutor = Executors.newFixedThreadPool(1);
+
+ public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, Uri subject, String idColumn, String sortOrder) {
+ this(context, onSelect, openNativePicker, subject, idColumn, new String[]{idColumn}, sortOrder);
+ }
+
+ public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, Uri subject, String idColumn, String[] requestedColumns, String sortOrder) {
+ this(context, onSelect, openNativePicker, idColumn, requireNonNull(context.getContentResolver().query(subject, requestedColumns, null, null, sortOrder)));
+ }
+
+ public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, String idColumn, @NonNull Cursor cursor) {
+ this.contentResolver = context.getContentResolver();
+ this.onSelect = onSelect;
+ this.openNativePicker = openNativePicker;
+ this.cursor = cursor;
+ this.cursor.moveToFirst();
+ this.columnIndex = this.cursor.getColumnIndex(idColumn);
+ this.count = cursor.getCount() + 1;
+ this.columnIndexType = (this.count > 1) ? this.cursor.getType(columnIndex) : FIELD_TYPE_NULL;
+ setHasStableIds(true);
+ }
+
+ /**
+ * Moves the {@link #cursor} to the given position
+ */
+ @Override
+ public long getItemId(int position) {
+ if (!cursor.isClosed() && cursor.moveToPosition(position - 1)) {
+ //noinspection SwitchStatementWithTooFewBranches
+ switch (columnIndexType) {
+ case FIELD_TYPE_INTEGER:
+ return cursor.getLong(columnIndex);
+ default:
+ throw new IllegalStateException("Unknown type for columnIndex \"" + columnIndex + "\": " + columnIndexType);
+ }
+ } else {
+ return NO_ID;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return count;
+ }
+
+ /**
+ * Call this method when the {@link AbstractCursorPickerAdapter} is no longer need to free resources.
+ */
+ public void onDestroy() {
+ cursor.close();
+ bindExecutor.shutdownNow();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java
new file mode 100644
index 000000000..901d204cd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java
@@ -0,0 +1,26 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+public abstract class AbstractPickerAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {
+
+ protected static final int VIEW_TYPE_NONE = -1;
+ protected static final int VIEW_TYPE_ITEM = 0;
+ protected static final int VIEW_TYPE_ITEM_NATIVE = 1;
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position > 0) {
+ return VIEW_TYPE_ITEM;
+ } else if (position == 0) {
+ return VIEW_TYPE_ITEM_NATIVE;
+ } else {
+ return VIEW_TYPE_NONE;
+ }
+ }
+
+ /**
+ * Call this method when the {@link AbstractPickerAdapter} is no longer need to free resources.
+ */
+ public abstract void onDestroy();
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java
new file mode 100644
index 000000000..22ac0c694
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java
@@ -0,0 +1,104 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding;
+
+import static android.provider.ContactsContract.CommonDataKinds.Email.DATA;
+import static android.provider.ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY;
+import static android.provider.ContactsContract.CommonDataKinds.Phone.NUMBER;
+import static android.provider.ContactsContract.Contacts.CONTENT_LOOKUP_URI;
+import static android.provider.ContactsContract.Contacts.CONTENT_URI;
+import static android.provider.ContactsContract.Contacts.DISPLAY_NAME;
+import static android.provider.ContactsContract.Contacts.SORT_KEY_PRIMARY;
+import static android.provider.ContactsContract.Contacts._ID;
+
+public class ContactAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> {
+
+ private final int lookupKeyColumnIndex;
+ private final int displayNameColumnIndex;
+
+ public ContactAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable onSelectPicker) {
+ super(context, onSelect, onSelectPicker, CONTENT_URI, _ID, new String[]{_ID, LOOKUP_KEY, DISPLAY_NAME}, SORT_KEY_PRIMARY);
+ lookupKeyColumnIndex = cursor.getColumnIndex(LOOKUP_KEY);
+ displayNameColumnIndex = cursor.getColumnIndex(DISPLAY_NAME);
+ notifyItemRangeInserted(0, getItemCount() + 1);
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new ContactNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new ContactItemViewHolder(ItemPickerUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((ContactNativeItemViewHolder) holder).bind(openNativePicker);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ final ContactItemViewHolder viewHolder = (ContactItemViewHolder) holder;
+ if (!cursor.isClosed()) {
+ cursor.moveToPosition(position - 1);
+ final String displayName = cursor.getString(displayNameColumnIndex);
+ final String lookupKey = cursor.getString(lookupKeyColumnIndex);
+ bindExecutor.execute(() -> {
+ try (InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, Uri.withAppendedPath(CONTENT_LOOKUP_URI, lookupKey))) {
+ final Bitmap thumbnail = BitmapFactory.decodeStream(inputStream);
+ String contactInformation = "";
+ try (final Cursor phoneCursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[]{NUMBER}, LOOKUP_KEY + " = ?", new String[]{lookupKey}, null)) {
+ if (phoneCursor != null && phoneCursor.moveToFirst()) {
+ contactInformation = phoneCursor.getString(phoneCursor.getColumnIndex(NUMBER));
+ }
+ }
+ if (TextUtils.isEmpty(contactInformation)) {
+ try (final Cursor emailCursor = contentResolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, new String[]{DATA}, LOOKUP_KEY + " = ?", new String[]{lookupKey}, null)) {
+ if (emailCursor != null && emailCursor.moveToFirst()) {
+ contactInformation = emailCursor.getString(emailCursor.getColumnIndex(DATA));
+ }
+ }
+ }
+ final String finalContactInformation = contactInformation;
+ new Handler(Looper.getMainLooper()).post(() -> viewHolder.bind(Uri.withAppendedPath(CONTENT_LOOKUP_URI, lookupKey), thumbnail, displayName, finalContactInformation, onSelect));
+ } catch (IOException ignored) {
+ new Handler(Looper.getMainLooper()).post(viewHolder::bindError);
+ }
+ });
+ } else {
+ new Handler(Looper.getMainLooper()).post(viewHolder::bindError);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java
new file mode 100644
index 000000000..f403fed21
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java
@@ -0,0 +1,66 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.request.RequestOptions;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.util.VCardUtil.getColorBasedOnDisplayName;
+
+public class ContactItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPickerUserBinding binding;
+
+ public ContactItemViewHolder(@NonNull ItemPickerUserBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Uri uri, @Nullable Bitmap image, @NonNull String displayName, @Nullable String contactInformation, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) {
+ itemView.setOnClickListener((v) -> onSelect.accept(uri, new Pair<>(displayName, image == null ? null : Glide.with(itemView.getContext()).load(image))));
+ binding.title.setText(displayName);
+ binding.contactInformation.setText(contactInformation);
+ if (image == null) {
+ binding.initials.setVisibility(VISIBLE);
+ binding.initials.setText(TextUtils.isEmpty(displayName)
+ ? null
+ : String.valueOf(displayName.charAt(0))
+ );
+ Glide.with(itemView.getContext())
+ .load(new ColorDrawable(getColorBasedOnDisplayName(itemView.getContext(), displayName)))
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
+ } else {
+ binding.initials.setVisibility(GONE);
+ binding.initials.setText(null);
+ Glide.with(itemView.getContext())
+ .load(image)
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
+ }
+ }
+
+ public void bindError() {
+ itemView.setOnClickListener(null);
+ Glide.with(itemView.getContext())
+ .load(R.drawable.ic_person_grey600_24dp)
+ .into(binding.avatar);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java
new file mode 100644
index 000000000..a1d7d5921
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java
@@ -0,0 +1,23 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+
+public class ContactNativeItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPickerNativeBinding binding;
+
+ public ContactNativeItemViewHolder(@NonNull ItemPickerNativeBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Runnable onOpenMajorPicker) {
+ binding.title.setText(R.string.show_all_contacts);
+ binding.subtitle.setText(R.string.contacts);
+ itemView.setOnClickListener((v) -> onOpenMajorPicker.run());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java
new file mode 100644
index 000000000..aa96a0e69
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java
@@ -0,0 +1,85 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.MediaStore;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+
+import static android.provider.MediaStore.Downloads.DATE_ADDED;
+import static android.provider.MediaStore.Downloads.DATE_MODIFIED;
+import static android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI;
+import static android.provider.MediaStore.Downloads.MIME_TYPE;
+import static android.provider.MediaStore.Downloads.SIZE;
+import static android.provider.MediaStore.Downloads.TITLE;
+import static android.provider.MediaStore.Downloads._ID;
+import static java.util.Objects.requireNonNull;
+
+@RequiresApi(api = 29)
+public class FileAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> {
+
+ private final int displayNameColumnIndex;
+ private final int sizeColumnIndex;
+ private final int modifiedColumnIndex;
+ private final int mimeTypeColumnIndex;
+
+ private FileAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable onSelectPicker) {
+ super(context, onSelect, onSelectPicker, _ID, requireNonNull(context.getContentResolver().query(EXTERNAL_CONTENT_URI, new String[]{_ID, TITLE, SIZE, DATE_MODIFIED, MIME_TYPE}, null, null, DATE_ADDED + " DESC")));
+ displayNameColumnIndex = cursor.getColumnIndex(TITLE);
+ sizeColumnIndex = cursor.getColumnIndex(SIZE);
+ modifiedColumnIndex = cursor.getColumnIndex(DATE_MODIFIED);
+ mimeTypeColumnIndex = cursor.getColumnIndex(MIME_TYPE);
+ notifyItemRangeInserted(0, getItemCount() + 1);
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new FileNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new FileItemViewHolder(ItemAttachmentDefaultBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((FileNativeItemViewHolder) holder).bind(openNativePicker);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ if (!cursor.isClosed()) {
+ bindExecutor.execute(() -> {
+ final long id = getItemId(position);
+ final String name = cursor.getString(displayNameColumnIndex);
+ final String mimeType = cursor.getString(mimeTypeColumnIndex);
+ final long size = cursor.getLong(sizeColumnIndex);
+ final long modified = cursor.getLong(modifiedColumnIndex);
+ new Handler(Looper.getMainLooper()).post(() -> ((FileItemViewHolder) holder).bind(ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id), name, mimeType, size, modified, onSelect));
+ });
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java
new file mode 100644
index 000000000..1ac14361a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java
@@ -0,0 +1,88 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
+
+import static java.util.Collections.reverseOrder;
+import static java.util.Comparator.comparingLong;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
+
+@Deprecated
+public class FileAdapterLegacy extends AbstractPickerAdapter<RecyclerView.ViewHolder> {
+
+ @NonNull
+ private final List<File> files;
+ @NonNull
+ protected final BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect;
+ @NonNull
+ protected final Runnable openNativePicker;
+
+ public FileAdapterLegacy(@NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker) {
+ // TODO run in separate thread?
+ this.onSelect = onSelect;
+ this.openNativePicker = openNativePicker;
+ this.files = Arrays.stream(requireNonNull(requireNonNull(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)).listFiles()))
+ .sorted(reverseOrder(comparingLong(File::lastModified)))
+ .collect(toList());
+
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new FileNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new FileItemViewHolder(ItemAttachmentDefaultBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((FileNativeItemViewHolder) holder).bind(openNativePicker);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ final File file = files.get(position - 1);
+ if (file.isFile()) {
+ ((FileItemViewHolder) holder).bind(Uri.fromFile(file), file.getName(), AttachmentUtil.getMimeType(file.getAbsolutePath()), file.length(), file.lastModified(), onSelect);
+ } else {
+ ((FileItemViewHolder) holder).bindError();
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return files.size();
+ }
+
+ public void onDestroy() {
+ // Let GarbageCollection do this stuff...
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java
new file mode 100644
index 000000000..f7d64aca8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java
@@ -0,0 +1,45 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.net.Uri;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+
+import static android.text.format.Formatter.formatFileSize;
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.getIconForMimeType;
+import static it.niedermann.nextcloud.deck.util.DateUtil.getRelativeDateTimeString;
+
+public class FileItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemAttachmentDefaultBinding binding;
+
+ public FileItemViewHolder(@NonNull ItemAttachmentDefaultBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Uri uri, @NonNull String name, String mimeType, long size, long modified, @Nullable BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) {
+ itemView.setOnClickListener(onSelect == null ? null : (v) -> onSelect.accept(uri, new Pair<>(name, null)));
+ binding.filename.setText(name);
+ binding.filesize.setText(formatFileSize(binding.filesize.getContext(), size));
+ binding.modified.setText(getRelativeDateTimeString(binding.modified.getContext(), modified));
+ binding.preview.setImageResource(getIconForMimeType(mimeType));
+ }
+
+ public void bindError() {
+ binding.filename.setText(R.string.simple_exception);
+ binding.filesize.setText(null);
+ binding.modified.setText(null);
+ itemView.setOnClickListener(null);
+ binding.preview.setImageResource(R.drawable.ic_attach_file_grey600_24dp);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java
new file mode 100644
index 000000000..79129f26a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java
@@ -0,0 +1,23 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+
+public class FileNativeItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPickerNativeBinding binding;
+
+ public FileNativeItemViewHolder(@NonNull ItemPickerNativeBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(Runnable onOpenMajorPicker) {
+ binding.title.setText(R.string.show_all_files);
+ binding.subtitle.setText(R.string.downloads);
+ itemView.setOnClickListener((v) -> onOpenMajorPicker.run());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java
new file mode 100644
index 000000000..658eb1ee3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java
@@ -0,0 +1,100 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.annotation.SuppressLint;
+import android.content.ContentUris;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.MediaStore;
+import android.util.Pair;
+import android.util.Size;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.io.IOException;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPhotoPreviewBinding;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.provider.BaseColumns._ID;
+import static android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+
+public class GalleryAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> {
+
+ @NonNull
+ private final LifecycleOwner lifecycleOwner;
+
+ @SuppressLint("InlinedApi")
+ private static final String sortOrder = (SDK_INT >= Q)
+ ? MediaStore.Images.Media.DATE_TAKEN
+ : MediaStore.Images.Media.DATE_ADDED;
+
+ public GalleryAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, @NonNull LifecycleOwner lifecycleOwner) {
+ super(context, onSelect, openNativePicker, EXTERNAL_CONTENT_URI, _ID, sortOrder + " DESC");
+ this.lifecycleOwner = lifecycleOwner;
+ notifyItemRangeInserted(0, getItemCount() + 1);
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new GalleryPhotoPreviewItemViewHolder(ItemPhotoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new GalleryItemViewHolder(ItemAttachmentImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((GalleryPhotoPreviewItemViewHolder) holder).bind(openNativePicker, lifecycleOwner);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ final long id = getItemId(position);
+ bindExecutor.execute(() -> {
+ try {
+ final Bitmap thumbnail;
+ if (SDK_INT >= Q) {
+ thumbnail = contentResolver.loadThumbnail(ContentUris.withAppendedId(
+ EXTERNAL_CONTENT_URI, id), new Size(512, 384), null);
+ } else {
+ thumbnail = MediaStore.Images.Thumbnails.getThumbnail(
+ contentResolver, id,
+ MediaStore.Images.Thumbnails.MINI_KIND, null);
+ }
+ new Handler(Looper.getMainLooper()).post(() -> ((GalleryItemViewHolder) holder).bind(ContentUris.withAppendedId(
+ EXTERNAL_CONTENT_URI, id), thumbnail, onSelect));
+ } catch (IOException ignored) {
+ new Handler(Looper.getMainLooper()).post(((GalleryItemViewHolder) holder)::bindError);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) {
+ super.onViewDetachedFromWindow(holder);
+ if (holder instanceof GalleryPhotoPreviewItemViewHolder) {
+ ((GalleryPhotoPreviewItemViewHolder) holder).unbind();
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java
new file mode 100644
index 000000000..c70dc8277
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java
@@ -0,0 +1,29 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class GalleryItemDecoration extends RecyclerView.ItemDecoration {
+
+ @Px
+ private final int gutter;
+
+ public GalleryItemDecoration(@Px int gutter) {
+ this.gutter = gutter;
+ }
+
+ @Override
+ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ final int position = parent.getChildAdapterPosition(view);
+ if (position >= 0) {
+ outRect.left = gutter;
+ outRect.top = gutter;
+ outRect.right = gutter;
+ outRect.bottom = gutter;
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java
new file mode 100644
index 000000000..346fca9c3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java
@@ -0,0 +1,42 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
+
+public class GalleryItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemAttachmentImageBinding binding;
+
+ public GalleryItemViewHolder(@NonNull ItemAttachmentImageBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Uri uri, @Nullable Bitmap image, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) {
+ itemView.setOnClickListener((v) -> onSelect.accept(uri, new Pair<>(null, Glide.with(itemView.getContext()).load(image))));
+ Glide.with(itemView.getContext())
+ .load(image)
+ .placeholder(R.drawable.ic_image_grey600_24dp)
+ .into(binding.preview);
+ }
+
+ public void bindError() {
+ itemView.setOnClickListener(null);
+ Glide.with(itemView.getContext())
+ .load(R.drawable.ic_image_grey600_24dp)
+ .into(binding.preview);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java
new file mode 100644
index 000000000..00a833e57
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java
@@ -0,0 +1,51 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ExecutionException;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.databinding.ItemPhotoPreviewBinding;
+
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+
+public class GalleryPhotoPreviewItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPhotoPreviewBinding binding;
+ private ProcessCameraProvider cameraProvider;
+
+ public GalleryPhotoPreviewItemViewHolder(@NonNull ItemPhotoPreviewBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Runnable openNativePicker, @NonNull LifecycleOwner lifecycleOwner) {
+ itemView.setOnClickListener((v) -> openNativePicker.run());
+ ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(itemView.getContext());
+ cameraProviderFuture.addListener(() -> {
+ try {
+ unbind();
+ cameraProvider = cameraProviderFuture.get();
+ Preview previewUseCase = new Preview.Builder().build();
+ previewUseCase.setSurfaceProvider(binding.preview.getSurfaceProvider());
+ cameraProvider.bindToLifecycle(lifecycleOwner, DEFAULT_BACK_CAMERA, previewUseCase);
+ } catch (ExecutionException | InterruptedException | IllegalArgumentException e) {
+ DeckLog.logError(e);
+ }
+ }, ContextCompat.getMainExecutor(itemView.getContext()));
+ }
+
+
+ public void unbind() {
+ if (cameraProvider != null) {
+ cameraProvider.unbindAll();
+ }
+ }
+}
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
new file mode 100644
index 000000000..8ebdf1b50
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java
@@ -0,0 +1,102 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
+
+import com.bumptech.glide.RequestBuilder;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.DialogPreviewBinding;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+
+public class PreviewDialog extends BrandedDialogFragment {
+
+ private DialogPreviewBinding binding;
+ private PreviewDialogViewModel viewModel;
+ private LiveData<RequestBuilder<?>> imageBuilder$;
+ private LiveData<String> title$;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ viewModel = new ViewModelProvider(requireActivity()).get(PreviewDialogViewModel.class);
+ binding = DialogPreviewBinding.inflate(LayoutInflater.from(requireContext()));
+
+ final Context context = requireContext();
+
+ this.imageBuilder$ = this.viewModel.getImageBuilder();
+ this.imageBuilder$.observe(requireActivity(), builder -> {
+ if (builder == null) {
+ binding.avatar.setVisibility(GONE);
+ } else {
+ final CircularProgressDrawable circularProgressDrawable = new CircularProgressDrawable(context);
+ circularProgressDrawable.setStrokeWidth(5f);
+ circularProgressDrawable.setCenterRadius(30f);
+ circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY);
+ circularProgressDrawable.start();
+ binding.avatar.setVisibility(VISIBLE);
+ binding.avatar.post(() -> builder
+ .placeholder(circularProgressDrawable)
+ .into(binding.avatar));
+ }
+ });
+ this.title$ = this.viewModel.getTitle();
+ this.title$.observe(requireActivity(), title -> {
+ if (TextUtils.isEmpty(title)) {
+ binding.title.setVisibility(GONE);
+ } else {
+ binding.title.setVisibility(VISIBLE);
+ binding.title.setText(title);
+ }
+ });
+
+ return new BrandedAlertDialogBuilder(requireContext())
+ .setPositiveButton(R.string.simple_attach, (d, w) -> {
+ viewModel.setResult(true);
+ dismiss();
+ })
+ .setNeutralButton(R.string.simple_close, (d, w) -> {
+ viewModel.setResult(false);
+ dismiss();
+ })
+ .setView(binding.getRoot())
+ .create();
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ viewModel.setResult(false);
+ super.onCancel(dialog);
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ }
+
+ @Override
+ public void onDestroy() {
+ this.imageBuilder$.removeObservers(requireActivity());
+ this.title$.removeObservers(requireActivity());
+ super.onDestroy();
+ }
+
+ public static DialogFragment newInstance() {
+ return new PreviewDialog();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java
new file mode 100644
index 000000000..8ee8a0e08
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java
@@ -0,0 +1,50 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.bumptech.glide.RequestBuilder;
+
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+
+public class PreviewDialogViewModel extends ViewModel {
+
+ @NonNull
+ private final MutableLiveData<String> title$ = new MutableLiveData<>();
+ @NonNull
+ private final MutableLiveData<RequestBuilder<?>> imageBuilder$ = new MutableLiveData<>();
+ private MutableLiveData<Boolean> result$ = new MutableLiveData<>();
+
+ /**
+ * Call this before observing {@link #getResult()} to prepare the {@link PreviewDialog}.
+ */
+ public void prepareDialog(@Nullable String title, @Nullable RequestBuilder<?> imageBuilder) {
+ this.result$ = new MutableLiveData<>();
+ this.title$.setValue(title);
+ this.imageBuilder$.setValue(imageBuilder);
+ }
+
+ /**
+ * This will be a new instance after each call of {@link #prepareDialog(String, RequestBuilder)}.
+ *
+ * @return {@link Boolean#TRUE} if a positive action has been submitted, {@link Boolean#FALSE} if the dialog has been canceled.
+ */
+ public LiveData<Boolean> getResult() {
+ return this.result$;
+ }
+
+ protected LiveData<String> getTitle() {
+ return distinctUntilChanged(this.title$);
+ }
+
+ protected LiveData<RequestBuilder<?>> getImageBuilder() {
+ return distinctUntilChanged(this.imageBuilder$);
+ }
+
+ protected void setResult(boolean submittedPositive) {
+ result$.setValue(submittedPositive);
+ }
+}
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 e261c37a2..3fd536aa6 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
@@ -15,7 +15,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
-import java.util.Date;
+import java.time.Instant;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
@@ -23,12 +23,15 @@ import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabCommentsBindi
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 it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
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 static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToFAB;
import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions;
@@ -38,7 +41,6 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
private FragmentCardEditTabCommentsBinding binding;
private EditCardViewModel mainViewModel;
private CommentsViewModel commentsViewModel;
- private SyncManager syncManager;
private CardCommentsAdapter adapter;
public static Fragment newInstance() {
@@ -68,7 +70,6 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
commentsViewModel = new ViewModelProvider(this).get(CommentsViewModel.class);
- syncManager = new SyncManager(requireActivity());
adapter = new CardCommentsAdapter(requireContext(), mainViewModel.getAccount(), requireActivity().getMenuInflater(), this, this, getChildFragmentManager());
binding.comments.setAdapter(adapter);
@@ -82,7 +83,7 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
setupMentions(mainViewModel.getAccount(), comment.getComment().getMentions(), binding.replyCommentText);
}
});
- syncManager.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(),
+ commentsViewModel.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(),
(comments) -> {
if (comments != null && comments.size() > 0) {
binding.emptyContentView.setVisibility(GONE);
@@ -100,13 +101,13 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
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(), new Date());
+ final DeckComment comment = new DeckComment(binding.message.getText().toString().trim(), mainViewModel.getAccount().getUserName(), Instant.now());
final FullDeckComment parent = commentsViewModel.getReplyToComment().getValue();
if (parent != null) {
comment.setParentId(parent.getId());
commentsViewModel.setReplyToComment(null);
}
- syncManager.addCommentToCard(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), comment);
+ commentsViewModel.addCommentToCard(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), comment);
}
binding.message.setText(null);
});
@@ -116,6 +117,7 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
}
return true;
});
+ binding.message.addTextChangedListener(new CardCommentsMentionProposer(getViewLifecycleOwner(), mainViewModel.getAccount(), mainViewModel.getBoardId(), binding.message, binding.mentionProposerWrapper, binding.mentionProposer));
} else {
binding.addCommentLayout.setVisibility(GONE);
}
@@ -133,12 +135,17 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
@Override
public void onCommentEdited(Long id, String comment) {
- syncManager.updateComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), id, comment);
+ commentsViewModel.updateComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), id, comment);
}
@Override
public void onCommentDeleted(Long localId) {
- syncManager.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId);
+ final WrappedLiveData<Void> deleteLiveData = commentsViewModel.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId);
+ observeOnce(deleteLiveData, this, (next) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), mainViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
@Override
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
new file mode 100644
index 000000000..7ca7a6384
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java
@@ -0,0 +1,139 @@
+package it.niedermann.nextcloud.deck.ui.card.comments;
+
+import android.annotation.SuppressLint;
+import android.net.Uri;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Pair;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+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.ui.card.comments.util.CommentsUtil;
+
+import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
+
+public class CardCommentsMentionProposer implements TextWatcher {
+
+ private final int avatarSize;
+ @NonNull
+ private final SyncManager syncManager;
+ @NonNull
+ private final LinearLayout.LayoutParams layoutParams;
+ @NonNull
+ private final LifecycleOwner owner;
+ @NonNull
+ private final Account account;
+ private final long boardLocalId;
+ @NonNull
+ private final EditText editText;
+ @NonNull
+ private final LinearLayout mentionProposer;
+ @NonNull
+ private final LinearLayout mentionProposerWrapper;
+
+ @NonNull
+ private final List<User> users = new ArrayList<>();
+
+ public CardCommentsMentionProposer(@NonNull LifecycleOwner owner, @NonNull Account account, long boardLocalId, @NonNull EditText editText, LinearLayout mentionProposerWrapper, @NonNull LinearLayout avatarProposer) {
+ this.owner = owner;
+ this.account = account;
+ this.boardLocalId = boardLocalId;
+ this.editText = editText;
+ this.mentionProposerWrapper = mentionProposerWrapper;
+ this.mentionProposer = avatarProposer;
+ syncManager = new SyncManager(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));
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ final int selectionStart = editText.getSelectionStart();
+ final int selectionEnd = editText.getSelectionEnd();
+ final Pair<String, Integer> mentionProposal = CommentsUtil.getUserNameForMentionProposal(s.toString(), selectionStart);
+ if (mentionProposal == null || (mentionProposal.first != null && mentionProposal.first.length() == 0) || selectionStart != selectionEnd) {
+ mentionProposer.removeAllViews();
+ mentionProposerWrapper.setVisibility(View.GONE);
+ 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 (User user : users) {
+ final ImageView 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);
+ }
+ } 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();
+ mentionProposerWrapper.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ private void updateListenerOfView(View avatar, CharSequence s, Pair<String, Integer> mentionProposal, User user) {
+ avatar.setOnClickListener((c) -> {
+ editText.setText(
+ s.subSequence(0, mentionProposal.second) +
+ user.getUid() +
+ s.subSequence(mentionProposal.second + mentionProposal.first.length(), s.length())
+ );
+ editText.setSelection(mentionProposal.second + user.getUid().length());
+ mentionProposerWrapper.setVisibility(View.GONE);
+ });
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+
+ }
+}
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 f7fd247a9..dada94d5b 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,15 +1,30 @@
package it.niedermann.nextcloud.deck.ui.card.comments;
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.ViewModel;
+import java.util.List;
+
+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 it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
@SuppressWarnings("WeakerAccess")
-public class CommentsViewModel extends ViewModel {
+public class CommentsViewModel extends AndroidViewModel {
- private MutableLiveData<FullDeckComment> replyToComment = new MutableLiveData<>();
+ private final SyncManager syncManager;
+
+ private final MutableLiveData<FullDeckComment> replyToComment = new MutableLiveData<>();
+
+ public CommentsViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
public void setReplyToComment(FullDeckComment replyToComment) {
this.replyToComment.postValue(replyToComment);
@@ -18,4 +33,20 @@ public class CommentsViewModel extends ViewModel {
public LiveData<FullDeckComment> getReplyToComment() {
return this.replyToComment;
}
+
+ public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) {
+ return syncManager.getFullCommentsForLocalCardId(localCardId);
+ }
+
+ public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) {
+ syncManager.addCommentToCard(accountId, cardId, comment);
+ }
+
+ public void updateComment(long accountId, long localCardId, long localCommentId, String comment) {
+ syncManager.updateComment(accountId, localCardId, localCommentId, comment);
+ }
+
+ public WrappedLiveData<Void> deleteComment(long accountId, long localCardId, long localCommentId) {
+ return syncManager.deleteComment(accountId, localCardId, localCommentId);
+ }
}
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 086d799af..3e540c95e 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
@@ -11,22 +11,25 @@ import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
-import java.text.DateFormat;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
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.util.DateUtil;
-import it.niedermann.nextcloud.deck.util.DimensionUtil;
import it.niedermann.nextcloud.deck.util.ViewUtil;
-import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard;
import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions;
public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
- private ItemCommentBinding binding;
+ private final ItemCommentBinding binding;
+ private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
@SuppressWarnings("WeakerAccess")
public ItemCommentViewHolder(ItemCommentBinding binding) {
@@ -35,15 +38,15 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
}
public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @ColorInt int mainColor, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager) {
- ViewUtil.addAvatar(binding.avatar, account.getUrl(), comment.getComment().getActorId(), DimensionUtil.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp);
+ 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);
binding.message.setText(comment.getComment().getMessage());
binding.actorDisplayName.setText(comment.getComment().getActorDisplayName());
- binding.creationDateTime.setText(DateUtil.getRelativeDateTimeString(binding.creationDateTime.getContext(), comment.getComment().getCreationDateTime().getTime()));
+ binding.creationDateTime.setText(DateUtil.getRelativeDateTimeString(binding.creationDateTime.getContext(), comment.getComment().getCreationDateTime().toEpochMilli()));
itemView.setOnClickListener(View::showContextMenu);
itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
inflater.inflate(R.menu.comment_menu, menu);
- menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> copyToClipboard(itemView.getContext(), comment.getComment().getMessage()));
+ menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), comment.getComment().getMessage()));
final MenuItem replyMenuItem = menu.findItem(R.id.reply);
if (comment.getStatusEnum() != DBStatus.LOCAL_EDITED && account.getServerDeckVersionAsObject().supportsCommentsReplys()) {
replyMenuItem.setOnMenuItemClickListener(item -> {
@@ -72,7 +75,7 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor);
binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(comment.getStatusEnum()) ? View.VISIBLE : View.GONE);
- TooltipCompat.setTooltipText(binding.creationDateTime, DateFormat.getDateTimeInstance().format(comment.getComment().getCreationDateTime()));
+ TooltipCompat.setTooltipText(binding.creationDateTime, comment.getComment().getCreationDateTime().atZone(ZoneId.systemDefault()).format(dateFormatter));
setupMentions(account, comment.getComment().getMentions(), binding.message);
if (comment.getParent() == null) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java
new file mode 100644
index 000000000..5251291c8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java
@@ -0,0 +1,48 @@
+package it.niedermann.nextcloud.deck.ui.card.comments.util;
+
+
+import androidx.core.util.Pair;
+
+public class CommentsUtil {
+
+ public static Pair<String, Integer> getUserNameForMentionProposal(String text, int cursorPosition) {
+ Pair result = null;
+
+ if (text != null) {
+ // find start of relevant substring
+ int cursor = cursorPosition;
+ if (cursor < 1) {
+ return null;
+ }
+ int start = 0;
+ while (cursor > 0) {
+ cursor--;
+ if (Character.isWhitespace(text.charAt(cursor))) {
+ start = cursor + 1;
+ break;
+ }
+ }
+ if (text.length()-1 < start || text.charAt(start) != '@') {
+ return null;
+ }
+
+ // find end of relevant substring
+ cursor = cursorPosition;
+ int textLength = text.length();
+ int end = textLength;
+ while (cursor < textLength) {
+ if (Character.isWhitespace(text.charAt(cursor))) {
+ end = cursor;
+ break;
+ }
+ cursor++;
+ }
+
+ start++;
+ result = Pair.create(text.substring(start, end), start);
+
+ }
+
+ return result;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java
new file mode 100644
index 000000000..aa8c3e8f6
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java
@@ -0,0 +1,80 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAssigneeBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.User;
+
+import static androidx.recyclerview.widget.RecyclerView.NO_ID;
+
+@SuppressWarnings("WeakerAccess")
+public class AssigneeAdapter extends RecyclerView.Adapter<AssigneeViewHolder> {
+
+ private final Account account;
+ @NonNull
+ private List<User> users = new ArrayList<>();
+ @NonNull
+ private final Consumer<User> userClickedListener;
+
+ AssigneeAdapter(
+ @NonNull Consumer<User> userClickedListener,
+ @NonNull Account account
+ ) {
+ super();
+ this.userClickedListener = userClickedListener;
+ this.account = account;
+ setHasStableIds(true);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ Long id = users.get(position).getLocalId();
+ return id == null ? NO_ID : id;
+ }
+
+ @NonNull
+ @Override
+ public AssigneeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final Context context = parent.getContext();
+ return new AssigneeViewHolder(ItemAssigneeBinding.inflate(LayoutInflater.from(context)));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull AssigneeViewHolder holder, int position) {
+ final User user = users.get(position);
+ holder.bind(account, user, userClickedListener);
+ }
+
+ @Override
+ public int getItemCount() {
+ return users.size();
+ }
+
+ public void setUsers(@NonNull List<User> users) {
+ this.users.clear();
+ this.users.addAll(users);
+ notifyDataSetChanged();
+ }
+
+ public void addUser(@NonNull User user) {
+ this.users.add(user);
+ notifyItemInserted(this.users.size());
+ }
+
+ public void removeUser(@NonNull User user) {
+ final int index = this.users.indexOf(user);
+ this.users.remove(user);
+ notifyItemRemoved(index);
+ }
+
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java
new file mode 100644
index 000000000..096dcfa53
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java
@@ -0,0 +1,28 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class AssigneeDecoration extends RecyclerView.ItemDecoration {
+
+ private final int gutter;
+
+ public AssigneeDecoration(@Px int gutter) {
+ this.gutter = gutter;
+ }
+
+ @Override
+ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ final int position = parent.getChildAdapterPosition(view);
+
+ if (position >= 0) {
+ // All columns get some spacing at the bottom and at the right side
+ outRect.right = gutter;
+ outRect.bottom = gutter;
+ }
+ }
+}
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
new file mode 100644
index 000000000..ddb1236b6
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java
@@ -0,0 +1,29 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+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;
+
+ @SuppressWarnings("WeakerAccess")
+ public AssigneeViewHolder(ItemAssigneeBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ 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);
+ if(onClickListener != null) {
+ itemView.setOnClickListener((v) -> onClickListener.accept(user));
+ }
+ }
+} \ No newline at end of file
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 3182fffa2..2c697de08 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
@@ -2,25 +2,25 @@ package it.niedermann.nextcloud.deck.ui.card.details;
import android.content.Context;
import android.content.res.ColorStateList;
-import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.Editable;
+import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
import com.google.android.material.chip.Chip;
import com.google.android.material.snackbar.Snackbar;
@@ -31,18 +31,21 @@ import com.wdullaer.materialdatetimepicker.time.TimePickerDialog.OnTimeSetListen
import com.yydcdut.markdown.MarkdownProcessor;
import com.yydcdut.markdown.syntax.edit.EditFactory;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabDetailsBinding;
import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDatePickerDialog;
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
@@ -51,26 +54,23 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedTimePickerDialog;
import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
import it.niedermann.nextcloud.deck.ui.card.LabelAutoCompleteAdapter;
import it.niedermann.nextcloud.deck.ui.card.UserAutoCompleteAdapter;
+import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeDialog;
+import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeListener;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
import it.niedermann.nextcloud.deck.util.MarkDownUtil;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
-import static android.text.format.DateFormat.getDateFormat;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
-public class CardDetailsFragment extends BrandedFragment implements OnDateSetListener, OnTimeSetListener {
+public class CardDetailsFragment extends BrandedFragment implements OnDateSetListener, OnTimeSetListener, CardAssigneeListener {
private FragmentCardEditTabDetailsBinding binding;
private EditCardViewModel viewModel;
- private SyncManager syncManager;
- private DateFormat dateFormat;
- private DateFormat dueTime = new SimpleDateFormat("HH:mm", Locale.ROOT);
- @Px
- private int avatarSize;
- private LinearLayout.LayoutParams avatarLayoutParams;
+ private AssigneeAdapter adapter;
+ private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
+ private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
private AppCompatActivity activity;
@Override
@@ -92,8 +92,6 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentCardEditTabDetailsBinding.inflate(inflater, container, false);
- dateFormat = getDateFormat(activity);
-
viewModel = new ViewModelProvider(activity).get(EditCardViewModel.class);
// This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance
@@ -103,22 +101,20 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
return binding.getRoot();
}
- syncManager = new SyncManager(requireContext());
-
- avatarSize = dpToPx(requireContext(), R.dimen.avatar_size);
- avatarLayoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize);
- avatarLayoutParams.setMargins(0, 0, dpToPx(requireContext(), R.dimen.spacer_1x), 0);
+ @Px final int avatarSize = DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size);
+ final LinearLayout.LayoutParams avatarLayoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize);
+ avatarLayoutParams.setMargins(0, 0, DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x), 0);
- setupPeople();
+ setupAssignees();
setupLabels();
setupDueDate();
setupDescription();
+ setupProjects();
binding.description.setText(viewModel.getFullCard().getCard().getDescription());
return binding.getRoot();
}
-
@Override
public void onResume() {
super.onResume();
@@ -173,49 +169,14 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
}
}
- private TimePickerDialog createTimePickerDialogFromDate(
- @Nullable OnTimeSetListener listener,
- @Nullable Date date
- ) {
- int hourOfDay = 0;
- int minutes = 0;
-
- if (date != null) {
- hourOfDay = date.getHours();
- minutes = date.getMinutes();
- }
- return BrandedTimePickerDialog.newInstance(listener, hourOfDay, minutes, true);
- }
-
- private DatePickerDialog createDatePickerDialogFromDate(
- @Nullable OnDateSetListener listener,
- @Nullable Date date
- ) {
- int year;
- int month;
- int day;
-
- Calendar cal = Calendar.getInstance();
- if (date != null) {
- cal.setTime(date);
- year = cal.get(Calendar.YEAR);
- month = cal.get(Calendar.MONTH);
- day = cal.get(Calendar.DAY_OF_MONTH);
- } else {
- year = cal.get(Calendar.YEAR);
- month = cal.get(Calendar.MONTH);
- day = cal.get(Calendar.DAY_OF_MONTH);
- }
- return BrandedDatePickerDialog.newInstance(listener, year, month, day);
- }
-
private void setupDueDate() {
if (this.viewModel.getFullCard().getCard().getDueDate() != null) {
- binding.dueDateDate.setText(dateFormat.format(this.viewModel.getFullCard().getCard().getDueDate()));
- binding.dueDateTime.setText(dueTime.format(this.viewModel.getFullCard().getCard().getDueDate()));
- binding.clearDueDate.setVisibility(View.VISIBLE);
+ final ZonedDateTime dueDate = this.viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault());
+ binding.dueDateDate.setText(dueDate == null ? null : dueDate.format(dateFormatter));
+ binding.dueDateTime.setText(dueDate == null ? null : dueDate.format(timeFormatter));
+ binding.clearDueDate.setVisibility(VISIBLE);
} else {
- binding.clearDueDate.setVisibility(View.GONE);
+ binding.clearDueDate.setVisibility(GONE);
binding.dueDateDate.setText(null);
binding.dueDateTime.setText(null);
}
@@ -223,31 +184,37 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
if (viewModel.canEdit()) {
binding.dueDateDate.setOnClickListener(v -> {
- if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null) {
- createDatePickerDialogFromDate(this, viewModel.getFullCard().getCard().getDueDate()).show(getChildFragmentManager(), BrandedDatePickerDialog.class.getCanonicalName());
+ final LocalDate date;
+ if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null && viewModel.getFullCard().getCard().getDueDate() != null) {
+ date = viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault()).toLocalDate();
} else {
- createDatePickerDialogFromDate(this, null).show(getChildFragmentManager(), BrandedDatePickerDialog.class.getCanonicalName());
+ date = LocalDate.now();
}
+ BrandedDatePickerDialog.newInstance(this, date.getYear(), date.getMonthValue(), date.getDayOfMonth())
+ .show(getChildFragmentManager(), BrandedDatePickerDialog.class.getCanonicalName());
});
binding.dueDateTime.setOnClickListener(v -> {
- if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null) {
- createTimePickerDialogFromDate(this, viewModel.getFullCard().getCard().getDueDate()).show(getChildFragmentManager(), BrandedTimePickerDialog.class.getCanonicalName());
+ final LocalTime time;
+ if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null && viewModel.getFullCard().getCard().getDueDate() != null) {
+ time = viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault()).toLocalTime();
} else {
- createTimePickerDialogFromDate(this, null).show(getChildFragmentManager(), BrandedTimePickerDialog.class.getCanonicalName());
+ time = LocalTime.now();
}
+ BrandedTimePickerDialog.newInstance(this, time.getHour(), time.getMinute(), true)
+ .show(getChildFragmentManager(), BrandedTimePickerDialog.class.getCanonicalName());
});
binding.clearDueDate.setOnClickListener(v -> {
binding.dueDateDate.setText(null);
binding.dueDateTime.setText(null);
viewModel.getFullCard().getCard().setDueDate(null);
- binding.clearDueDate.setVisibility(View.GONE);
+ binding.clearDueDate.setVisibility(GONE);
});
} else {
binding.dueDateDate.setEnabled(false);
binding.dueDateTime.setEnabled(false);
- binding.clearDueDate.setVisibility(View.GONE);
+ binding.clearDueDate.setVisibility(GONE);
}
}
@@ -266,7 +233,7 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
newLabel.setBoardId(boardId);
newLabel.setTitle(((LabelAutoCompleteAdapter) binding.labels.getAdapter()).getLastFilterText());
newLabel.setLocalId(null);
- WrappedLiveData<Label> createLabelLiveData = syncManager.createLabel(accountId, newLabel, boardId);
+ WrappedLiveData<Label> createLabelLiveData = viewModel.createLabel(accountId, newLabel, boardId);
observeOnce(createLabelLiveData, CardDetailsFragment.this, createdLabel -> {
if (createLabelLiveData.hasError()) {
DeckLog.logError(createLabelLiveData.getError());
@@ -277,14 +244,14 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(createdLabel);
viewModel.getFullCard().getLabels().add(createdLabel);
binding.labelsGroup.addView(createChipFromLabel(newLabel));
- binding.labelsGroup.setVisibility(View.VISIBLE);
+ binding.labelsGroup.setVisibility(VISIBLE);
}
});
} else {
((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(label);
viewModel.getFullCard().getLabels().add(label);
binding.labelsGroup.addView(createChipFromLabel(label));
- binding.labelsGroup.setVisibility(View.VISIBLE);
+ binding.labelsGroup.setVisibility(VISIBLE);
}
binding.labels.setText("");
@@ -296,18 +263,17 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
for (Label label : viewModel.getFullCard().getLabels()) {
binding.labelsGroup.addView(createChipFromLabel(label));
}
- binding.labelsGroup.setVisibility(View.VISIBLE);
+ binding.labelsGroup.setVisibility(VISIBLE);
} else {
binding.labelsGroup.setVisibility(View.INVISIBLE);
}
}
-
private Chip createChipFromLabel(Label label) {
final Chip chip = new Chip(activity);
chip.setText(label.getTitle());
if (viewModel.canEdit()) {
- chip.setCloseIcon(getResources().getDrawable(R.drawable.ic_close_circle_grey600));
+ chip.setCloseIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_close_circle_grey600));
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(v -> {
binding.labelsGroup.removeView(chip);
@@ -316,9 +282,9 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
});
}
try {
- final int labelColor = Color.parseColor("#" + label.getColor());
+ final int labelColor = label.getColor();
chip.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
- final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor);
+ final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
chip.setTextColor(color);
if (chip.getCloseIcon() != null) {
@@ -331,7 +297,15 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
return chip;
}
- private void setupPeople() {
+ private void setupAssignees() {
+ 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);
+ final int spanCount = (int) (float) binding.assignees.getWidth() / (DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size) + gutter);
+ binding.assignees.setLayoutManager(new GridLayoutManager(getContext(), spanCount));
+ binding.assignees.addItemDecoration(new AssigneeDecoration(gutter));
+ });
if (viewModel.canEdit()) {
Long localCardId = viewModel.getFullCard().getCard().getLocalId();
localCardId = localCardId == null ? -1 : localCardId;
@@ -340,81 +314,90 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
User user = (User) adapterView.getItemAtPosition(position);
viewModel.getFullCard().getAssignedUsers().add(user);
((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user);
- addAvatar(viewModel.getAccount().getUrl(), user);
+ adapter.addUser(user);
binding.people.setText("");
});
if (this.viewModel.getFullCard().getAssignedUsers() != null) {
- binding.peopleList.removeAllViews();
- for (User user : this.viewModel.getFullCard().getAssignedUsers()) {
- addAvatar(viewModel.getAccount().getUrl(), user);
- }
+ adapter.setUsers(this.viewModel.getFullCard().getAssignedUsers());
}
} else {
binding.people.setEnabled(false);
}
}
- private void addAvatar(String baseUrl, User user) {
- ImageView avatar = new ImageView(activity);
- avatar.setLayoutParams(avatarLayoutParams);
- if (viewModel.canEdit()) {
- avatar.setOnClickListener(v -> {
- viewModel.getFullCard().getAssignedUsers().remove(user);
- binding.peopleList.removeView(avatar);
- ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user);
- BrandedSnackbar.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);
- addAvatar(baseUrl, user);
- }).show();
- });
- }
- binding.peopleList.addView(avatar);
- avatar.requestLayout();
- ViewUtil.addAvatar(avatar, baseUrl, user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp);
- }
-
@Override
- public void onDateSet(com.wdullaer.materialdatetimepicker.date.DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) {
- Calendar c = Calendar.getInstance();
+ public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) {
int hourOfDay;
int minute;
- if (binding.dueDateTime.getText() != null && binding.dueDateTime.length() > 0) {
- hourOfDay = this.viewModel.getFullCard().getCard().getDueDate().getHours();
- minute = this.viewModel.getFullCard().getCard().getDueDate().getMinutes();
- } else {
+ final CharSequence selectedTime = binding.dueDateTime.getText();
+ if (TextUtils.isEmpty(selectedTime)) {
hourOfDay = 0;
minute = 0;
+ } else {
+ final LocalTime oldTime = LocalTime.from(this.viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault()));
+ hourOfDay = oldTime.getHour();
+ minute = oldTime.getMinute();
}
- c.set(year, monthOfYear, dayOfMonth, hourOfDay, minute);
- this.viewModel.getFullCard().getCard().setDueDate(c.getTime());
- binding.dueDateDate.setText(dateFormat.format(c.getTime()));
+ final ZonedDateTime newDateTime = ZonedDateTime.of(
+ LocalDate.of(year, monthOfYear + 1, dayOfMonth),
+ LocalTime.of(hourOfDay, minute),
+ ZoneId.systemDefault()
+ );
+ this.viewModel.getFullCard().getCard().setDueDate(newDateTime.toInstant());
+ binding.dueDateDate.setText(newDateTime.format(dateFormatter));
- if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().getTime() == 0) {
- binding.clearDueDate.setVisibility(View.GONE);
+ if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().toEpochMilli() == 0) {
+ binding.clearDueDate.setVisibility(GONE);
} else {
- binding.clearDueDate.setVisibility(View.VISIBLE);
+ binding.clearDueDate.setVisibility(VISIBLE);
}
}
@Override
- public void onTimeSet(com.wdullaer.materialdatetimepicker.time.TimePickerDialog view, int hourOfDay, int minute, int second) {
- if (this.viewModel.getFullCard().getCard().getDueDate() == null) {
- this.viewModel.getFullCard().getCard().setDueDate(new Date());
+ public void onTimeSet(TimePickerDialog view, int hourOfDay, int minute, int second) {
+ final Instant oldInstant = this.viewModel.getFullCard().getCard().getDueDate();
+ final ZonedDateTime oldDateTime = oldInstant == null ? ZonedDateTime.now() : oldInstant.atZone(ZoneId.systemDefault());
+ final ZonedDateTime newDateTime = oldDateTime.with(
+ LocalTime.of(hourOfDay, minute)
+ );
+
+ this.viewModel.getFullCard().getCard().setDueDate(newDateTime.toInstant());
+ binding.dueDateTime.setText(newDateTime.format(timeFormatter));
+ if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().toEpochMilli() == 0) {
+ binding.clearDueDate.setVisibility(GONE);
+ } else {
+ binding.clearDueDate.setVisibility(VISIBLE);
}
- this.viewModel.getFullCard().getCard().getDueDate().setHours(hourOfDay);
- this.viewModel.getFullCard().getCard().getDueDate().setMinutes(minute);
- binding.dueDateTime.setText(dueTime.format(this.viewModel.getFullCard().getCard().getDueDate().getTime()));
- if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().getTime() == 0) {
- binding.clearDueDate.setVisibility(View.GONE);
+ }
+
+ private void setupProjects() {
+ if (viewModel.getFullCard().getProjects().size() > 0) {
+ binding.projectsTitle.setVisibility(VISIBLE);
+ binding.projects.setNestedScrollingEnabled(false);
+ final CardProjectsAdapter adapter = new CardProjectsAdapter(viewModel.getFullCard().getProjects(), getChildFragmentManager());
+ binding.projects.setAdapter(adapter);
+ binding.projects.setVisibility(VISIBLE);
} else {
- binding.clearDueDate.setVisibility(View.VISIBLE);
+ binding.projectsTitle.setVisibility(GONE);
+ binding.projects.setVisibility(GONE);
}
}
+
+ @Override
+ public void onUnassignUser(@NonNull User user) {
+ viewModel.getFullCard().getAssignedUsers().remove(user);
+ adapter.removeUser(user);
+ ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user);
+ BrandedSnackbar.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();
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java
deleted file mode 100644
index 2efbab789..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package it.niedermann.nextcloud.deck.ui.card.details;
-
-import java.util.Date;
-
-import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.model.User;
-
-public interface CardDetailsListener {
-
- void onDescriptionChanged(String toString);
-
- void onDueDateChanged(Date dueDate);
-
- void onUserAdded(User user);
-
- void onUserRemoved(User user);
-
- void onLabelRemoved(Label label);
-
- void onLabelAdded(Label createdLabel);
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java
new file mode 100644
index 000000000..0c2d63d74
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java
@@ -0,0 +1,52 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.databinding.ItemProjectBinding;
+import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources;
+import it.niedermann.nextcloud.deck.ui.card.projectresources.CardProjectResourcesDialog;
+
+public class CardProjectsAdapter extends RecyclerView.Adapter<CardProjectsViewHolder> {
+
+ @NonNull
+ private final List<OcsProjectWithResources> projects;
+ @NonNull
+ private final FragmentManager fragmentManager;
+
+ public CardProjectsAdapter(@NonNull List<OcsProjectWithResources> projects, @NonNull FragmentManager fragmentManager) {
+ this.projects = new ArrayList<>(projects.size());
+ this.projects.addAll(projects);
+ this.fragmentManager = fragmentManager;
+ setHasStableIds(true);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return projects.get(position).getLocalId();
+ }
+
+ @NonNull
+ @Override
+ public CardProjectsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new CardProjectsViewHolder(ItemProjectBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull CardProjectsViewHolder holder, int position) {
+ final OcsProjectWithResources project = projects.get(position);
+ holder.bind(project, (v) -> CardProjectResourcesDialog.newInstance(project.getName(), project.getResources()).show(fragmentManager, CardProjectResourcesDialog.class.getSimpleName()));
+ }
+
+ @Override
+ public int getItemCount() {
+ return projects.size();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java
new file mode 100644
index 000000000..9c5494af7
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java
@@ -0,0 +1,32 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import android.view.View.OnClickListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemProjectBinding;
+import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources;
+
+public class CardProjectsViewHolder extends RecyclerView.ViewHolder {
+
+ private ItemProjectBinding binding;
+
+ public CardProjectsViewHolder(@NonNull ItemProjectBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull OcsProjectWithResources project, @Nullable OnClickListener onClickListener) {
+ binding.projectName.setText(project.getName());
+ final int resourcesCount = project.getResources().size();
+ binding.resourcesCount.setText(itemView.getContext().getResources().getQuantityString(R.plurals.resources_count, resourcesCount, resourcesCount));
+ if (resourcesCount > 0) {
+ binding.getRoot().setOnClickListener(onClickListener);
+ } else {
+ binding.getRoot().setOnClickListener(null);
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java
new file mode 100644
index 000000000..4c95574c3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java
@@ -0,0 +1,54 @@
+package it.niedermann.nextcloud.deck.ui.card.projectresources;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.databinding.ItemProjectResourceBinding;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+
+public class CardProjectResourceAdapter extends RecyclerView.Adapter<CardProjectResourceViewHolder> {
+
+ @NonNull
+ private final EditCardViewModel viewModel;
+ @NonNull
+ private final List<OcsProjectResource> resources;
+ @NonNull
+ private final LifecycleOwner owner;
+
+ public CardProjectResourceAdapter(@NonNull EditCardViewModel viewModel, @NonNull List<OcsProjectResource> resources, @NonNull LifecycleOwner owner) {
+ this.viewModel = viewModel;
+ this.resources = new ArrayList<>(resources.size());
+ this.resources.addAll(resources);
+ this.owner = owner;
+ setHasStableIds(true);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return resources.get(position).getLocalId();
+ }
+
+ @NonNull
+ @Override
+ public CardProjectResourceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new CardProjectResourceViewHolder(ItemProjectResourceBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull CardProjectResourceViewHolder holder, int position) {
+ holder.bind(viewModel, resources.get(position), owner);
+ }
+
+ @Override
+ public int getItemCount() {
+ return this.resources.size();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java
new file mode 100644
index 000000000..272945e45
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java
@@ -0,0 +1,110 @@
+package it.niedermann.nextcloud.deck.ui.card.projectresources;
+
+import android.content.Intent;
+import android.content.res.Resources;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemProjectResourceBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.ui.card.EditActivity;
+import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+import it.niedermann.nextcloud.deck.util.ProjectUtil;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.util.ProjectUtil.getResourceUri;
+
+public class CardProjectResourceViewHolder extends RecyclerView.ViewHolder {
+ @NonNull
+ private final ItemProjectResourceBinding binding;
+
+ public CardProjectResourceViewHolder(@NonNull ItemProjectResourceBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull EditCardViewModel viewModel, @NonNull OcsProjectResource resource, @NonNull LifecycleOwner owner) {
+ final Account account = viewModel.getAccount();
+ final Resources resources = itemView.getResources();
+ binding.name.setText(resource.getName());
+ final @Nullable String link = resource.getLink();
+ binding.type.setVisibility(VISIBLE);
+ if (resource.getType() != null) {
+ switch (resource.getType()) {
+ case "deck": {
+ // TODO https://github.com/stefan-niedermann/nextcloud-deck/issues/671
+ linkifyViewHolder(account, link);
+ binding.type.setText(resources.getString(R.string.project_type_deck_board));
+ binding.image.setImageResource(R.drawable.project_deck_36dp);
+ break;
+ }
+ case "deck-card": {
+ try {
+ long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link);
+ if (ids.length == 2) {
+ viewModel.getCardByRemoteID(account.getId(), ids[1]).observe(owner, (fullCard) -> {
+ if (fullCard != null) {
+ viewModel.getBoardByRemoteId(account.getId(), ids[0]).observe(owner, (board) -> {
+ if (board != null) {
+ binding.getRoot().setOnClickListener((v) -> itemView.getContext().startActivity(EditActivity.createEditCardIntent(itemView.getContext(), account, board.getLocalId(), fullCard.getLocalId())));
+ } else {
+ linkifyViewHolder(account, link);
+ }
+ });
+ } else {
+ linkifyViewHolder(account, link);
+ }
+ });
+ } else {
+ linkifyViewHolder(account, link);
+ }
+ } catch (IllegalArgumentException e) {
+ DeckLog.logError(e);
+ linkifyViewHolder(account, link);
+ }
+ binding.type.setText(resources.getString(R.string.project_type_deck_card));
+ binding.image.setImageResource(R.drawable.project_deck_36dp);
+ break;
+ }
+ case "file": {
+ binding.type.setText(resources.getString(R.string.project_type_file));
+ linkifyViewHolder(account, link);
+ binding.image.setImageResource(R.drawable.project_file_36dp);
+ break;
+ }
+ case "room": {
+ binding.type.setText(resources.getString(R.string.project_type_room));
+ linkifyViewHolder(account, link);
+ binding.image.setImageResource(R.drawable.project_talk_36dp);
+ break;
+ }
+ default: {
+ DeckLog.info("Unknown resource type for " + resource.getName() + ": " + resource.getType());
+ binding.type.setVisibility(GONE);
+ linkifyViewHolder(account, link);
+ break;
+ }
+ }
+ } else {
+ DeckLog.warn("Resource type for " + resource.getName() + " is null");
+ binding.type.setVisibility(GONE);
+ }
+ }
+
+ private void linkifyViewHolder(@NonNull Account account, @Nullable String link) {
+ if (link != null) {
+ try {
+ binding.getRoot().setOnClickListener((v) -> itemView.getContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(getResourceUri(account, link))));
+ } catch (IllegalArgumentException e) {
+ DeckLog.logError(e);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java
new file mode 100644
index 000000000..46195b309
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java
@@ -0,0 +1,83 @@
+package it.niedermann.nextcloud.deck.ui.card.projectresources;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.DialogProjectResourcesBinding;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
+import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+
+public class CardProjectResourcesDialog extends BrandedDialogFragment {
+
+ private static final String KEY_RESOURCES = "resources";
+ private static final String KEY_PROJECT_NAME = "projectName";
+ private DialogProjectResourcesBinding binding;
+ private EditCardViewModel viewModel;
+
+ private String projectName;
+ @NonNull
+ private List<OcsProjectResource> resources = new ArrayList<>();
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ final Bundle args = requireArguments();
+ if (!args.containsKey(KEY_RESOURCES)) {
+ throw new IllegalArgumentException("Provide at least " + KEY_RESOURCES);
+ }
+ //noinspection unchecked
+ this.resources.addAll((ArrayList<OcsProjectResource>) Objects.requireNonNull(args.getSerializable(KEY_RESOURCES)));
+ this.projectName = args.getString(KEY_PROJECT_NAME);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ binding = DialogProjectResourcesBinding.inflate(LayoutInflater.from(requireContext()));
+ viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+
+ AlertDialog.Builder dialogBuilder = new BrandedAlertDialogBuilder(requireContext());
+
+ return dialogBuilder
+ .setTitle(projectName)
+ .setView(binding.getRoot())
+ .setNeutralButton(R.string.simple_close, null)
+ .create();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ final CardProjectResourceAdapter adapter = new CardProjectResourceAdapter(viewModel, resources, requireActivity());
+ binding.getRoot().setAdapter(adapter);
+ super.onActivityCreated(savedInstanceState);
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+
+ }
+
+ public static DialogFragment newInstance(@Nullable String projectName, @NonNull List<OcsProjectResource> resources) {
+ final DialogFragment fragment = new CardProjectResourcesDialog();
+ final Bundle args = new Bundle();
+ args.putString(KEY_PROJECT_NAME, projectName);
+ args.putSerializable(KEY_RESOURCES, new ArrayList<>(resources));
+ fragment.setArguments(args);
+ return fragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java
index 9eef878c3..ac6335b90 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java
@@ -8,13 +8,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
-import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.nextcloud.deck.BuildConfig;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ActivityExceptionBinding;
import it.niedermann.nextcloud.deck.ui.exception.tips.TipsAdapter;
-import it.niedermann.nextcloud.deck.util.ExceptionUtil;
-
-import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard;
+import it.niedermann.nextcloud.exception.ExceptionUtil;
public class ExceptionActivity extends AppCompatActivity {
@@ -22,9 +21,12 @@ public class ExceptionActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
final ActivityExceptionBinding binding = ActivityExceptionBinding.inflate(getLayoutInflater());
+
setContentView(binding.getRoot());
- super.onCreate(savedInstanceState);
+ setSupportActionBar(binding.toolbar);
Throwable throwable = ((Throwable) getIntent().getSerializableExtra(KEY_THROWABLE));
@@ -32,23 +34,18 @@ public class ExceptionActivity extends AppCompatActivity {
throwable = new Exception("Could not get exception");
}
- DeckLog.logError(throwable);
+ final TipsAdapter adapter = new TipsAdapter(this::startActivity);
+ final String debugInfo = "Full Crash:\n\n" + ExceptionUtil.INSTANCE.getDebugInfos(this, throwable, BuildConfig.FLAVOR);
- setSupportActionBar(binding.toolbar);
+ binding.tips.setAdapter(adapter);
+ binding.tips.setNestedScrollingEnabled(false);
binding.toolbar.setTitle(R.string.error);
binding.message.setText(throwable.getMessage());
-
- final String debugInfo = ExceptionUtil.getDebugInfos(this, throwable, null);
-
binding.stacktrace.setText(debugInfo);
+ binding.copy.setOnClickListener((v) -> ClipboardUtil.INSTANCE.copyToClipboard(this, getString(R.string.simple_exception), "```\n" + debugInfo + "\n```"));
+ binding.close.setOnClickListener((v) -> finish());
- final TipsAdapter adapter = new TipsAdapter(this::startActivity);
- binding.tips.setAdapter(adapter);
- binding.tips.setNestedScrollingEnabled(false);
adapter.setThrowable(this, null, throwable);
-
- binding.copy.setOnClickListener((v) -> copyToClipboard(this, getString(R.string.simple_exception), "```\n" + debugInfo + "\n```"));
- binding.close.setOnClickListener((v) -> finish());
}
@NonNull
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 6c0d0ba79..7a84ce0b6 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
@@ -11,14 +11,14 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.fragment.app.DialogFragment;
+import it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.nextcloud.deck.BuildConfig;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogExceptionBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.ui.exception.tips.TipsAdapter;
-import it.niedermann.nextcloud.deck.util.ExceptionUtil;
-
-import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard;
+import it.niedermann.nextcloud.exception.ExceptionUtil;
public class ExceptionDialogFragment extends AppCompatDialogFragment {
@@ -52,7 +52,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment {
final TipsAdapter adapter = new TipsAdapter((actionIntent) -> requireActivity().startActivity(actionIntent));
- final String debugInfos = ExceptionUtil.getDebugInfos(requireContext(), throwable, account);
+ final String debugInfos = ExceptionUtil.INSTANCE.getDebugInfos(requireContext(), throwable, BuildConfig.FLAVOR, account == null ? null : account.getServerDeckVersion());
binding.tips.setAdapter(adapter);
binding.stacktrace.setText(debugInfos);
@@ -65,7 +65,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment {
.setView(binding.getRoot())
.setTitle(R.string.error_dialog_title)
.setPositiveButton(android.R.string.copy, (a, b) -> {
- copyToClipboard(requireContext(), getString(R.string.simple_exception), "```\n" + debugInfos + "\n```");
+ ClipboardUtil.INSTANCE.copyToClipboard(requireContext(), getString(R.string.simple_exception), "```\n" + debugInfos + "\n```");
a.dismiss();
})
.setNegativeButton(R.string.simple_close, null)
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java
index 8f0bfce33..c62b23e51 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java
@@ -2,21 +2,24 @@ package it.niedermann.nextcloud.deck.ui.exception;
import android.app.Activity;
-import org.jetbrains.annotations.NotNull;
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.DeckLog;
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
- private Activity context;
+ @NonNull
+ private final Activity activity;
- public ExceptionHandler(Activity context) {
- super();
- this.context = context;
+ public ExceptionHandler(@NonNull Activity activity) {
+ this.activity = activity;
}
@Override
- public void uncaughtException(@NotNull Thread t, Throwable e) {
- context.getApplicationContext().startActivity(ExceptionActivity.createIntent(context, e));
- context.finish();
+ public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
+ DeckLog.logError(e);
+ activity.getApplicationContext().startActivity(ExceptionActivity.createIntent(activity, e));
+ activity.finish();
Runtime.getRuntime().exit(0);
}
}
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 a059b2956..6bfd82b13 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
@@ -39,6 +39,10 @@ import static it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment.
public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> {
+ private static final Intent INTENT_APP_INFO = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
+ .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info);
+
@NonNull
private Consumer<Intent> actionButtonClickedListener;
@NonNull
@@ -68,11 +72,8 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> {
public void setThrowable(@NonNull Context context, @Nullable Account account, @NonNull Throwable throwable) {
if (throwable instanceof TokenMismatchException) {
add(R.string.error_dialog_tip_token_mismatch_retry);
- add(R.string.error_dialog_tip_token_mismatch_clear_storage);
- Intent intent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
- .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
- .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info);
- add(R.string.error_dialog_tip_clear_storage, intent);
+ 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_tip_files_outdated);
} else if (throwable instanceof NextcloudApiNotRespondingException) {
@@ -122,6 +123,10 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> {
} else {
add(R.string.error_dialog_version_not_parsable);
}
+ add(R.string.error_dialog_account_might_not_be_authorized);
+ break;
+ case UNKNOWN_ACCOUNT_USER_ID:
+ add(R.string.error_dialog_user_not_found_in_database);
break;
case CAPABILITIES_NOT_PARSABLE:
default:
@@ -133,15 +138,15 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> {
add(R.string.error_dialog_capabilities_not_parsable);
}
}
+ // Files app might no longer be authenticated: https://github.com/stefan-niedermann/nextcloud-deck/issues/621#issuecomment-665533567
+ 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 RuntimeException) {
if (throwable.getMessage() != null && throwable.getMessage().contains("database")) {
Intent reportIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.url_report_bug)))
.putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_report_issue);
add(R.string.error_dialog_tip_database_upgrade_failed, reportIntent);
- Intent clearIntent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
- .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
- .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info);
- add(R.string.error_dialog_tip_clear_storage, clearIntent);
+ add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO);
}
}
}
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 aa6f59d04..6aa03b811 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
@@ -8,6 +8,7 @@ import android.os.Bundle;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@@ -19,6 +20,7 @@ import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayoutMediator;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogFilterBinding;
import it.niedermann.nextcloud.deck.model.enums.EDueType;
@@ -45,8 +47,9 @@ public class FilterDialogFragment extends BrandedDialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- indicator = getResources().getDrawable(R.drawable.circle_grey600_8dp);
- indicator.setColorFilter(getResources().getColor(R.color.primary), PorterDuff.Mode.SRC_ATOP);
+ indicator = ContextCompat.getDrawable(requireContext(), R.drawable.circle_grey600_8dp);
+ assert indicator != null;
+ indicator.setColorFilter(getResources().getColor(R.color.defaultBrand), PorterDuff.Mode.SRC_ATOP);
filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class);
@@ -61,10 +64,10 @@ public class FilterDialogFragment extends BrandedDialogFragment {
filterInformationDraft.observe(this, (draft) -> {
switch (position) {
case 0:
- tab.setIcon(draft.getLabels().size() > 0 ? indicator : null);
+ tab.setIcon(draft.getLabels().size() > 0 || draft.isNoAssignedLabel() ? indicator : null);
break;
case 1:
- tab.setIcon(draft.getUsers().size() > 0 ? indicator : null);
+ tab.setIcon(draft.getUsers().size() > 0 || draft.isNoAssignedUser() ? indicator : null);
break;
case 2:
tab.setIcon(draft.getDueType() != EDueType.NO_FILTER ? indicator : null);
@@ -103,9 +106,10 @@ public class FilterDialogFragment extends BrandedDialogFragment {
@Override
public void applyBrand(int mainColor) {
- @ColorInt int finalMainColor = getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor);
- binding.tabLayout.setSelectedTabIndicatorColor(finalMainColor);
- indicator.setColorFilter(finalMainColor, PorterDuff.Mode.SRC_ATOP);
+ @ColorInt final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(binding.tabLayout.getContext(), mainColor);
+ final boolean contrastRatioIsSufficient = ColorUtil.INSTANCE.getContrastRatio(mainColor, ContextCompat.getColor(binding.tabLayout.getContext(), R.color.primary)) > 1.7d;
+ binding.tabLayout.setSelectedTabIndicatorColor(contrastRatioIsSufficient ? mainColor : finalMainColor);
+ indicator.setColorFilter(contrastRatioIsSufficient ? mainColor : finalMainColor, PorterDuff.Mode.SRC_ATOP);
}
private static class TabsPagerAdapter extends FragmentStateAdapter {
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 096f0db9c..39fb791be 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,20 +1,21 @@
package it.niedermann.nextcloud.deck.ui.filter;
import android.content.res.ColorStateList;
-import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.ViewGroup;
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.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.util.ColorUtil;
@SuppressWarnings("WeakerAccess")
public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapter.LabelViewHolder> {
@@ -23,11 +24,17 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
@NonNull
private final List<Label> selectedLabels = new ArrayList<>();
@Nullable
+ private static final Label NOT_ASSIGNED = null;
+ @Nullable
private final SelectionListener<Label> selectionListener;
- public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, @Nullable SelectionListener<Label> selectionListener) {
+ public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, boolean noAssignedLabel, @Nullable SelectionListener<Label> selectionListener) {
super();
+ this.labels.add(NOT_ASSIGNED);
this.labels.addAll(labels);
+ if (noAssignedLabel) {
+ this.selectedLabels.add(NOT_ASSIGNED);
+ }
this.selectedLabels.addAll(selectedLabels);
this.selectionListener = selectionListener;
setHasStableIds(true);
@@ -36,7 +43,8 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
@Override
public long getItemId(int position) {
- return labels.get(position).getLocalId();
+ @Nullable final Label label = labels.get(position);
+ return label == null ? -1L : label.getLocalId();
}
@NonNull
@@ -47,7 +55,11 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
@Override
public void onBindViewHolder(@NonNull LabelViewHolder viewHolder, int position) {
- viewHolder.bind(labels.get(position));
+ if (position == 0) {
+ viewHolder.bindNotAssigned();
+ } else {
+ viewHolder.bind(labels.get(position));
+ }
}
@Override
@@ -55,26 +67,36 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
return labels.size();
}
- public List<Label> getSelected() {
- return selectedLabels;
- }
-
class LabelViewHolder extends RecyclerView.ViewHolder {
private ItemFilterLabelBinding binding;
LabelViewHolder(@NonNull ItemFilterLabelBinding binding) {
super(binding.getRoot());
this.binding = binding;
+ this.binding.label.setClickable(false);
}
void bind(final Label label) {
binding.label.setText(label.getTitle());
- final int labelColor = Color.parseColor("#" + label.getColor());
+ final int labelColor = label.getColor();
binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
- final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor);
+ final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
binding.label.setTextColor(color);
itemView.setSelected(selectedLabels.contains(label));
+ bindClickListener(label);
+ }
+
+ public void bindNotAssigned() {
+ binding.label.setText(itemView.getContext().getString(R.string.no_assigned_label));
+ binding.label.setTextColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.accent)));
+ binding.label.setChipIcon(ContextCompat.getDrawable(itemView.getContext(), R.drawable.ic_baseline_block_24));
+ binding.label.setChipBackgroundColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.primary)));
+ binding.label.setRippleColor(null);
+ itemView.setSelected(selectedLabels.contains(NOT_ASSIGNED));
+ bindClickListener(NOT_ASSIGNED);
+ }
+ private void bindClickListener(@Nullable Label label) {
itemView.setOnClickListener(view -> {
if (selectedLabels.contains(label)) {
selectedLabels.remove(label);
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 357f93cf9..e7d693185 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
@@ -12,7 +12,6 @@ import androidx.lifecycle.ViewModelProvider;
import it.niedermann.nextcloud.deck.databinding.DialogFilterLabelsBinding;
import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
@@ -31,21 +30,33 @@ public class FilterLabelsFragment extends Fragment implements SelectionListener<
filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class);
- observeOnce(new SyncManager(requireContext()).findProposalsForLabelsToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (labels) -> {
+ 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(), this));
+ binding.labels.setAdapter(new FilterLabelsAdapter(
+ labels,
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(),
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedLabel(),
+ this));
});
return binding.getRoot();
}
@Override
- public void onItemSelected(Label item) {
- filterViewModel.addFilterInformationDraftLabel(item);
+ public void onItemSelected(@Nullable Label item) {
+ if (item == null) {
+ filterViewModel.setNotAssignedLabel(true);
+ } else {
+ filterViewModel.addFilterInformationDraftLabel(item);
+ }
}
@Override
- public void onItemDeselected(Label item) {
- filterViewModel.removeFilterInformationLabel(item);
+ public void onItemDeselected(@Nullable Label item) {
+ if (item == null) {
+ filterViewModel.setNotAssignedLabel(false);
+ } else {
+ filterViewModel.removeFilterInformationLabel(item);
+ }
}
}
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 b4ae8f679..4b75b985f 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
@@ -8,6 +8,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+
import java.util.ArrayList;
import java.util.List;
@@ -23,6 +25,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
final int avatarSize;
@NonNull
private final Account account;
+ @Nullable
+ private static final User NOT_ASSIGNED = null;
@NonNull
private final List<User> users = new ArrayList<>();
@NonNull
@@ -30,11 +34,15 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
@Nullable
private final SelectionListener<User> selectionListener;
- public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, @Nullable SelectionListener selectionListener) {
+ public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, boolean noAssignedUser, @Nullable SelectionListener<User> selectionListener) {
super();
this.avatarSize = avatarSize;
this.account = account;
+ this.users.add(NOT_ASSIGNED);
this.users.addAll(users);
+ if (noAssignedUser) {
+ this.selectedUsers.add(NOT_ASSIGNED);
+ }
this.selectedUsers.addAll(selectedUsers);
this.selectionListener = selectionListener;
setHasStableIds(true);
@@ -43,7 +51,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
@Override
public long getItemId(int position) {
- return users.get(position).getLocalId();
+ @Nullable final User user = users.get(position);
+ return user == null ? -1L : user.getLocalId();
}
@NonNull
@@ -54,7 +63,11 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
@Override
public void onBindViewHolder(@NonNull UserViewHolder viewHolder, int position) {
- viewHolder.bind(users.get(position));
+ if (position == 0) {
+ viewHolder.bindNotAssigned();
+ } else {
+ viewHolder.bind(users.get(position));
+ }
}
@Override
@@ -62,10 +75,6 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
return users.size();
}
- public List<User> getSelected() {
- return selectedUsers;
- }
-
class UserViewHolder extends RecyclerView.ViewHolder {
private ItemFilterUserBinding binding;
@@ -74,22 +83,34 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
this.binding = binding;
}
- void bind(final User user) {
- binding.displayName.setText(user.getDisplayname());
+ 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);
itemView.setSelected(selectedUsers.contains(user));
+ bindClickListener(user);
+ }
+
+ public void bindNotAssigned() {
+ binding.title.setText(itemView.getContext().getString(R.string.simple_unassigned));
+ Glide.with(itemView.getContext())
+ .load(R.drawable.ic_baseline_block_24)
+ .into(binding.avatar);
+ itemView.setSelected(selectedUsers.contains(NOT_ASSIGNED));
+ bindClickListener(NOT_ASSIGNED);
+ }
+ private void bindClickListener(@Nullable User user) {
itemView.setOnClickListener(view -> {
if (selectedUsers.contains(user)) {
selectedUsers.remove(user);
itemView.setSelected(false);
- if(selectionListener != null) {
+ if (selectionListener != null) {
selectionListener.onItemDeselected(user);
}
} else {
selectedUsers.add(user);
itemView.setSelected(true);
- if(selectionListener != null) {
+ if (selectionListener != null) {
selectionListener.onItemSelected(user);
}
}
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 64bc1db1f..6ffaec6a6 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
@@ -10,14 +10,13 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogFilterAssigneesBinding;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
import static java.util.Objects.requireNonNull;
public class FilterUserFragment extends Fragment implements SelectionListener<User> {
@@ -33,21 +32,35 @@ public class FilterUserFragment extends Fragment implements SelectionListener<Us
filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class);
- observeOnce(new SyncManager(requireContext()).findProposalsForUsersToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (users) -> {
+ observeOnce(filterViewModel.findProposalsForUsersToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (users) -> {
binding.users.setNestedScrollingEnabled(false);
- binding.users.setAdapter(new FilterUserAdapter(dpToPx(requireContext(), R.dimen.avatar_size), mainViewModel.getCurrentAccount(), users, requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(), this));
+ 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));
});
return binding.getRoot();
}
@Override
- public void onItemSelected(User item) {
- filterViewModel.addFilterInformationUser(item);
+ public void onItemSelected(@Nullable User item) {
+ if (item == null) {
+ filterViewModel.setNotAssignedUser(true);
+ } else {
+ filterViewModel.addFilterInformationUser(item);
+ }
}
@Override
- public void onItemDeselected(User item) {
- filterViewModel.removeFilterInformationUser(item);
+ public void onItemDeselected(@Nullable User item) {
+ if (item == null) {
+ filterViewModel.setNotAssignedUser(false);
+ } else {
+ filterViewModel.removeFilterInformationUser(item);
+ }
}
}
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 cf8dc1754..c42a61ebd 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,28 +1,40 @@
package it.niedermann.nextcloud.deck.ui.filter;
+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 androidx.lifecycle.ViewModel;
+
+import java.util.List;
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 static it.niedermann.nextcloud.deck.model.internal.FilterInformation.hasActiveFilter;
@SuppressWarnings("WeakerAccess")
-public class FilterViewModel extends ViewModel {
+public class FilterViewModel extends AndroidViewModel {
+
+ private final SyncManager syncManager;
@IntRange(from = 0, to = 2)
private int currentFilterTab = 0;
@NonNull
- private MutableLiveData<FilterInformation> filterInformationDraft = new MutableLiveData<>(new FilterInformation());
+ private final MutableLiveData<FilterInformation> filterInformationDraft = new MutableLiveData<>(new FilterInformation());
@NonNull
- private MutableLiveData<FilterInformation> filterInformation = new MutableLiveData<>();
+ private final MutableLiveData<FilterInformation> filterInformation = new MutableLiveData<>();
+
+ public FilterViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
public void publishFilterInformationDraft() {
this.filterInformation.postValue(hasActiveFilter(filterInformationDraft.getValue()) ? filterInformationDraft.getValue() : null);
@@ -66,6 +78,18 @@ public class FilterViewModel extends ViewModel {
this.filterInformationDraft.postValue(newDraft);
}
+ public void setNotAssignedUser(boolean notAssignedUser) {
+ FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue());
+ newDraft.setNoAssignedUser(notAssignedUser);
+ this.filterInformationDraft.postValue(newDraft);
+ }
+
+ public void setNotAssignedLabel(boolean notAssignedLabel) {
+ FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue());
+ newDraft.setNoAssignedLabel(notAssignedLabel);
+ this.filterInformationDraft.postValue(newDraft);
+ }
+
public void removeFilterInformationLabel(@NonNull Label label) {
FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue());
newDraft.removeLabel(label);
@@ -86,4 +110,12 @@ public class FilterViewModel extends ViewModel {
public int getCurrentFilterTab() {
return this.currentFilterTab;
}
+
+ public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId) {
+ return syncManager.findProposalsForUsersToAssign(accountId, boardId, -1L, -1);
+ }
+
+ public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) {
+ return syncManager.findProposalsForLabelsToAssign(accountId, boardId, -1L);
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java
index d2635a860..3fad71773 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java
@@ -1,9 +1,11 @@
package it.niedermann.nextcloud.deck.ui.filter;
+import androidx.annotation.Nullable;
+
public interface SelectionListener<T> {
- void onItemSelected(T item);
+ void onItemSelected(@Nullable T item);
- default void onItemDeselected(T item) {
+ default void onItemDeselected(@Nullable T item) {
// Deselecting is optional
}
}
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 4b43cbed6..0892eb437 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
@@ -11,14 +11,14 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
-import it.niedermann.android.glidesso.SingleSignOnUrl;
+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 static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
@@ -33,7 +33,7 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
binding.accountName.setText(account.getUserName());
binding.accountHost.setText(Uri.parse(account.getUrl()).getHost());
Glide.with(itemView.getContext())
- .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))))
+ .load(new SingleSignOnUrl(account.getName(), 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())
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 9d273cdcb..8aa45e39a 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
@@ -5,15 +5,12 @@ import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
-
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import androidx.lifecycle.ViewModelProvider;
import it.niedermann.nextcloud.deck.databinding.ActivityManageAccountsBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId;
-import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
public class ManageAccountsActivity extends AppCompatActivity {
@@ -21,44 +18,37 @@ public class ManageAccountsActivity extends AppCompatActivity {
private static final String TAG = ManageAccountsActivity.class.getSimpleName();
private ActivityManageAccountsBinding binding;
+ private ManageAccountsViewModel viewModel;
private ManageAccountAdapter adapter;
- private SyncManager syncManager = null;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityManageAccountsBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
+ viewModel = new ViewModelProvider(this).get(ManageAccountsViewModel.class);
+ setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
- syncManager = new SyncManager(this);
-
- adapter = new ManageAccountAdapter((account) -> {
- SingleAccountHelper.setCurrentAccount(getApplicationContext(), account.getName());
- syncManager = new SyncManager(this);
- saveCurrentAccountId(this, account.getId());
- }, (accountPair) -> {
+ adapter = new ManageAccountAdapter((account) -> viewModel.setNewAccount(account), (accountPair) -> {
if (accountPair.first != null) {
- syncManager.deleteAccount(accountPair.first.getId());
+ viewModel.deleteAccount(accountPair.first.getId());
} else {
throw new IllegalArgumentException("Could not delete account because given account was null.");
}
Account newAccount = accountPair.second;
if (newAccount != null) {
- SingleAccountHelper.setCurrentAccount(getApplicationContext(), newAccount.getName());
- saveCurrentAccountId(this, newAccount.getId());
- syncManager = new SyncManager(this);
+ viewModel.setNewAccount(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(syncManager.readAccount(readCurrentAccountId(this)), this, (account -> {
+ observeOnce(viewModel.readAccount(readCurrentAccountId(this)), this, (account -> {
adapter.setCurrentAccount(account);
- syncManager.readAccounts().observe(this, (localAccounts -> {
+ viewModel.readAccounts().observe(this, (localAccounts -> {
if (localAccounts.size() == 0) {
Log.i(TAG, "No accounts, finishing " + ManageAccountsActivity.class.getSimpleName());
setResult(AppCompatActivity.RESULT_FIRST_USER);
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
new file mode 100644
index 000000000..66e9d3850
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java
@@ -0,0 +1,45 @@
+package it.niedermann.nextcloud.deck.ui.manageaccounts;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+
+import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId;
+
+@SuppressWarnings("WeakerAccess")
+public class ManageAccountsViewModel extends AndroidViewModel {
+
+ private SyncManager syncManager;
+
+ public ManageAccountsViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
+
+ public LiveData<Account> readAccount(long id) {
+ return syncManager.readAccount(id);
+ }
+
+ public LiveData<List<Account>> readAccounts() {
+ return syncManager.readAccounts();
+ }
+
+ public void setNewAccount(@NonNull Account account) {
+ SingleAccountHelper.setCurrentAccount(getApplication(), account.getName());
+ syncManager = new SyncManager(getApplication());
+ saveCurrentAccountId(getApplication(), account.getId());
+ }
+
+ public void deleteAccount(long id) {
+ syncManager.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
new file mode 100644
index 000000000..2b7eb52fe
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java
@@ -0,0 +1,128 @@
+package it.niedermann.nextcloud.deck.ui.movecard;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.DialogMoveCardBinding;
+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.branding.BrandedDialogFragment;
+import it.niedermann.nextcloud.deck.ui.branding.BrandingUtil;
+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 static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+public class MoveCardDialogFragment extends BrandedDialogFragment implements PickStackListener {
+
+ private static final String KEY_ORIGIN_ACCOUNT_ID = "account_id";
+ private static final String KEY_ORIGIN_BOARD_LOCAL_ID = "board_local_id";
+ private static final String KEY_ORIGIN_CARD_TITLE = "card_title";
+ private static final String KEY_ORIGIN_CARD_LOCAL_ID = "card_local_id";
+ private Long originAccountId;
+ private Long originBoardLocalId;
+ private String originCardTitle;
+ private Long originCardLocalId;
+
+ private DialogMoveCardBinding binding;
+ private PickStackViewModel viewModel;
+ private MoveCardListener moveCardListener;
+
+ private Account selectedAccount;
+ private Board selectedBoard;
+ private Stack selectedStack;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (getParentFragment() instanceof MoveCardListener) {
+ this.moveCardListener = (MoveCardListener) getParentFragment();
+ } else if (context instanceof MoveCardListener) {
+ this.moveCardListener = (MoveCardListener) context;
+ } else {
+ throw new IllegalArgumentException("Caller must implement " + MoveCardListener.class.getSimpleName());
+ }
+
+ final Bundle args = requireArguments();
+ originAccountId = args.getLong(KEY_ORIGIN_ACCOUNT_ID, -1L);
+ if (originAccountId < 0) {
+ throw new IllegalArgumentException("Missing " + KEY_ORIGIN_ACCOUNT_ID);
+ }
+ originCardLocalId = args.getLong(KEY_ORIGIN_CARD_LOCAL_ID, -1L);
+ if (originCardLocalId < 0) {
+ throw new IllegalArgumentException("Missing " + KEY_ORIGIN_CARD_LOCAL_ID);
+ }
+ originBoardLocalId = args.getLong(KEY_ORIGIN_BOARD_LOCAL_ID, -1L);
+ if (originBoardLocalId < 0) {
+ throw new IllegalArgumentException("Missing " + KEY_ORIGIN_BOARD_LOCAL_ID);
+ }
+ originCardTitle = args.getString(KEY_ORIGIN_CARD_TITLE);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ binding = DialogMoveCardBinding.inflate(inflater);
+ 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());
+ this.moveCardListener.move(originAccountId, originCardLocalId, selectedAccount.getId(), selectedBoard.getLocalId(), selectedStack.getLocalId());
+ dismiss();
+ });
+ binding.cancel.setOnClickListener((v) -> dismiss());
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ getChildFragmentManager()
+ .beginTransaction()
+ .add(R.id.fragment_container, PickStackFragment.newInstance(false))
+ .commit();
+ }
+
+ @Override
+ public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) {
+ this.selectedAccount = account;
+ this.selectedBoard = board;
+ this.selectedStack = stack;
+ if (board == null || stack == null) {
+ binding.submit.setEnabled(false);
+ binding.moveWarning.setVisibility(GONE);
+ } else {
+ binding.submit.setEnabled(true);
+ binding.moveWarning.setVisibility(board.getLocalId().equals(originBoardLocalId) ? GONE : VISIBLE);
+ }
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ final ColorStateList mainColorStateList = ColorStateList.valueOf(BrandingUtil.getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor));
+ binding.cancel.setTextColor(mainColorStateList);
+ binding.submit.setTextColor(mainColorStateList);
+ }
+
+ public static DialogFragment newInstance(long originAccountId, long originBoardLocalId, String originCardTitle, Long originCardLocalId) {
+ final DialogFragment dialogFragment = new MoveCardDialogFragment();
+ final Bundle args = new Bundle();
+ args.putLong(KEY_ORIGIN_ACCOUNT_ID, originAccountId);
+ args.putLong(KEY_ORIGIN_BOARD_LOCAL_ID, originBoardLocalId);
+ args.putString(KEY_ORIGIN_CARD_TITLE, originCardTitle);
+ args.putLong(KEY_ORIGIN_CARD_LOCAL_ID, originCardLocalId);
+ dialogFragment.setArguments(args);
+ return dialogFragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java
new file mode 100644
index 000000000..f6f7a7a1f
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java
@@ -0,0 +1,5 @@
+package it.niedermann.nextcloud.deck.ui.movecard;
+
+public interface MoveCardListener {
+ void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId);
+}
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
new file mode 100644
index 000000000..d65971cf4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java
@@ -0,0 +1,204 @@
+package it.niedermann.nextcloud.deck.ui.pickstack;
+
+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 android.widget.ArrayAdapter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.List;
+
+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.SelectedListener;
+import it.niedermann.nextcloud.deck.ui.preparecreate.StackAdapter;
+
+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;
+
+public class PickStackFragment extends Fragment {
+
+ private FragmentPickStackBinding binding;
+ private PickStackViewModel viewModel;
+
+ private static final String KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION = "show_boards_without_edit_permission";
+
+ 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;
+
+ @Nullable
+ private LiveData<List<Board>> boardsLiveData;
+ @NonNull
+ private 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 (Board 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 Observer<List<Stack>> stacksObserver = (stacks) -> {
+ stackAdapter.clear();
+ stackAdapter.addAll(stacks);
+
+ if (stacks.size() > 0) {
+ binding.stackSelect.setEnabled(true);
+
+ Stack stackToSelect = null;
+ for (Stack 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);
+ }
+ };
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (getParentFragment() instanceof PickStackListener) {
+ this.pickStackListener = (PickStackListener) getParentFragment();
+ } else if (context instanceof PickStackListener) {
+ this.pickStackListener = (PickStackListener) context;
+ } else {
+ throw new IllegalArgumentException("Caller must implement " + PickStackListener.class.getSimpleName());
+ }
+ final Bundle args = getArguments();
+ if (args != null) {
+ this.showBoardsWithoutEditPermission = args.getBoolean(KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION, false);
+ }
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ binding = FragmentPickStackBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(requireActivity()).get(PickStackViewModel.class);
+
+ accountAdapter = new AccountAdapter(requireContext());
+ binding.accountSelect.setAdapter(accountAdapter);
+ 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 {
+ startActivityForResult(new Intent(requireActivity(), ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
+ 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);
+
+ for (Account account : accounts) {
+ if (account.getId() == lastAccountId) {
+ binding.accountSelect.setSelection(accountAdapter.getPosition(account));
+ break;
+ }
+ }
+ });
+
+ binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
+ updateLiveDataSource(boardsLiveData, boardsObserver, showBoardsWithoutEditPermission
+ ? viewModel.getBoards(parent.getSelectedItemId())
+ : viewModel.getBoardsWithEditPermission(parent.getSelectedItemId()));
+ });
+
+ binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
+ updateLiveDataSource(stacksLiveData, stacksObserver, viewModel.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId()));
+ });
+
+ binding.stackSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
+ pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), (Stack) parent.getSelectedItem());
+ });
+
+ return binding.getRoot();
+ }
+
+ /**
+ * 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);
+ }
+
+ public static PickStackFragment newInstance(boolean showBoardsWithoutEditPermission) {
+ final PickStackFragment fragment = new PickStackFragment();
+ final Bundle args = new Bundle();
+ args.putBoolean(KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION, showBoardsWithoutEditPermission);
+ fragment.setArguments(args);
+ return fragment;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java
new file mode 100644
index 000000000..ce227746a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java
@@ -0,0 +1,12 @@
+package it.niedermann.nextcloud.deck.ui.pickstack;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Stack;
+
+public interface PickStackListener {
+ void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack);
+}
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
new file mode 100644
index 000000000..cc9fc2259
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java
@@ -0,0 +1,45 @@
+package it.niedermann.nextcloud.deck.ui.pickstack;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+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;
+
+@SuppressWarnings("WeakerAccess")
+public class PickStackViewModel extends AndroidViewModel {
+
+ private final SyncManager syncManager;
+
+ public PickStackViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
+
+ public LiveData<Boolean> hasAccounts() {
+ return syncManager.hasAccounts();
+ }
+
+ public LiveData<List<Account>> readAccounts() {
+ return syncManager.readAccounts();
+ }
+
+ public LiveData<List<Board>> getBoards(long accountId) {
+ return syncManager.getBoards(accountId);
+ }
+
+ public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) {
+ return syncManager.getBoardsWithEditPermission(accountId);
+ }
+
+ public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
+ return syncManager.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 35a98efd7..f537c9fb4 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
@@ -6,14 +6,17 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import org.jetbrains.annotations.NotNull;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import java.net.URL;
+
+import it.niedermann.android.util.DimensionUtil;
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.deck.util.DimensionUtil;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
public class AccountAdapter extends AbstractAdapter<Account> {
@@ -27,9 +30,9 @@ public class AccountAdapter extends AbstractAdapter<Account> {
return item.getId();
}
- @NotNull
+ @NonNull
@Override
- public View getView(int position, View convertView, @NotNull ViewGroup parent) {
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
final ItemPrepareCreateAccountBinding binding;
if (convertView == null) {
binding = ItemPrepareCreateAccountBinding.inflate(inflater, parent, false);
@@ -40,8 +43,18 @@ public class AccountAdapter extends AbstractAdapter<Account> {
final Account item = getItem(position);
if (item != null) {
binding.username.setText(item.getUserName());
- binding.instance.setText(item.getUrl());
- ViewUtil.addAvatar(binding.avatar, item.getUrl(), item.getUserName(), DimensionUtil.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp);
+ try {
+ binding.instance.setText(new URL(item.getUrl()).getHost());
+ } catch (Throwable t) {
+ binding.instance.setText(item.getUrl());
+ }
+
+ Glide.with(getContext())
+ .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details))))
+ .placeholder(R.drawable.ic_baseline_account_circle_24)
+ .error(R.drawable.ic_baseline_account_circle_24)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
} else {
DeckLog.logError(new IllegalArgumentException("No item for position " + position));
}
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 190c55e4b..c27fa04ee 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
@@ -6,8 +6,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import org.jetbrains.annotations.NotNull;
-
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateBoardBinding;
@@ -26,9 +24,9 @@ public class BoardAdapter extends AbstractAdapter<Board> {
return item.getLocalId();
}
- @NotNull
+ @NonNull
@Override
- public View getView(int position, View convertView, @NotNull ViewGroup parent) {
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
final ItemPrepareCreateBoardBinding binding;
if (convertView == null) {
binding = ItemPrepareCreateBoardBinding.inflate(inflater, parent, false);
@@ -39,7 +37,7 @@ public class BoardAdapter extends AbstractAdapter<Board> {
final Board item = getItem(position);
if (item != null) {
binding.boardTitle.setText(item.getTitle());
- binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, "#" + item.getColor()));
+ binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, item.getColor()));
} else {
DeckLog.logError(new IllegalArgumentException("No item for position " + position));
}
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 ab0cc4816..18317e078 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
@@ -2,214 +2,52 @@ package it.niedermann.nextcloud.deck.ui.preparecreate;
import android.content.ClipData;
import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
-import android.widget.ArrayAdapter;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Observer;
+import androidx.appcompat.app.ActionBar;
-import java.util.List;
-
-import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.databinding.ActivityPrepareCreateBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.model.Board;
-import it.niedermann.nextcloud.deck.model.full.FullStack;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.ImportAccountActivity;
-import it.niedermann.nextcloud.deck.ui.branding.Branded;
+import it.niedermann.nextcloud.deck.ui.PickStackActivity;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
-import static android.graphics.Color.parseColor;
-import static androidx.lifecycle.Transformations.switchMap;
-import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
-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.saveCurrentAccountId;
import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentBoardId;
import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentStackId;
-import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
-import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas;
-
-public class PrepareCreateActivity extends AppCompatActivity implements Branded {
-
- private ActivityPrepareCreateBinding binding;
-
- private SyncManager syncManager;
-
- private boolean brandingEnabled;
-
- private long lastAccountId;
- private long lastBoardId;
- private long lastStackId;
- private ArrayAdapter<Account> accountAdapter;
- private ArrayAdapter<Board> boardAdapter;
- private ArrayAdapter<FullStack> stackAdapter;
-
- @Nullable
- private LiveData<List<Board>> boardsLiveData;
- @NonNull
- private Observer<List<Board>> boardsObserver = (boards) -> {
- boardAdapter.clear();
- boardAdapter.addAll(boards);
- binding.boardSelect.setEnabled(true);
-
- if (boards.size() > 0) {
- binding.boardSelect.setEnabled(true);
-
- for (Board board : boards) {
- if (board.getLocalId() == lastBoardId) {
- binding.boardSelect.setSelection(boardAdapter.getPosition(board));
- applyBrand(Color.parseColor('#' + board.getColor()));
- break;
- }
- }
- } else {
- applyBrand(ContextCompat.getColor(this, R.color.defaultBrand));
- binding.boardSelect.setEnabled(false);
- binding.submit.setEnabled(false);
- }
- };
-
- @Nullable
- private LiveData<List<FullStack>> stacksLiveData;
- @NonNull
- private Observer<List<FullStack>> stacksObserver = (fullStacks) -> {
- stackAdapter.clear();
- stackAdapter.addAll(fullStacks);
-
- if (fullStacks.size() > 0) {
- binding.stackSelect.setEnabled(true);
- binding.submit.setEnabled(true);
-
- for (FullStack fullStack : fullStacks) {
- if (fullStack.getLocalId() == lastStackId) {
- binding.stackSelect.setSelection(stackAdapter.getPosition(fullStack));
- break;
- }
- }
- } else {
- binding.stackSelect.setEnabled(false);
- binding.submit.setEnabled(false);
- }
- };
+public class PrepareCreateActivity extends PickStackActivity {
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
- Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this));
-
- brandingEnabled = isBrandingEnabled(this);
-
- binding = ActivityPrepareCreateBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
- setSupportActionBar(binding.toolbar);
-
- accountAdapter = new AccountAdapter(this);
- binding.accountSelect.setAdapter(accountAdapter);
- binding.accountSelect.setEnabled(false);
- boardAdapter = new BoardAdapter(this);
- binding.boardSelect.setAdapter(boardAdapter);
- binding.stackSelect.setEnabled(false);
- stackAdapter = new StackAdapter(this);
- binding.stackSelect.setAdapter(stackAdapter);
- binding.stackSelect.setEnabled(false);
-
- syncManager = new SyncManager(this);
-
- switchMap(syncManager.hasAccounts(), hasAccounts -> {
- if (hasAccounts) {
- return syncManager.readAccounts();
- } else {
- startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
- 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");
- }
-
- lastAccountId = readCurrentAccountId(this);
- lastBoardId = readCurrentBoardId(this, lastAccountId);
- lastStackId = readCurrentStackId(this, lastAccountId, lastBoardId);
-
- accountAdapter.clear();
- accountAdapter.addAll(accounts);
- binding.accountSelect.setEnabled(true);
-
- for (Account account : accounts) {
- if (account.getId() == lastAccountId) {
- binding.accountSelect.setSelection(accountAdapter.getPosition(account));
- break;
- }
- }
- });
-
- binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
- updateLiveDataSource(boardsLiveData, boardsObserver, syncManager.getBoardsWithEditPermission(parent.getSelectedItemId()));
- });
-
- binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
- applyBrand(Color.parseColor('#' + ((Board) binding.boardSelect.getSelectedItem()).getColor()));
- updateLiveDataSource(stacksLiveData, stacksObserver, syncManager.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId()));
- });
-
- binding.cancel.setOnClickListener((v) -> finish());
- binding.submit.setOnClickListener((v) -> onSubmit());
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.add_card);
+ }
}
- /**
- * 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);
+ @Override
+ protected void onSubmit(Account account, long boardId, long stackId) {
+ final String receivedClipData = getReceivedClipData(getIntent());
+ if (receivedClipData == null) {
+ startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId));
+ } else {
+ startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, receivedClipData));
}
- liveData = newSource;
- liveData.observe(PrepareCreateActivity.this, observer);
- }
- /**
- * Starts EditActivity and passes parameters.
- */
- private void onSubmit() {
- final Account account = accountAdapter.getItem(binding.accountSelect.getSelectedItemPosition());
- if (account != null) {
- final long boardId = binding.boardSelect.getSelectedItemId();
- final long stackId = binding.stackSelect.getSelectedItemId();
- final String receivedClipData = getReceivedClipData(getIntent());
- if (receivedClipData == null) {
- startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId));
- } else {
- startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, receivedClipData));
- }
+ saveCurrentAccountId(this, account.getId());
+ saveCurrentBoardId(this, account.getId(), boardId);
+ saveCurrentStackId(this, account.getId(), boardId, stackId);
+ applyBrand(account.getColor());
- saveCurrentAccountId(this, account.getId());
- saveCurrentBoardId(this, account.getId(), boardId);
- saveCurrentStackId(this, account.getId(), boardId, stackId);
- applyBrand(parseColor(account.getColor()));
+ finish();
+ }
- finish();
- } else {
- ExceptionDialogFragment.newInstance(new IllegalStateException("Selected account at position " + binding.accountSelect.getSelectedItemPosition() + " is null."), null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
+ @Override
+ protected boolean showBoardsWithoutEditPermission() {
+ return false;
}
@Nullable
@@ -232,20 +70,4 @@ public class PrepareCreateActivity extends AppCompatActivity implements Branded
final CharSequence text = item.getText();
return TextUtils.isEmpty(text) ? null : text.toString();
}
-
- @Override
- public void applyBrand(int mainColor) {
- try {
- if (brandingEnabled) {
- @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(this, R.color.primary))
- ? mainColor
- : isDarkTheme(this) ? Color.WHITE : Color.BLACK;
- DrawableCompat.setTintList(binding.submit.getBackground(), ColorStateList.valueOf(finalMainColor));
- binding.submit.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor));
- binding.cancel.setTextColor(getSecondaryForegroundColorDependingOnTheme(this, mainColor));
- }
- } catch (Throwable t) {
- DeckLog.logError(t);
- }
- }
} \ No newline at end of file
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
index 89c702075..e3e0ad5b5 100644
--- 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
@@ -6,14 +6,12 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import org.jetbrains.annotations.NotNull;
-
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.full.FullStack;
+import it.niedermann.nextcloud.deck.model.Stack;
-public class StackAdapter extends AbstractAdapter<FullStack> {
+public class StackAdapter extends AbstractAdapter<Stack> {
@SuppressWarnings("WeakerAccess")
public StackAdapter(@NonNull Context context) {
@@ -21,13 +19,13 @@ public class StackAdapter extends AbstractAdapter<FullStack> {
}
@Override
- protected long getItemId(@NonNull FullStack item) {
+ protected long getItemId(@NonNull Stack item) {
return item.getLocalId();
}
- @NotNull
+ @NonNull
@Override
- public View getView(int position, View convertView, @NotNull ViewGroup parent) {
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
final ItemPrepareCreateStackBinding binding;
if (convertView == null) {
binding = ItemPrepareCreateStackBinding.inflate(inflater, parent, false);
@@ -35,9 +33,9 @@ public class StackAdapter extends AbstractAdapter<FullStack> {
binding = ItemPrepareCreateStackBinding.bind(convertView);
}
- final FullStack item = getItem(position);
+ final Stack item = getItem(position);
if (item != null) {
- binding.stackTitle.setText(item.getStack().getTitle());
+ binding.stackTitle.setText(item.getTitle());
} else {
DeckLog.logError(new IllegalArgumentException("No item for position " + position));
}
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 ea8d90f22..04429f225 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
@@ -23,6 +23,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande
private BrandedSwitchPreference wifiOnlyPref;
private BrandedSwitchPreference themePref;
private BrandedSwitchPreference brandingPref;
+ private BrandedSwitchPreference compactPref;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@@ -67,6 +68,8 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande
DeckLog.error("Could not find preference with key: \"" + getString(R.string.pref_key_dark_theme) + "\"");
}
+ compactPref = findPreference(getString(R.string.pref_key_compact));
+
final ListPreference backgroundSyncPref = findPreference(getString(R.string.pref_key_background_sync));
if (backgroundSyncPref != null) {
backgroundSyncPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> {
@@ -92,5 +95,6 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande
wifiOnlyPref.applyBrand(mainColor);
themePref.applyBrand(mainColor);
brandingPref.applyBrand(mainColor);
+ compactPref.applyBrand(mainColor);
}
}
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 271b60489..3028ea952 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
@@ -14,15 +14,16 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
+import it.niedermann.nextcloud.deck.BuildConfig;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogShareProgressBinding;
import it.niedermann.nextcloud.deck.exceptions.UploadAttachmentFailedException;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+import it.niedermann.nextcloud.exception.ExceptionUtil;
import static android.graphics.PorterDuff.Mode;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
-import static it.niedermann.nextcloud.deck.util.ExceptionUtil.getDebugInfos;
public class ShareProgressDialogFragment extends BrandedDialogFragment {
@@ -70,7 +71,7 @@ public class ShareProgressDialogFragment extends BrandedDialogFragment {
binding.errorReportButton.setOnClickListener((v) -> {
final StringBuilder debugInfos = new StringBuilder(exceptionsCount + " attachments failed to upload:");
for (Throwable t : exceptions) {
- debugInfos.append(getDebugInfos(requireContext(), t, null));
+ debugInfos.append(ExceptionUtil.INSTANCE.getDebugInfos(requireContext(), t, BuildConfig.FLAVOR));
}
ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException(debugInfos.toString()), null)
.show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
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 a64629bd7..3a0005fb1 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
@@ -16,9 +16,9 @@ import androidx.lifecycle.ViewModelProvider;
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.Date;
import java.util.List;
import it.niedermann.nextcloud.deck.DeckLog;
@@ -126,7 +126,7 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
throw new IllegalArgumentException("MimeType of uri is null. [" + uri + "]");
}
runOnUiThread(() -> {
- final WrappedLiveData<Attachment> liveData = syncManager.addAttachmentToCard(fullCard.getAccountId(), fullCard.getCard().getLocalId(), mimeType, tempFile);
+ final WrappedLiveData<Attachment> liveData = mainViewModel.addAttachmentToCard(fullCard.getAccountId(), fullCard.getCard().getLocalId(), mimeType, tempFile);
liveData.observe(ShareTargetActivity.this, (next) -> {
if (liveData.hasError()) {
if (liveData.getError() instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) liveData.getError()).getStatusCode() == HTTP_CONFLICT) {
@@ -160,7 +160,7 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
? receivedText
: oldDescription + "\n\n" + receivedText
);
- WrappedLiveData<FullCard> liveData = syncManager.updateCard(fullCard);
+ WrappedLiveData<FullCard> liveData = mainViewModel.updateCard(fullCard);
observeOnce(liveData, this, (next) -> {
if (liveData.hasError()) {
cardSelected = false;
@@ -173,8 +173,8 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
break;
case 1:
final Account currentAccount = mainViewModel.getCurrentAccount();
- final DeckComment comment = new DeckComment(receivedText.trim(), currentAccount.getUserName(), new Date());
- syncManager.addCommentToCard(currentAccount.getId(), fullCard.getLocalId(), comment);
+ final DeckComment comment = new DeckComment(receivedText.trim(), currentAccount.getUserName(), Instant.now());
+ mainViewModel.addCommentToCard(currentAccount.getId(), fullCard.getLocalId(), comment);
Toast.makeText(getApplicationContext(), getString(R.string.share_success, "\"" + receivedText + "\"", "\"" + fullCard.getCard().getTitle() + "\""), Toast.LENGTH_LONG).show();
finish();
break;
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 e2cc83372..8fc56759f 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
@@ -8,11 +8,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.ArrayList;
import java.util.List;
-import it.niedermann.nextcloud.deck.model.full.FullStack;
+import it.niedermann.nextcloud.deck.model.Stack;
public class StackAdapter extends FragmentStateAdapter {
@NonNull
- private List<FullStack> stackList = new ArrayList<>();
+ private final List<Stack> stackList = new ArrayList<>();
public StackAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
@@ -23,7 +23,7 @@ public class StackAdapter extends FragmentStateAdapter {
return stackList.size();
}
- public FullStack getItem(int position) {
+ public Stack getItem(int position) {
return stackList.get(position);
}
@@ -34,7 +34,7 @@ public class StackAdapter extends FragmentStateAdapter {
@Override
public boolean containsItem(long itemId) {
- for (FullStack stack : stackList) {
+ for (Stack stack : stackList) {
if (stack.getLocalId() == itemId) {
return true;
}
@@ -48,9 +48,9 @@ public class StackAdapter extends FragmentStateAdapter {
return StackFragment.newInstance(stackList.get(position).getLocalId());
}
- public void setStacks(@NonNull List<FullStack> fullStacks) {
+ public void setStacks(@NonNull List<Stack> stacks) {
this.stackList.clear();
- this.stackList.addAll(fullStacks);
+ this.stackList.addAll(stacks);
notifyDataSetChanged();
}
} \ 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 726a74184..313e6e821 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
@@ -20,20 +20,27 @@ import java.util.List;
import it.niedermann.android.crosstabdnd.DragAndDropTab;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.databinding.FragmentStackBinding;
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.Stack;
import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
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.MoveCardListener;
-public class StackFragment extends BrandedFragment implements DragAndDropTab<CardAdapter> {
+import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
+
+public class StackFragment extends BrandedFragment implements DragAndDropTab<CardAdapter>, MoveCardListener {
private static final String KEY_STACK_ID = "stackId";
private FragmentStackBinding binding;
- private SyncManager syncManager;
+ private MainViewModel mainViewModel;
private FragmentActivity activity;
private OnScrollListener onScrollListener;
@@ -61,10 +68,10 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- binding = FragmentStackBinding.inflate(inflater, container, false);
activity = requireActivity();
+ binding = FragmentStackBinding.inflate(inflater, container, false);
+ mainViewModel = new ViewModelProvider(activity).get(MainViewModel.class);
- final MainViewModel mainViewModel = new ViewModelProvider(activity).get(MainViewModel.class);
final FilterViewModel filterViewModel = new ViewModelProvider(activity).get(FilterViewModel.class);
// This might be a zombie fragment with an empty MainViewModel after Android killed the activity (but not the fragment instance
@@ -74,19 +81,10 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car
return binding.getRoot();
}
- syncManager = new SyncManager(activity);
-
- adapter = new CardAdapter(
- requireContext(),
- getChildFragmentManager(),
- mainViewModel.getCurrentAccount(),
- mainViewModel.getCurrentBoardLocalId(),
- mainViewModel.getCurrentBoardRemoteId(),
- stackId,
- mainViewModel.currentBoardHasEditPermission(),
- syncManager,
- this,
- (requireActivity() instanceof SelectCardListener) ? (SelectCardListener) requireActivity() : null);
+ adapter = new CardAdapter(requireContext(), getChildFragmentManager(), stackId, mainViewModel, this,
+ (requireActivity() instanceof SelectCardListener)
+ ? (SelectCardListener) requireActivity()
+ : null);
binding.recyclerView.setAdapter(adapter);
if (onScrollListener != null) {
@@ -114,12 +112,12 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car
}
});
- cardsLiveData = syncManager.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterViewModel.getFilterInformation().getValue());
+ cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterViewModel.getFilterInformation().getValue());
cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver);
filterViewModel.getFilterInformation().observe(getViewLifecycleOwner(), (filterInformation -> {
cardsLiveData.removeObserver(cardsObserver);
- cardsLiveData = syncManager.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterInformation);
+ cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterInformation);
cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver);
}));
@@ -153,4 +151,17 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car
return fragment;
}
+
+ @Override
+ public void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) {
+ final WrappedLiveData<Void> liveData = mainViewModel.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId);
+ observeOnce(liveData, requireActivity(), (next) -> {
+ if (liveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(liveData.getError())) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), null).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ } else {
+ DeckLog.log("Moved " + Card.class.getSimpleName() + " \"" + originCardLocalId + "\" to " + Stack.class.getSimpleName() + " \"" + targetStackLocalId + "\"");
+ }
+ });
+ }
+
} \ No newline at end of file
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
new file mode 100644
index 000000000..af17464dc
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java
@@ -0,0 +1,182 @@
+package it.niedermann.nextcloud.deck.ui.takephoto;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Size;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Camera;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.ExecutionException;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.databinding.ActivityTakePhotoBinding;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
+
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.IMAGE_JPEG;
+
+@RequiresApi(LOLLIPOP)
+public class TakePhotoActivity extends BrandedActivity {
+
+ private ActivityTakePhotoBinding binding;
+ private TakePhotoViewModel viewModel;
+
+ private View[] brandedViews;
+
+ private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
+ private OrientationEventListener orientationEventListener;
+
+ private final DateTimeFormatter fileNameFromCameraFormatter = DateTimeFormatter.ofPattern("'JPG_'yyyyMMdd'_'HHmmss'.jpg'");
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+
+ binding = ActivityTakePhotoBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(TakePhotoViewModel.class);
+
+ setContentView(binding.getRoot());
+
+ cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(() -> {
+ try {
+ final ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ final Preview previewUseCase = getPreviewUseCase();
+ final ImageCapture captureUseCase = getCaptureUseCase();
+ final Camera camera = cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), captureUseCase, previewUseCase);
+
+ viewModel.getCameraSelectorToggleButtonImageResource().observe(this, res -> binding.switchCamera.setImageDrawable(ContextCompat.getDrawable(this, res)));
+ viewModel.getTorchToggleButtonImageResource().observe(this, res -> binding.toggleTorch.setImageDrawable(ContextCompat.getDrawable(this, res)));
+ viewModel.isTorchEnabled().observe(this, enabled -> camera.getCameraControl().enableTorch(enabled));
+
+ binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled());
+ binding.switchCamera.setOnClickListener((v) -> {
+ viewModel.toggleCameraSelector();
+ cameraProvider.unbindAll();
+ cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), captureUseCase, previewUseCase);
+ });
+ } catch (ExecutionException | InterruptedException e) {
+ DeckLog.logError(e);
+ finish();
+ }
+ }, ContextCompat.getMainExecutor(this));
+
+ brandedViews = new View[]{binding.takePhoto, binding.switchCamera, binding.toggleTorch};
+ }
+
+ private ImageCapture getCaptureUseCase() {
+ final ImageCapture captureUseCase = new ImageCapture.Builder().setTargetResolution(new Size(720, 1280)).build();
+
+ orientationEventListener = new OrientationEventListener(this) {
+ @Override
+ public void onOrientationChanged(int orientation) {
+ int rotation;
+
+ // Monitors orientation values to determine the target rotation value
+ if (orientation >= 45 && orientation < 135) {
+ rotation = Surface.ROTATION_270;
+ } else if (orientation >= 135 && orientation < 225) {
+ rotation = Surface.ROTATION_180;
+ } else if (orientation >= 225 && orientation < 315) {
+ rotation = Surface.ROTATION_90;
+ } else {
+ rotation = Surface.ROTATION_0;
+ }
+
+ captureUseCase.setTargetRotation(rotation);
+ }
+ };
+ orientationEventListener.enable();
+
+ binding.takePhoto.setOnClickListener((v) -> {
+ binding.takePhoto.setEnabled(false);
+ final String photoFileName = Instant.now().atZone(ZoneId.systemDefault()).format(fileNameFromCameraFormatter);
+ try {
+ final File photoFile = AttachmentUtil.getTempCacheFile(this, "photos/" + photoFileName);
+ final ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(photoFile).build();
+ captureUseCase.takePicture(options, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
+ final Uri savedUri = Uri.fromFile(photoFile);
+ DeckLog.info("onImageSaved - savedUri: " + savedUri.toString());
+ setResult(RESULT_OK, new Intent().setDataAndType(savedUri, IMAGE_JPEG));
+ finish();
+ }
+
+ @Override
+ public void onError(@NonNull ImageCaptureException e) {
+ e.printStackTrace();
+ //noinspection ResultOfMethodCallIgnored
+ photoFile.delete();
+ binding.takePhoto.setEnabled(true);
+ }
+ });
+ } catch (Exception e) {
+ ExceptionDialogFragment.newInstance(e, null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+
+ return captureUseCase;
+ }
+
+ private Preview getPreviewUseCase() {
+ Preview previewUseCase = new Preview.Builder().build();
+ previewUseCase.setSurfaceProvider(binding.preview.getSurfaceProvider());
+ return previewUseCase;
+ }
+
+ @Override
+ protected void onPause() {
+ if (this.orientationEventListener != null) {
+ this.orientationEventListener.disable();
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (this.orientationEventListener != null) {
+ this.orientationEventListener.enable();
+ }
+ }
+
+ @RequiresApi(LOLLIPOP)
+ public static Intent createIntent(@NonNull Context context) {
+ return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ final ColorStateList colorStateList = ColorStateList.valueOf(mainColor);
+ for (View v : brandedViews) {
+ v.setBackgroundTintList(colorStateList);
+ }
+ }
+}
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
new file mode 100644
index 000000000..a71291ff2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java
@@ -0,0 +1,57 @@
+package it.niedermann.nextcloud.deck.ui.takephoto;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraSelector;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+
+import it.niedermann.nextcloud.deck.R;
+
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+import static androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA;
+
+public class TakePhotoViewModel extends ViewModel {
+
+ @NonNull
+ private CameraSelector cameraSelector = DEFAULT_BACK_CAMERA;
+ @NonNull
+ private final MutableLiveData<Integer> cameraSelectorToggleButtonImageResource = new MutableLiveData<>(R.drawable.ic_baseline_camera_front_24);
+ @NonNull
+ private final MutableLiveData<Boolean> torchEnabled = new MutableLiveData<>(false);
+
+ @NonNull
+ public CameraSelector getCameraSelector() {
+ return this.cameraSelector;
+ }
+
+ public LiveData<Integer> getCameraSelectorToggleButtonImageResource() {
+ return this.cameraSelectorToggleButtonImageResource;
+ }
+
+ public void toggleCameraSelector() {
+ if (this.cameraSelector == DEFAULT_BACK_CAMERA) {
+ this.cameraSelector = DEFAULT_FRONT_CAMERA;
+ this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_rear_24);
+ } else {
+ this.cameraSelector = DEFAULT_BACK_CAMERA;
+ this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_front_24);
+ }
+ }
+
+ public void toggleTorchEnabled() {
+ //noinspection ConstantConditions
+ this.torchEnabled.postValue(!this.torchEnabled.getValue());
+ }
+
+ public LiveData<Boolean> isTorchEnabled() {
+ return this.torchEnabled;
+ }
+
+ public LiveData<Integer> getTorchToggleButtonImageResource() {
+ return Transformations.map(isTorchEnabled(), 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/view/ColorChooser.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java
index 30dc0ada4..0dd431ff9 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
@@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.ui.view;
import android.content.Context;
import android.content.res.TypedArray;
+import android.graphics.Color;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -9,31 +10,31 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import com.google.android.flexbox.FlexboxLayout;
import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener;
+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 static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
-
public class ColorChooser extends LinearLayout {
- private WidgetColorChooserBinding binding;
-
- private final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- );
+ private final WidgetColorChooserBinding binding;
- private Context context;
- private String[] colors;
+ private final Context context;
+ private final int[] colors;
- private String selectedColor;
- private String previouslySelectedColor;
+ @ColorInt
+ private int selectedColor;
+ @ColorInt
+ private int previouslySelectedColor;
@Nullable
private ImageView previouslySelectedImageView;
@@ -41,17 +42,22 @@ public class ColorChooser extends LinearLayout {
super(context, attrs);
this.context = context;
- params.setMargins(0, dpToPx(context, R.dimen.spacer_1x), 0, 0);
+ final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ );
+ params.setMargins(0, DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1x), 0, 0);
params.setFlexBasisPercent(.15f);
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.ColorChooser, 0, 0);
- colors = getResources().getStringArray(a.getResourceId(R.styleable.ColorChooser_colors, 0));
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ColorChooser, 0, 0);
+ colors = Arrays.stream(getResources().getStringArray(a.getResourceId(R.styleable.ColorChooser_colors, 0)))
+ .mapToInt(Color::parseColor)
+ .toArray();
a.recycle();
binding = WidgetColorChooserBinding.inflate(LayoutInflater.from(context), this, true);
- for (final String color : colors) {
- ImageView image = new ImageView(getContext());
+ for (final int color : colors) {
+ final ImageView image = new ImageView(getContext());
image.setLayoutParams(params);
image.setOnClickListener((imageView) -> {
if (previouslySelectedImageView != null) { // null when first selection
@@ -61,7 +67,7 @@ public class ColorChooser extends LinearLayout {
selectedColor = color;
this.previouslySelectedColor = color;
this.previouslySelectedImageView = image;
- binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, R.color.board_default_custom_color));
+ binding.customColorChooser.setImageDrawable(ViewUtil.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);
});
@@ -84,19 +90,20 @@ public class ColorChooser extends LinearLayout {
previouslySelectedImageView.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor));
previouslySelectedImageView = null;
}
- String customColor = "#" + envelope.getHexCode().substring(2);
+ @ColorInt
+ final int customColor = envelope.getColor();
selectedColor = customColor;
previouslySelectedColor = customColor;
binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.circle_alpha_colorize_36dp, selectedColor));
});
}
- public void selectColor(String newColor) {
+ public void selectColor(@ColorInt int newColor) {
boolean newColorIsCustomColor = true;
selectedColor = newColor;
for (int i = 0; i < colors.length; i++) {
- if (colors[i].equals(newColor)) {
- binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, R.color.board_default_custom_color));
+ 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.colorPicker.getChildAt(i).performClick();
newColorIsCustomColor = false;
break;
@@ -107,7 +114,8 @@ public class ColorChooser extends LinearLayout {
}
}
- public String getSelectedColor() {
+ @ColorInt
+ public int getSelectedColor() {
return this.selectedColor;
}
}
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 501d33106..0facc385c 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
@@ -10,6 +10,7 @@ import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Px;
+import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import com.bumptech.glide.Glide;
@@ -17,12 +18,11 @@ import com.bumptech.glide.request.RequestOptions;
import java.util.List;
+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 static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
-
public class OverlappingAvatars extends RelativeLayout {
final int maxAvatarCount;
@Px
@@ -44,11 +44,12 @@ public class OverlappingAvatars extends RelativeLayout {
public OverlappingAvatars(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
maxAvatarCount = context.getResources().getInteger(R.integer.max_avatar_count);
- avatarBorderSize = dpToPx(context, R.dimen.avatar_size_small_overlapping_border);
- avatarSize = dpToPx(context, R.dimen.avatar_size_small) + avatarBorderSize * 2;
- overlapPx = dpToPx(context, R.dimen.avatar_size_small_overlapping);
- borderDrawable = getResources().getDrawable(R.drawable.avatar_border);
- DrawableCompat.setTint(borderDrawable, getResources().getColor(R.color.bg_card));
+ avatarBorderSize = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small_overlapping_border);
+ avatarSize = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small) + avatarBorderSize * 2;
+ overlapPx = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small_overlapping);
+ borderDrawable = ContextCompat.getDrawable(context, R.drawable.avatar_border);
+ assert borderDrawable != null;
+ DrawableCompat.setTint(borderDrawable, ContextCompat.getColor(context, R.color.bg_card));
}
public void setAvatars(@NonNull Account account, @NonNull List<User> assignedUsers) {
@@ -70,6 +71,7 @@ public class OverlappingAvatars extends RelativeLayout {
avatar.requestLayout();
Glide.with(context)
.load(account.getUrl() + "/index.php/avatar/" + Uri.encode(assignedUsers.get(avatarCount).getUid()) + "/" + avatarSize)
+ .placeholder(R.drawable.ic_person_grey600_24dp)
.error(R.drawable.ic_person_grey600_24dp)
.apply(RequestOptions.circleCropTransform())
.into(avatar);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java
new file mode 100644
index 000000000..0912a07dd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java
@@ -0,0 +1,35 @@
+package it.niedermann.nextcloud.deck.ui.view;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+public class SquareConstraintLayout extends ConstraintLayout {
+
+ public SquareConstraintLayout(Context context) {
+ super(context);
+ }
+
+ public SquareConstraintLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SquareConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public SquareConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Set a square layout.
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java
new file mode 100644
index 000000000..a2a50430c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java
@@ -0,0 +1,21 @@
+package it.niedermann.nextcloud.deck.ui.view.labelchip;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+
+import it.niedermann.android.util.DimensionUtil;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Label;
+
+@SuppressLint("ViewConstructor")
+public class CompactLabelChip extends LabelChip {
+
+ public CompactLabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) {
+ super(context, label, gutter);
+ params.setFlexBasisPercent(1 / 6.5f);
+ setHeight(DimensionUtil.INSTANCE.dpToPx(context, R.dimen.compact_label_height));
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java
new file mode 100644
index 000000000..80e44d7e0
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java
@@ -0,0 +1,21 @@
+package it.niedermann.nextcloud.deck.ui.view.labelchip;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+
+import it.niedermann.nextcloud.deck.model.Label;
+
+import static android.text.TextUtils.TruncateAt.MIDDLE;
+
+@SuppressLint("ViewConstructor")
+public class DefaultLabelChip extends LabelChip {
+
+ public DefaultLabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) {
+ super(context, label, gutter);
+ setText(label.getTitle());
+ setEllipsize(MIDDLE);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java
index db4e123d3..83853f0b2 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java
@@ -1,9 +1,8 @@
-package it.niedermann.nextcloud.deck.ui.view;
+package it.niedermann.nextcloud.deck.ui.view.labelchip;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
-import android.graphics.Color;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
@@ -12,26 +11,24 @@ import androidx.annotation.Px;
import com.google.android.flexbox.FlexboxLayout;
import com.google.android.material.chip.Chip;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
-
-import static android.text.TextUtils.TruncateAt.MIDDLE;
@SuppressLint("ViewConstructor")
public class LabelChip extends Chip {
private final Label label;
+ protected final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ );
+
public LabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) {
super(context);
this.label = label;
- FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- );
-
params.setMargins(0, 0, gutter, 0);
setLayoutParams(params);
setEnsureMinTouchTargetSize(false);
@@ -42,15 +39,13 @@ public class LabelChip extends Chip {
setTextStartPadding(gutter);
setTextEndPadding(gutter);
setChipEndPadding(gutter);
-
- setText(label.getTitle());
- setEllipsize(MIDDLE);
+ setClickable(false);
try {
- int labelColor = Color.parseColor("#" + label.getColor());
+ int labelColor = label.getColor();
ColorStateList c = ColorStateList.valueOf(labelColor);
setChipBackgroundColor(c);
- setTextColor(ColorUtil.getForegroundColorForBackgroundColor(labelColor));
+ setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor));
} catch (IllegalArgumentException e) {
DeckLog.logError(e);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java
new file mode 100644
index 000000000..1c5e35d97
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java
@@ -0,0 +1,22 @@
+package it.niedermann.nextcloud.deck.ui.view.labellayout;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.ui.view.labelchip.CompactLabelChip;
+import it.niedermann.nextcloud.deck.ui.view.labelchip.LabelChip;
+
+public class CompactLabelLayout extends LabelLayout {
+
+ public CompactLabelLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected LabelChip createLabelChip(@NonNull Label label) {
+ return new CompactLabelChip(getContext(), label, gutter);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java
new file mode 100644
index 000000000..f2d6d0752
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java
@@ -0,0 +1,21 @@
+package it.niedermann.nextcloud.deck.ui.view.labellayout;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.ui.view.labelchip.DefaultLabelChip;
+
+public class DefaultLabelLayout extends LabelLayout {
+
+ public DefaultLabelLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected DefaultLabelChip createLabelChip(@NonNull Label label) {
+ return new DefaultLabelChip(getContext(), label, gutter);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java
index 814c63ce1..3db539e53 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java
@@ -1,4 +1,4 @@
-package it.niedermann.nextcloud.deck.ui.view;
+package it.niedermann.nextcloud.deck.ui.view.labellayout;
import android.content.Context;
import android.util.AttributeSet;
@@ -11,21 +11,22 @@ import com.google.android.flexbox.FlexboxLayout;
import java.util.LinkedList;
import java.util.List;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.ui.view.labelchip.LabelChip;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
-
-public class LabelLayout extends FlexboxLayout {
+public abstract class LabelLayout extends FlexboxLayout {
@Px
- private int gutter;
- private List<LabelChip> chipList = new LinkedList<>();
+ final protected int gutter;
+ @NonNull
+ final private List<LabelChip> chipList = new LinkedList<>();
public LabelLayout(Context context, AttributeSet attrs) {
super(context, attrs);
- this.gutter = dpToPx(context, R.dimen.spacer_1hx);
+ this.gutter = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1hx);
}
/**
@@ -82,9 +83,11 @@ public class LabelLayout extends FlexboxLayout {
continue labelList;
}
}
- LabelChip chip = new LabelChip(getContext(), label, gutter);
+ final LabelChip chip = createLabelChip(label);
addView(chip);
chipList.add(chip);
}
}
+
+ protected abstract LabelChip createLabelChip(@NonNull Label label);
}
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 8c1fbe1fa..fe6e969e7 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
@@ -45,7 +45,7 @@ public class SelectCardForWidgetActivity extends MainActivity implements SelectC
@Override
public void onCardSelected(FullCard fullCard) {
- syncManager.addOrUpdateSingleCardWidget(appWidgetId, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId());
+ mainViewModel.addOrUpdateSingleCardWidget(appWidgetId, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId());
final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
getApplicationContext(), SingleCardWidget.class)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
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 783f98e00..b44b80d1b 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
@@ -43,7 +43,7 @@ public class SingleCardWidget extends AppWidgetProvider {
views.setTextViewText(R.id.description, fullModel.getFullCard().getCard().getDescription());
if (fullModel.getFullCard().getCard().getDueDate() != null) {
- views.setTextViewText(R.id.card_due_date, DateUtil.getRelativeDateTimeString(context, fullModel.getFullCard().getCard().getDueDate().getTime()));
+ views.setTextViewText(R.id.card_due_date, DateUtil.getRelativeDateTimeString(context, fullModel.getFullCard().getCard().getDueDate().toEpochMilli()));
// TODO Use multiple views for background colors and only set the necessary to View.VISIBLE
// https://stackoverflow.com/a/3376537
// Because otherwise using Reflection is the only way
@@ -141,13 +141,10 @@ public class SingleCardWidget extends AppWidgetProvider {
super.onDeleted(context, appWidgetIds);
}
-
/**
* Updates UI data of all {@link SingleCardWidget} instances
*/
public static void notifyDatasetChanged(Context context) {
- Intent intent = new Intent(context, SingleCardWidget.class);
- intent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
- context.sendBroadcast(intent);
+ context.sendBroadcast(new Intent(context, SingleCardWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
}
}
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
new file mode 100644
index 000000000..cb179953c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java
@@ -0,0 +1,121 @@
+package it.niedermann.nextcloud.deck.ui.widget.stack;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+
+import java.util.NoSuchElementException;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.MainActivity;
+import it.niedermann.nextcloud.deck.ui.card.EditActivity;
+
+import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE;
+
+public class StackWidget extends AppWidgetProvider {
+ public static final String ACCOUNT_ID_KEY = "stack_widget_account_id";
+ public static final String ACCOUNT_KEY = "stack_widget_account";
+ public static final String STACK_ID_KEY = "stack_widget_stack_id";
+ public static final String BUNDLE_KEY = "stack_widget_bundle";
+ private static final int PENDING_INTENT_OPEN_APP_RQ = 0;
+ private static final int PENDING_INTENT_EDIT_CARD_RQ = 1;
+
+ static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds, Account account) {
+ final SyncManager syncManager = new SyncManager(context);
+
+ for (int appWidgetId : appWidgetIds) {
+ new Thread(() -> {
+ try {
+ final StackWidgetModel model = syncManager.getStackWidgetModelDirectly(appWidgetId);
+ RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack);
+ Intent serviceIntent = new Intent(context, StackWidgetService.class);
+
+ serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ serviceIntent.putExtra(ACCOUNT_ID_KEY + appWidgetId, model.getAccountId());
+ serviceIntent.putExtra(STACK_ID_KEY + appWidgetId, model.getStackId());
+ if (account != null) {
+ Bundle extras = new Bundle();
+ extras.putSerializable(StackWidget.ACCOUNT_KEY + appWidgetId, account);
+ serviceIntent.putExtra(BUNDLE_KEY + appWidgetId, extras);
+ }
+ serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)));
+
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setComponent(new ComponentName(context.getPackageName(), MainActivity.class.getName()));
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_APP_RQ,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ views.setOnClickPendingIntent(R.id.widget_stack_header_rl, pendingIntent);
+
+ PendingIntent templatePI = PendingIntent.getActivity(context, PENDING_INTENT_EDIT_CARD_RQ,
+ new Intent(context, EditActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
+
+ views.setPendingIntentTemplate(R.id.stack_widget_lv, templatePI);
+ views.setRemoteAdapter(R.id.stack_widget_lv, serviceIntent);
+ views.setEmptyView(R.id.stack_widget_lv, R.id.widget_stack_placeholder_iv);
+ awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.stack_widget_lv);
+ awm.updateAppWidget(appWidgetId, views);
+ } catch (NoSuchElementException e) {
+ // onUpdate has been triggered before the user finished configuring the widget
+ }
+ }).start();
+ }
+ }
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds);
+ updateAppWidget(context, appWidgetManager, appWidgetIds, null);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final Account account;
+
+ super.onReceive(context, intent);
+
+ AppWidgetManager awm = AppWidgetManager.getInstance(context);
+
+ if (intent.getAction() != null) {
+ if (intent.getAction().equals(ACTION_APPWIDGET_UPDATE)) {
+ if (intent.hasExtra(BUNDLE_KEY)) {
+ Bundle extras = intent.getBundleExtra(StackWidget.BUNDLE_KEY);
+ account = (Account) extras.getSerializable(ACCOUNT_KEY);
+
+ if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
+ if (intent.getExtras() != null) {
+ updateAppWidget(context, awm, new int[]{intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)}, account);
+ }
+ } else {
+ updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, StackWidget.class)), account);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onDeleted(Context context, int[] appWidgetIds) {
+ super.onDeleted(context, appWidgetIds);
+ final SyncManager syncManager = new SyncManager(context);
+
+ for (int appWidgetId : appWidgetIds) {
+ syncManager.deleteStackWidgetModel(appWidgetId);
+ }
+ }
+
+ /**
+ * Updates UI data of all {@link StackWidget} instances
+ */
+ public static void notifyDatasetChanged(Context context) {
+ context.sendBroadcast(new Intent(context, StackWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java
new file mode 100644
index 000000000..96b5cf672
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java
@@ -0,0 +1,68 @@
+package it.niedermann.nextcloud.deck.ui.widget.stack;
+
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.lifecycle.ViewModelProvider;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.ui.PickStackActivity;
+
+public class StackWidgetConfigurationActivity extends PickStackActivity {
+ private int appWidgetId;
+ private StackWidgetConfigurationViewModel stackWidgetConfigurationViewModel;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ stackWidgetConfigurationViewModel = new ViewModelProvider(this).get(StackWidgetConfigurationViewModel.class);
+
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.add_stack_widget);
+ }
+
+ setResult(RESULT_CANCELED);
+ final Bundle extras = getIntent().getExtras();
+
+ if (extras != null) {
+ appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID);
+ }
+
+ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ DeckLog.error("INVALID_APPWIDGET_ID");
+ finish();
+ }
+ }
+
+ @Override
+ protected void onSubmit(Account account, long boardId, long stackId) {
+ final Bundle extras = new Bundle();
+
+ stackWidgetConfigurationViewModel.addStackWidget(appWidgetId, account.getId(), stackId, false);
+ Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
+ getApplicationContext(), StackWidget.class);
+ extras.putSerializable(StackWidget.ACCOUNT_KEY, account);
+ extras.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+
+ // The `extras` bundle is added to the intent this way because using putExtras(extras)
+ // would have the OS attempt to reassemle the data and cause a crash
+ // when it finds classes that are only known to this application.
+ updateIntent.putExtra(StackWidget.BUNDLE_KEY, extras);
+ setResult(RESULT_OK, updateIntent);
+ getApplicationContext().sendBroadcast(updateIntent);
+
+ finish();
+ }
+
+ @Override
+ protected boolean showBoardsWithoutEditPermission() {
+ return true;
+ }
+}
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
new file mode 100644
index 000000000..cc669accf
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java
@@ -0,0 +1,23 @@
+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.persistence.sync.SyncManager;
+
+@SuppressWarnings("WeakerAccess")
+public class StackWidgetConfigurationViewModel extends AndroidViewModel {
+
+ private final SyncManager syncManager;
+
+ public StackWidgetConfigurationViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
+
+ public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) {
+ syncManager.addStackWidget(appWidgetId, accountId, stackId, darkTheme);
+ }
+}
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
new file mode 100644
index 000000000..87f357e20
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java
@@ -0,0 +1,134 @@
+package it.niedermann.nextcloud.deck.ui.widget.stack;
+
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import androidx.lifecycle.LiveData;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+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.model.full.FullStack;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.card.EditActivity;
+
+public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
+ private final Context context;
+ private final int appWidgetId;
+ private final long accountId;
+ private final long stackId;
+
+ private Account account;
+ private FullStack stack;
+ private List<FullCard> cardList;
+
+ StackWidgetFactory(Context context, Intent intent) {
+ this.context = context;
+ appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID);
+ accountId = intent.getLongExtra(StackWidget.ACCOUNT_ID_KEY + appWidgetId, -1);
+ stackId = intent.getLongExtra(StackWidget.STACK_ID_KEY + appWidgetId, -1);
+ if (intent.hasExtra(StackWidget.BUNDLE_KEY + appWidgetId)) {
+ account = (Account) intent.getBundleExtra(StackWidget.BUNDLE_KEY + appWidgetId).getSerializable(StackWidget.ACCOUNT_KEY + appWidgetId);
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ SyncManager syncManager = new SyncManager(context);
+
+ LiveData<FullStack> stackLiveData = syncManager.getStack(accountId, stackId);
+ stackLiveData.observeForever((FullStack fullStack) -> {
+ if (fullStack != null) {
+ RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack);
+ stack = fullStack;
+ views.setTextViewText(R.id.widget_stack_title_tv, stack.getStack().getTitle());
+
+ LiveData<FullBoard> fullBoardLiveData = syncManager.getFullBoardById(accountId, stack.getStack().getBoardId());
+ fullBoardLiveData.observeForever((FullBoard fullBoard) -> {
+ if (fullBoard != null) {
+ views.setInt(R.id.widget_stack_header_icon, "setColorFilter", fullBoard.getBoard().getColor());
+ notifyAppWidgetUpdate(views);
+ }
+ });
+
+ LiveData<List<FullCard>> fullCardData = syncManager.getFullCardsForStack(accountId, stackId, null);
+ fullCardData.observeForever((List<FullCard> fullCards) -> cardList = fullCards);
+ notifyAppWidgetUpdate(views);
+ }
+ });
+ }
+
+ @Override
+ public void onDataSetChanged() {
+
+ }
+
+
+ @Override
+ public void onDestroy() {
+
+ }
+
+ @Override
+ public int getCount() {
+ return stack == null ? 0 : stack.getCards().size();
+ }
+
+ @Override
+ public RemoteViews getViewAt(int i) {
+ RemoteViews widget_entry;
+
+ if (cardList == null || i > (cardList.size() - 1) || cardList.get(i) == null) {
+ DeckLog.error("Card not found at position " + i);
+ return null;
+ }
+
+ FullCard card = cardList.get(i);
+
+ widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_stack_entry);
+ widget_entry.setTextViewText(R.id.widget_entry_content_tv, card.card.getTitle());
+
+ final Intent intent = EditActivity.createEditCardIntent(context, account, stack.getStack().getBoardId(), card.getCard().getLocalId());
+ intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
+ widget_entry.setOnClickFillInIntent(R.id.widget_stack_entry, intent);
+
+ return widget_entry;
+ }
+
+ @Override
+ public RemoteViews getLoadingView() {
+ return null;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getItemId(int i) {
+ return i;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ private void notifyAppWidgetUpdate(RemoteViews views) {
+ AppWidgetManager awm = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = awm.getAppWidgetIds(new ComponentName(context, StackWidget.class));
+ awm.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.stack_widget_lv);
+ awm.updateAppWidget(appWidgetId, views);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java
new file mode 100644
index 000000000..9299a96e2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java
@@ -0,0 +1,11 @@
+package it.niedermann.nextcloud.deck.ui.widget.stack;
+
+import android.content.Intent;
+import android.widget.RemoteViewsService;
+
+public class StackWidgetService extends RemoteViewsService {
+ @Override
+ public RemoteViewsFactory onGetViewFactory(Intent intent) {
+ return new StackWidgetFactory(this.getApplicationContext(), intent);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java
index 6b2d6925f..ef3e14b37 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java
@@ -1,9 +1,16 @@
package it.niedermann.nextcloud.deck.util;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+import android.widget.Toast;
+import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
import java.io.File;
import java.io.FileNotFoundException;
@@ -12,6 +19,9 @@ import java.io.IOException;
import java.io.InputStream;
import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Attachment;
+import it.niedermann.nextcloud.deck.model.ocs.Version;
/**
* Created by stefan on 07.03.20.
@@ -22,38 +32,121 @@ public class AttachmentUtil {
private AttachmentUtil() {
}
- public static String getRemoteUrl(String accountUrl, long cardRemoteId, long attachmentRemoteId) {
+ /**
+ * @return a link to the thumbnail of the given {@link Attachment}.
+ * If a thumbnail is not available (see {@link Version#supportsFileAttachments()}), a link to
+ * the {@link Attachment} itself will be returned instead.
+ */
+ public static String getThumbnailUrl(@NonNull Version version, @NonNull String accountUrl, @NonNull Long cardRemoteId, @NonNull Attachment attachment, @Px int previewSize) {
+ return version.supportsFileAttachments() && !TextUtils.isEmpty(String.valueOf(attachment.getFileId()))
+ ? accountUrl + "/index.php/core/preview?fileId=" + attachment.getFileId() + "&x=" + previewSize + "&y=" + previewSize
+ : getRemoteOrLocalUrl(accountUrl, cardRemoteId, attachment);
+ }
+
+ /**
+ * @return {@link AttachmentUtil#getRemoteUrl} or {@link Attachment#getLocalPath()} as fallback
+ * in case this {@param attachment} has not yet been synced.
+ */
+ @Nullable
+ public static String getRemoteOrLocalUrl(@NonNull String accountUrl, @Nullable Long cardRemoteId, @NonNull Attachment attachment) {
+ return (attachment.getId() == null || cardRemoteId == null)
+ ? attachment.getLocalPath()
+ : getRemoteUrl(accountUrl, cardRemoteId, attachment.getId());
+ }
+
+ /**
+ * Tries to open the given {@link Attachment} in web browser. Displays a toast on failure.
+ */
+ public static void openAttachmentInBrowser(@NonNull Context context, @NonNull String accountUrl, Long cardRemoteId, Long attachmentRemoteId) {
+ if (cardRemoteId == null) {
+ Toast.makeText(context, R.string.card_does_not_yet_exist, Toast.LENGTH_LONG).show();
+ DeckLog.logError(new IllegalArgumentException("cardRemoteId must not be null."));
+ return;
+ }
+ if (attachmentRemoteId == null) {
+ Toast.makeText(context, R.string.attachment_does_not_yet_exist, Toast.LENGTH_LONG).show();
+ DeckLog.logError(new IllegalArgumentException("attachmentRemoteId must not be null."));
+ return;
+ }
+ context.startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(AttachmentUtil.getRemoteUrl(accountUrl, cardRemoteId, attachmentRemoteId))));
+ }
+
+ private static String getRemoteUrl(@NonNull String accountUrl, @NonNull Long cardRemoteId, @NonNull Long attachmentRemoteId) {
return accountUrl + "/index.php/apps/deck/cards/" + cardRemoteId + "/attachment/" + attachmentRemoteId;
}
- public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localId) throws IOException, IllegalArgumentException {
- String fullTempPath = context.getApplicationContext().getFilesDir().getAbsolutePath() + "/attachments/account-" + accountId + "/card-" + (localId == null ? "pending-creation" : localId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context);
- DeckLog.verbose("----- fullTempPath: " + fullTempPath);
- InputStream inputStream = context.getContentResolver().openInputStream(currentUri);
+ public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localCardId) throws IOException, IllegalArgumentException {
+ final InputStream inputStream = context.getContentResolver().openInputStream(currentUri);
if (inputStream == null) {
throw new IOException("Could not open input stream for " + currentUri.getPath());
}
- File cacheFile = new File(fullTempPath);
- File tempDir = cacheFile.getParentFile();
+ final File cacheFile = getTempCacheFile(context, "attachments/account-" + accountId + "/card-" + (localCardId == null ? "pending-creation" : localCardId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context));
+ final FileOutputStream outputStream = new FileOutputStream(cacheFile);
+ byte[] buffer = new byte[4096];
+
+ int count;
+ while ((count = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ }
+ DeckLog.verbose("----- wrote");
+ return cacheFile;
+ }
+
+ /**
+ * Creates a new {@link File}
+ */
+ public static File getTempCacheFile(@NonNull Context context, String fileName) throws IOException {
+ File cacheFile = new File(context.getApplicationContext().getFilesDir().getAbsolutePath() + "/" + fileName);
+
+ DeckLog.verbose("- Full path for new cache file: " + cacheFile.getAbsolutePath());
+
+ final File tempDir = cacheFile.getParentFile();
if (tempDir == null) {
- throw new FileNotFoundException("could not cacheFile.getPranetFile()");
+ throw new FileNotFoundException("could not cacheFile.getParentFile()");
}
if (!tempDir.exists()) {
- if (!tempDir.mkdirs()) {
+ DeckLog.verbose("-- The folder in which the new file should be created does not exist yet. Trying to create it...");
+ if (tempDir.mkdirs()) {
+ DeckLog.verbose("--- Creation successful");
+ } else {
throw new IOException("Directory for temporary file does not exist and could not be created.");
}
}
- if (!cacheFile.createNewFile()) {
+
+ DeckLog.verbose("- Try to create actual cache file");
+ if (cacheFile.createNewFile()) {
+ DeckLog.verbose("-- Successfully created cache file");
+ } else {
throw new IOException("Failed to create cacheFile");
}
- FileOutputStream outputStream = new FileOutputStream(fullTempPath);
- byte[] buffer = new byte[4096];
- int count;
- while ((count = inputStream.read(buffer)) > 0) {
- outputStream.write(buffer, 0, count);
- }
- DeckLog.verbose("----- wrote");
return cacheFile;
}
+
+ @DrawableRes
+ public static int getIconForMimeType(@NonNull String mimeType) {
+ if (TextUtils.isEmpty(mimeType)) {
+ return R.drawable.ic_attach_file_grey600_24dp;
+ } else if (MimeTypeUtil.isAudio(mimeType)) {
+ return R.drawable.ic_music_note_grey600_24dp;
+ } else if (MimeTypeUtil.isVideo(mimeType)) {
+ return R.drawable.ic_local_movies_grey600_24dp;
+ } else if (MimeTypeUtil.isPdf(mimeType)) {
+ return R.drawable.ic_baseline_picture_as_pdf_24;
+ } else if (MimeTypeUtil.isContact(mimeType)) {
+ return R.drawable.ic_baseline_contact_mail_24;
+ } else {
+ return R.drawable.ic_attach_file_grey600_24dp;
+ }
+ }
+
+ public static String getMimeType(@Nullable String url) {
+ String type = null;
+ String extension = MimeTypeMap.getFileExtensionFromUrl(url);
+ if (extension != null) {
+ type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+ }
+ return type;
+ }
+
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java
deleted file mode 100644
index 1dad224e9..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package it.niedermann.nextcloud.deck.util;
-
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import it.niedermann.nextcloud.deck.DeckLog;
-import it.niedermann.nextcloud.deck.R;
-
-import static android.content.Context.CLIPBOARD_SERVICE;
-
-public class ClipboardUtil {
-
- private ClipboardUtil() {
- }
-
- public static boolean copyToClipboard(@NonNull Context context, @Nullable String text) {
- return copyToClipboard(context, text, text);
- }
-
- public static boolean copyToClipboard(@NonNull Context context, @Nullable String label, @Nullable String text) {
- final ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE);
- if (clipboardManager == null) {
- DeckLog.error("ClipboardManager is null");
- Toast.makeText(context, R.string.could_not_copy_to_clipboard, Toast.LENGTH_LONG).show();
- return false;
- }
- final ClipData clipData = ClipData.newPlainText(label, text);
- clipboardManager.setPrimaryClip(clipData);
- DeckLog.info("Copied to clipboard: [" + label + "] \"" + text + "\"");
- Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
- return true;
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java
deleted file mode 100644
index 08354c5fb..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java
+++ /dev/null
@@ -1,165 +0,0 @@
-package it.niedermann.nextcloud.deck.util;
-
-import android.graphics.Color;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.util.Pair;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public final class ColorUtil {
-
- private static final Map<ColorPair, Boolean> CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>();
- private static final Map<Integer, Integer> FOREGROUND_CACHE = new HashMap<>();
- private static final Map<Integer, Boolean> IS_DARK_COLOR_CACHE = new HashMap<>();
-
- private ColorUtil() {
- }
-
- @ColorInt
- public static int getForegroundColorForBackgroundColor(@ColorInt int color) {
- Integer ret = FOREGROUND_CACHE.get(color);
- if (ret == null) {
- if (Color.TRANSPARENT == color)
- ret = Color.BLACK;
- else if (isColorDark(color))
- ret = Color.WHITE;
- else
- ret = Color.BLACK;
-
- FOREGROUND_CACHE.put(color, ret);
- }
- return ret;
- }
-
- /**
- * @return well formatted string starting with a hash followed by 6 hex numbers that is parsable by {@link Color#parseColor(String)}.
- */
- public static String formatColorToParsableHexString(String input) {
- if (input == null) {
- throw new IllegalArgumentException("input color string is null");
- }
- if (isParsableValidHexColorString(input)) {
- return input;
- }
- final char[] chars = input.replaceAll("#", "").toCharArray();
- final StringBuilder sb = new StringBuilder(7).append("#");
- if (chars.length == 6) {
- sb.append(chars);
- } else if (chars.length == 3) {
- for (char c : chars) {
- sb.append(c).append(c);
- }
- } else {
- throw new IllegalArgumentException("unparsable color string: \"" + input + "\"");
- }
- final String formattedHexColor = sb.toString();
- if (isParsableValidHexColorString(formattedHexColor)) {
- return formattedHexColor;
- } else {
- throw new IllegalArgumentException("\"" + input + "\" is not a valid color string. Result of tried normalizing: " + formattedHexColor);
- }
- }
-
- /**
- * Checking for {@link Color#parseColor(String)} being able to parse the input is the important part because we don't know the implementation and rely on it to be able to parse the color.
- *
- * @return true, if the input starts with a hash followed by 6 characters of hex numbers and is parsable by {@link Color#parseColor(String)}.
- */
- private static boolean isParsableValidHexColorString(@NonNull String input) {
- try {
- Color.parseColor(input);
- return input.matches("#[a-fA-F0-9]{6}");
- } catch (Exception e) {
- return false;
- }
- }
-
- public static boolean isColorDark(@ColorInt int color) {
- Boolean ret = IS_DARK_COLOR_CACHE.get(color);
- if (ret == null) {
- ret = getBrightness(color) < 200;
- IS_DARK_COLOR_CACHE.put(color, ret);
- }
- return ret;
- }
-
- private static int getBrightness(@ColorInt int color) {
- final int[] rgb = {Color.red(color), Color.green(color), Color.blue(color)};
-
- return (int) Math.sqrt(rgb[0] * rgb[0] * .241 + rgb[1]
- * rgb[1] * .691 + rgb[2] * rgb[2] * .068);
- }
-
- // ---------------------------------------------------
- // Based on https://github.com/LeaVerou/contrast-ratio
- // ---------------------------------------------------
-
- public static boolean contrastRatioIsSufficient(@ColorInt int colorOne, @ColorInt int colorTwo) {
- ColorPair key = new ColorPair(colorOne, colorTwo);
- Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key);
- if (ret == null) {
- ret = getContrastRatio(colorOne, colorTwo) > 3d;
- CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret);
- return ret;
- }
- return ret;
- }
-
- public static boolean contrastRatioIsSufficientBigAreas(@ColorInt int colorOne, @ColorInt int colorTwo) {
- ColorPair key = new ColorPair(colorOne, colorTwo);
- Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key);
- if (ret == null) {
- ret = getContrastRatio(colorOne, colorTwo) > 1.47d;
- CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret);
- return ret;
- }
- return ret;
- }
-
- public static double getContrastRatio(@ColorInt int colorOne, @ColorInt int colorTwo) {
- final double lum1 = getLuminanace(colorOne);
- final double lum2 = getLuminanace(colorTwo);
- final double brightest = Math.max(lum1, lum2);
- final double darkest = Math.min(lum1, lum2);
- return (brightest + 0.05) / (darkest + 0.05);
- }
-
- private static double getLuminanace(@ColorInt int color) {
- final int[] rgb = {Color.red(color), Color.green(color), Color.blue(color)};
- return getSubcolorLuminance(rgb[0]) * 0.2126 + getSubcolorLuminance(rgb[1]) * 0.7152 + getSubcolorLuminance(rgb[2]) * 0.0722;
- }
-
- private static double getSubcolorLuminance(@ColorInt int color) {
- final double value = color / 255d;
- return value <= 0.03928
- ? value / 12.92
- : Math.pow((value + 0.055) / 1.055, 2.4);
- }
-
- private static class ColorPair extends Pair<Integer, Integer> {
-
- private ColorPair(@Nullable Integer first, @Nullable Integer second) {
- super(first, second);
- }
-
- @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "NumberEquality"})
- @Override
- public boolean equals(Object o) {
- final ColorPair colorPair = (ColorPair) o;
- if (first != colorPair.first) return false;
- return second == colorPair.second;
- }
-
- @SuppressWarnings("ConstantConditions")
- @Override
- public int hashCode() {
- int result = first;
- result = 31 * result + second;
- return result;
- }
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DateUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DateUtil.java
index 5c2ffd76c..ac88519ff 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DateUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/DateUtil.java
@@ -3,9 +3,9 @@ package it.niedermann.nextcloud.deck.util;
import android.content.Context;
import android.text.format.DateUtils;
-import java.util.Date;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
+import androidx.annotation.NonNull;
+
+import java.time.ZonedDateTime;
import it.niedermann.nextcloud.deck.R;
@@ -15,48 +15,9 @@ public final class DateUtil {
private DateUtil() {
}
- public static Date nowInGMT() {
- return convertToGMT(new Date());
- }
-
- private static Date convertToGMT(Date date ){
- TimeZone tz = TimeZone.getDefault();
- Date ret = new Date( date.getTime() - tz.getRawOffset() );
-
- // if we are now in DST, back off by the delta. Note that we are checking the GMT date, this is the KEY.
- if ( tz.inDaylightTime( ret )){
- Date dstDate = new Date( ret.getTime() - tz.getDSTSavings() );
-
- // check to make sure we have not crossed back into standard time
- // this happens when we are on the cusp of DST (7pm the day before the change for PDT)
- if ( tz.inDaylightTime( dstDate )){
- ret = dstDate;
- }
- }
- return ret;
- }
-
- /**
- * Get difference between 2 dates in days (hours, minutes will be set to zero).
- *
- * @param sourceDateFrom start date
- * @param sourceDateUntil end date
- * @return difference between the to dates in days.
- */
- public static long getDayDifference(Date sourceDateFrom, Date sourceDateUntil) {
- Date dateFrom = new Date(sourceDateFrom.getTime());
- dateFrom.setHours(0);
- dateFrom.setMinutes(0);
-
- Date dateUntil = new Date(sourceDateUntil.getTime());
- dateUntil.setHours(0);
- dateUntil.setMinutes(0);
-
- return TimeUnit.DAYS.convert(dateUntil.getTime() - dateFrom.getTime(), TimeUnit.MILLISECONDS);
- }
-
- public static CharSequence getRelativeDateTimeString(Context context, long time) {
- if ((System.currentTimeMillis() - time) < 60 * 1000 && System.currentTimeMillis() > time) {
+ public static CharSequence getRelativeDateTimeString(@NonNull Context context, long time) {
+ long now = ZonedDateTime.now().toInstant().toEpochMilli();
+ if ((now - time) < 60 * 1000 && now > time) {
// < 60 seconds -> seconds ago
return context.getString(R.string.seconds_ago);
} else {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java
new file mode 100644
index 000000000..e01d0ec29
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java
@@ -0,0 +1,59 @@
+package it.niedermann.nextcloud.deck.util;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import it.niedermann.android.util.ColorUtil;
+
+public final class DeckColorUtil {
+
+ private static final Map<ColorPair, Boolean> CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>();
+
+ public static boolean contrastRatioIsSufficient(@ColorInt int colorOne, @ColorInt int colorTwo) {
+ ColorPair key = new ColorPair(colorOne, colorTwo);
+ Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key);
+ if (ret == null) {
+ ret = ColorUtil.INSTANCE.getContrastRatio(colorOne, colorTwo) > 3d;
+ CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret);
+ return ret;
+ }
+ return ret;
+ }
+
+ public static boolean contrastRatioIsSufficientBigAreas(@ColorInt int colorOne, @ColorInt int colorTwo) {
+ ColorPair key = new ColorPair(colorOne, colorTwo);
+ Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key);
+ if (ret == null) {
+ ret = ColorUtil.INSTANCE.getContrastRatio(colorOne, colorTwo) > 1.47d;
+ CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret);
+ return ret;
+ }
+ return ret;
+ }
+
+ private static class ColorPair extends Pair<Integer, Integer> {
+
+ private ColorPair(@Nullable Integer first, @Nullable Integer second) {
+ super(first, second);
+ }
+
+ @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "NumberEquality"})
+ @Override
+ public boolean equals(Object o) {
+ final ColorPair colorPair = (ColorPair) o;
+ if (first != colorPair.first) return false;
+ return second == colorPair.second;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = first;
+ result = 31 * result + second;
+ return result;
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java
deleted file mode 100644
index 6ff22eace..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package it.niedermann.nextcloud.deck.util;
-
-import android.content.Context;
-
-import androidx.annotation.DimenRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Px;
-
-public final class DimensionUtil {
- private DimensionUtil() {
- }
-
- @Px
- public static int dpToPx(@NonNull Context context, @DimenRes int resource) {
- return context.getResources().getDimensionPixelSize(resource);
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java
index 7457c0df7..1a88bec97 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java
@@ -7,6 +7,7 @@ import android.view.SubMenu;
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.List;
@@ -38,12 +39,12 @@ public class DrawerMenuUtil {
SubMenu boardsMenu = menu.addSubMenu(R.string.simple_boards);
int index = 0;
for (Board board : boards) {
- MenuItem m = boardsMenu.add(Menu.NONE, index++, Menu.NONE, board.getTitle()).setIcon(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, "#" + board.getColor()));
+ MenuItem m = boardsMenu.add(Menu.NONE, index++, Menu.NONE, board.getTitle()).setIcon(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, board.getColor()));
if (currentServerVersionIsSupported) {
if (board.isPermissionManage()) {
AppCompatImageButton contextMenu = new AppCompatImageButton(context);
contextMenu.setBackgroundDrawable(null);
- contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, R.color.grey600));
+ contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, ContextCompat.getColor(context, R.color.grey600)));
contextMenu.setOnClickListener((v) -> {
PopupMenu popup = new PopupMenu(context, contextMenu);
popup.getMenuInflater().inflate(R.menu.navigation_context_menu, popup.getMenu());
@@ -53,25 +54,27 @@ public class DrawerMenuUtil {
}
popup.setOnMenuItemClickListener((MenuItem item) -> {
final String editBoard = context.getString(R.string.edit_board);
- switch (item.getItemId()) {
- case SHARE_BOARD_ID:
- AccessControlDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName());
- return true;
- case R.id.edit_board:
- EditBoardDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard);
- return true;
- case R.id.manage_labels:
- ManageLabelsDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard);
- return true;
- case R.id.archive_board:
- context.onArchive(board);
- return true;
- case R.id.delete_board:
- DeleteBoardDialogFragment.newInstance(board).show(context.getSupportFragmentManager(), DeleteBoardDialogFragment.class.getCanonicalName());
- return true;
- default:
- return false;
+ int itemId = item.getItemId();
+ if (itemId == SHARE_BOARD_ID) {
+ AccessControlDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), AccessControlDialogFragment.class.getSimpleName());
+ return true;
+ } else if (itemId == R.id.edit_board) {
+ EditBoardDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard);
+ return true;
+ } else if (itemId == R.id.manage_labels) {
+ ManageLabelsDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard);
+ return true;
+ } else if (itemId == R.id.clone_board) {
+ context.onClone(board);
+ return true;
+ } else if (itemId == R.id.archive_board) {
+ context.onArchive(board);
+ return true;
+ } else if (itemId == R.id.delete_board) {
+ DeleteBoardDialogFragment.newInstance(board).show(context.getSupportFragmentManager(), DeleteBoardDialogFragment.class.getCanonicalName());
+ return true;
}
+ return false;
});
popup.show();
});
@@ -79,7 +82,7 @@ public class DrawerMenuUtil {
} else if (board.isPermissionShare()) {
AppCompatImageButton contextMenu = new AppCompatImageButton(context);
contextMenu.setBackgroundDrawable(null);
- contextMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, R.color.grey600));
+ 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()));
m.setActionView(contextMenu);
}
@@ -87,7 +90,7 @@ public class DrawerMenuUtil {
}
if (hasArchivedBoards) {
- boardsMenu.add(Menu.NONE, MENU_ID_ARCHIVED_BOARDS, Menu.NONE, R.string.archived_boards).setIcon(ViewUtil.getTintedImageView(context, R.drawable.ic_archive_white_24dp, R.color.grey600));
+ boardsMenu.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)));
}
if (currentServerVersionIsSupported) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ExceptionUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ExceptionUtil.java
deleted file mode 100644
index 8599f0b6d..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ExceptionUtil.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package it.niedermann.nextcloud.deck.util;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
-import com.nextcloud.android.sso.helper.VersionCheckHelper;
-import com.nextcloud.android.sso.ui.UiExceptionManager;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
-import it.niedermann.nextcloud.deck.BuildConfig;
-import it.niedermann.nextcloud.deck.DeckLog;
-import it.niedermann.nextcloud.deck.model.Account;
-
-public class ExceptionUtil {
-
- private ExceptionUtil() {
-
- }
-
- public static String getDebugInfos(@NonNull Context context, Throwable throwable, @Nullable Account account) {
- return "" +
- getAppVersions(context, account) +
- "\n\n---\n" +
- getDeviceInfos() +
- "\n\n---" +
- "\n\n" +
- getStacktraceOf(throwable);
- }
-
- private static String getAppVersions(Context context, @Nullable Account account) {
- String versions = ""
- + "App Version: " + BuildConfig.VERSION_NAME + "\n"
- + "App Version Code: " + BuildConfig.VERSION_CODE + "\n"
- + "App Flavor: " + BuildConfig.FLAVOR + "\n";
-
- if (account != null) {
- versions += "\n";
- versions += "Deck Server Version: " + account.getServerDeckVersion() + "\n";
- }
-
- versions += "\n";
- try {
- versions += "Files App Version Code: " + VersionCheckHelper.getNextcloudFilesVersionCode(context);
- } catch (PackageManager.NameNotFoundException e) {
- versions += "Files App Version Code: " + e.getMessage();
- e.printStackTrace();
- }
- return versions;
- }
-
- private static String getDeviceInfos() {
- return ""
- + "\nOS Version: " + System.getProperty("os.version") + "(" + Build.VERSION.INCREMENTAL + ")"
- + "\nOS API Level: " + Build.VERSION.SDK_INT
- + "\nDevice: " + Build.DEVICE
- + "\nManufacturer: " + Build.MANUFACTURER
- + "\nModel (and Product): " + Build.MODEL + " (" + Build.PRODUCT + ")";
- }
-
- private static String getStacktraceOf(Throwable e) {
- StringWriter sw = new StringWriter();
- e.printStackTrace(new PrintWriter(sw));
- return sw.toString();
- }
-
- @UiThread
- public static void handleNextcloudFilesAppNotInstalledException(@NonNull Context context, @NonNull NextcloudFilesAppNotInstalledException exception) {
- UiExceptionManager.showDialogForException(context, exception);
- DeckLog.warn("=============================================================");
- DeckLog.warn("Nextcloud app is not installed. Cannot choose account");
- exception.printStackTrace();
- }
-}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java
index 0390bf96d..04694a058 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java
@@ -6,6 +6,7 @@ import java.util.Locale;
public class MimeTypeUtil {
+ public static final String IMAGE_JPEG = "image/jpeg";
public static final String TEXT_PLAIN = "text/plain";
public static final String TEXT_VCARD = "text/vcard";
public static final String APPLICATION_PDF = "application/pdf";
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java
new file mode 100644
index 000000000..58c465290
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java
@@ -0,0 +1,90 @@
+package it.niedermann.nextcloud.deck.util;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.net.URL;
+
+import it.niedermann.nextcloud.deck.model.Account;
+
+public class ProjectUtil {
+
+ private ProjectUtil() {
+ }
+
+ @NonNull
+ public static Uri getResourceUri(@NonNull Account account, @NonNull String link) throws IllegalArgumentException {
+ try {
+ // Assume link contains a fully qualified Uri including host
+ final URL u = new URL(link);
+ return Uri.parse(u.toString());
+ } catch (Throwable linkIsNotQualified) {
+ try {
+ // Assume link is a absolute path that needs to be concatenated with account url for a complete Uri
+ final URL u = new URL(account.getUrl() + link);
+ return Uri.parse(u.toString());
+ } catch (Throwable throwable) {
+ throw new IllegalArgumentException("Could not parse " + Uri.class.getSimpleName() + ": " + link, throwable);
+ }
+ }
+ }
+
+ /**
+ * extracts the values of board- and card-ID from url.
+ * Depending on what kind of url it gets, it will return a long[] of length 1 or 2:
+ * If the url contains both values, you'll get 2, if it contains only the board, you'll get 1.
+ * <p>
+ * The order is fixed here: [boardId, cardId]
+ *
+ * @param url to extract from
+ * @return extracted and parsed values as long[] with length 1-2
+ */
+ public static long[] extractBoardIdAndCardIdFromUrl(@Nullable String url) throws IllegalArgumentException {
+ if (url == null) {
+ throw new IllegalArgumentException("provided url is null");
+ }
+ url = url.trim();
+ if (url.length() == 0) {
+ throw new IllegalArgumentException("trimmed url is empty");
+ }
+ // extract important part
+ String[] splitByPrefix = url.split(".*(index\\.php/)?apps/deck(/#)?/board/");
+ // split into board- and card part
+ if (splitByPrefix.length < 2) {
+ throw new IllegalArgumentException("This URL doesn't seem to be an URL containing the boardId: \"" + url + "\"");
+ }
+ String[] splitBySeparator = splitByPrefix[1].split("/card/");
+
+ // remove any unexpected stuff
+ if (splitBySeparator.length > 1 && splitBySeparator[1].contains("/")) {
+ splitBySeparator[1] = splitBySeparator[1].split("/")[0];
+ }
+ if (splitBySeparator.length > 0 && splitBySeparator[0].contains("/")) {
+ splitBySeparator[0] = splitBySeparator[0].split("/")[0];
+ }
+
+ if (splitBySeparator.length < 1) {
+ throw new IllegalArgumentException("This URL doesn't seem to be an URL containing the boardId: \"" + url + "\"");
+ }
+
+ // return result
+ long boardId = Long.parseLong(splitBySeparator[0]);
+ if (boardId < 1) {
+ throw new IllegalArgumentException("Invalid boardId \"" + boardId + "\" for url \"" + url + "\".");
+ }
+ if (splitBySeparator.length == 1) {
+ return new long[]{boardId};
+ } else if (splitBySeparator.length == 2) {
+ long cardId = Long.parseLong(splitBySeparator[1]);
+ if (cardId > 0) {
+ return new long[]{boardId, cardId};
+ } else {
+ return new long[]{boardId};
+ }
+ } else {
+ throw new IllegalArgumentException("could not parse URL for boardId and/or cardId: \"" + url + "\"");
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/SpannableUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/SpannableUtil.java
index 54f3c8adc..c2ba6f46a 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/SpannableUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/SpannableUtil.java
@@ -21,28 +21,28 @@ import it.niedermann.nextcloud.deck.R;
public class SpannableUtil {
public static SpannableString strong(@NonNull CharSequence text) {
- SpannableString span = new SpannableString(text);
+ final SpannableString span = new SpannableString(text);
span.setSpan(new StyleSpan(Typeface.BOLD), 0, span.length(), 0);
return span;
}
public static SpannableString disabled(@NonNull CharSequence text, @NonNull Context context) {
- SpannableString span = new SpannableString(text);
+ final SpannableString span = new SpannableString(text);
span.setSpan(new StyleSpan(Typeface.ITALIC), 0, span.length(), 0);
span.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.fg_secondary)), 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return span;
}
public static SpannableString url(@NonNull CharSequence text, @NonNull String target) {
- SpannableString span = new SpannableString(text);
+ final SpannableString span = new SpannableString(text);
span.setSpan(new URLSpan(target), 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return span;
}
public static void setTextWithURL(@NonNull TextView textView, @NonNull Resources resources, @StringRes int containerTextId, @StringRes int linkLabelId, @StringRes int urlId) {
- String linkLabel = resources.getString(linkLabelId);
- String finalText = resources.getString(containerTextId, linkLabel);
- SpannableStringBuilder finalTextBuilder = new SpannableStringBuilder(finalText);
+ final String linkLabel = resources.getString(linkLabelId);
+ final String finalText = resources.getString(containerTextId, linkLabel);
+ final SpannableStringBuilder finalTextBuilder = new SpannableStringBuilder(finalText);
finalTextBuilder.setSpan(new URLSpan(resources.getString(urlId)), finalText.indexOf(linkLabel), finalText.indexOf(linkLabel) + linkLabel.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(finalTextBuilder);
textView.setMovementMethod(new LinkMovementMethod());
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
new file mode 100644
index 000000000..274af332d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java
@@ -0,0 +1,42 @@
+package it.niedermann.nextcloud.deck.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+import it.niedermann.nextcloud.deck.R;
+
+public class VCardUtil {
+
+ private VCardUtil() {
+ // You shall not pass
+ }
+
+ public static Uri getVCardContentUri(@NonNull Context context, @NonNull Uri contactUri) throws NoSuchElementException {
+ final ContentResolver cr = context.getContentResolver();
+ try (final Cursor 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);
+ } else {
+ throw new NoSuchElementException("Cursor has zero entries");
+ }
+ }
+ }
+
+ @ColorInt
+ public static int getColorBasedOnDisplayName(@NonNull Context context, @NonNull String displayName) {
+ final String[] colors = context.getResources().getStringArray(R.array.board_default_colors);
+ final int hashCode = Objects.hashCode(displayName);
+ return Color.parseColor(colors[(hashCode < 0 ? hashCode * -1 : hashCode) % colors.length]);
+ }
+}
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
index 6abbde557..fbc7fc3fc 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java
@@ -3,7 +3,6 @@ package it.niedermann.nextcloud.deck.util;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
-import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.Spannable;
@@ -12,10 +11,13 @@ import android.text.style.ImageSpan;
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.Px;
+import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.widget.TextViewCompat;
@@ -24,34 +26,39 @@ import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
-import java.util.Date;
+import java.time.LocalDate;
import java.util.List;
+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.ocs.comment.Mention;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+import static java.time.temporal.ChronoUnit.DAYS;
public final class ViewUtil {
private ViewUtil() {
}
public static void addAvatar(@NonNull ImageView avatar, @NonNull String baseUrl, @NonNull String userId, @DrawableRes int errorResource) {
- addAvatar(avatar, baseUrl, userId, DimensionUtil.dpToPx(avatar.getContext(), R.dimen.avatar_size), 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(Context context, TextView cardDueDate, Date dueDate) {
- long diff = DateUtil.getDayDifference(new Date(), dueDate);
+ 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;
@@ -69,21 +76,18 @@ public final class ViewUtil {
}
cardDueDate.setBackgroundResource(backgroundDrawable);
- cardDueDate.setTextColor(context.getResources().getColor(textColor));
- TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(context.getResources().getColor(textColor)));
+ 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, @NonNull String color) {
- final Drawable drawable = context.getResources().getDrawable(imageId);
+ public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, @ColorInt int color) {
+ final Drawable drawable = ContextCompat.getDrawable(context, imageId);
+ assert drawable != null;
final Drawable wrapped = DrawableCompat.wrap(drawable).mutate();
- DrawableCompat.setTint(wrapped, Color.parseColor(color));
+ DrawableCompat.setTint(wrapped, color);
return drawable;
}
- public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, int colorId) {
- return getTintedImageView(context, imageId, context.getResources().getString(colorId));
- }
-
/**
* Replaces all mentions in the textView with an avatar and the display name
*
@@ -118,7 +122,7 @@ public final class ViewUtil {
Glide.with(context)
.asBitmap()
.placeholder(R.drawable.ic_person_grey600_24dp)
- .load(account.getUrl() + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd).toString() + "/" + DimensionUtil.dpToPx(context, R.dimen.icon_size_details))
+ .load(account.getUrl() + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd).toString() + "/" + DimensionUtil.INSTANCE.dpToPx(context, R.dimen.icon_size_details))
.apply(RequestOptions.circleCropTransform())
.into(new CustomTarget<Bitmap>() {
@Override
@@ -135,4 +139,12 @@ public final class ViewUtil {
}
textView.setText(messageBuilder);
}
+
+ public static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @ColorRes int colorRes) {
+ if (SDK_INT >= LOLLIPOP) {
+ imageView.setImageTintList(ColorStateList.valueOf(ContextCompat.getColor(context, colorRes)));
+ } else {
+ imageView.setColorFilter(ContextCompat.getColor(context, colorRes));
+ }
+ }
}