From 3ea462ca9e2ae18ba9d869125da8d8d07f2c7854 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Wed, 1 Mar 2023 12:43:53 +0100 Subject: refactor: Unidirectional data flow and single point of truth for current state Signed-off-by: Stefan Niedermann --- app/src/main/AndroidManifest.xml | 24 +- .../niedermann/nextcloud/deck/DeckApplication.java | 156 +-- .../niedermann/nextcloud/deck/api/ApiProvider.java | 43 +- .../deck/exceptions/HandledServerErrors.java | 4 +- .../niedermann/nextcloud/deck/model/Account.java | 23 +- .../it/niedermann/nextcloud/deck/model/Board.java | 7 +- .../nextcloud/deck/model/ocs/Version.java | 27 +- .../nextcloud/deck/persistence/BaseRepository.java | 575 ++++++++++ .../deck/persistence/PreferencesRepository.java | 79 ++ .../deck/persistence/sync/SyncManager.java | 797 +++---------- .../deck/persistence/sync/SyncWorker.java | 69 +- .../persistence/sync/adapters/ServerAdapter.java | 54 +- .../sync/adapters/db/DataBaseAdapter.java | 444 ++++++-- .../persistence/sync/adapters/db/DeckDatabase.java | 4 +- .../sync/adapters/db/dao/AccountDao.java | 6 + .../persistence/sync/adapters/db/dao/BoardDao.java | 18 +- .../persistence/sync/adapters/db/dao/UserDao.java | 2 +- .../adapters/db/migration/Migration_31_32.java | 29 + .../sync/adapters/db/util/LiveDataHelper.java | 75 -- .../sync/adapters/db/util/WrappedLiveData.java | 32 - .../adapters/db/util/extrawurst/Debouncer.java | 75 -- .../db/util/extrawurst/UserSearchLiveData.java | 93 -- .../sync/helpers/providers/BoardDataProvider.java | 20 +- .../helpers/providers/OcsProjectDataProvider.java | 2 +- .../sync/helpers/providers/StackDataProvider.java | 4 + .../persistence/sync/helpers/util/AsyncUtil.java | 14 +- .../sync/helpers/util/ConnectivityUtil.java | 38 + .../nextcloud/deck/ui/ImportAccountActivity.java | 269 +++-- .../nextcloud/deck/ui/ImportAccountViewModel.java | 33 + .../niedermann/nextcloud/deck/ui/MainActivity.java | 1194 -------------------- .../nextcloud/deck/ui/MainViewModel.java | 303 ----- .../nextcloud/deck/ui/PickStackActivity.java | 57 +- .../deck/ui/PushNotificationViewModel.java | 21 +- .../nextcloud/deck/ui/StackChangeCallback.java | 71 ++ .../nextcloud/deck/ui/about/AboutActivity.java | 32 +- .../deck/ui/about/AboutFragmentLicenseTab.java | 42 +- .../ui/accountswitcher/AccountSwitcherDialog.java | 107 +- .../accountswitcher/AccountSwitcherViewHolder.java | 10 +- .../deck/ui/accountswitcher/AccountViewModel.java | 32 + .../ui/archivedboards/ArchivedBoardViewHolder.java | 27 +- .../ui/archivedboards/ArchivedBoardsActivity.java | 164 +++ .../ui/archivedboards/ArchivedBoardsActvitiy.java | 143 --- .../ui/archivedboards/ArchivedBoardsAdapter.java | 12 +- .../ui/archivedboards/ArchivedBoardsViewModel.java | 42 + .../ui/archivedcards/ArchivedCardsActvitiy.java | 88 -- .../ui/archivedcards/ArchivedCardsAdapter.java | 68 -- .../deck/ui/attachments/AttachmentsViewModel.java | 10 +- .../deck/ui/board/ArchiveBoardListener.java | 5 +- .../nextcloud/deck/ui/board/BoardAdapter.java | 52 - .../deck/ui/board/EditBoardDialogFragment.java | 115 -- .../nextcloud/deck/ui/board/EditBoardListener.java | 13 - .../board/accesscontrol/AccessControlAdapter.java | 23 +- .../accesscontrol/AccessControlDialogFragment.java | 59 +- .../accesscontrol/AccessControlViewModel.java | 48 + .../ui/board/edit/EditBoardDialogFragment.java | 155 +++ .../deck/ui/board/edit/EditBoardListener.java | 17 + .../deck/ui/board/edit/EditBoardViewModel.java | 24 + .../ui/board/managelabels/LabelsViewModel.java | 42 + .../managelabels/ManageLabelsDialogFragment.java | 34 +- .../deck/ui/card/AbstractCardViewHolder.java | 17 +- .../nextcloud/deck/ui/card/CardActionListener.java | 23 + .../nextcloud/deck/ui/card/CardAdapter.java | 163 +-- .../nextcloud/deck/ui/card/CardTabAdapter.java | 12 +- .../deck/ui/card/CompactCardViewHolder.java | 6 +- .../deck/ui/card/DefaultCardViewHolder.java | 6 +- .../nextcloud/deck/ui/card/EditActivity.java | 73 +- .../nextcloud/deck/ui/card/EditCardViewModel.java | 55 +- .../deck/ui/card/LabelAutoCompleteAdapter.java | 134 ++- .../nextcloud/deck/ui/card/NewCardDialog.java | 153 ++- .../nextcloud/deck/ui/card/NewCardViewModel.java | 76 ++ .../nextcloud/deck/ui/card/SelectCardListener.java | 4 +- .../deck/ui/card/UserAutoCompleteAdapter.java | 97 +- .../ui/card/activities/CardActivityViewHolder.java | 8 +- .../deck/ui/card/assignee/CardAssigneeDialog.java | 7 +- .../ui/card/attachments/AttachmentViewHolder.java | 9 +- .../card/attachments/CardAttachmentsFragment.java | 68 +- .../DeleteAttachmentDialogFragment.java | 8 +- .../attachments/previewdialog/PreviewDialog.java | 8 +- .../deck/ui/card/comments/CardCommentsAdapter.java | 32 +- .../ui/card/comments/CardCommentsFragment.java | 78 +- .../card/comments/CardCommentsMentionProposer.java | 73 +- .../deck/ui/card/comments/CommentsViewModel.java | 21 +- .../ui/card/comments/ItemCommentViewHolder.java | 29 +- .../deck/ui/card/details/AssigneeViewHolder.java | 14 +- .../deck/ui/card/details/CardDetailsFragment.java | 84 +- .../deck/ui/exception/ExceptionDialogFragment.java | 2 +- .../deck/ui/exception/tips/TipsAdapter.java | 6 + .../deck/ui/filter/FilterDialogFragment.java | 7 +- .../deck/ui/filter/FilterDueTypeAdapter.java | 23 +- .../deck/ui/filter/FilterDueTypeFragment.java | 3 +- .../deck/ui/filter/FilterLabelsAdapter.java | 29 +- .../deck/ui/filter/FilterLabelsFragment.java | 23 +- .../deck/ui/filter/FilterUserAdapter.java | 39 +- .../deck/ui/filter/FilterUserFragment.java | 28 +- .../nextcloud/deck/ui/filter/FilterViewModel.java | 41 +- .../nextcloud/deck/ui/main/DrawerMenuInflater.java | 122 ++ .../nextcloud/deck/ui/main/MainActivity.java | 928 +++++++++++++++ .../ui/main/MainActivityNavigationHandler.java | 120 ++ .../nextcloud/deck/ui/main/MainViewModel.java | 256 +++++ .../ui/manageaccounts/ManageAccountViewHolder.java | 16 +- .../ui/manageaccounts/ManageAccountsActivity.java | 37 +- .../ui/manageaccounts/ManageAccountsViewModel.java | 26 +- .../deck/ui/movecard/MoveCardDialogFragment.java | 43 +- .../deck/ui/pickstack/PickStackFragment.java | 320 ++++-- .../deck/ui/pickstack/PickStackViewModel.java | 36 +- .../deck/ui/preparecreate/AccountAdapter.java | 13 +- .../deck/ui/preparecreate/BoardAdapter.java | 6 +- .../deck/ui/preparecreate/PickStackAdapter.java | 95 ++ .../deck/ui/preparecreate/PickStackViewHolder.java | 46 + .../ui/preparecreate/PrepareCreateActivity.java | 10 +- .../ui/preparecreate/PrepareCreateViewModel.java | 24 +- .../deck/ui/preparecreate/StackAdapter.java | 44 - .../deck/ui/settings/PreferencesViewModel.java | 39 + .../deck/ui/settings/SettingsActivity.java | 36 +- .../deck/ui/settings/SettingsFragment.java | 30 +- .../sharetarget/ShareProgressDialogFragment.java | 8 +- .../deck/ui/sharetarget/ShareTargetActivity.java | 32 +- .../deck/ui/stack/DeleteStackDialogFragment.java | 30 +- .../deck/ui/stack/DeleteStackListener.java | 2 +- .../deck/ui/stack/EditStackDialogFragment.java | 91 +- .../nextcloud/deck/ui/stack/EditStackListener.java | 6 +- .../nextcloud/deck/ui/stack/StackAdapter.java | 43 +- .../nextcloud/deck/ui/stack/StackFragment.java | 238 ++-- .../nextcloud/deck/ui/stack/StackViewModel.java | 87 ++ .../deck/ui/takephoto/TakePhotoActivity.java | 7 +- .../deck/ui/takephoto/TakePhotoViewModel.java | 31 +- .../deck/ui/theme/DeckViewThemeUtils.java | 119 +- .../nextcloud/deck/ui/theme/ThemeUtils.java | 32 +- .../deck/ui/theme/ThemedDatePickerDialog.java | 29 +- .../deck/ui/theme/ThemedDialogFragment.java | 16 +- .../deck/ui/theme/ThemedPreferenceCategory.java | 17 +- .../nextcloud/deck/ui/theme/ThemedSnackbar.java | 9 +- .../deck/ui/theme/ThemedTimePickerDialog.java | 32 +- .../deck/ui/tiles/EditCardTileService.java | 3 - .../ui/upcomingcards/UpcomingCardsActivity.java | 22 +- .../ui/upcomingcards/UpcomingCardsAdapter.java | 7 +- .../UpcomingCardsOptionsItemSelectedListener.java | 21 +- .../ui/upcomingcards/UpcomingCardsViewModel.java | 55 +- .../nextcloud/deck/ui/view/ColorChooser.java | 23 +- .../nextcloud/deck/ui/view/EmptyContentView.java | 20 +- .../nextcloud/deck/ui/view/OverlappingAvatars.java | 3 +- .../nextcloud/deck/ui/viewmodel/BaseViewModel.java | 40 + .../nextcloud/deck/ui/viewmodel/SyncViewModel.java | 80 ++ .../deck/ui/widget/filter/FilterWidget.java | 12 +- .../deck/ui/widget/filter/FilterWidgetFactory.java | 6 +- .../ui/widget/filter/FilterWidgetViewModel.java | 10 +- .../singlecard/SelectCardForWidgetActivity.java | 47 +- .../ui/widget/singlecard/SingleCardWidget.java | 10 +- .../widget/singlecard/SingleCardWidgetFactory.java | 8 +- .../deck/ui/widget/stack/StackWidget.java | 18 +- .../stack/StackWidgetConfigurationViewModel.java | 10 +- .../deck/ui/widget/stack/StackWidgetFactory.java | 10 +- .../deck/ui/widget/upcoming/UpcomingWidget.java | 20 +- .../ui/widget/upcoming/UpcomingWidgetFactory.java | 8 +- .../nextcloud/deck/util/AutoCompleteAdapter.java | 74 +- .../nextcloud/deck/util/DrawerMenuUtil.java | 105 -- .../deck/util/ExecutorServiceProvider.java | 6 + .../nextcloud/deck/util/OnTextChangedWatcher.java | 35 + .../niedermann/nextcloud/deck/util/VCardUtil.java | 9 +- .../niedermann/nextcloud/deck/util/ViewUtil.java | 84 -- app/src/main/res/drawable/selected.xml | 14 - app/src/main/res/drawable/selected_check.xml | 14 + app/src/main/res/layout/activity_about.xml | 4 +- app/src/main/res/layout/activity_archived.xml | 4 +- app/src/main/res/layout/activity_attachments.xml | 2 +- app/src/main/res/layout/activity_edit.xml | 4 +- app/src/main/res/layout/activity_exception.xml | 4 +- app/src/main/res/layout/activity_filter_widget.xml | 4 +- .../main/res/layout/activity_import_account.xml | 2 +- app/src/main/res/layout/activity_main.xml | 10 +- .../main/res/layout/activity_manage_accounts.xml | 4 +- app/src/main/res/layout/activity_pick_stack.xml | 17 +- .../main/res/layout/activity_push_notification.xml | 4 +- app/src/main/res/layout/activity_settings.xml | 20 +- .../main/res/layout/activity_upcoming_cards.xml | 4 +- .../main/res/layout/dialog_account_switcher.xml | 2 +- app/src/main/res/layout/dialog_move_card.xml | 26 +- .../main/res/layout/fragment_about_credits_tab.xml | 12 - app/src/main/res/layout/fragment_pick_stack.xml | 16 +- app/src/main/res/layout/fragment_stack.xml | 2 +- app/src/main/res/layout/item_account_choose.xml | 2 +- app/src/main/res/layout/item_board.xml | 10 - app/src/main/res/layout/item_card_compact.xml | 4 +- app/src/main/res/layout/item_card_default.xml | 2 - .../res/layout/item_card_default_only_title.xml | 2 - app/src/main/res/layout/item_filter_duetype.xml | 3 +- app/src/main/res/layout/item_filter_label.xml | 3 +- app/src/main/res/layout/item_filter_user.xml | 3 +- .../main/res/layout/item_prepare_create_stack.xml | 31 +- app/src/main/res/layout/item_tip.xml | 3 +- .../main/res/layout/widget_empty_content_view.xml | 29 +- app/src/main/res/values-night/colors.xml | 30 +- app/src/main/res/values-v24/styles.xml | 9 - app/src/main/res/values/colors.xml | 58 +- app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/setup.xml | 5 +- app/src/main/res/values/strings.xml | 9 +- app/src/main/res/values/styles.xml | 4 +- 198 files changed, 6454 insertions(+), 5231 deletions(-) create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/migration/Migration_31_32.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/LiveDataHelper.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/WrappedLiveData.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/Debouncer.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/ConnectivityUtil.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountViewModel.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/StackChangeCallback.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActivity.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsViewModel.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardDialogFragment.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardListener.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/board/edit/EditBoardViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/LabelsViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardActionListener.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/card/NewCardViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/main/DrawerMenuInflater.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivityNavigationHandler.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackAdapter.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PickStackViewHolder.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/PreferencesViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/BaseViewModel.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/ui/viewmodel/SyncViewModel.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java create mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/util/OnTextChangedWatcher.java delete mode 100644 app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java delete mode 100644 app/src/main/res/drawable/selected.xml create mode 100644 app/src/main/res/drawable/selected_check.xml delete mode 100644 app/src/main/res/layout/item_board.xml delete mode 100644 app/src/main/res/values-v24/styles.xml (limited to 'app/src/main') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e8e3ae6e5..c05e81516 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,8 +29,9 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" + android:enableOnBackInvokedCallback="true" tools:ignore="GoogleAppIndexingWarning" - tools:targetApi="q"> + tools:targetApi="tiramisu"> @@ -67,7 +68,7 @@ - - + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.main.MainActivity" /> currentAccountColor$; - private static LiveData currentBoardColor$; + private final ExecutorService executor = new ThreadPoolExecutor(0, 2, 0L, TimeUnit.SECONDS, new SynchronousQueue<>()); @Override public void onCreate() { + final var repo = new PreferencesRepository(this); + if (BuildConfig.DEBUG) { enableStrictModeLogging(); } - PREF_KEY_THEME = getString(R.string.pref_key_dark_theme); - PREF_KEY_DEBUGGING = getString(R.string.pref_key_debugging); - setAppTheme(getAppThemeSetting(this)); - DeckLog.enablePersistentLogs(isPersistentLoggingEnabled(this)); - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - currentAccountColor$ = distinctUntilChanged(new SharedPreferenceIntLiveData(sharedPreferences, - getString(R.string.shared_preference_last_account_color), - ContextCompat.getColor(this, R.color.defaultBrand))); - currentBoardColor$ = distinctUntilChanged(new SharedPreferenceIntLiveData(sharedPreferences, - getString(R.string.shared_preference_theme_main), - ContextCompat.getColor(this, R.color.defaultBrand))); + repo.getAppThemeSetting().thenAcceptAsync(repo::setAppTheme, executor); + repo.isDebugModeEnabled().thenAcceptAsync(DeckLog::enablePersistentLogs, executor); + super.onCreate(); } @Override public void onLowMemory() { super.onLowMemory(); + DeckLog.error("--- Low memory: Clear Glide cache ---"); + CustomAppGlideModule.clearCache(this); + DeckLog.error("--- Low memory: Clear debug log ---"); DeckLog.clearDebugLog(); - DeckLog.error("--- cleared log because of low memory ---"); - } - - // --------- - // Debugging - // --------- - - private static boolean isPersistentLoggingEnabled(@NonNull Context context) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - final boolean enabled = sharedPreferences.getBoolean(PREF_KEY_DEBUGGING, false); - DeckLog.log("--- Read:", PREF_KEY_DEBUGGING, "→", enabled); - return enabled; } - private static void enableStrictModeLogging() { + private void enableStrictModeLogging() { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() - // SSO library: setCurrentSingleSignOnAccount works synchronously - // Deck app: Handling of current account / board / stack works synchronously .permitDiskReads() .penaltyLog() .build()); @@ -82,95 +49,4 @@ public class DeckApplication extends Application { .penaltyLog() .build()); } - - // ----------------- - // Day / Night theme - // ----------------- - - public static void setAppTheme(int setting) { - setDefaultNightMode(setting); - } - - public static int getAppThemeSetting(@NonNull Context context) { - final var prefs = PreferenceManager.getDefaultSharedPreferences(context); - String mode; - try { - mode = prefs.getString(PREF_KEY_THEME, context.getString(R.string.pref_value_theme_system_default)); - } catch (ClassCastException e) { - mode = prefs.getBoolean(PREF_KEY_THEME, false) ? context.getString(R.string.pref_value_theme_dark) : context.getString(R.string.pref_value_theme_light); - } - return Integer.parseInt(mode); - } - - public static boolean isDarkTheme(@NonNull Context context) { - final var darkModeSetting = getAppThemeSetting(context); - return darkModeSetting == Integer.parseInt(context.getString(R.string.pref_value_theme_system_default)) - ? PlatformThemeUtil.isDarkMode(context) - : darkModeSetting == Integer.parseInt(context.getString(R.string.pref_value_theme_dark)); - } - - // -------------------------------------- - // Current account / board / stack states - // -------------------------------------- - - public static void saveCurrentAccount(@NonNull Context context, @NonNull Account account) { - SingleAccountHelper.setCurrentAccount(context, account.getName()); - final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_account), "→", account.getId()); - editor.putLong(context.getString(R.string.shared_preference_last_account), account.getId()); - DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_account_color), "→", account.getColor()); - editor.putInt(context.getString(R.string.shared_preference_last_account_color), account.getColor()); - editor.apply(); - } - - public static LiveData readCurrentAccountColor() { - return currentAccountColor$; - } - - public static LiveData readCurrentBoardColor() { - return currentBoardColor$; - } - - @ColorInt - public static int readCurrentAccountColor(@NonNull Context context) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - @ColorInt final int accountColor = sharedPreferences.getInt(context.getString(R.string.shared_preference_last_account_color), context.getApplicationContext().getResources().getColor(R.color.defaultBrand)); - DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account_color), "→", accountColor); - return accountColor; - } - - public static long readCurrentAccountId(@NonNull Context context) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - final long accountId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_account), NO_ACCOUNT_ID); - DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_account), "→", accountId); - return accountId; - } - - public static void saveCurrentBoardId(@NonNull Context context, long accountId, long boardId) { - final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId); - editor.putLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, boardId); - editor.apply(); - } - - public static long readCurrentBoardId(@NonNull Context context, long accountId) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - final long boardId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_board_for_account_) + accountId, NO_BOARD_ID); - DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_board_for_account_) + accountId, "→", boardId); - return boardId; - } - - public static void saveCurrentStackId(@NonNull Context context, long accountId, long boardId, long stackId) { - final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - DeckLog.log("--- Write:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, "→", stackId); - editor.putLong(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, stackId); - editor.apply(); - } - - public static long readCurrentStackId(@NonNull Context context, long accountId, long boardId) { - final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - final long savedStackId = sharedPreferences.getLong(context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, NO_STACK_ID); - DeckLog.log("--- Read:", context.getString(R.string.shared_preference_last_stack_for_account_and_board_) + accountId + "_" + boardId, "→", savedStackId); - return savedStackId; - } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java index aa5733044..2118fc068 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/ApiProvider.java @@ -3,16 +3,10 @@ package it.niedermann.nextcloud.deck.api; import android.content.Context; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.api.NextcloudAPI; -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; -import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; -import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; -import it.niedermann.nextcloud.deck.DeckLog; import retrofit2.NextcloudRetrofitApiBuilder; /** @@ -27,36 +21,21 @@ public class ApiProvider { private NextcloudServerAPI nextcloudAPI; @NonNull private final Context context; - @Nullable - private final String ssoAccountName; - private SingleSignOnAccount ssoAccount; + private final SingleSignOnAccount ssoAccount; - public ApiProvider(@NonNull Context context, @Nullable String ssoAccountName) { + public ApiProvider(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { this.context = context; - this.ssoAccountName = ssoAccountName; - setAccount(); + this.ssoAccount = ssoAccount; } public synchronized void initSsoApi(@NonNull final NextcloudAPI.ApiConnectedListener callback) { - if(this.deckAPI == null) { + if (this.deckAPI == null) { final NextcloudAPI nextcloudAPI = new NextcloudAPI(context, ssoAccount, GsonConfig.getGson(), callback); this.deckAPI = new NextcloudRetrofitApiBuilder(nextcloudAPI, DECK_API_ENDPOINT).create(DeckAPI.class); this.nextcloudAPI = new NextcloudRetrofitApiBuilder(nextcloudAPI, NC_API_ENDPOINT).create(NextcloudServerAPI.class); } } - private void setAccount() { - try { - if (ssoAccountName == null) { - this.ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context); - } else { - this.ssoAccount = AccountImporter.getSingleSignOnAccount(context, ssoAccountName); - } - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { - DeckLog.logError(e); - } - } - public DeckAPI getDeckAPI() { return deckAPI; } @@ -65,18 +44,4 @@ public class ApiProvider { return nextcloudAPI; } - public String getServerUrl(){ - if (ssoAccount == null) { - setAccount(); - } - return ssoAccount.url; - } - - public String getApiPath() { - return DECK_API_ENDPOINT; - } - - public String getApiUrl() { - return getServerUrl() + getApiPath(); - } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java index 11fbf8e14..a5d3e3155 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/HandledServerErrors.java @@ -7,8 +7,8 @@ import com.google.gson.JsonSyntaxException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; public enum HandledServerErrors { - UNKNOWN(1337, "hopefully won't occurr"), - LABELS_TITLE_MUST_BE_UNIQUE(400, "title must be unique"), + UNKNOWN(1337, "hopefully won't occur"), + LABELS_TITLE_MUST_BE_UNIQUE(400, "Title must be unique"), ATTACHMENTS_FILE_ALREADY_EXISTS(409, "File already exists."), ; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java index d7c4e8e8f..a54ad0986 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java @@ -12,7 +12,7 @@ import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; -import com.nextcloud.android.sso.model.SingleSignOnAccount; +import com.bumptech.glide.Glide; import java.io.Serializable; import java.util.Objects; @@ -20,7 +20,7 @@ import java.util.Objects; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.Version; -import it.niedermann.nextcloud.deck.ui.accountswitcher.AccountSwitcherDialog; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; @Entity(indices = {@Index(value = "name", unique = true)}) public class Account implements Serializable { @@ -218,12 +218,17 @@ public class Account implements Serializable { } /** - * A cache buster parameter is added for duplicate account names on different hosts which shall be fetched from the same {@link SingleSignOnAccount} (e. g. {@link AccountSwitcherDialog}) - * - * @return an {@link String} to fetch the avatar for this account. + * @return The {@link #getAvatarUrl(int, String)} of this {@link Account} */ - public String getAvatarUrl(@Px int size) { - return getUrl() + "/index.php/avatar/" + Uri.encode(getUserName()) + "/" + size; + public SingleSignOnUrl getAvatarUrl(@Px int size) { + return getAvatarUrl(size, getUserName()); + } + + /** + * @return a {@link SingleSignOnUrl} to fetch the avatar of the given userName from the instance of this {@link Account} via {@link Glide}. + */ + public SingleSignOnUrl getAvatarUrl(@Px int size, @NonNull String userName) { + return new SingleSignOnUrl(getName(), getUrl() + "/index.php/avatar/" + Uri.encode(userName) + "/" + size); } @Override @@ -239,9 +244,7 @@ public class Account implements Serializable { url.equals(account.url) && color.equals(account.color) && textColor.equals(account.textColor) && - serverDeckVersion.equals(account.serverDeckVersion) && - Objects.equals(etag, account.etag) && - Objects.equals(boardsEtag, account.boardsEtag); + serverDeckVersion.equals(account.serverDeckVersion); } @Override diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java index ebe9805ed..d27c35545 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java @@ -12,6 +12,7 @@ import com.google.gson.annotations.JsonAdapter; import java.io.Serializable; import java.time.Instant; +import java.util.Objects; import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; @@ -218,9 +219,9 @@ public class Board extends AbstractRemoteEntity implements Serializable { if (permissionEdit != board.permissionEdit) return false; if (permissionManage != board.permissionManage) return false; if (permissionShare != board.permissionShare) return false; - if (title != null ? !title.equals(board.title) : board.title != null) return false; - if (color != null ? !color.equals(board.color) : board.color != null) return false; - return deletedAt != null ? deletedAt.equals(board.deletedAt) : board.deletedAt == null; + if (!Objects.equals(title, board.title)) return false; + if (!Objects.equals(color, board.color)) return false; + return Objects.equals(deletedAt, board.deletedAt); } @Override diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java index 4f4698a5a..355307ce0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Version.java @@ -19,9 +19,9 @@ public class Version implements Comparable { private static final Version VERSION_1_3_0 = new Version("1.3.0", 1, 3, 0); private String originalVersion = "?"; - private int major; - private int minor; - private int patch; + private final int major; + private final int minor; + private final int patch; public Version(String originalVersion, int major, int minor, int patch) { this(major, minor, patch); @@ -54,12 +54,8 @@ public class Version implements Comparable { return originalVersion; } - public void setOriginalVersion(String originalVersion) { - this.originalVersion = originalVersion; - } - public static Version of(String versionString) { - int major = 0, minor = 0, micro = 0; + int major = 0, minor = 0, patch = 0; if (versionString != null) { final String[] split = versionString.split("\\."); if (split.length > 0) { @@ -67,12 +63,12 @@ public class Version implements Comparable { if (split.length > 1) { minor = extractNumber(split[1]); if (split.length > 2) { - micro = extractNumber(split[2]); + patch = extractNumber(split[2]); } } } } - return new Version(versionString, major, minor, micro); + return new Version(versionString, major, minor, patch); } private static int extractNumber(String containsNumbers) { @@ -186,6 +182,17 @@ public class Version implements Comparable { : 100; } + /** + * The first response structure of the very first call to at least the /boards endpoint of the Deck API can be different compared to all following calls. + * This behavior is tracked in an upstream issue and might be resolved in the future with a specific version. + * + * @return whether or not it is needed to make one request that must be ignored due to a different data structure before performing any other requests. + * @see issue + */ + public boolean firstCallHasDifferentResponseStructure() { + return true; + } + /** * 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/persistence/BaseRepository.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java new file mode 100644 index 000000000..570b36b13 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/BaseRepository.java @@ -0,0 +1,575 @@ +package it.niedermann.nextcloud.deck.persistence; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.AnyThread; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; +import androidx.lifecycle.LiveData; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import it.niedermann.android.reactivelivedata.ReactiveLiveData; +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.api.LastSyncUtil; +import it.niedermann.nextcloud.deck.api.ResponseCallback; +import it.niedermann.nextcloud.deck.model.AccessControl; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; +import it.niedermann.nextcloud.deck.model.enums.DBStatus; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; +import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; +import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.internal.FilterInformation; +import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; +import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil; +import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterItem; + +/** + * Allows basic local access to the {@link DataBaseAdapter} layer but also to some app states which are stored in {@link SharedPreferences}. + *

+ * This repository does not know anything about remote synchronization. + */ +@SuppressWarnings("WeakerAccess") +public class BaseRepository { + + @NonNull + protected final Context context; + @NonNull + protected final DataBaseAdapter dataBaseAdapter; + @NonNull + protected final ExecutorService executor; + @NonNull + protected final ConnectivityUtil connectivityUtil; + @NonNull + protected final ReactiveLiveData currentAccountId$; + + public BaseRepository(@NonNull Context context) { + this(context, new ConnectivityUtil(context)); + } + + protected BaseRepository(@NonNull Context context, @NonNull ConnectivityUtil connectivityUtil) { + this(context, connectivityUtil, new DataBaseAdapter(context.getApplicationContext()), Executors.newCachedThreadPool()); + } + + protected BaseRepository(@NonNull Context context, + @NonNull ConnectivityUtil connectivityUtil, + @NonNull DataBaseAdapter databaseAdapter, + @NonNull ExecutorService executor) { + this.context = context.getApplicationContext(); + this.connectivityUtil = connectivityUtil; + this.dataBaseAdapter = databaseAdapter; + this.executor = executor; + this.currentAccountId$ = new ReactiveLiveData<>(dataBaseAdapter.getCurrentAccountId$()).distinctUntilChanged(); + LastSyncUtil.init(context.getApplicationContext()); + } + + public void saveCurrentAccount(@NonNull Account account) { + dataBaseAdapter.saveCurrentAccount(account); + } + + public LiveData getCurrentAccountId$() { + return this.currentAccountId$; + } + + public CompletableFuture getCurrentAccountId() { + return dataBaseAdapter.getCurrentAccountId(); + } + + public LiveData getAccountColor(long accountId) { + return dataBaseAdapter.getAccountColor(accountId); + } + + @ColorInt + public CompletableFuture getCurrentAccountColor(long accountId) { + return dataBaseAdapter.getCurrentAccountColor(accountId); + } + + // ------------- + // Current board + // ------------- + + public void saveCurrentBoardId(long accountId, long boardId) { + dataBaseAdapter.saveCurrentBoardId(accountId, boardId); + } + + public LiveData getCurrentBoardId$(long accountId) { + return dataBaseAdapter.getCurrentBoardId$(accountId); + } + + public LiveData getBoardColor$(long accountId, long boardId) { + return dataBaseAdapter.getBoardColor$(accountId, boardId); + } + + public CompletableFuture getCurrentBoardColor(long accountId, long boardId) { + return dataBaseAdapter.getCurrentBoardColor(accountId, boardId); + } + + // ------------- + // Current stack + // ------------- + + public void saveCurrentStackId(long accountId, long boardId, long stackId) { + dataBaseAdapter.saveCurrentStackId(accountId, boardId, stackId); + } + + public LiveData getCurrentStackId$(long accountId, long boardId) { + return dataBaseAdapter.getCurrentStackId$(accountId, boardId); + } + + // ================================================================================================================================== + + @AnyThread + public void createAccount(@NonNull Account account, @NonNull IResponseCallback callback) { + executor.submit(() -> { + try { + callback.onResponse(dataBaseAdapter.createAccountDirectly(account)); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void deleteAccount(long id) { + executor.submit(() -> { + dataBaseAdapter.saveNeighbourOfAccount(id); + dataBaseAdapter.removeCurrentBoardId(id); + dataBaseAdapter.deleteAccount(id); + LastSyncUtil.resetLastSyncDate(id); + }); + } + + @AnyThread + public LiveData hasAccounts() { + return dataBaseAdapter.hasAccounts(); + } + + @UiThread + public LiveData readAccount(long id) { + return dataBaseAdapter.readAccount(id); + } + + @WorkerThread + public Account readAccountDirectly(long id) { + return dataBaseAdapter.readAccountDirectly(id); + } + + @WorkerThread + public Account readAccountDirectly(@Nullable String name) { + return dataBaseAdapter.readAccountDirectly(name); + } + + @UiThread + public LiveData readAccount(@Nullable String name) { + return dataBaseAdapter.readAccount(name); + } + + @WorkerThread + public Long getBoardLocalIdByAccountAndCardRemoteIdDirectly(long accountId, long cardRemoteId) { + return dataBaseAdapter.getBoardLocalIdByAccountAndCardRemoteIdDirectly(accountId, cardRemoteId); + } + + @UiThread + public LiveData> readAccounts() { + return dataBaseAdapter.readAccounts(); + } + + @WorkerThread + public List readAccountsDirectly() { + return dataBaseAdapter.getAllAccountsDirectly(); + } + + /** + * @param localProjectId LocalId of the OcsProject + * @return all {@link OcsProjectResource}s of the Project + */ + @AnyThread + public LiveData> 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 Boards depending on archived parameter + */ + @AnyThread + public LiveData> getBoards(long accountId, boolean archived) { + return dataBaseAdapter.getBoards(accountId, archived); + } + + /** + * @param accountId ID of the account + * @param archived Decides whether only archived or not-archived boards for the specified account will be returned + * @return all archived or non-archived FullBoards depending on archived parameter + */ + @AnyThread + public LiveData> getFullBoards(long accountId, boolean archived) { + return dataBaseAdapter.getFullBoards(accountId, archived); + } + + /** + * Get all non-archived FullBoards with edit permissions for the specified account. + * + * @param accountId ID of the account + * @return all non-archived Boards with edit permission + */ + @AnyThread + public LiveData> getBoardsWithEditPermission(long accountId) { + return dataBaseAdapter.getBoardsWithEditPermission(accountId); + } + + @AnyThread + public LiveData hasArchivedBoards(long accountId) { + return dataBaseAdapter.hasArchivedBoards(accountId); + } + + public LiveData getFullBoardById(Long accountId, Long localId) { + return dataBaseAdapter.getFullBoardById(accountId, localId); + } + + public Board getBoardById(Long localId) { + return dataBaseAdapter.getBoardByLocalIdDirectly(localId); + } + + public LiveData> getFullCommentsForLocalCardId(long localCardId) { + return dataBaseAdapter.getFullCommentsForLocalCardId(localCardId); + } + + public LiveData> getStacksForBoard(long accountId, long localBoardId) { + return dataBaseAdapter.getStacksForBoard(accountId, localBoardId); + } + + public LiveData getStack(long accountId, long localStackId) { + return dataBaseAdapter.getStack(accountId, localStackId); + } + + public void countCardsInStackDirectly(long accountId, long localStackId, @NonNull IResponseCallback callback) { + executor.submit(() -> dataBaseAdapter.countCardsInStackDirectly(accountId, localStackId, callback)); + } + + public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback callback) { + executor.submit(() -> dataBaseAdapter.countCardsWithLabel(localLabelId, callback)); + } + + public LiveData getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { + return dataBaseAdapter.getCardWithProjectsByLocalId(accountId, cardLocalId); + } + + public LiveData> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { + return dataBaseAdapter.getFullCardsForStack(accountId, localStackId, filter); + } + + @WorkerThread + public Long getBoardLocalIdByLocalCardIdDirectly(long localCardId) { + return dataBaseAdapter.getBoardLocalIdByLocalCardIdDirectly(localCardId); + } + + @WorkerThread + public AccessControl getAccessControlByRemoteIdDirectly(long accountId, Long id) { + return dataBaseAdapter.getAccessControlByRemoteIdDirectly(accountId, id); + } + + public LiveData> getAccessControlByLocalBoardId(long accountId, Long id) { + return dataBaseAdapter.getAccessControlByLocalBoardId(accountId, id); + } + + // --- User search --- + + public LiveData> findProposalsForUsersToAssignForACL(final long accountId, long boardId, final int topX) { + return dataBaseAdapter.findProposalsForUsersToAssignForACL(accountId, boardId, topX); + } + + public LiveData> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedToACL, final String constraint) { + return dataBaseAdapter.searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedToACL, constraint); + } + + public LiveData> findProposalsForUsersToAssignForCards(final long accountId, long boardId, long notAssignedToLocalCardId, final int topX) { + return dataBaseAdapter.findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX); + } + + public LiveData> searchUserByUidOrDisplayNameForCards(final long accountId, final long boardId, final long notYetAssignedToLocalCardId, final String searchTerm) { + return dataBaseAdapter.searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, searchTerm); + } + + // --- Label search --- + + public LiveData> findProposalsForLabelsToAssign(final long accountId, final long boardId) { + return findProposalsForLabelsToAssign(accountId, boardId, -1L); + } + + public LiveData> findProposalsForLabelsToAssign(final long accountId, final long boardId, long notAssignedToLocalCardId) { + return dataBaseAdapter.findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId); + } + + public LiveData> searchNotYetAssignedLabelsByTitle(@NonNull Account account, final long boardId, final long notYetAssignedToLocalCardId, @NonNull String searchTerm) { + return dataBaseAdapter.searchNotYetAssignedLabelsByTitle(account.getId(), boardId, notYetAssignedToLocalCardId, searchTerm); + } + + public LiveData getUserByLocalId(long accountId, long localId) { + return dataBaseAdapter.getUserByLocalId(accountId, localId); + } + + public LiveData getUserByUid(long accountId, String uid) { + return dataBaseAdapter.getUserByUid(accountId, uid); + } + + @WorkerThread + public User getUserByUidDirectly(long accountId, String uid) { + return dataBaseAdapter.getUserByUidDirectly(accountId, uid); + } + + public LiveData getBoardByRemoteId(long accountId, long remoteId) { + return dataBaseAdapter.getBoardByRemoteId(accountId, remoteId); + } + + @WorkerThread + public Board getBoardByRemoteIdDirectly(long accountId, long remoteId) { + return dataBaseAdapter.getBoardByRemoteIdDirectly(accountId, remoteId); + } + + public LiveData getStackByRemoteId(long accountId, long localBoardId, long remoteId) { + return dataBaseAdapter.getStackByRemoteId(accountId, localBoardId, remoteId); + } + + public LiveData getCardByRemoteID(long accountId, long remoteId) { + return dataBaseAdapter.getCardByRemoteID(accountId, remoteId); + } + + @WorkerThread + public Optional getCardByRemoteIDDirectly(long accountId, long remoteId) { + return Optional.ofNullable(dataBaseAdapter.getCardByRemoteIDDirectly(accountId, remoteId)); + } + + public long createUser(long accountId, User user) { + return dataBaseAdapter.createUser(accountId, user); + } + + public void updateUser(long accountId, @NonNull User user) { + dataBaseAdapter.updateUser(accountId, user, true); + } + + protected void reorderLocally(List cardsOfNewStack, @NonNull FullCard movedCard, long newStackId, int newOrder) { + // set new stack and order + Card movedInnerCard = movedCard.getCard(); + int oldOrder = movedInnerCard.getOrder(); + long oldStackId = movedInnerCard.getStackId(); + + + List changedCards = new ArrayList<>(); + + int startingAtOrder = newOrder; + if (oldStackId == newStackId) { + // card was only reordered in the same stack + movedInnerCard.setStatusEnum(movedInnerCard.getStatus() == DBStatus.LOCAL_MOVED.getId() ? DBStatus.LOCAL_MOVED : DBStatus.LOCAL_EDITED); + // move direction? + if (oldOrder > newOrder) { + // up + changedCards.add(movedCard.getCard()); + for (FullCard cardToUpdate : cardsOfNewStack) { + Card cardEntity = cardToUpdate.getCard(); + if (cardEntity.getOrder() < newOrder) { + continue; + } + if (cardEntity.getOrder() >= oldOrder) { + break; + } + changedCards.add(cardEntity); + } + } else { + // down + startingAtOrder = oldOrder; + for (FullCard cardToUpdate : cardsOfNewStack) { + Card cardEntity = cardToUpdate.getCard(); + if (cardEntity.getOrder() <= oldOrder) { + continue; + } + if (cardEntity.getOrder() > newOrder) { + break; + } + changedCards.add(cardEntity); + } + changedCards.add(movedCard.getCard()); + } + } else { + // card was moved to an other stack + movedInnerCard.setStackId(newStackId); + movedInnerCard.setStatusEnum(DBStatus.LOCAL_MOVED); + changedCards.add(movedCard.getCard()); + for (FullCard fullCard : cardsOfNewStack) { + // skip unchanged cards + if (fullCard.getCard().getOrder() < newOrder) { + continue; + } + changedCards.add(fullCard.getCard()); + } + } + reorderAscending(movedInnerCard, changedCards, startingAtOrder); + } + + private void reorderAscending(@NonNull Card movedCard, @NonNull List cardsToReorganize, int startingAtOrder) { + final Instant now = Instant.now(); + for (Card card : cardsToReorganize) { + card.setOrder(startingAtOrder); + if (card.getStatus() == DBStatus.UP_TO_DATE.getId()) { + card.setStatusEnum(DBStatus.LOCAL_EDITED_SILENT); + card.setLastModifiedLocal(now); + } + startingAtOrder++; + } + //update the moved one first, because otherwise a bunch of livedata is fired, leading the card to dispose and reappear + cardsToReorganize.remove(movedCard); + dataBaseAdapter.updateCard(movedCard, false); + for (Card card : cardsToReorganize) { + dataBaseAdapter.updateCard(card, false); + } + } + + // ------------------- + // Widgets + // ------------------- + + // # filter widget + + @AnyThread + public void createFilterWidget(@NonNull FilterWidget filterWidget, @NonNull IResponseCallback callback) { + executor.submit(() -> { + try { + int filterWidgetId = dataBaseAdapter.createFilterWidgetDirectly(filterWidget); + callback.onResponse(filterWidgetId); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void updateFilterWidget(@NonNull FilterWidget filterWidget, @NonNull ResponseCallback callback) { + executor.submit(() -> { + try { + dataBaseAdapter.updateFilterWidgetDirectly(filterWidget); + callback.onResponse(Boolean.TRUE); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void getFilterWidget(@NonNull Integer filterWidgetId, @NonNull IResponseCallback callback) { + executor.submit(() -> { + try { + callback.onResponse(dataBaseAdapter.getFilterWidgetByIdDirectly(filterWidgetId)); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + @AnyThread + public void deleteFilterWidget(int filterWidgetId, @NonNull IResponseCallback callback) { + executor.submit(() -> { + try { + dataBaseAdapter.deleteFilterWidgetDirectly(filterWidgetId); + callback.onResponse(Boolean.TRUE); + } catch (Throwable t) { + callback.onError(t); + } + }); + } + + public boolean filterWidgetExists(int id) { + return dataBaseAdapter.filterWidgetExists(id); + } + + @WorkerThread + public List getCardsForFilterWidget(@NonNull Integer filterWidgetId) { + return dataBaseAdapter.getCardsForFilterWidget(filterWidgetId); + } + + @WorkerThread + public LiveData> getCardsForUpcomingCards() { + return dataBaseAdapter.getCardsForUpcomingCard(); + } + + @WorkerThread + public List getCardsForUpcomingCardsForWidget() { + return dataBaseAdapter.getCardsForUpcomingCardForWidget(); + } + + // # single card widget + + /** + * Can be called from a configuration screen or a picker. + * Creates a new entry in the database, if row with given widgetId does not yet exist. + */ + @AnyThread + public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) { + executor.submit(() -> dataBaseAdapter.createSingleCardWidget(widgetId, accountId, boardId, localCardId)); + } + + @WorkerThread + public FullSingleCardWidgetModel getSingleCardWidgetModelDirectly(int appWidgetId) throws NoSuchElementException { + final FullSingleCardWidgetModel model = dataBaseAdapter.getFullSingleCardWidgetModel(appWidgetId); + if (model == null) { + throw new NoSuchElementException("There is no " + FullSingleCardWidgetModel.class.getSimpleName() + " with the given appWidgetId " + appWidgetId); + } + return model; + } + + @AnyThread + public void deleteSingleCardWidgetModel(int widgetId) { + executor.submit(() -> dataBaseAdapter.deleteSingleCardWidget(widgetId)); + } + + public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { + executor.submit(() -> dataBaseAdapter.createStackWidget(appWidgetId, accountId, stackId, darkTheme)); + } + + @WorkerThread + public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) throws NoSuchElementException { + final StackWidgetModel model = dataBaseAdapter.getStackWidgetModelDirectly(appWidgetId); + if (model == null) { + throw new NoSuchElementException(); + } + return model; + } + + public void deleteStackWidgetModel(int appWidgetId) { + executor.submit(() -> dataBaseAdapter.deleteStackWidget(appWidgetId)); + } + + @WorkerThread + public Stack getStackDirectly(long stackLocalId) { + return dataBaseAdapter.getStackByLocalIdDirectly(stackLocalId); + } + + @ColorInt + @WorkerThread + public Integer getBoardColorDirectly(long accountId, long localBoardId) { + return dataBaseAdapter.getBoardColorDirectly(accountId, localBoardId); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java new file mode 100644 index 000000000..83101ad3d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/PreferencesRepository.java @@ -0,0 +1,79 @@ +package it.niedermann.nextcloud.deck.persistence; + +import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode; +import static java.util.concurrent.CompletableFuture.supplyAsync; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.preference.PreferenceManager; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import it.niedermann.android.sharedpreferences.SharedPreferenceBooleanLiveData; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; + +public class PreferencesRepository { + + private final ExecutorService executor; + private final String PREF_KEY_THEME; + private final String PREF_KEY_DEBUGGING; + private final SharedPreferences sharedPreferences; + private final Context context; + + public PreferencesRepository(@NonNull Context context) { + this(context, new ThreadPoolExecutor(0, 2, 0L, TimeUnit.SECONDS, new SynchronousQueue<>())); + } + + public PreferencesRepository(@NonNull Context context, @NonNull ExecutorService executor) { + this.context = context.getApplicationContext(); + this.executor = executor; + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + + PREF_KEY_THEME = this.context.getString(R.string.pref_key_dark_theme); + PREF_KEY_DEBUGGING = this.context.getString(R.string.pref_key_debugging); + } + + // --------- + // Debugging + // --------- + + public CompletableFuture isDebugModeEnabled() { + return supplyAsync(() -> { + final boolean enabled = sharedPreferences.getBoolean(PREF_KEY_DEBUGGING, false); + DeckLog.log("--- Read:", PREF_KEY_DEBUGGING, "→", enabled); + return enabled; + }, executor); + } + + public LiveData isDebugModeEnabled$() { + return new SharedPreferenceBooleanLiveData(sharedPreferences, PREF_KEY_DEBUGGING, false); + } + + // ----- + // Theme + // ----- + + public void setAppTheme(int setting) { + setDefaultNightMode(setting); + } + + public CompletableFuture getAppThemeSetting() { + return supplyAsync(() -> { + String mode; + try { + mode = sharedPreferences.getString(PREF_KEY_THEME, context.getString(R.string.pref_value_theme_system_default)); + } catch (ClassCastException e) { + mode = sharedPreferences.getBoolean(PREF_KEY_THEME, false) ? context.getString(R.string.pref_value_theme_dark) : context.getString(R.string.pref_value_theme_light); + } + return Integer.parseInt(mode); + }, executor); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java index 50de4d2e4..9067809ce 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java @@ -12,15 +12,16 @@ import androidx.annotation.AnyThread; import androidx.annotation.ColorInt; import androidx.annotation.MainThread; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -import androidx.annotation.WorkerThread; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; +import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.api.ParsedResponse; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; +import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.io.File; import java.time.Instant; @@ -29,12 +30,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -54,25 +52,18 @@ import it.niedermann.nextcloud.deck.model.JoinCardWithUser; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.User; -import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; -import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; -import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.model.internal.FilterInformation; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment; -import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; -import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; -import it.niedermann.nextcloud.deck.model.widget.filter.FilterWidget; -import it.niedermann.nextcloud.deck.model.widget.filter.dto.FilterWidgetCard; +import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; +import it.niedermann.nextcloud.deck.persistence.BaseRepository; import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; -import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; -import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst.UserSearchLiveData; import it.niedermann.nextcloud.deck.persistence.sync.helpers.DataPropagationHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AbstractSyncDataProvider; @@ -87,146 +78,81 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.LabelData import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.StackDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithAclDownSyncDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithStacksAndLabelsUpSyncDataProvider; -import it.niedermann.nextcloud.deck.ui.upcomingcards.UpcomingCardsAdapterItem; -import it.niedermann.nextcloud.deck.util.ExecutorServiceProvider; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.util.ConnectivityUtil; +/** + * Extends {@link BaseRepository} by synchronization capabilities. + * Therefore it always requires an {@link Account} to choose the correct {@link SingleSignOnAccount} for network operations. + */ @SuppressWarnings("WeakerAccess") -public class SyncManager { +public class SyncManager extends BaseRepository { - @NonNull - private final Context appContext; - @NonNull - private final DataBaseAdapter dataBaseAdapter; @NonNull private final ServerAdapter serverAdapter; @NonNull - private final ExecutorService executor; - @NonNull private final SyncHelper.Factory syncHelperFactory; @AnyThread - public SyncManager(@NonNull Context context) { - this(context, null); + public SyncManager(@NonNull Context context, @NonNull Account account) throws NextcloudFilesAppAccountNotFoundException { + this(context, AccountImporter.getSingleSignOnAccount(context, account.getName()), new ConnectivityUtil(context)); } - @AnyThread - public SyncManager(@NonNull Context context, @Nullable String ssoAccountName) { - this(context, - new DataBaseAdapter(context.getApplicationContext()), - new ServerAdapter(context.getApplicationContext(), ssoAccountName), - ExecutorServiceProvider.getExecutorService(), - SyncHelper::new); - LastSyncUtil.init(context.getApplicationContext()); + private SyncManager(@NonNull Context context, + @NonNull SingleSignOnAccount ssoAccount, + @NonNull ConnectivityUtil connectivityUtil) { + this(context, new ServerAdapter(context.getApplicationContext(), ssoAccount, connectivityUtil), connectivityUtil, SyncHelper::new); } - private SyncManager(@NonNull Context context, - @NonNull DataBaseAdapter databaseAdapter, - @NonNull ServerAdapter serverAdapter, - @NonNull ExecutorService executor, - @NonNull SyncHelper.Factory syncHelperFactory) { - this.appContext = context.getApplicationContext(); - this.dataBaseAdapter = databaseAdapter; + protected SyncManager(@NonNull Context context, + @NonNull ServerAdapter serverAdapter, + @NonNull ConnectivityUtil connectivityUtil, + @NonNull SyncHelper.Factory syncHelperFactory) { + super(context, connectivityUtil); this.serverAdapter = serverAdapter; - this.executor = executor; this.syncHelperFactory = syncHelperFactory; + LastSyncUtil.init(context.getApplicationContext()); } - @WorkerThread - public Long getBoardLocalIdByAccountAndCardRemoteIdDirectly(long accountId, long cardRemoteId) { - return dataBaseAdapter.getBoardLocalIdByAccountAndCardRemoteIdDirectly(accountId, cardRemoteId); - } - - @WorkerThread - public boolean synchronizeEverything() { - List accounts = dataBaseAdapter.getAllAccountsDirectly(); - if (accounts.size() > 0) { - final AtomicBoolean success = new AtomicBoolean(true); - CountDownLatch latch = new CountDownLatch(accounts.size()); - try { - for (Account account : accounts) { - new SyncManager(dataBaseAdapter.getContext(), account.getName()).synchronize(new ResponseCallback<>(account) { - @Override - public void onResponse(Boolean response) { - success.set(success.get() && Boolean.TRUE.equals(response)); - latch.countDown(); - } - - @Override - public void onError(Throwable throwable) { - success.set(false); - super.onError(throwable); - latch.countDown(); - } - }); - } - latch.await(); - return success.get(); - } catch (InterruptedException e) { - DeckLog.logError(e); - return false; - } - } - return true; - } - - @AnyThread - public void synchronizeBoard(long localBoardId, @NonNull ResponseCallback responseCallback) { - executor.submit(() -> { - FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(responseCallback.getAccount().getId(), localBoardId); - try { - syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) - .setResponseCallback(responseCallback) - .doSyncFor(new StackDataProvider(null, board)); - } catch (OfflineException e) { - responseCallback.onError(e); - } - }); + @VisibleForTesting + protected SyncManager(@NonNull Context context, + @NonNull ServerAdapter serverAdapter, + @NonNull ConnectivityUtil connectivityUtil, + @NonNull SyncHelper.Factory syncHelperFactory, + @NonNull DataBaseAdapter databaseAdapter, + @NonNull ExecutorService executor) { + super(context, connectivityUtil, databaseAdapter, executor); + this.serverAdapter = serverAdapter; + this.syncHelperFactory = syncHelperFactory; + LastSyncUtil.init(context.getApplicationContext()); } @AnyThread - public void synchronizeCard(@NonNull ResponseCallback responseCallback, @NonNull Card card) { - executor.submit(() -> { - FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); - Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); - try { - syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) - .setResponseCallback(responseCallback) - .doSyncFor(new CardDataProvider(null, board, stack)); - } catch (OfflineException e) { - responseCallback.onError(e); - } - }); + public void fetchBoardsFromServer(@NonNull ResponseCallback>> callback) { + executor.submit(() -> serverAdapter.getBoards(callback)); } @AnyThread public LiveData> synchronize(@NonNull ResponseCallback responseCallback) { - MutableLiveData> progress$ = new MutableLiveData<>(); - Account callbackAccount = responseCallback.getAccount(); - if (callbackAccount == null) { - throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + ResponseCallback.class.getSimpleName() + " must not be null."); - } - Long callbackAccountId = callbackAccount.getId(); - if (callbackAccountId == null) { - throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + ResponseCallback.class.getSimpleName() + " must contain a valid id, but given id was null."); - } + final var progress$ = new MutableLiveData>(); + final var callbackAccount = responseCallback.getAccount(); + final long callbackAccountId = callbackAccount.getId(); + executor.submit(() -> { refreshCapabilities(new ResponseCallback<>(responseCallback.getAccount()) { @Override public void onResponse(Capabilities response) { if (response != null && !response.isMaintenanceEnabled()) { if (response.getDeckVersion().isSupported()) { - long accountId = callbackAccountId; - Instant lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId); + final var lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId); + final var syncHelper = syncHelperFactory.create(serverAdapter, dataBaseAdapter, lastSyncDate); - final SyncHelper syncHelper = syncHelperFactory.create(serverAdapter, dataBaseAdapter, lastSyncDate); - - ResponseCallback callback = new ResponseCallback<>(callbackAccount) { + final var callback = new ResponseCallback(callbackAccount) { @Override public void onResponse(Boolean response) { syncHelper.setResponseCallback(new ResponseCallback<>(account) { @Override public void onResponse(Boolean response) { - LastSyncUtil.setLastSyncDate(accountId, Instant.now()); + LastSyncUtil.setLastSyncDate(callbackAccountId, Instant.now()); responseCallback.onResponse(response); } @@ -284,114 +210,44 @@ public class SyncManager { return progress$; } -// -// private IResponseCallback wrapCallForUi(IResponseCallback responseCallback) { -// Account account = responseCallback.getAccount(); -// if (account == null || account.getId() == null) { -// throw new IllegalArgumentException("Bro. Please just give me a damn Account!"); -// } -// return new IResponseCallback(responseCallback.getAccount()) { -// @Override -// public void onResponse(T response) { -// sourceActivity.runOnUiThread(() -> { -// fillAccountIDs(response); -// responseCallback.onResponse(response); -// }); -// } -// -// @Override -// public void onError(Throwable throwable) { -// responseCallback.onError(throwable); -// } -// }; -// } - -// private T applyUpdatesFromRemote(T localEntity, T remoteEntity, Long accountId) { -// if (!localEntity.getId().equals(remoteEntity.getId()) -// || !accountId.equals(localEntity.getAccountId())) { -// throw new IllegalArgumentException("IDs of Account or Entity are not matching! WTF are you doin?!"); -// } -// remoteEntity.setLastModifiedLocal(remoteEntity.getLastModified()); // not an error! local-modification = remote-mod -// remoteEntity.setLocalId(localEntity.getLocalId()); -// return remoteEntity; -// } - - @AnyThread - public LiveData hasAccounts() { - return dataBaseAdapter.hasAccounts(); - } - @AnyThread - public void createAccount(@NonNull Account account, @NonNull IResponseCallback callback) { + public void synchronizeBoard(long localBoardId, @NonNull ResponseCallback responseCallback) { executor.submit(() -> { + FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(responseCallback.getAccount().getId(), localBoardId); try { - final Account createdAccount = dataBaseAdapter.createAccountDirectly(account); - if (createdAccount == null) { - throw new RuntimeException("Created account is null. Source: " + account); - } - // TODO: throw this shit away - // that's why we do this: https://github.com/nextcloud/deck/issues/3229 - serverAdapter.getBoards(new ResponseCallback<>(createdAccount) { - @Override - public void onResponse(ParsedResponse> response) { - callback.onResponse(createdAccount); - } - - @SuppressLint("MissingSuperCall") - @Override - public void onError(Throwable throwable) { - callback.onResponse(createdAccount); - } - }); - // TODO: and replace with this line: -// callback.onResponse(createdAccount); - } catch (Throwable t) { - callback.onError(t); + syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) + .setResponseCallback(responseCallback) + .doSyncFor(new StackDataProvider(null, board)); + } catch (OfflineException e) { + responseCallback.onError(e); } }); } - public boolean hasInternetConnection() { - return serverAdapter.hasInternetConnection(); - } - @AnyThread - public void deleteAccount(long id) { + public void synchronizeCard(@NonNull ResponseCallback responseCallback, @NonNull Card card) { executor.submit(() -> { - dataBaseAdapter.deleteAccount(id); - LastSyncUtil.resetLastSyncDate(id); + FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); + Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); + try { + syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) + .setResponseCallback(responseCallback) + .doSyncFor(new CardDataProvider(null, board, stack)); + } catch (OfflineException e) { + responseCallback.onError(e); + } }); } - @UiThread - public LiveData readAccount(long id) { - return dataBaseAdapter.readAccount(id); - } - - @WorkerThread - public Account readAccountDirectly(long id) { - return dataBaseAdapter.readAccountDirectly(id); - } - - @WorkerThread - public Account readAccountDirectly(@Nullable String name) { - return dataBaseAdapter.readAccountDirectly(name); - } - - @UiThread - public LiveData readAccount(@Nullable String name) { - return dataBaseAdapter.readAccount(name); - } - - @UiThread - public LiveData> readAccounts() { - return dataBaseAdapter.readAccounts(); - } - - @WorkerThread - public List readAccountsDirectly() { - return dataBaseAdapter.getAllAccountsDirectly(); - } +// private T applyUpdatesFromRemote(T localEntity, T remoteEntity, Long accountId) { +// if (!localEntity.getId().equals(remoteEntity.getId()) +// || !accountId.equals(localEntity.getAccountId())) { +// throw new IllegalArgumentException("IDs of Account or Entity are not matching! WTF are you doin?!"); +// } +// remoteEntity.setLastModifiedLocal(remoteEntity.getLastModified()); // not an error! local-modification = remote-mod +// remoteEntity.setLocalId(localEntity.getLocalId()); +// return remoteEntity; +// } /** *

@@ -487,66 +343,10 @@ public class SyncManager { }); } - /** - * @param accountId ID of the account - * @return all {@link Board}s no matter if {@link Board#archived} or not. - */ - @SuppressWarnings("JavadocReference") - @AnyThread - public LiveData> getBoards(long accountId) { - return dataBaseAdapter.getBoards(accountId); - } - - /** - * @param localProjectId LocalId of the OcsProject - * @return all {@link OcsProjectResource}s of the Project - */ - @AnyThread - public LiveData> 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 Boards depending on archived parameter - */ - @AnyThread - public LiveData> getBoards(long accountId, boolean archived) { - return dataBaseAdapter.getBoards(accountId, archived); - } - - /** - * @param accountId ID of the account - * @param archived Decides whether only archived or not-archived boards for the specified account will be returned - * @return all archived or non-archived FullBoards depending on archived parameter - */ - @AnyThread - public LiveData> getFullBoards(long accountId, boolean archived) { - return dataBaseAdapter.getFullBoards(accountId, archived); - } - - /** - * Get all non-archived FullBoards with edit permissions for the specified account. - * - * @param accountId ID of the account - * @return all non-archived Boards with edit permission - */ - @AnyThread - public LiveData> getBoardsWithEditPermission(long accountId) { - return dataBaseAdapter.getBoardsWithEditPermission(accountId); - } - - @AnyThread - public LiveData hasArchivedBoards(long accountId) { - return dataBaseAdapter.hasArchivedBoards(accountId); - } - @AnyThread - public void createBoard(long accountId, @NonNull Board board, @NonNull IResponseCallback callback) { + public void createBoard(@NonNull Account account, @NonNull Board board, @NonNull IResponseCallback callback) { executor.submit(() -> { - final Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); - final User owner = dataBaseAdapter.getUserByUidDirectly(accountId, account.getUserName()); + final User owner = dataBaseAdapter.getUserByUidDirectly(account.getId(), account.getUserName()); if (owner == null) { callback.onError(new IllegalStateException("Owner is null. This can be the case if the Deck app has never before been opened in the webinterface")); } else { @@ -554,8 +354,8 @@ public class SyncManager { board.setOwnerId(owner.getLocalId()); fullBoard.setOwner(owner); fullBoard.setBoard(board); - board.setAccountId(accountId); - fullBoard.setAccountId(accountId); + board.setAccountId(account.getId()); + fullBoard.setAccountId(account.getId()); new DataPropagationHelper(serverAdapter, dataBaseAdapter, executor).createEntity(new BoardDataProvider(), fullBoard, ResponseCallback.from(account, callback)); } }); @@ -675,11 +475,18 @@ public class SyncManager { } } } - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId); - ServerAdapter serverAdapterToUse = this.serverAdapter; - if (originAccountId != targetAccountId) { - serverAdapterToUse = new ServerAdapter(appContext, targetAccount.getName()); + final ServerAdapter serverAdapterToUse; + if (originAccountId == targetAccountId) { + serverAdapterToUse = this.serverAdapter; + } else { + try { + serverAdapterToUse = new ServerAdapter(context, AccountImporter.getSingleSignOnAccount(context, targetAccount.getName()), connectivityUtil); + } catch (NextcloudFilesAppAccountNotFoundException e) { + callback.onError(e); + return; + } } syncHelperFactory.create(serverAdapterToUse, dataBaseAdapter, null) .setResponseCallback(new ResponseCallback<>(targetAccount) { @@ -703,7 +510,7 @@ public class SyncManager { @AnyThread public LiveData> syncActivitiesForCard(@NonNull Card card) { executor.submit(() -> { - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { if (card.getId() != null) { syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) .setResponseCallback(new ResponseCallback<>(dataBaseAdapter.getAccountByIdDirectly(card.getAccountId())) { @@ -764,10 +571,6 @@ public class SyncManager { }); } - public LiveData> getFullCommentsForLocalCardId(long localCardId) { - return dataBaseAdapter.getFullCommentsForLocalCardId(localCardId); - } - @AnyThread public void deleteBoard(@NonNull Board board, @NonNull IResponseCallback callback) { executor.submit(() -> { @@ -787,19 +590,6 @@ public class SyncManager { }); } - public LiveData> getStacksForBoard(long accountId, long localBoardId) { - return dataBaseAdapter.getStacksForBoard(accountId, localBoardId); - } - - public LiveData getStack(long accountId, long localStackId) { - return dataBaseAdapter.getStack(accountId, localStackId); - } - - @WorkerThread - public Long getBoardLocalIdByLocalCardIdDirectly(long localCardId) { - return dataBaseAdapter.getBoardLocalIdByLocalCardIdDirectly(localCardId); - } - @AnyThread public void createAccessControl(long accountId, @NonNull AccessControl entity, @NonNull IResponseCallback callback) { executor.submit(() -> { @@ -814,15 +604,6 @@ public class SyncManager { }); } - @WorkerThread - public AccessControl getAccessControlByRemoteIdDirectly(long accountId, Long id) { - return dataBaseAdapter.getAccessControlByRemoteIdDirectly(accountId, id); - } - - public LiveData> getAccessControlByLocalBoardId(long accountId, Long id) { - return dataBaseAdapter.getAccessControlByLocalBoardId(accountId, id); - } - @AnyThread public void updateAccessControl(@NonNull AccessControl entity, @NonNull IResponseCallback callback) { executor.submit(() -> { @@ -844,6 +625,8 @@ public class SyncManager { public void onResponse(Void response) { // revoked own board-access? if (entity.getAccountId() == entity.getAccountId() && entity.getUser().getUid().equals(account.getUserName())) { + dataBaseAdapter.saveNeighbourOfBoard(board.getAccountId(), board.getLocalId()); + dataBaseAdapter.removeCurrentStackId(board.getAccountId(), board.getLocalId()); dataBaseAdapter.deleteBoardPhysically(board.getBoard()); } callback.onResponse(response); @@ -858,17 +641,10 @@ public class SyncManager { }); } - public LiveData getFullBoardById(Long accountId, Long localId) { - return dataBaseAdapter.getFullBoardById(accountId, localId); - } - - public Board getBoardById(Long localId) { - return dataBaseAdapter.getBoardByLocalIdDirectly(localId); - } - @AnyThread - public void createStack(long accountId, @NonNull String title, long boardLocalId, @NonNull IResponseCallback callback) { + public void createStack(long accountId, long boardLocalId, @NonNull String title, @NonNull IResponseCallback callback) { executor.submit(() -> { + DeckLog.info("Create Stack in account", accountId, "on board with local ID ", boardLocalId); Stack stack = new Stack(title, boardLocalId); Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId()); @@ -883,7 +659,7 @@ public class SyncManager { } @AnyThread - public void deleteStack(long accountId, long stackLocalId, long boardLocalId, @NonNull IResponseCallback callback) { + public void deleteStack(long accountId, long boardLocalId, long stackLocalId, @NonNull IResponseCallback callback) { executor.submit(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullStack fullStack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackLocalId); @@ -956,22 +732,6 @@ public class SyncManager { }); } - public LiveData getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { - return dataBaseAdapter.getCardWithProjectsByLocalId(accountId, cardLocalId); - } - - public LiveData> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { - return dataBaseAdapter.getFullCardsForStack(accountId, localStackId, filter); - } - - public void countCardsInStackDirectly(long accountId, long localStackId, @NonNull IResponseCallback callback) { - executor.submit(() -> dataBaseAdapter.countCardsInStackDirectly(accountId, localStackId, callback)); - } - - public void countCardsWithLabel(long localLabelId, @NonNull IResponseCallback callback) { - executor.submit(() -> dataBaseAdapter.countCardsWithLabel(localLabelId, callback)); - } - // TODO implement, see https://github.com/stefan-niedermann/nextcloud-deck/issues/395 public LiveData> getArchivedFullCardsForBoard(long accountId, long localBoardId) { MutableLiveData> dummyData = new MutableLiveData<>(); @@ -1043,7 +803,7 @@ public class SyncManager { } } - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) .setResponseCallback(new ResponseCallback<>(account) { @Override @@ -1153,9 +913,21 @@ public class SyncManager { public void archiveBoard(@NonNull Board board, @NonNull IResponseCallback callback) { executor.submit(() -> { try { - FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); - b.getBoard().setArchived(true); - updateBoard(b, callback); + final var fullBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); + fullBoard.getBoard().setArchived(true); + updateBoard(fullBoard, new IResponseCallback<>() { + @Override + public void onResponse(FullBoard response) { + dataBaseAdapter.saveNeighbourOfBoard(fullBoard.getAccountId(), fullBoard.getLocalId()); + callback.onResponse(response); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + callback.onError(throwable); + } + }); } catch (Throwable e) { callback.onError(e); } @@ -1168,7 +940,19 @@ public class SyncManager { try { FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); b.getBoard().setArchived(false); - updateBoard(b, callback); + updateBoard(b, new IResponseCallback<>() { + @Override + public void onResponse(FullBoard response) { + dataBaseAdapter.saveCurrentBoardId(b.getAccountId(), b.getLocalId()); + callback.onResponse(response); + } + + @SuppressLint("MissingSuperCall") + @Override + public void onError(Throwable throwable) { + callback.onError(throwable); + } + }); } catch (Throwable e) { callback.onError(e); } @@ -1207,7 +991,7 @@ public class SyncManager { fullCardFromDB.setCard(card.getCard()); card.getCard().setStatus(DBStatus.LOCAL_EDITED.getId()); dataBaseAdapter.updateCard(card.getCard(), false); - if (serverAdapter.hasInternetConnection()) { + if (connectivityUtil.hasInternetConnection()) { Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); syncHelperFactory.create(serverAdapter, dataBaseAdapter, null) .setResponseCallback(new ResponseCallback<>(account) { @@ -1286,7 +1070,12 @@ public class SyncManager { ServerAdapter serverToUse = serverAdapter; if (originAccountId != targetAccountId) { - serverToUse = new ServerAdapter(appContext, targetAccount.getName()); + try { + serverToUse = new ServerAdapter(context, AccountImporter.getSingleSignOnAccount(context, targetAccount.getName()), connectivityUtil); + } catch (NextcloudFilesAppAccountNotFoundException e) { + callback.onError(e); + throw new RuntimeException(e); + } } new DataPropagationHelper(serverToUse, dataBaseAdapter, executor).createEntity(new CardPropagationDataProvider(null, targetBoard.getBoard(), targetFullStack), fullCardForServerPropagation, new ResponseCallback<>(targetAccount) { @Override @@ -1394,10 +1183,6 @@ public class SyncManager { }); } - public MutableLiveData