From a2718c397ab439bfd5f4f3f225d428f92ca4275b Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Mon, 22 Jun 2020 13:59:07 +0200 Subject: Refactor packages to match actual use case --- .../owncloud/notes/AppendToNoteActivity.java | 58 ++ .../owncloud/notes/ExceptionHandler.java | 48 - .../niedermann/owncloud/notes/ExceptionUtil.java | 69 -- .../owncloud/notes/FormattingHelpActivity.java | 252 ++++++ .../niedermann/owncloud/notes/LockedActivity.java | 107 +++ .../owncloud/notes/NotesApplication.java | 92 ++ .../owncloud/notes/SplashscreenActivity.java | 26 + .../owncloud/notes/about/AboutActivity.java | 94 ++ .../notes/about/AboutFragmentContributingTab.java | 25 + .../notes/about/AboutFragmentCreditsTab.java | 26 + .../notes/about/AboutFragmentLicenseTab.java | 44 + .../accountpicker/AccountPickerDialogFragment.java | 106 +++ .../notes/accountpicker/AccountPickerListener.java | 9 + .../accountswitcher/AccountSwitcherAdapter.java | 2 +- .../accountswitcher/AccountSwitcherDialog.java | 7 +- .../accountswitcher/AccountSwitcherListener.java | 2 +- .../accountswitcher/AccountSwitcherViewHolder.java | 2 +- .../notes/android/AlwaysAutoCompleteTextView.java | 56 -- .../owncloud/notes/android/DarkModeSetting.java | 66 -- .../android/MultiSelectedActionModeCallback.java | 161 ---- .../android/NotesListViewItemTouchHelper.java | 137 --- .../notes/android/activity/AboutActivity.java | 96 -- .../android/activity/AppendToNoteActivity.java | 58 -- .../notes/android/activity/EditNoteActivity.java | 282 ------ .../notes/android/activity/ExceptionActivity.java | 55 -- .../notes/android/activity/LockedActivity.java | 109 --- .../NoteListWidgetConfigurationActivity.java | 181 ---- .../android/activity/NotesListViewActivity.java | 976 -------------------- .../android/activity/PreferencesActivity.java | 37 - .../android/activity/SelectSingleNoteActivity.java | 76 -- .../android/activity/SplashscreenActivity.java | 25 - .../notes/android/appwidget/CreateNoteWidget.java | 51 -- .../notes/android/appwidget/NoteListWidget.java | 194 ---- .../android/appwidget/NoteListWidgetFactory.java | 148 ---- .../android/appwidget/NoteListWidgetService.java | 11 - .../notes/android/appwidget/NoteWidgetHelper.java | 23 - .../notes/android/appwidget/SingleNoteWidget.java | 97 -- .../android/appwidget/SingleNoteWidgetFactory.java | 147 ---- .../android/appwidget/SingleNoteWidgetService.java | 11 - .../android/fragment/AccountChooserAdapter.java | 79 -- .../notes/android/fragment/BaseNoteFragment.java | 411 --------- .../notes/android/fragment/CategoryAdapter.java | 133 --- .../android/fragment/CategoryDialogFragment.java | 184 ---- .../android/fragment/EditTitleDialogFragment.java | 96 -- .../android/fragment/ExceptionDialogFragment.java | 167 ---- .../fragment/MoveAccountDialogFragment.java | 82 -- .../notes/android/fragment/NoteEditFragment.java | 259 ------ .../android/fragment/NotePreviewFragment.java | 231 ----- .../android/fragment/NoteReadonlyFragment.java | 167 ---- .../android/fragment/PreferencesFragment.java | 138 --- .../fragment/SearchableBaseNoteFragment.java | 302 ------- .../about/AboutFragmentContributingTab.java | 25 - .../fragment/about/AboutFragmentCreditsTab.java | 26 - .../fragment/about/AboutFragmentLicenseTab.java | 44 - .../android/quicksettings/NewNoteTileService.java | 34 - .../owncloud/notes/branding/BrandedSnackbar.java | 2 +- .../owncloud/notes/branding/BrandingUtil.java | 6 +- .../owncloud/notes/edit/BaseNoteFragment.java | 413 +++++++++ .../owncloud/notes/edit/EditNoteActivity.java | 280 ++++++ .../owncloud/notes/edit/NoteEditFragment.java | 258 ++++++ .../owncloud/notes/edit/NotePreviewFragment.java | 230 +++++ .../owncloud/notes/edit/NoteReadonlyFragment.java | 166 ++++ .../owncloud/notes/edit/NotesTextWatcher.java | 100 +++ .../notes/edit/SearchableBaseNoteFragment.java | 302 +++++++ .../notes/edit/category/CategoryAdapter.java | 133 +++ .../edit/category/CategoryDialogFragment.java | 184 ++++ .../format/ContextBasedFormattingCallback.java | 115 +++ .../ContextBasedRangeFormattingCallback.java | 150 ++++ .../notes/edit/title/EditTitleDialogFragment.java | 96 ++ .../notes/exception/ExceptionActivity.java | 54 ++ .../notes/exception/ExceptionDialogFragment.java | 166 ++++ .../owncloud/notes/exception/ExceptionHandler.java | 46 + .../owncloud/notes/exception/ExceptionUtil.java | 71 ++ .../formattinghelp/FormattingHelpActivity.java | 252 ------ .../owncloud/notes/main/MainActivity.java | 978 +++++++++++++++++++++ .../main/MultiSelectedActionModeCallback.java | 162 ++++ .../owncloud/notes/main/NavigationAdapter.java | 171 ++++ .../owncloud/notes/main/items/ItemAdapter.java | 260 ++++++ .../owncloud/notes/main/items/NoteViewHolder.java | 138 +++ .../notes/main/items/grid/GridItemDecoration.java | 56 ++ .../notes/main/items/grid/NoteViewGridHolder.java | 57 ++ .../items/grid/NoteViewGridHolderOnlyTitle.java | 47 + .../main/items/list/NoteViewHolderWithExcerpt.java | 47 + .../items/list/NoteViewHolderWithoutExcerpt.java | 45 + .../items/list/NotesListViewItemTouchHelper.java | 137 +++ .../notes/main/items/section/SectionItem.java | 25 + .../main/items/section/SectionItemDecoration.java | 40 + .../main/items/section/SectionViewHolder.java | 18 + .../notes/manageaccounts/ManageAccountAdapter.java | 2 +- .../manageaccounts/ManageAccountViewHolder.java | 2 +- .../manageaccounts/ManageAccountsActivity.java | 5 +- .../owncloud/notes/model/AbstractWidgetData.java | 42 - .../owncloud/notes/model/ApiVersion.java | 86 -- .../owncloud/notes/model/Capabilities.java | 106 --- .../niedermann/owncloud/notes/model/Category.java | 18 - .../niedermann/owncloud/notes/model/CloudNote.java | 103 --- .../it/niedermann/owncloud/notes/model/DBNote.java | 80 -- .../niedermann/owncloud/notes/model/DBStatus.java | 50 -- .../owncloud/notes/model/GridItemDecoration.java | 53 -- .../owncloud/notes/model/ISyncCallback.java | 13 - .../it/niedermann/owncloud/notes/model/Item.java | 5 - .../owncloud/notes/model/ItemAdapter.java | 251 ------ .../owncloud/notes/model/LocalAccount.java | 155 ---- .../owncloud/notes/model/NavigationAdapter.java | 172 ---- .../owncloud/notes/model/NoteClickListener.java | 11 - .../owncloud/notes/model/NoteListsWidgetData.java | 42 - .../owncloud/notes/model/NoteViewGridHolder.java | 55 -- .../notes/model/NoteViewGridHolderOnlyTitle.java | 47 - .../owncloud/notes/model/NoteViewHolder.java | 135 --- .../notes/model/NoteViewHolderWithExcerpt.java | 43 - .../notes/model/NoteViewHolderWithoutExcerpt.java | 41 - .../owncloud/notes/model/SectionItem.java | 23 - .../notes/model/SectionItemDecoration.java | 38 - .../owncloud/notes/model/SectionViewHolder.java | 18 - .../owncloud/notes/model/SingleNoteWidgetData.java | 23 - .../owncloud/notes/model/SyncResultStatus.java | 6 - .../notes/persistence/AbstractNotesDatabase.java | 2 +- .../notes/persistence/CapabilitiesClient.java | 2 +- .../notes/persistence/CapabilitiesWorker.java | 4 +- .../notes/persistence/LoadNotesListTask.java | 12 +- .../notes/persistence/NoteServerSyncHelper.java | 18 +- .../owncloud/notes/persistence/NotesClient.java | 8 +- .../owncloud/notes/persistence/NotesClientV02.java | 6 +- .../owncloud/notes/persistence/NotesClientV1.java | 6 +- .../owncloud/notes/persistence/NotesDatabase.java | 42 +- .../owncloud/notes/persistence/SyncWorker.java | 2 +- .../persistence/migration/Migration_10_11.java | 2 +- .../persistence/migration/Migration_11_12.java | 2 +- .../persistence/migration/Migration_12_13.java | 2 +- .../persistence/migration/Migration_13_14.java | 2 +- .../persistence/migration/Migration_14_15.java | 2 +- .../persistence/migration/Migration_15_16.java | 2 +- .../notes/persistence/migration/Migration_4_5.java | 4 +- .../notes/persistence/migration/Migration_6_7.java | 2 +- .../notes/persistence/migration/Migration_7_8.java | 2 +- .../notes/persistence/migration/Migration_8_9.java | 6 +- .../persistence/migration/Migration_9_10.java | 2 +- .../notes/preferences/DarkModeSetting.java | 66 ++ .../notes/preferences/PreferencesActivity.java | 37 + .../notes/preferences/PreferencesFragment.java | 137 +++ .../notes/quicksettings/NewNoteTileService.java | 34 + .../shared/account/AccountChooserAdapter.java | 44 + .../shared/account/AccountChooserViewHolder.java | 37 + .../owncloud/notes/shared/model/ApiVersion.java | 86 ++ .../owncloud/notes/shared/model/Capabilities.java | 106 +++ .../owncloud/notes/shared/model/Category.java | 18 + .../notes/shared/model/CategorySortingMethod.java | 41 + .../owncloud/notes/shared/model/CloudNote.java | 103 +++ .../owncloud/notes/shared/model/DBNote.java | 80 ++ .../owncloud/notes/shared/model/DBStatus.java | 50 ++ .../owncloud/notes/shared/model/ISyncCallback.java | 13 + .../owncloud/notes/shared/model/Item.java | 5 + .../owncloud/notes/shared/model/LocalAccount.java | 155 ++++ .../notes/shared/model/NoteClickListener.java | 11 + .../notes/shared/model/ServerResponse.java | 103 +++ .../notes/shared/model/SyncResultStatus.java | 6 + .../owncloud/notes/shared/util/ClipboardUtil.java | 74 ++ .../owncloud/notes/shared/util/ColorUtil.java | 154 ++++ .../notes/shared/util/CustomAppGlideModule.java | 18 + .../notes/shared/util/DatabaseIndexUtil.java | 40 + .../notes/shared/util/DeviceCredentialUtil.java | 28 + .../owncloud/notes/shared/util/DisplayUtils.java | 132 +++ .../owncloud/notes/shared/util/MarkDownUtil.java | 124 +++ .../owncloud/notes/shared/util/NoteLinksUtils.java | 66 ++ .../owncloud/notes/shared/util/NoteUtil.java | 171 ++++ .../owncloud/notes/shared/util/SSOUtil.java | 52 ++ .../owncloud/notes/shared/util/ShareUtil.java | 20 + .../owncloud/notes/shared/util/SupportUtil.java | 49 ++ .../owncloud/notes/util/CategorySortingMethod.java | 41 - .../owncloud/notes/util/ClipboardUtil.java | 74 -- .../niedermann/owncloud/notes/util/ColorUtil.java | 154 ---- .../owncloud/notes/util/CustomAppGlideModule.java | 18 - .../owncloud/notes/util/DatabaseIndexUtil.java | 40 - .../owncloud/notes/util/DeviceCredentialUtil.java | 29 - .../owncloud/notes/util/DisplayUtils.java | 131 --- .../owncloud/notes/util/MarkDownUtil.java | 124 --- .../owncloud/notes/util/NoteLinksUtils.java | 66 -- .../niedermann/owncloud/notes/util/NoteUtil.java | 171 ---- .../it/niedermann/owncloud/notes/util/Notes.java | 93 -- .../owncloud/notes/util/NotesImageLoader.java | 42 - .../owncloud/notes/util/NotesTextWatcher.java | 98 --- .../it/niedermann/owncloud/notes/util/SSOUtil.java | 52 -- .../owncloud/notes/util/ServerResponse.java | 104 --- .../niedermann/owncloud/notes/util/ShareUtil.java | 20 - .../owncloud/notes/util/SupportUtil.java | 49 -- .../format/ContextBasedFormattingCallback.java | 115 --- .../ContextBasedRangeFormattingCallback.java | 150 ---- .../owncloud/notes/widget/AbstractWidgetData.java | 42 + .../notes/widget/createnote/CreateNoteWidget.java | 51 ++ .../notes/widget/notelist/NoteListWidget.java | 193 ++++ .../NoteListWidgetConfigurationActivity.java | 181 ++++ .../widget/notelist/NoteListWidgetFactory.java | 147 ++++ .../widget/notelist/NoteListWidgetService.java | 11 + .../notes/widget/notelist/NoteListsWidgetData.java | 44 + .../notes/widget/singlenote/SingleNoteWidget.java | 96 ++ .../SingleNoteWidgetConfigurationActivity.java | 74 ++ .../widget/singlenote/SingleNoteWidgetData.java | 25 + .../widget/singlenote/SingleNoteWidgetFactory.java | 146 +++ .../widget/singlenote/SingleNoteWidgetService.java | 11 + 199 files changed, 8945 insertions(+), 9011 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/ExceptionHandler.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/ExceptionUtil.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/SplashscreenActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/about/AboutActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentContributingTab.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentCreditsTab.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentLicenseTab.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/AlwaysAutoCompleteTextView.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/DarkModeSetting.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/MultiSelectedActionModeCallback.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/NotesListViewItemTouchHelper.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/AboutActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/AppendToNoteActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/EditNoteActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/ExceptionActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/LockedActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/NoteListWidgetConfigurationActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/NotesListViewActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/PreferencesActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/SelectSingleNoteActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/activity/SplashscreenActivity.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/CreateNoteWidget.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidget.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidgetFactory.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidgetService.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteWidgetHelper.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidget.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidgetFactory.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidgetService.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/AccountChooserAdapter.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/BaseNoteFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/CategoryAdapter.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/CategoryDialogFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/EditTitleDialogFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/ExceptionDialogFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/MoveAccountDialogFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NoteEditFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NotePreviewFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NoteReadonlyFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/PreferencesFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/SearchableBaseNoteFragment.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentContributingTab.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentCreditsTab.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentLicenseTab.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/android/quicksettings/NewNoteTileService.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/NotesTextWatcher.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/format/ContextBasedFormattingCallback.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/format/ContextBasedRangeFormattingCallback.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionHandler.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/formattinghelp/FormattingHelpActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/NavigationAdapter.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItemDecoration.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionViewHolder.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/AbstractWidgetData.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/ApiVersion.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/Capabilities.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/Category.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/CloudNote.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/DBNote.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/DBStatus.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/GridItemDecoration.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/ISyncCallback.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/Item.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/ItemAdapter.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/LocalAccount.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/NavigationAdapter.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/NoteClickListener.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/NoteListsWidgetData.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewGridHolder.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewGridHolderOnlyTitle.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolder.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolderWithExcerpt.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolderWithoutExcerpt.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/SectionItem.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/SectionItemDecoration.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/SectionViewHolder.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/SingleNoteWidgetData.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/model/SyncResultStatus.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/preferences/DarkModeSetting.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/quicksettings/NewNoteTileService.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserAdapter.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/Category.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/CloudNote.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBNote.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/ISyncCallback.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/Item.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/LocalAccount.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/ClipboardUtil.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/ColorUtil.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/DeviceCredentialUtil.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/MarkDownUtil.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtils.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/SSOUtil.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/ShareUtil.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/SupportUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/CategorySortingMethod.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/ClipboardUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/ColorUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/CustomAppGlideModule.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/DatabaseIndexUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/DeviceCredentialUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/DisplayUtils.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/MarkDownUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/NoteLinksUtils.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/NoteUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/Notes.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/NotesImageLoader.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/NotesTextWatcher.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/SSOUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/ServerResponse.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/ShareUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/SupportUtil.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/format/ContextBasedFormattingCallback.java delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/format/ContextBasedRangeFormattingCallback.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/AbstractWidgetData.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/createnote/CreateNoteWidget.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetService.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListsWidgetData.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetData.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetService.java (limited to 'app/src/main/java') diff --git a/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java new file mode 100644 index 00000000..23e68804 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java @@ -0,0 +1,58 @@ +package it.niedermann.owncloud.notes; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; + +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.main.MainActivity; + +public class AppendToNoteActivity extends MainActivity { + + private static final String TAG = AppendToNoteActivity.class.getSimpleName(); + + String receivedText = ""; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Intent receivedIntent = getIntent(); + receivedText = receivedIntent.getStringExtra(Intent.EXTRA_TEXT); + @Nullable final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + getSupportActionBar().setTitle(R.string.append_to_note); + } else { + Log.e(TAG, "SupportActionBar is null. Expected toolbar to be present to set a title."); + } + binding.activityNotesListView.toolbar.setSubtitle(receivedText); + } + + @Override + public void onNoteClick(int position, View v) { + if (receivedText != null && receivedText.length() > 0) { + final DBNote note = db.getNote(localAccount.getId(), ((DBNote) adapter.getItem(position)).getId()); + final String oldContent = note.getContent(); + String newContent; + if (oldContent != null && oldContent.length() > 0) { + newContent = oldContent + "\n\n" + receivedText; + } else { + newContent = receivedText; + } + db.updateNoteAndSync(ssoAccount, localAccount, note, newContent, () -> Toast.makeText(this, getString(R.string.added_content, receivedText), Toast.LENGTH_SHORT).show()); + } else { + Toast.makeText(this, R.string.shared_text_empty, Toast.LENGTH_SHORT).show(); + } + finish(); + } + + @Override + public boolean onNoteLongClick(int position, View v) { + return false; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/ExceptionHandler.java b/app/src/main/java/it/niedermann/owncloud/notes/ExceptionHandler.java deleted file mode 100644 index 1f73d2db..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/ExceptionHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -package it.niedermann.owncloud.notes; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.NonNull; - -import it.niedermann.owncloud.notes.android.activity.ExceptionActivity; - - -public class ExceptionHandler implements Thread.UncaughtExceptionHandler { - - private static final String TAG = ExceptionHandler.class.getSimpleName(); - private Context context; - private Class errorActivity; - public static final String KEY_THROWABLE = "T"; - - public ExceptionHandler(Context context) { - super(); - this.context = context; - this.errorActivity = ExceptionActivity.class; - } - - public ExceptionHandler(Context context, Class errorActivity) { - super(); - this.context = context; - this.errorActivity = errorActivity; - } - - @Override - public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { - Log.e(TAG, e.getMessage(), e); - Intent intent = new Intent(context.getApplicationContext(), errorActivity); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - Bundle extras = new Bundle(); - intent.putExtra(KEY_THROWABLE, e); - extras.putSerializable(KEY_THROWABLE, e); - intent.putExtras(extras); - context.getApplicationContext().startActivity(intent); - if (context instanceof Activity) { - ((Activity) context).finish(); - } - Runtime.getRuntime().exit(0); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/ExceptionUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/ExceptionUtil.java deleted file mode 100644 index f00c6fca..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/ExceptionUtil.java +++ /dev/null @@ -1,69 +0,0 @@ -package it.niedermann.owncloud.notes; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; - -import androidx.annotation.NonNull; - -import com.nextcloud.android.sso.helper.VersionCheckHelper; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.List; - -public class ExceptionUtil { - - private ExceptionUtil() { - - } - - public static String getDebugInfos(Context context, Throwable throwable) { - List throwables = new ArrayList<>(); - throwables.add(throwable); - return getDebugInfos(context, throwables); - } - - public static String getDebugInfos(@NonNull Context context, List throwables) { - StringBuilder debugInfos = new StringBuilder() - .append(getAppVersions(context)) - .append("\n\n---\n") - .append(getDeviceInfos()) - .append("\n\n---"); - for (Throwable throwable : throwables) { - debugInfos.append("\n\n").append(getStacktraceOf(throwable)); - } - return debugInfos.toString(); - } - - private static String getAppVersions(Context context) { - String versions = "" - + "App Version: " + BuildConfig.VERSION_NAME + "\n" - + "App Version Code: " + BuildConfig.VERSION_CODE + "\n" - + "App Flavor: " + BuildConfig.FLAVOR + "\n"; - - try { - versions += "\nFiles App Version Code: " + VersionCheckHelper.getNextcloudFilesVersionCode(context); - } catch (PackageManager.NameNotFoundException e) { - versions += "\nFiles 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(); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java new file mode 100644 index 00000000..0e537956 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java @@ -0,0 +1,252 @@ +package it.niedermann.owncloud.notes; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.util.TypedValue; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; + +import com.yydcdut.markdown.MarkdownProcessor; +import com.yydcdut.markdown.syntax.text.TextFactory; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandedActivity; +import it.niedermann.owncloud.notes.databinding.ActivityFormattingHelpBinding; + +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_CHECKED_MINUS; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_CHECKED_STAR; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_UNCHECKED_MINUS; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_UNCHECKED_STAR; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.getMarkDownConfiguration; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.parseCompat; +import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences; + +public class FormattingHelpActivity extends BrandedActivity { + + private ActivityFormattingHelpBinding binding; + private String content; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityFormattingHelpBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setSupportActionBar(binding.toolbar); + + content = buildFormattingHelp(); + + final MarkdownProcessor markdownProcessor = new MarkdownProcessor(this); + markdownProcessor.factory(TextFactory.create()); + markdownProcessor.config(getMarkDownConfiguration(binding.content.getContext()) + .setOnTodoClickCallback((view, line, lineNumber) -> { + try { + String[] lines = TextUtils.split(content, "\\r?\\n"); + /* + * Workaround for RxMarkdown-bug: + * When (un)checking a checkbox in a note which contains code-blocks, the "`"-characters get stripped out in the TextView and therefore the given lineNumber is wrong + * Find number of lines starting with ``` before lineNumber + */ + boolean inCodefence = false; + for (int i = 0; i < lines.length; i++) { + if (lines[i].startsWith("```")) { + inCodefence = !inCodefence; + lineNumber++; + } + if (inCodefence && TextUtils.isEmpty(lines[i])) { + lineNumber++; + } + if (i == lineNumber) { + break; + } + } + + /* + * Workaround for multiple RxMarkdown-bugs: + * When (un)checking a checkbox which is in the last line, every time it gets toggled, the last character of the line gets lost. + * When (un)checking a checkbox, every markdown gets stripped in the given line argument + */ + if (lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_MINUS) || lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_STAR)) { + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_MINUS, CHECKBOX_CHECKED_MINUS); + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_STAR, CHECKBOX_CHECKED_STAR); + } else { + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_MINUS, CHECKBOX_UNCHECKED_MINUS); + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_STAR, CHECKBOX_UNCHECKED_STAR); + } + + content = TextUtils.join("\n", lines); + binding.content.setText(parseCompat(markdownProcessor, content)); + } catch (IndexOutOfBoundsException e) { + Toast.makeText(this, R.string.checkbox_could_not_be_toggled, Toast.LENGTH_SHORT).show(); + e.printStackTrace(); + } + return line; + } + ) + .setOnLinkClickCallback((view, link) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(link)))) + .build()); + binding.content.setMovementMethod(LinkMovementMethod.getInstance()); + binding.content.setText(parseCompat(markdownProcessor, content)); + + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + binding.content.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(this, sp)); + if (sp.getBoolean(getString(R.string.pref_key_font), false)) { + binding.content.setTypeface(Typeface.MONOSPACE); + } + } + + @NonNull + private String buildFormattingHelp() { + final String lineBreak = "\n"; + final String indention = " "; + final String divider = getString(R.string.formatting_help_divider); + final String codefence = getString(R.string.formatting_help_codefence); + + int numberedListItem = 1; + final String lists = getString(R.string.formatting_help_lists_body_1) + lineBreak + + lineBreak + + getString(R.string.formatting_help_ol, numberedListItem++, getString(R.string.formatting_help_lists_body_2)) + lineBreak + + getString(R.string.formatting_help_ol, numberedListItem++, getString(R.string.formatting_help_lists_body_3)) + lineBreak + + getString(R.string.formatting_help_ol, numberedListItem, getString(R.string.formatting_help_lists_body_4)) + lineBreak + + lineBreak + + getString(R.string.formatting_help_lists_body_5) + lineBreak + + lineBreak + + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_6)) + lineBreak + + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_7)) + lineBreak + + indention + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_8)) + lineBreak + + indention + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_9)) + lineBreak; + + final String checkboxes = getString(R.string.formatting_help_checkboxes_body_1) + lineBreak + + lineBreak + + getString(R.string.formatting_help_checkbox_checked, getString(R.string.formatting_help_checkboxes_body_2)) + lineBreak + + getString(R.string.formatting_help_checkbox_unchecked, getString(R.string.formatting_help_checkboxes_body_3)) + lineBreak; + + final String structuredDocuments = getString(R.string.formatting_help_structured_documents_body_1, "`#`", "`##`") + lineBreak + + lineBreak + + getString(R.string.formatting_help_title_level_3, getString(R.string.formatting_help_structured_documents_body_2)) + lineBreak + + lineBreak + + getString(R.string.formatting_help_structured_documents_body_3, "`#`", "`######`") + lineBreak + + lineBreak + + getString(R.string.formatting_help_structured_documents_body_4, getString(R.string.formatting_help_quote_keyword)) + lineBreak + + lineBreak + + getString(R.string.formatting_help_quote, getString(R.string.formatting_help_structured_documents_body_5)) + lineBreak + + getString(R.string.formatting_help_quote, getString(R.string.formatting_help_structured_documents_body_6)) + lineBreak; + + final String javascript = getString(R.string.formatting_help_javascript_1) + lineBreak + + indention + indention + getString(R.string.formatting_help_javascript_2) + lineBreak + + getString(R.string.formatting_help_javascript_3) + lineBreak; + + return getString(R.string.formatting_help_title, getString(R.string.formatting_help_cbf_title)) + lineBreak + + lineBreak + + getString(R.string.formatting_help_cbf_body_1) + lineBreak + + getString(R.string.formatting_help_cbf_body_2, + getString(R.string.formatting_help_codefence_inline, getString(android.R.string.cut)), + getString(R.string.formatting_help_codefence_inline, getString(android.R.string.copy)), + getString(R.string.formatting_help_codefence_inline, getString(android.R.string.selectAll)), + getString(R.string.formatting_help_codefence_inline, getString(R.string.simple_link)), + getString(R.string.formatting_help_codefence_inline, getString(R.string.simple_checkbox)) + ) + lineBreak + + lineBreak + + divider + lineBreak + + lineBreak + + getString(R.string.formatting_help_title, getString(R.string.formatting_help_text_title)) + lineBreak + + lineBreak + + getString(R.string.formatting_help_text_body, + getString(R.string.formatting_help_bold), + getString(R.string.formatting_help_italic), + getString(R.string.formatting_help_strike_through) + ) + lineBreak + + lineBreak + + codefence + lineBreak + + getString(R.string.formatting_help_text_body, + getString(R.string.formatting_help_bold), + getString(R.string.formatting_help_italic), + getString(R.string.formatting_help_strike_through) + ) + lineBreak + + codefence + lineBreak + + lineBreak + + divider + lineBreak + + lineBreak + + getString(R.string.formatting_help_title, getString(R.string.formatting_help_lists_title)) + lineBreak + + lineBreak + + lists + + lineBreak + + codefence + lineBreak + + lists + + codefence + lineBreak + + lineBreak + + divider + lineBreak + + lineBreak + + getString(R.string.formatting_help_title, getString(R.string.formatting_help_checkboxes_title)) + lineBreak + + lineBreak + + checkboxes + + lineBreak + + codefence + lineBreak + + checkboxes + + codefence + lineBreak + + lineBreak + + divider + lineBreak + + lineBreak + + getString(R.string.formatting_help_title, getString(R.string.formatting_help_structured_documents_title)) + lineBreak + + lineBreak + + structuredDocuments + + lineBreak + + codefence + lineBreak + + structuredDocuments + + codefence + lineBreak + + lineBreak + + divider + lineBreak + + lineBreak + + getString(R.string.formatting_help_title, getString(R.string.formatting_help_code_title)) + lineBreak + + lineBreak + + getString(R.string.formatting_help_code_body_1) + lineBreak + + lineBreak + + getString(R.string.formatting_help_codefence_inline_escaped, getString(R.string.formatting_help_code_javascript_inline)) + lineBreak + + getString(R.string.formatting_help_codefence_inline, getString(R.string.formatting_help_code_javascript_inline)) + lineBreak + + lineBreak + + getString(R.string.formatting_help_code_body_2) + lineBreak + + lineBreak + + getString(R.string.formatting_help_codefence_escaped) + lineBreak + + javascript + + getString(R.string.formatting_help_codefence_escaped) + lineBreak + + lineBreak + + codefence + lineBreak + + javascript + + codefence + lineBreak + + lineBreak + + getString(R.string.formatting_help_code_body_3) + lineBreak + + lineBreak + + getString(R.string.formatting_help_codefence_javascript_escaped) + lineBreak + + javascript + + getString(R.string.formatting_help_codefence_escaped) + lineBreak + + lineBreak + + getString(R.string.formatting_help_codefence_javascript) + lineBreak + + javascript + + codefence + lineBreak + + lineBreak + + divider + lineBreak + + lineBreak + + getString(R.string.formatting_help_title, getString(R.string.formatting_help_unsupported_title)) + lineBreak + + lineBreak + + getString(R.string.formatting_help_unsupported_body_1) + lineBreak + + lineBreak + + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_unsupported_body_2)) + lineBreak + + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_unsupported_body_3)) + lineBreak + + lineBreak + + getString(R.string.formatting_help_unsupported_body_4) + lineBreak; + } + + @Override + public void applyBrand(int mainColor, int textColor) { + applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java new file mode 100644 index 00000000..8b024050 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java @@ -0,0 +1,107 @@ +package it.niedermann.owncloud.notes; + +import android.app.KeyguardManager; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.Nullable; + +import it.niedermann.owncloud.notes.exception.ExceptionHandler; +import it.niedermann.owncloud.notes.branding.BrandedActivity; + +public abstract class LockedActivity extends BrandedActivity { + + private static final String TAG = LockedActivity.class.getSimpleName(); + + private static final int REQUEST_CODE_UNLOCK = 100; + + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + + if (isTaskRoot()) { + askToUnlock(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (!isTaskRoot()) { + askToUnlock(); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (isTaskRoot()) { + NotesApplication.updateLastInteraction(); + } + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + NotesApplication.updateLastInteraction(); + } + + @Override + public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { + NotesApplication.updateLastInteraction(); + super.startActivityForResult(intent, requestCode, options); + } + + @Override + public void startActivityForResult(Intent intent, int requestCode) { + NotesApplication.updateLastInteraction(); + super.startActivityForResult(intent, requestCode); + } + + @Override + public void startActivity(Intent intent) { + NotesApplication.updateLastInteraction(); + super.startActivity(intent); + } + + @Override + public void startActivity(Intent intent, @Nullable Bundle options) { + NotesApplication.updateLastInteraction(); + super.startActivity(intent, options); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_CODE_UNLOCK) { + if (resultCode == RESULT_OK) { + Log.v(TAG, "Successfully unlocked device"); + NotesApplication.unlock(); + } else { + Log.e(TAG, "Result code of unlocking was " + resultCode); + finish(); + } + } + } + + private void askToUnlock() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && NotesApplication.isLocked()) { + KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + if (keyguardManager != null) { + Intent i = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.unlock_notes), null); + i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivityForResult(i, REQUEST_CODE_UNLOCK); + } else { + Log.e(TAG, "Keyguard manager is null"); + } + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java new file mode 100644 index 00000000..dce36d2b --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java @@ -0,0 +1,92 @@ +package it.niedermann.owncloud.notes; + +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.util.Log; + +import androidx.appcompat.app.AppCompatDelegate; +import androidx.preference.PreferenceManager; + +import it.niedermann.owncloud.notes.preferences.DarkModeSetting; + +import static androidx.preference.PreferenceManager.getDefaultSharedPreferences; + +public class NotesApplication extends Application { + private static final String TAG = NotesApplication.class.getSimpleName(); + + private static final long LOCK_TIME = 30 * 1000; + private static boolean lockedPreference = false; + private static boolean isLocked = true; + private static long lastInteraction = 0; + private static String PREF_KEY_THEME; + private static boolean isGridViewEnabled = false; + + @Override + public void onCreate() { + PREF_KEY_THEME = getString(R.string.pref_key_theme); + setAppTheme(getAppTheme(getApplicationContext())); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + lockedPreference = prefs.getBoolean(getString(R.string.pref_key_lock), false); + isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), false); + super.onCreate(); + } + + public static void setAppTheme(DarkModeSetting setting) { + AppCompatDelegate.setDefaultNightMode(setting.getModeId()); + } + + public static boolean isGridViewEnabled() { + return isGridViewEnabled; + } + + public static void updateGridViewEnabled(boolean gridView) { + isGridViewEnabled = gridView; + } + + public static DarkModeSetting getAppTheme(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String mode; + try { + mode = prefs.getString(PREF_KEY_THEME, DarkModeSetting.SYSTEM_DEFAULT.name()); + } catch (ClassCastException e) { + boolean darkModeEnabled = prefs.getBoolean(PREF_KEY_THEME, false); + mode = darkModeEnabled ? DarkModeSetting.DARK.name() : DarkModeSetting.LIGHT.name(); + } + return DarkModeSetting.valueOf(mode); + } + + public static boolean isDarkThemeActive(Context context, DarkModeSetting setting) { + if (setting == DarkModeSetting.SYSTEM_DEFAULT) { + return isDarkThemeActive(context); + } else { + return setting == DarkModeSetting.DARK; + } + } + + public static boolean isDarkThemeActive(Context context) { + int uiMode = context.getResources().getConfiguration().uiMode; + return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } + + public static void setLockedPreference(boolean lockedPreference) { + Log.i(TAG, "New locked preference: " + lockedPreference); + NotesApplication.lockedPreference = lockedPreference; + } + + public static boolean isLocked() { + if (!isLocked && System.currentTimeMillis() > (LOCK_TIME + lastInteraction)) { + isLocked = true; + } + return lockedPreference && isLocked; + } + + public static void unlock() { + isLocked = false; + } + + public static void updateLastInteraction() { + lastInteraction = System.currentTimeMillis(); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/SplashscreenActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/SplashscreenActivity.java new file mode 100644 index 00000000..ad14204f --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/SplashscreenActivity.java @@ -0,0 +1,26 @@ +package it.niedermann.owncloud.notes; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import it.niedermann.owncloud.notes.exception.ExceptionHandler; +import it.niedermann.owncloud.notes.main.MainActivity; + + +/** + * Created by stefan on 18.04.17. + */ +public class SplashscreenActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + + Intent intent = new Intent(this, MainActivity.class); + startActivity(intent); + finish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/about/AboutActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutActivity.java new file mode 100644 index 00000000..d764ba7a --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutActivity.java @@ -0,0 +1,94 @@ +package it.niedermann.owncloud.notes.about; + +import android.os.Bundle; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.LockedActivity; +import it.niedermann.owncloud.notes.branding.BrandingUtil; +import it.niedermann.owncloud.notes.databinding.ActivityAboutBinding; + +public class AboutActivity extends LockedActivity { + + private ActivityAboutBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityAboutBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setSupportActionBar(binding.toolbar); + binding.pager.setAdapter(new TabsPagerAdapter(getSupportFragmentManager())); + binding.tabs.setupWithViewPager(binding.pager); + } + + @Override + public void applyBrand(int mainColor, int textColor) { + applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar); + @ColorInt int finalMainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(this, mainColor); + binding.tabs.setSelectedTabIndicatorColor(finalMainColor); + } + + private class TabsPagerAdapter extends FragmentPagerAdapter { + + TabsPagerAdapter(FragmentManager fragmentManager) { + super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @Override + public int getCount() { + return 3; + } + + /** + * return the right fragment for the given position + */ + @NonNull + @Override + public Fragment getItem(int position) { + switch (position) { + case 1: + return new AboutFragmentContributingTab(); + + case 2: + return new AboutFragmentLicenseTab(); + + default: + return new AboutFragmentCreditsTab(); + } + } + + /** + * generate title based on given position + */ + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case 0: + return getString(R.string.about_credits_tab_title); + + case 1: + return getString(R.string.about_contribution_tab_title); + + case 2: + return getString(R.string.about_license_tab_title); + + default: + return null; + } + } + } + + @Override + public boolean onSupportNavigateUp() { + finish(); // close this activity as oppose to navigating up + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentContributingTab.java b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentContributingTab.java new file mode 100644 index 00000000..369c77a0 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentContributingTab.java @@ -0,0 +1,25 @@ +package it.niedermann.owncloud.notes.about; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.FragmentAboutContributionTabBinding; +import it.niedermann.owncloud.notes.shared.util.SupportUtil; + +public class AboutFragmentContributingTab extends Fragment { + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + FragmentAboutContributionTabBinding b = FragmentAboutContributionTabBinding.inflate(inflater, container, false); + SupportUtil.setHtml(b.aboutSource, R.string.about_source, getString(R.string.url_source)); + SupportUtil.setHtml(b.aboutIssues, R.string.about_issues, getString(R.string.url_issues)); + SupportUtil.setHtml(b.aboutTranslate, R.string.about_translate, getString(R.string.url_translations)); + return b.getRoot(); + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentCreditsTab.java b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentCreditsTab.java new file mode 100644 index 00000000..fc18f5ea --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentCreditsTab.java @@ -0,0 +1,26 @@ +package it.niedermann.owncloud.notes.about; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import it.niedermann.owncloud.notes.BuildConfig; +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.FragmentAboutCreditsTabBinding; +import it.niedermann.owncloud.notes.shared.util.SupportUtil; + +public class AboutFragmentCreditsTab extends Fragment { + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + FragmentAboutCreditsTabBinding binding = FragmentAboutCreditsTabBinding.inflate(inflater, container, false); + SupportUtil.setHtml(binding.aboutVersion, R.string.about_version, "v" + BuildConfig.VERSION_NAME); + SupportUtil.setHtml(binding.aboutMaintainer, R.string.about_maintainer); + SupportUtil.setHtml(binding.aboutTranslators, R.string.about_translators_transifex, getString(R.string.url_translations)); + return binding.getRoot(); + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentLicenseTab.java b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentLicenseTab.java new file mode 100644 index 00000000..41f8266a --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentLicenseTab.java @@ -0,0 +1,44 @@ +package it.niedermann.owncloud.notes.about; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.core.graphics.drawable.DrawableCompat; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandedFragment; +import it.niedermann.owncloud.notes.branding.BrandingUtil; +import it.niedermann.owncloud.notes.databinding.FragmentAboutLicenseTabBinding; +import it.niedermann.owncloud.notes.shared.util.ColorUtil; +import it.niedermann.owncloud.notes.shared.util.SupportUtil; + +public class AboutFragmentLicenseTab extends BrandedFragment { + + private FragmentAboutLicenseTabBinding binding; + + private void openLicense() { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_license)))); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = FragmentAboutLicenseTabBinding.inflate(inflater, container, false); + binding.aboutAppLicenseButton.setOnClickListener((v) -> openLicense()); + SupportUtil.setHtml(binding.aboutIconsDisclaimer, R.string.about_icons_disclaimer, getString(R.string.about_app_icon_author)); + return binding.getRoot(); + } + + @Override + public void applyBrand(int mainColor, int textColor) { + @ColorInt final int finalMainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor); + DrawableCompat.setTintList(binding.aboutAppLicenseButton.getBackground(), ColorStateList.valueOf(finalMainColor)); + binding.aboutAppLicenseButton.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor)); + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java new file mode 100644 index 00000000..5ac670db --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java @@ -0,0 +1,106 @@ +package it.niedermann.owncloud.notes.accountpicker; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; +import java.util.Objects; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandedAlertDialogBuilder; +import it.niedermann.owncloud.notes.branding.BrandedDialogFragment; +import it.niedermann.owncloud.notes.databinding.DialogChooseAccountBinding; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.shared.account.AccountChooserAdapter; +import it.niedermann.owncloud.notes.shared.account.AccountChooserViewHolder; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; + +/** + * A {@link DialogFragment} which provides an {@link LocalAccount} chooser that hides the given {@link LocalAccount}. + * This can be useful when one wants to pick e. g. a target for move a note from one {@link LocalAccount} to another.. + */ +public class AccountPickerDialogFragment extends BrandedDialogFragment { + + private AccountPickerListener accountPickerListener; + private static final String PARAM_ACCOUNT_ID_TO_EXCLUDE = "account_id_to_exclude"; + private long accountIdToExclude; + + /** + * Use newInstance()-Method + */ + public AccountPickerDialogFragment() { + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof AccountPickerListener) { + this.accountPickerListener = (AccountPickerListener) context; + } else { + throw new ClassCastException("Caller must implement " + AccountPickerListener.class.getSimpleName()); + } + accountIdToExclude = requireArguments().getLong(PARAM_ACCOUNT_ID_TO_EXCLUDE, -1L); + if (accountIdToExclude < 0) { + throw new IllegalArgumentException(PARAM_ACCOUNT_ID_TO_EXCLUDE + " must be greater 0"); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + View view = View.inflate(getContext(), R.layout.dialog_choose_account, null); + DialogChooseAccountBinding binding = DialogChooseAccountBinding.bind(view); + + NotesDatabase db = NotesDatabase.getInstance(getActivity()); + List accountsList = db.getAccounts(); + + for (int i = 0; i < accountsList.size(); i++) { + if (accountsList.get(i).getId() == accountIdToExclude) { + accountsList.remove(i); + break; + } + } + + RecyclerView.Adapter adapter = new AccountChooserAdapter(accountsList, (account -> { + accountPickerListener.onAccountPicked(account); + dismiss(); + })); + binding.accountsList.setAdapter(adapter); + + return new BrandedAlertDialogBuilder(requireActivity()) + .setView(binding.getRoot()) + .setTitle(R.string.simple_move) + .setNegativeButton(android.R.string.cancel, null) + .create(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + Objects.requireNonNull(requireDialog().getWindow()).setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + return super.onCreateView(inflater, container, savedInstanceState); + } + + public static DialogFragment newInstance(long accountIdToExclude) { + final DialogFragment fragment = new AccountPickerDialogFragment(); + final Bundle args = new Bundle(); + args.putLong(PARAM_ACCOUNT_ID_TO_EXCLUDE, accountIdToExclude); + fragment.setArguments(args); + return fragment; + } + + @Override + public void applyBrand(int mainColor, int textColor) { + // Nothing to do... + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java new file mode 100644 index 00000000..c77e1824 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java @@ -0,0 +1,9 @@ +package it.niedermann.owncloud.notes.accountpicker; + +import androidx.annotation.NonNull; + +import it.niedermann.owncloud.notes.shared.model.LocalAccount; + +public interface AccountPickerListener { + void onAccountPicked(@NonNull LocalAccount account); +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java index 0223a53f..92e77e74 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java @@ -11,7 +11,7 @@ import java.util.ArrayList; import java.util.List; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; public class AccountSwitcherAdapter extends RecyclerView.Adapter { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index 78f20984..4286cf1e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -20,12 +20,15 @@ import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.branding.BrandedDialogFragment; import it.niedermann.owncloud.notes.databinding.DialogAccountSwitcherBinding; import it.niedermann.owncloud.notes.manageaccounts.ManageAccountsActivity; -import it.niedermann.owncloud.notes.model.LocalAccount; import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; -import static it.niedermann.owncloud.notes.android.activity.NotesListViewActivity.manage_account; import static it.niedermann.owncloud.notes.branding.BrandingUtil.applyBrandToLayerDrawable; +import static it.niedermann.owncloud.notes.main.MainActivity.manage_account; +/** + * Displays all available {@link LocalAccount} entries and provides basic operations for them, like adding or switching + */ public class AccountSwitcherDialog extends BrandedDialogFragment { private static final String KEY_CURRENT_ACCOUNT_ID = "current_account_id"; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java index 2e26277f..35750f2f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java @@ -1,6 +1,6 @@ package it.niedermann.owncloud.notes.accountswitcher; -import it.niedermann.owncloud.notes.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; public interface AccountSwitcherListener { void addAccount(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java index 1196d878..fcc3d91c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java @@ -13,7 +13,7 @@ import com.bumptech.glide.request.RequestOptions; import it.niedermann.android.glidesso.SingleSignOnUrl; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding; -import it.niedermann.owncloud.notes.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/AlwaysAutoCompleteTextView.java b/app/src/main/java/it/niedermann/owncloud/notes/android/AlwaysAutoCompleteTextView.java deleted file mode 100644 index b391bd9b..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/AlwaysAutoCompleteTextView.java +++ /dev/null @@ -1,56 +0,0 @@ -package it.niedermann.owncloud.notes.android; - -import android.content.Context; -import android.util.AttributeSet; -import android.util.Log; -import android.view.WindowManager; - -import androidx.appcompat.widget.AppCompatAutoCompleteTextView; - -/** - * Extension of the {@link AppCompatAutoCompleteTextView}, but this one is always open, i.e. you can see the list of suggestions even the TextView is empty. - */ -public class AlwaysAutoCompleteTextView extends AppCompatAutoCompleteTextView { - - private static final String TAG = AlwaysAutoCompleteTextView.class.getSimpleName(); - - private int myThreshold; - - public AlwaysAutoCompleteTextView(Context context) { - super(context); - } - - public AlwaysAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public AlwaysAutoCompleteTextView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public void setThreshold(int threshold) { - myThreshold = Math.max(threshold, 0); - } - - @Override - public boolean enoughToFilter() { - return getText().length() >= myThreshold; - } - - @Override - public int getThreshold() { - return myThreshold; - } - - public void showFullDropDown() { - try { - performFiltering(getText(), 0); - showDropDown(); - } catch (WindowManager.BadTokenException e) { - // https://github.com/stefan-niedermann/nextcloud-notes/issues/366 - e.printStackTrace(); - Log.e(TAG, "Exception", e); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/DarkModeSetting.java b/app/src/main/java/it/niedermann/owncloud/notes/android/DarkModeSetting.java deleted file mode 100644 index 410a5719..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/DarkModeSetting.java +++ /dev/null @@ -1,66 +0,0 @@ -package it.niedermann.owncloud.notes.android; - -import androidx.appcompat.app.AppCompatDelegate; - -import java.util.NoSuchElementException; - -/** - * Possible values of the Dark Mode Setting. - *

- * The Dark Mode Setting can be stored in {@link android.content.SharedPreferences} as String by using {@link DarkModeSetting#name()} and received via {@link DarkModeSetting#valueOf(String)}. - *

- * Additionally, the equivalent {@link AppCompatDelegate}-Mode can be received via {@link #getModeId()}. To convert a {@link AppCompatDelegate}-Mode to a {@link DarkModeSetting}, use {@link #fromModeID(int)} - * - * @see AppCompatDelegate#MODE_NIGHT_YES - * @see AppCompatDelegate#MODE_NIGHT_NO - * @see AppCompatDelegate#MODE_NIGHT_FOLLOW_SYSTEM - */ -public enum DarkModeSetting { - // WARNING - The names of the constants must *NOT* be changed since they are used as keys in SharedPreferences - - /** - * Always use light mode. - */ - LIGHT(AppCompatDelegate.MODE_NIGHT_NO), - /** - * Always use dark mode. - */ - DARK(AppCompatDelegate.MODE_NIGHT_YES), - /** - * Follow the global system setting for dark mode. - */ - SYSTEM_DEFAULT(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - - private final int modeId; - - DarkModeSetting(int modeId) { - this.modeId = modeId; - } - - public int getModeId() { - return modeId; - } - - /** - * Returns the instance of {@link DarkModeSetting} that corresponds to the ModeID of {@link AppCompatDelegate} - *

- * Possible ModeIDs are: - *

    - *
  • {@link AppCompatDelegate#MODE_NIGHT_YES}
  • - *
  • {@link AppCompatDelegate#MODE_NIGHT_NO}
  • - *
  • {@link AppCompatDelegate#MODE_NIGHT_FOLLOW_SYSTEM}
  • - *
- * - * @param id One of the {@link AppCompatDelegate}-Night-Modes - * @return An instance of {@link DarkModeSetting} - */ - public static DarkModeSetting fromModeID(int id) { - for (DarkModeSetting value : DarkModeSetting.values()) { - if (value.modeId == id) { - return value; - } - } - - throw new NoSuchElementException("No NightMode with ID " + id + " found"); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/android/MultiSelectedActionModeCallback.java deleted file mode 100644 index b90be5d7..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/MultiSelectedActionModeCallback.java +++ /dev/null @@ -1,161 +0,0 @@ -package it.niedermann.owncloud.notes.android; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; -import android.util.TypedValue; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import androidx.annotation.ColorInt; -import androidx.appcompat.view.ActionMode; -import androidx.appcompat.view.ActionMode.Callback; -import androidx.appcompat.widget.SearchView; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.fragment.app.FragmentManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.snackbar.Snackbar; -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 java.util.ArrayList; -import java.util.List; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.activity.NotesListViewActivity; -import it.niedermann.owncloud.notes.android.fragment.MoveAccountDialogFragment; -import it.niedermann.owncloud.notes.branding.BrandedSnackbar; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.ItemAdapter; -import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper.ViewProvider; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.ShareUtil; - -public class MultiSelectedActionModeCallback implements Callback { - - @ColorInt - private int colorAccent; - - private final Context context; - private final ViewProvider viewProvider; - private final NotesDatabase db; - private final ItemAdapter adapter; - private final RecyclerView recyclerView; - private final Runnable refreshLists; - private final FragmentManager fragmentManager; - private final SearchView searchView; - - public MultiSelectedActionModeCallback( - Context context, ViewProvider viewProvider, NotesDatabase db, ActionMode actionMode, ItemAdapter adapter, RecyclerView recyclerView, Runnable refreshLists, FragmentManager fragmentManager, SearchView searchView) { - this.context = context; - this.viewProvider = viewProvider; - this.db = db; - this.adapter = adapter; - this.recyclerView = recyclerView; - this.refreshLists = refreshLists; - this.fragmentManager = fragmentManager; - this.searchView = searchView; - - final TypedValue typedValue = new TypedValue(); - context.getTheme().resolveAttribute(R.attr.colorAccent, typedValue, true); - colorAccent = typedValue.data; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - // inflate contextual menu - mode.getMenuInflater().inflate(R.menu.menu_list_context_multiple, menu); - for (int i = 0; i < menu.size(); i++) { - Drawable drawable = menu.getItem(i).getIcon(); - if (drawable != null) { - drawable = DrawableCompat.wrap(drawable); - DrawableCompat.setTint(drawable, colorAccent); - menu.getItem(i).setIcon(drawable); - } - } - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - /** - * @param mode ActionMode - used to close the Action Bar after all work is done. - * @param item MenuItem - the item in the List that contains the Node - * @return boolean - */ - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_delete: - try { - SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context); - List deletedNotes = new ArrayList<>(); - List selection = adapter.getSelected(); - for (Integer i : selection) { - DBNote note = (DBNote) adapter.getItem(i); - deletedNotes.add(db.getNote(note.getAccountId(), note.getId())); - db.deleteNoteAndSync(ssoAccount, note.getId()); - } - mode.finish(); // Action picked, so close the CAB - //after delete selection has to be cleared - searchView.setIconified(true); - refreshLists.run(); - String deletedSnackbarTitle = deletedNotes.size() == 1 - ? context.getString(R.string.action_note_deleted, deletedNotes.get(0).getTitle()) - : context.getString(R.string.bulk_notes_deleted, deletedNotes.size()); - BrandedSnackbar.make(viewProvider.getView(), deletedSnackbarTitle, Snackbar.LENGTH_LONG) - .setAction(R.string.action_undo, (View v) -> { - db.getNoteServerSyncHelper().addCallbackPush(ssoAccount, refreshLists::run); - for (DBNote deletedNote : deletedNotes) { - db.addNoteAndSync(ssoAccount, deletedNote.getAccountId(), deletedNote); - } - refreshLists.run(); - String restoreSnackbarTitle = deletedNotes.size() == 1 - ? context.getString(R.string.action_note_restored, deletedNotes.get(0).getTitle()) - : context.getString(R.string.bulk_notes_restored, deletedNotes.size()); - BrandedSnackbar.make(viewProvider.getView(), restoreSnackbarTitle, Snackbar.LENGTH_SHORT) - .show(); - }) - .show(); - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { - e.printStackTrace(); - } - return true; - case R.id.menu_move: - MoveAccountDialogFragment.newInstance().show(fragmentManager, NotesListViewActivity.class.getSimpleName()); - return true; - case R.id.menu_share: - final String subject = (adapter.getSelected().size() == 1) - ? ((DBNote) adapter.getItem(adapter.getSelected().get(0))).getTitle() - : context.getString(R.string.share_multiple, adapter.getSelected().size()); - final StringBuilder noteContents = new StringBuilder(); - for (Integer i : adapter.getSelected()) { - final DBNote noteWithoutContent = (DBNote) adapter.getItem(i); - final String tempFullNote = db.getNote(noteWithoutContent.getAccountId(), noteWithoutContent.getId()).getContent(); - if (!TextUtils.isEmpty(tempFullNote)) { - if (noteContents.length() > 0) { - noteContents.append("\n\n"); - } - noteContents.append(tempFullNote); - } - } - ShareUtil.openShareDialog(context, subject, noteContents.toString()); - return true; - default: - return false; - } - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - adapter.clearSelection(recyclerView); - adapter.notifyDataSetChanged(); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/NotesListViewItemTouchHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/android/NotesListViewItemTouchHelper.java deleted file mode 100644 index ed3cc8c2..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/NotesListViewItemTouchHelper.java +++ /dev/null @@ -1,137 +0,0 @@ -package it.niedermann.owncloud.notes.android; - -import android.content.Context; -import android.graphics.Canvas; -import android.util.Log; -import android.view.View; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.snackbar.Snackbar; -import com.nextcloud.android.sso.model.SingleSignOnAccount; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandedSnackbar; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.ISyncCallback; -import it.niedermann.owncloud.notes.model.ItemAdapter; -import it.niedermann.owncloud.notes.model.NoteViewHolder; -import it.niedermann.owncloud.notes.model.SectionViewHolder; -import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper.ViewProvider; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; - -public class NotesListViewItemTouchHelper extends ItemTouchHelper { - - private static final String TAG = NotesListViewItemTouchHelper.class.getSimpleName(); - - public NotesListViewItemTouchHelper( - @NonNull SingleSignOnAccount ssoAccount, - @NonNull Context context, - @NonNull NotesDatabase db, - @NonNull ItemAdapter adapter, - @NonNull ISyncCallback syncCallBack, - @NonNull Runnable refreshLists, - @Nullable SwipeRefreshLayout swipeRefreshLayout, - @Nullable ViewProvider viewProvider, - boolean gridView) { - super(new SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { - private boolean swipeRefreshLayoutEnabled; - - @Override - public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { - return false; - } - - /** - * Disable swipe on sections and if grid view is enabled - * - * @param recyclerView RecyclerView - * @param viewHolder RecyclerView.ViewHoler - * @return 0 if viewHolder is section or grid view is enabled, otherwise super() - */ - @Override - public int getSwipeDirs(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { - if (gridView || viewHolder instanceof SectionViewHolder) return 0; - return super.getSwipeDirs(recyclerView, viewHolder); - } - - /** - * Delete note if note is swiped to left or right - * - * @param viewHolder RecyclerView.ViewHoler - * @param direction int - */ - @Override - public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { - switch (direction) { - case ItemTouchHelper.LEFT: - final DBNote dbNoteWithoutContent = (DBNote) adapter.getItem(viewHolder.getAdapterPosition()); - final DBNote dbNote = db.getNote(dbNoteWithoutContent.getAccountId(), dbNoteWithoutContent.getId()); - db.deleteNoteAndSync(ssoAccount, dbNote.getId()); - adapter.remove(dbNote); - refreshLists.run(); - Log.v(TAG, "Item deleted through swipe ----------------------------------------------"); - if (viewProvider == null) { - Toast.makeText(context, context.getString(R.string.action_note_deleted, dbNote.getTitle()), Toast.LENGTH_LONG).show(); - } else { - BrandedSnackbar.make(viewProvider.getView(), context.getString(R.string.action_note_deleted, dbNote.getTitle()), Snackbar.LENGTH_LONG) - .setAction(R.string.action_undo, (View v) -> { - db.getNoteServerSyncHelper().addCallbackPush(ssoAccount, refreshLists::run); - db.addNoteAndSync(ssoAccount, dbNote.getAccountId(), dbNote); - refreshLists.run(); - BrandedSnackbar.make(viewProvider.getView(), context.getString(R.string.action_note_restored, dbNote.getTitle()), Snackbar.LENGTH_SHORT) - .show(); - }) - .show(); - } - break; - case ItemTouchHelper.RIGHT: - final DBNote adapterNote = (DBNote) adapter.getItem(viewHolder.getAdapterPosition()); - db.toggleFavorite(ssoAccount, adapterNote, syncCallBack); - refreshLists.run(); - break; - default: - //NoOp - } - } - - @Override - public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { - NoteViewHolder noteViewHolder = (NoteViewHolder) viewHolder; - // show swipe icon on the side - noteViewHolder.showSwipe(dX > 0); - // move only swipeable part of item (not leave-behind) - getDefaultUIUtil().onDraw(c, recyclerView, noteViewHolder.getNoteSwipeable(), dX, dY, actionState, isCurrentlyActive); - } - - @Override - public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) { - if (actionState == ACTION_STATE_SWIPE && swipeRefreshLayout != null) { - Log.i(TAG, "Start swiping, disable swipeRefreshLayout"); - swipeRefreshLayoutEnabled = swipeRefreshLayout.isEnabled(); - swipeRefreshLayout.setEnabled(false); - } - super.onSelectedChanged(viewHolder, actionState); - } - - @Override - public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { - Log.i(TAG, "End swiping, resetting swipeRefreshLayout state"); - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setEnabled(swipeRefreshLayoutEnabled); - } - getDefaultUIUtil().clearView(((NoteViewHolder) viewHolder).getNoteSwipeable()); - } - - @Override - public float getSwipeEscapeVelocity(float defaultValue) { - return defaultValue * 3; - } - }); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/AboutActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/AboutActivity.java deleted file mode 100644 index 9dbdc062..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/AboutActivity.java +++ /dev/null @@ -1,96 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.os.Bundle; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.fragment.about.AboutFragmentContributingTab; -import it.niedermann.owncloud.notes.android.fragment.about.AboutFragmentCreditsTab; -import it.niedermann.owncloud.notes.android.fragment.about.AboutFragmentLicenseTab; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.databinding.ActivityAboutBinding; - -public class AboutActivity extends LockedActivity { - - private ActivityAboutBinding binding; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityAboutBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setSupportActionBar(binding.toolbar); - binding.pager.setAdapter(new TabsPagerAdapter(getSupportFragmentManager())); - binding.tabs.setupWithViewPager(binding.pager); - } - - @Override - public void applyBrand(int mainColor, int textColor) { - applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar); - @ColorInt int finalMainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(this, mainColor); - binding.tabs.setSelectedTabIndicatorColor(finalMainColor); - } - - private class TabsPagerAdapter extends FragmentPagerAdapter { - - TabsPagerAdapter(FragmentManager fragmentManager) { - super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); - } - - @Override - public int getCount() { - return 3; - } - - /** - * return the right fragment for the given position - */ - @NonNull - @Override - public Fragment getItem(int position) { - switch (position) { - case 1: - return new AboutFragmentContributingTab(); - - case 2: - return new AboutFragmentLicenseTab(); - - default: - return new AboutFragmentCreditsTab(); - } - } - - /** - * generate title based on given position - */ - @Override - public CharSequence getPageTitle(int position) { - switch (position) { - case 0: - return getString(R.string.about_credits_tab_title); - - case 1: - return getString(R.string.about_contribution_tab_title); - - case 2: - return getString(R.string.about_license_tab_title); - - default: - return null; - } - } - } - - @Override - public boolean onSupportNavigateUp() { - finish(); // close this activity as oppose to navigating up - return true; - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/AppendToNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/AppendToNoteActivity.java deleted file mode 100644 index 4f5722aa..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/AppendToNoteActivity.java +++ /dev/null @@ -1,58 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.widget.Toast; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.model.DBNote; - -public class AppendToNoteActivity extends NotesListViewActivity { - - private static final String TAG = AppendToNoteActivity.class.getSimpleName(); - - String receivedText = ""; - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Intent receivedIntent = getIntent(); - receivedText = receivedIntent.getStringExtra(Intent.EXTRA_TEXT); - @Nullable final ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - getSupportActionBar().setTitle(R.string.append_to_note); - } else { - Log.e(TAG, "SupportActioBar is null. Expected toolbar to be present to set a title."); - } - binding.activityNotesListView.toolbar.setSubtitle(receivedText); - } - - @Override - public void onNoteClick(int position, View v) { - if (receivedText != null && receivedText.length() > 0) { - final DBNote note = db.getNote(localAccount.getId(), ((DBNote) adapter.getItem(position)).getId()); - final String oldContent = note.getContent(); - String newContent; - if (oldContent != null && oldContent.length() > 0) { - newContent = oldContent + "\n\n" + receivedText; - } else { - newContent = receivedText; - } - db.updateNoteAndSync(ssoAccount, localAccount, note, newContent, () -> Toast.makeText(this, getString(R.string.added_content, receivedText), Toast.LENGTH_SHORT).show()); - } else { - Toast.makeText(this, R.string.shared_text_empty, Toast.LENGTH_SHORT).show(); - } - finish(); - } - - @Override - public boolean onNoteLongClick(int position, View v) { - return false; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/EditNoteActivity.java deleted file mode 100644 index d8d032ee..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/EditNoteActivity.java +++ /dev/null @@ -1,282 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; - -import androidx.fragment.app.Fragment; -import androidx.preference.PreferenceManager; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.Calendar; -import java.util.Objects; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.fragment.BaseNoteFragment; -import it.niedermann.owncloud.notes.android.fragment.NoteEditFragment; -import it.niedermann.owncloud.notes.android.fragment.NotePreviewFragment; -import it.niedermann.owncloud.notes.android.fragment.NoteReadonlyFragment; -import it.niedermann.owncloud.notes.databinding.ActivityEditBinding; -import it.niedermann.owncloud.notes.model.Category; -import it.niedermann.owncloud.notes.model.CloudNote; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.LocalAccount; -import it.niedermann.owncloud.notes.util.NoteUtil; - -import static it.niedermann.owncloud.notes.android.fragment.AccountChooserAdapter.MoveAccountListener; - -public class EditNoteActivity extends LockedActivity implements BaseNoteFragment.NoteFragmentListener, MoveAccountListener { - - private static final String TAG = EditNoteActivity.class.getSimpleName(); - - public static final String ACTION_SHORTCUT = "it.niedermann.owncloud.notes.shortcut"; - private static final String INTENT_GOOGLE_ASSISTANT = "com.google.android.gm.action.AUTO_SEND"; - private static final String MIMETYPE_TEXT_PLAIN = "text/plain"; - public static final String PARAM_NOTE_ID = "noteId"; - public static final String PARAM_ACCOUNT_ID = "accountId"; - public static final String PARAM_CATEGORY = "category"; - public static final String PARAM_CONTENT = "content"; - public static final String PARAM_FAVORITE = "favorite"; - - private ActivityEditBinding binding; - - private BaseNoteFragment fragment; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityEditBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbar); - - if (savedInstanceState == null) { - launchNoteFragment(); - } else { - fragment = (BaseNoteFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container_view); - } - - setSupportActionBar(binding.toolbar); - if (!(fragment instanceof NoteReadonlyFragment)) { - binding.toolbar.setOnClickListener((v) -> fragment.showEditTitleDialog()); - } - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - Log.d(TAG, "onNewIntent: " + intent.getLongExtra(PARAM_NOTE_ID, 0)); - setIntent(intent); - if (fragment != null) { - getSupportFragmentManager().beginTransaction().detach(fragment).commit(); - fragment = null; - } - launchNoteFragment(); - } - - private long getNoteId() { - return getIntent().getLongExtra(PARAM_NOTE_ID, 0); - } - - private long getAccountId() { - return getIntent().getLongExtra(PARAM_ACCOUNT_ID, 0); - } - - /** - * Starts the note fragment for an existing note or a new note. - * The actual behavior is triggered by the activity's intent. - */ - private void launchNoteFragment() { - long noteId = getNoteId(); - if (noteId > 0) { - launchExistingNote(getAccountId(), noteId); - } else { - if (Intent.ACTION_VIEW.equals(getIntent().getAction())) { - launchReadonlyNote(); - } else { - launchNewNote(); - } - } - } - - /** - * Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note. - * The type of fragment (view-mode) is chosen based on the user preferences. - * - * @param noteId ID of the existing note. - */ - private void launchExistingNote(long accountId, long noteId) { - final String prefKeyNoteMode = getString(R.string.pref_key_note_mode); - final String prefKeyLastMode = getString(R.string.pref_key_last_note_mode); - final String prefValueEdit = getString(R.string.pref_value_mode_edit); - final String prefValuePreview = getString(R.string.pref_value_mode_preview); - final String prefValueLast = getString(R.string.pref_value_mode_last); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - String mode = preferences.getString(prefKeyNoteMode, prefValueEdit); - String lastMode = preferences.getString(prefKeyLastMode, prefValueEdit); - boolean editMode = true; - if (prefValuePreview.equals(mode) || (prefValueLast.equals(mode) && prefValuePreview.equals(lastMode))) { - editMode = false; - } - launchExistingNote(accountId, noteId, editMode); - } - - /** - * Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note. - * - * @param noteId ID of the existing note. - * @param edit View-mode of the fragment: - * true for {@link NoteEditFragment}, - * false for {@link NotePreviewFragment}. - */ - private void launchExistingNote(long accountId, long noteId, boolean edit) { - // save state of the fragment in order to resume with the same note and originalNote - Fragment.SavedState savedState = null; - if (fragment != null) { - savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment); - } - fragment = edit - ? NoteEditFragment.newInstance(accountId, noteId) - : NotePreviewFragment.newInstance(accountId, noteId); - - if (savedState != null) { - fragment.setInitialSavedState(savedState); - } - getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit(); - } - - /** - * Starts the {@link NoteEditFragment} with a new note. - * Content ("share" functionality), category and favorite attribute can be preset. - */ - private void launchNewNote() { - Intent intent = getIntent(); - - String category = null; - boolean favorite = false; - if (intent.hasExtra(PARAM_CATEGORY)) { - Category categoryPreselection = (Category) Objects.requireNonNull(intent.getSerializableExtra(PARAM_CATEGORY)); - category = categoryPreselection.category; - favorite = categoryPreselection.favorite != null ? categoryPreselection.favorite : false; - } - - String content = ""; - if ( - intent.hasExtra(Intent.EXTRA_TEXT) && - MIMETYPE_TEXT_PLAIN.equals(intent.getType()) && - (Intent.ACTION_SEND.equals(intent.getAction()) || - INTENT_GOOGLE_ASSISTANT.equals(intent.getAction())) - ) { - content = intent.getStringExtra(Intent.EXTRA_TEXT); - } else if (intent.hasExtra(PARAM_CONTENT)) { - content = intent.getStringExtra(PARAM_CONTENT); - } - - if (content == null) { - content = ""; - } - CloudNote newNote = new CloudNote(0, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, favorite, category, null); - fragment = NoteEditFragment.newInstanceWithNewNote(newNote); - getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit(); - } - - private void launchReadonlyNote() { - Intent intent = getIntent(); - StringBuilder content = new StringBuilder(); - try { - InputStream inputStream = getContentResolver().openInputStream(Objects.requireNonNull(intent.getData())); - BufferedReader r = new BufferedReader(new InputStreamReader(Objects.requireNonNull(inputStream))); - String line; - while ((line = r.readLine()) != null) { - content.append(line).append('\n'); - } - } catch (IOException e) { - e.printStackTrace(); - } - - fragment = NoteReadonlyFragment.newInstance(content.toString()); - getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit(); - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - close(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_note_activity, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - close(); - return true; - case R.id.menu_preview: - launchExistingNote(getAccountId(), getNoteId(), false); - return true; - case R.id.menu_edit: - launchExistingNote(getAccountId(), getNoteId(), true); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - - /** - * Send result and closes the Activity - */ - public void close() { - /* TODO enhancement: store last mode in note - * for cross device functionality per note mode should be stored on the server. - */ - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - final String prefKeyLastMode = getString(R.string.pref_key_last_note_mode); - if (fragment instanceof NoteEditFragment) { - preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_edit)).apply(); - } else { - preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_preview)).apply(); - } - fragment.onCloseNote(); - finish(); - } - - @Override - public void onNoteUpdated(DBNote note) { - if (note != null) { - binding.toolbar.setTitle(note.getTitle()); - if (note.getCategory().isEmpty()) { - binding.toolbar.setSubtitle(null); - } else { - binding.toolbar.setSubtitle(NoteUtil.extendCategory(note.getCategory())); - } - } else { - // Maybe account is not authenticated -> note == null - Log.e(TAG, "note is null, start " + NotesListViewActivity.class.getSimpleName()); - startActivity(new Intent(this, NotesListViewActivity.class)); - finish(); - } - } - - @Override - public void moveToAccount(LocalAccount account) { - fragment.moveNote(account); - } - - @Override - public void applyBrand(int mainColor, int textColor) { - applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar); - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/ExceptionActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/ExceptionActivity.java deleted file mode 100644 index 4b232cc4..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/ExceptionActivity.java +++ /dev/null @@ -1,55 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.annotation.SuppressLint; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.os.Bundle; -import android.widget.Toast; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; - -import java.util.Objects; - -import it.niedermann.owncloud.notes.ExceptionUtil; -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.ActivityExceptionBinding; - -import static it.niedermann.owncloud.notes.ExceptionHandler.KEY_THROWABLE; - - -public class ExceptionActivity extends AppCompatActivity { - - private ActivityExceptionBinding binding; - - @SuppressLint("SetTextI18n") // only used for logging - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityExceptionBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - binding.copy.setOnClickListener((v) -> copyStacktraceToClipboard()); - binding.close.setOnClickListener((v) -> close()); - - setSupportActionBar(binding.toolbar); - Throwable throwable = (Throwable) Objects.requireNonNull(getIntent().getSerializableExtra(KEY_THROWABLE)); - throwable.printStackTrace(); - binding.toolbar.setTitle(getString(R.string.simple_error)); - binding.message.setText(throwable.getMessage()); - binding.stacktrace.setText(ExceptionUtil.getDebugInfos(this, throwable)); - } - - - private void copyStacktraceToClipboard() { - final ClipboardManager clipboardManager = (ClipboardManager) Objects.requireNonNull(getSystemService(CLIPBOARD_SERVICE)); - ClipData clipData = ClipData.newPlainText(getString(R.string.simple_exception), "```\n" + binding.stacktrace.getText() + "\n```"); - clipboardManager.setPrimaryClip(clipData); - Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - - private void close() { - finish(); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/LockedActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/LockedActivity.java deleted file mode 100644 index 07a544df..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/LockedActivity.java +++ /dev/null @@ -1,109 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.app.KeyguardManager; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.Nullable; - -import it.niedermann.owncloud.notes.ExceptionHandler; -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandedActivity; -import it.niedermann.owncloud.notes.util.Notes; - -public abstract class LockedActivity extends BrandedActivity { - - private static final String TAG = LockedActivity.class.getSimpleName(); - - private static final int REQUEST_CODE_UNLOCK = 100; - - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); - - if (isTaskRoot()) { - askToUnlock(); - } - } - - @Override - protected void onResume() { - super.onResume(); - - if (!isTaskRoot()) { - askToUnlock(); - } - } - - @Override - protected void onStop() { - super.onStop(); - if (isTaskRoot()) { - Notes.updateLastInteraction(); - } - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - Notes.updateLastInteraction(); - } - - @Override - public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { - Notes.updateLastInteraction(); - super.startActivityForResult(intent, requestCode, options); - } - - @Override - public void startActivityForResult(Intent intent, int requestCode) { - Notes.updateLastInteraction(); - super.startActivityForResult(intent, requestCode); - } - - @Override - public void startActivity(Intent intent) { - Notes.updateLastInteraction(); - super.startActivity(intent); - } - - @Override - public void startActivity(Intent intent, @Nullable Bundle options) { - Notes.updateLastInteraction(); - super.startActivity(intent, options); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (requestCode == REQUEST_CODE_UNLOCK) { - if (resultCode == RESULT_OK) { - Log.v(TAG, "Successfully unlocked device"); - Notes.unlock(); - } else { - Log.e(TAG, "Result code of unlocking was " + resultCode); - finish(); - } - } - } - - private void askToUnlock() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Notes.isLocked()) { - KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); - if (keyguardManager != null) { - Intent i = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.unlock_notes), null); - i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - startActivityForResult(i, REQUEST_CODE_UNLOCK); - } else { - Log.e(TAG, "Keyguard manager is null"); - } - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NoteListWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NoteListWidgetConfigurationActivity.java deleted file mode 100644 index 01d35902..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NoteListWidgetConfigurationActivity.java +++ /dev/null @@ -1,181 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.app.Activity; -import android.appwidget.AppWidgetManager; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import android.util.Log; -import android.widget.Toast; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; -import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; -import com.nextcloud.android.sso.helper.SingleAccountHelper; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.appwidget.NoteListWidget; -import it.niedermann.owncloud.notes.model.LocalAccount; -import it.niedermann.owncloud.notes.model.NavigationAdapter; -import it.niedermann.owncloud.notes.model.NavigationAdapter.CategoryNavigationItem; -import it.niedermann.owncloud.notes.model.NoteListsWidgetData; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.Notes; - -public class NoteListWidgetConfigurationActivity extends LockedActivity { - private static final String TAG = Activity.class.getSimpleName(); - - private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; - - - private LocalAccount localAccount = null; - - private NavigationAdapter adapterCategories; - private NavigationAdapter.NavigationItem itemRecent; - private NavigationAdapter.NavigationItem itemFavorites; - private NotesDatabase db = null; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setResult(RESULT_CANCELED); - setContentView(R.layout.activity_note_list_configuration); - - db = NotesDatabase.getInstance(this); - try { - this.localAccount = db.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name); - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { - e.printStackTrace(); - Toast.makeText(this, R.string.widget_not_logged_in, Toast.LENGTH_LONG).show(); - // TODO Present user with app login screen - Log.w(TAG, "onCreate: user not logged in"); - finish(); - return; - } - 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) { - Log.d(TAG, "INVALID_APPWIDGET_ID"); - finish(); - } - - itemRecent = new NavigationAdapter.NavigationItem(NotesListViewActivity.ADAPTER_KEY_RECENT, - getString(R.string.label_all_notes), - null, - R.drawable.ic_access_time_grey600_24dp); - itemFavorites = new NavigationAdapter.NavigationItem(NotesListViewActivity.ADAPTER_KEY_STARRED, - getString(R.string.label_favorites), - null, - R.drawable.ic_star_yellow_24dp); - RecyclerView recyclerView; - RecyclerView.LayoutManager layoutManager; - - adapterCategories = new NavigationAdapter(this, new NavigationAdapter.ClickListener() { - @Override - public void onItemClick(NavigationAdapter.NavigationItem item) { - NoteListsWidgetData data = new NoteListsWidgetData(); - - data.setAppWidgetId(appWidgetId); - if (itemRecent.equals(item)) { - data.setMode(NoteListsWidgetData.MODE_DISPLAY_ALL); - } else if (itemFavorites.equals(item)) { - data.setMode(NoteListsWidgetData.MODE_DISPLAY_STARRED); - } else { - data.setMode(NoteListsWidgetData.MODE_DISPLAY_CATEGORY); - if (item instanceof CategoryNavigationItem) { - data.setCategoryId(((CategoryNavigationItem) item).categoryId); - } else { - throw new IllegalStateException("Tried to choose a category, but "); - } - } - - data.setAccountId(localAccount.getId()); - data.setThemeMode(Notes.getAppTheme(getApplicationContext()).getModeId()); - - db.createOrUpdateNoteListWidgetData(data); - - Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, - getApplicationContext(), NoteListWidget.class); - updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - setResult(RESULT_OK, updateIntent); - getApplicationContext().sendBroadcast(updateIntent); - finish(); - } - - public void onIconClick(NavigationAdapter.NavigationItem item) { - onItemClick(item); - } - }); - - recyclerView = findViewById(R.id.recycler_view); - recyclerView.setHasFixedSize(true); - layoutManager = new LinearLayoutManager(this); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setAdapter(adapterCategories); - } - - @Override - protected void onResume() { - super.onResume(); - new LoadCategoryListTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - @Override - public void applyBrand(int mainColor, int textColor) { - } - - private class LoadCategoryListTask extends AsyncTask> { - @Override - protected List doInBackground(Void... voids) { - if (localAccount == null) { - return new ArrayList<>(); - } - NavigationAdapter.NavigationItem itemUncategorized; - List categories = db.getCategories(localAccount.getId()); - - if (!categories.isEmpty() && categories.get(0).label.isEmpty()) { - itemUncategorized = categories.get(0); - itemUncategorized.label = getString(R.string.action_uncategorized); - itemUncategorized.icon = NavigationAdapter.ICON_NOFOLDER; - } - - Map favorites = db.getFavoritesCount(localAccount.getId()); - //noinspection ConstantConditions - int numFavorites = favorites.containsKey("1") ? favorites.get("1") : 0; - //noinspection ConstantConditions - int numNonFavorites = favorites.containsKey("0") ? favorites.get("0") : 0; - itemFavorites.count = numFavorites; - itemRecent.count = numFavorites + numNonFavorites; - - ArrayList items = new ArrayList<>(); - items.add(itemRecent); - items.add(itemFavorites); - - for (NavigationAdapter.NavigationItem item : categories) { - int slashIndex = item.label.indexOf('/'); - - item.label = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex); - item.id = "category:" + item.label; - items.add(item); - } - return items; - } - - @Override - protected void onPostExecute(List items) { - adapterCategories.setItems(items); - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NotesListViewActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NotesListViewActivity.java deleted file mode 100644 index ab6eaa68..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/NotesListViewActivity.java +++ /dev/null @@ -1,976 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.animation.AnimatorInflater; -import android.annotation.SuppressLint; -import android.app.SearchManager; -import android.content.Intent; -import android.database.sqlite.SQLiteException; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.view.ViewTreeObserver; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.appcompat.view.ActionMode; -import androidx.appcompat.widget.SearchView; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.core.view.GravityCompat; -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.StaggeredGridLayoutManager; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; -import com.nextcloud.android.sso.AccountImporter; -import com.nextcloud.android.sso.exceptions.AccountImportCancelledException; -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; -import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; -import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; -import com.nextcloud.android.sso.exceptions.TokenMismatchException; -import com.nextcloud.android.sso.helper.SingleAccountHelper; -import com.nextcloud.android.sso.model.SingleSignOnAccount; - -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.accountswitcher.AccountSwitcherDialog; -import it.niedermann.owncloud.notes.accountswitcher.AccountSwitcherListener; -import it.niedermann.owncloud.notes.android.MultiSelectedActionModeCallback; -import it.niedermann.owncloud.notes.android.NotesListViewItemTouchHelper; -import it.niedermann.owncloud.notes.android.fragment.AccountChooserAdapter.MoveAccountListener; -import it.niedermann.owncloud.notes.android.fragment.ExceptionDialogFragment; -import it.niedermann.owncloud.notes.branding.BrandedSnackbar; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.databinding.ActivityNotesListViewBinding; -import it.niedermann.owncloud.notes.databinding.DrawerLayoutBinding; -import it.niedermann.owncloud.notes.formattinghelp.FormattingHelpActivity; -import it.niedermann.owncloud.notes.model.Capabilities; -import it.niedermann.owncloud.notes.model.Category; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.GridItemDecoration; -import it.niedermann.owncloud.notes.model.ISyncCallback; -import it.niedermann.owncloud.notes.model.Item; -import it.niedermann.owncloud.notes.model.ItemAdapter; -import it.niedermann.owncloud.notes.model.LocalAccount; -import it.niedermann.owncloud.notes.model.NavigationAdapter; -import it.niedermann.owncloud.notes.model.NavigationAdapter.CategoryNavigationItem; -import it.niedermann.owncloud.notes.model.NavigationAdapter.NavigationItem; -import it.niedermann.owncloud.notes.model.NoteClickListener; -import it.niedermann.owncloud.notes.model.SectionItemDecoration; -import it.niedermann.owncloud.notes.persistence.CapabilitiesClient; -import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker; -import it.niedermann.owncloud.notes.persistence.LoadNotesListTask; -import it.niedermann.owncloud.notes.persistence.LoadNotesListTask.NotesLoadedListener; -import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper; -import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper.ViewProvider; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.CategorySortingMethod; -import it.niedermann.owncloud.notes.util.NoteUtil; - -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.owncloud.notes.util.ColorUtil.contrastRatioIsSufficient; -import static it.niedermann.owncloud.notes.util.Notes.isDarkThemeActive; -import static it.niedermann.owncloud.notes.util.Notes.isGridViewEnabled; -import static it.niedermann.owncloud.notes.util.SSOUtil.askForNewAccount; -import static java.util.Arrays.asList; - -public class NotesListViewActivity extends LockedActivity implements NoteClickListener, ViewProvider, MoveAccountListener, AccountSwitcherListener { - - private static final String TAG = NotesListViewActivity.class.getSimpleName(); - - private boolean gridView = true; - - public static final String CREATED_NOTE = "it.niedermann.owncloud.notes.created_notes"; - public static final String ADAPTER_KEY_RECENT = "recent"; - public static final String ADAPTER_KEY_STARRED = "starred"; - public static final String ACTION_FAVORITES = "it.niedermann.owncloud.notes.favorites"; - public static final String ACTION_RECENT = "it.niedermann.owncloud.notes.recent"; - - private static final String SAVED_STATE_NAVIGATION_SELECTION = "navigationSelection"; - private static final String SAVED_STATE_NAVIGATION_ADAPTER_SLECTION = "navigationAdapterSelection"; - private static final String SAVED_STATE_NAVIGATION_OPEN = "navigationOpen"; - - private final static int create_note_cmd = 0; - private final static int show_single_note_cmd = 1; - private final static int server_settings = 2; - private final static int about = 3; - public final static int manage_account = 4; - - /** - * Used to detect the onResume() call after the import dialog has been displayed. - * https://github.com/stefan-niedermann/nextcloud-notes/pull/599/commits/f40eab402d122f113020200751894fa39c8b9fcc#r334239634 - */ - private boolean notAuthorizedAccountHandled = false; - - protected SingleSignOnAccount ssoAccount; - protected LocalAccount localAccount; - - protected DrawerLayoutBinding binding; - protected ActivityNotesListViewBinding activityBinding; - - private CoordinatorLayout coordinatorLayout; - private SwipeRefreshLayout swipeRefreshLayout; - protected FloatingActionButton fabCreate; - private RecyclerView listView; - - protected ItemAdapter adapter; - - protected NotesDatabase db = null; - private NavigationAdapter adapterCategories; - private NavigationItem itemRecent; - private NavigationItem itemFavorites; - private NavigationItem itemUncategorized; - @NonNull - private Category navigationSelection = new Category(null, null); - private String navigationOpen = ""; - private ActionMode mActionMode; - private final ISyncCallback syncCallBack = () -> { - adapter.clearSelection(listView); - if (mActionMode != null) { - mActionMode.finish(); - } - refreshLists(); - swipeRefreshLayout.setRefreshing(false); - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - CapabilitiesWorker.update(this); - binding = DrawerLayoutBinding.inflate(getLayoutInflater()); - activityBinding = ActivityNotesListViewBinding.bind(binding.activityNotesListView.getRoot()); - - setContentView(binding.getRoot()); - - this.coordinatorLayout = binding.activityNotesListView.activityNotesListView; - this.swipeRefreshLayout = binding.activityNotesListView.swiperefreshlayout; - this.fabCreate = binding.activityNotesListView.fabCreate; - this.listView = binding.activityNotesListView.recyclerView; - - String categoryAdapterSelectedItem = ADAPTER_KEY_RECENT; - if (savedInstanceState == null) { - if (ACTION_RECENT.equals(getIntent().getAction())) { - categoryAdapterSelectedItem = ADAPTER_KEY_RECENT; - } else if (ACTION_FAVORITES.equals(getIntent().getAction())) { - categoryAdapterSelectedItem = ADAPTER_KEY_STARRED; - navigationSelection = new Category(null, true); - } - } else { - Object savedCategory = savedInstanceState.getSerializable(SAVED_STATE_NAVIGATION_SELECTION); - if (savedCategory != null) { - navigationSelection = (Category) savedCategory; - } - navigationOpen = savedInstanceState.getString(SAVED_STATE_NAVIGATION_OPEN); - categoryAdapterSelectedItem = savedInstanceState.getString(SAVED_STATE_NAVIGATION_ADAPTER_SLECTION); - } - - db = NotesDatabase.getInstance(this); - - gridView = isGridViewEnabled(); - if (!gridView || isDarkThemeActive(this)) { - activityBinding.activityNotesListView.setBackgroundColor(ContextCompat.getColor(this, R.color.primary)); - } - - setupToolbars(); - setupNavigationList(categoryAdapterSelectedItem); - setupNavigationMenu(); - setupNotesList(); - } - - @Override - protected void onResume() { - try { - ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()); - if (localAccount == null || !localAccount.getAccountName().equals(ssoAccount.name)) { - selectAccount(ssoAccount.name); - } - } catch (NoCurrentAccountSelectedException | NextcloudFilesAppAccountNotFoundException e) { - if (localAccount == null) { - List localAccounts = db.getAccounts(); - if (localAccounts.size() > 0) { - localAccount = localAccounts.get(0); - } - } - if (!notAuthorizedAccountHandled) { - handleNotAuthorizedAccount(); - } - } - - // refresh and sync every time the activity gets - refreshLists(); - if (localAccount != null) { - synchronize(); - db.getNoteServerSyncHelper().addCallbackPull(ssoAccount, syncCallBack); - } - super.onResume(); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (localAccount != null) { - outState.putSerializable(SAVED_STATE_NAVIGATION_SELECTION, navigationSelection); - outState.putString(SAVED_STATE_NAVIGATION_ADAPTER_SLECTION, adapterCategories.getSelectedItem()); - outState.putString(SAVED_STATE_NAVIGATION_OPEN, navigationOpen); - } - } - - private void selectAccount(String accountName) { - fabCreate.hide(); - SingleAccountHelper.setCurrentAccount(getApplicationContext(), accountName); - localAccount = db.getLocalAccountByAccountName(accountName); - if (localAccount != null) { - try { - BrandingUtil.saveBrandColors(this, localAccount.getColor(), localAccount.getTextColor()); - ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()); - new NotesListViewItemTouchHelper(ssoAccount, this, db, adapter, syncCallBack, this::refreshLists, swipeRefreshLayout, this, gridView) - .attachToRecyclerView(listView); - synchronize(); - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { - Log.i(TAG, "Tried to select account, but got an " + e.getClass().getSimpleName() + ". Asking for importing an account..."); - handleNotAuthorizedAccount(); - } - refreshLists(); - fabCreate.show(); - activityBinding.launchAccountSwitcher.setOnClickListener((v) -> { - if (localAccount == null) { - handleNotAuthorizedAccount(); - } else { - AccountSwitcherDialog.newInstance(localAccount.getId()).show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName()); - } - }); - setupNavigationList(ADAPTER_KEY_RECENT); - } else { - if (!notAuthorizedAccountHandled) { - handleNotAuthorizedAccount(); - } - binding.navigationList.setAdapter(null); - } - updateCurrentAccountAvatar(); - } - - private void handleNotAuthorizedAccount() { - fabCreate.hide(); - swipeRefreshLayout.setRefreshing(false); - askForNewAccount(this); - notAuthorizedAccountHandled = true; - } - - private void setupToolbars() { - setSupportActionBar(binding.activityNotesListView.toolbar); - updateCurrentAccountAvatar(); - activityBinding.homeToolbar.setOnClickListener((v) -> { - if (activityBinding.toolbar.getVisibility() == GONE) { - updateToolbars(false); - } - }); - - activityBinding.launchAccountSwitcher.setOnClickListener((v) -> askForNewAccount(this)); - activityBinding.menuButton.setOnClickListener((v) -> binding.drawerLayout.openDrawer(GravityCompat.START)); - - final LinearLayout searchEditFrame = activityBinding.searchView.findViewById(R.id - .search_edit_frame); - - searchEditFrame.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - int oldVisibility = -1; - - @Override - public void onGlobalLayout() { - int currentVisibility = searchEditFrame.getVisibility(); - - if (currentVisibility != oldVisibility) { - if (currentVisibility == VISIBLE) { - fabCreate.hide(); - } else { - new Handler().postDelayed(() -> fabCreate.show(), 150); - } - - oldVisibility = currentVisibility; - } - } - - }); - activityBinding.searchView.setOnCloseListener(() -> { - if (activityBinding.toolbar.getVisibility() == VISIBLE && TextUtils.isEmpty(activityBinding.searchView.getQuery())) { - updateToolbars(true); - return true; - } - return false; - }); - activityBinding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - return false; - } - - @Override - public boolean onQueryTextChange(String newText) { - refreshLists(); - return true; - } - }); - } - - private void setupNotesList() { - initRecyclerView(); - - ((RecyclerView) findViewById(R.id.recycler_view)).addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - if (dy > 0) - fabCreate.hide(); - else if (dy < 0) - fabCreate.show(); - } - }); - - swipeRefreshLayout.setOnRefreshListener(() -> { - if (ssoAccount == null) { - swipeRefreshLayout.setRefreshing(false); - askForNewAccount(this); - } else { - Log.i(TAG, "Clearing Glide memory cache"); - Glide.get(this).clearMemory(); - new Thread(() -> { - Log.i(TAG, "Clearing Glide disk cache"); - Glide.get(getApplicationContext()).clearDiskCache(); - }).start(); - new Thread(() -> { - Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name); - final Capabilities capabilities; - try { - capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, localAccount.getCapabilitiesETag()); - db.updateCapabilitiesETag(localAccount.getId(), capabilities.getETag()); - db.updateBrand(localAccount.getId(), capabilities); - db.updateBrand(localAccount.getId(), capabilities); - localAccount.setColor(Color.parseColor(capabilities.getColor())); - localAccount.setTextColor(Color.parseColor(capabilities.getTextColor())); - BrandingUtil.saveBrandColors(this, localAccount.getColor(), localAccount.getTextColor()); - db.updateApiVersion(localAccount.getId(), capabilities.getApiVersion()); - Log.i(TAG, capabilities.toString()); - } catch (Exception e) { - if (e instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { - Log.i(TAG, "Capabilities not modified."); - } else { - e.printStackTrace(); - } - } finally { - // Even if the capabilities endpoint makes trouble, we can still try to synchronize the notes - synchronize(); - } - }).start(); - } - }); - - // Floating Action Button - fabCreate.setOnClickListener((View view) -> { - Intent createIntent = new Intent(getApplicationContext(), EditNoteActivity.class); - createIntent.putExtra(EditNoteActivity.PARAM_CATEGORY, navigationSelection); - if (activityBinding.searchView.getQuery().length() > 0) { - createIntent.putExtra(EditNoteActivity.PARAM_CONTENT, activityBinding.searchView.getQuery().toString()); - invalidateOptionsMenu(); - } - startActivityForResult(createIntent, create_note_cmd); - }); - - activityBinding.sortingMethod.setOnClickListener((v) -> { - CategorySortingMethod method; - - method = db.getCategoryOrder(localAccount.getId(), navigationSelection); - - if (method == CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC) { - method = CategorySortingMethod.SORT_MODIFIED_DESC; - } else { - method = CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC; - } - db.modifyCategoryOrder(localAccount.getId(), navigationSelection, method); - refreshLists(); - updateSortMethodIcon(localAccount.getId()); - }); - } - - private void setupNavigationList(final String selectedItem) { - itemRecent = new NavigationItem(ADAPTER_KEY_RECENT, getString(R.string.label_all_notes), null, R.drawable.ic_access_time_grey600_24dp); - itemFavorites = new NavigationItem(ADAPTER_KEY_STARRED, getString(R.string.label_favorites), null, R.drawable.ic_star_yellow_24dp); - adapterCategories = new NavigationAdapter(this, new NavigationAdapter.ClickListener() { - @Override - public void onItemClick(NavigationItem item) { - selectItem(item, true); - } - - private void selectItem(NavigationItem item, boolean closeNavigation) { - adapterCategories.setSelectedItem(item.id); - // update current selection - if (itemRecent.equals(item)) { - navigationSelection = new Category(null, null); - } else if (itemFavorites.equals(item)) { - navigationSelection = new Category(null, true); - } else if (itemUncategorized != null && itemUncategorized.equals(item)) { - navigationSelection = new Category("", null); - } else { - navigationSelection = new Category(item.label, null); - } - - // auto-close sub-folder in Navigation if selection is outside of that folder - if (navigationOpen != null) { - int slashIndex = navigationSelection.category == null ? -1 : navigationSelection.category.indexOf('/'); - String rootCategory = slashIndex < 0 ? navigationSelection.category : navigationSelection.category.substring(0, slashIndex); - if (!navigationOpen.equals(rootCategory)) { - navigationOpen = null; - } - } - - // update views - if (closeNavigation) { - binding.drawerLayout.closeDrawer(GravityCompat.START); - } - refreshLists(true); - } - - @Override - public void onIconClick(NavigationItem item) { - if (item.icon == NavigationAdapter.ICON_MULTIPLE && !item.label.equals(navigationOpen)) { - navigationOpen = item.label; - selectItem(item, false); - } else if (item.icon == NavigationAdapter.ICON_MULTIPLE || item.icon == NavigationAdapter.ICON_MULTIPLE_OPEN && item.label.equals(navigationOpen)) { - navigationOpen = null; - refreshLists(); - } else { - onItemClick(item); - } - } - }); - adapterCategories.setSelectedItem(selectedItem); - binding.navigationList.setAdapter(adapterCategories); - } - - @Override - public CoordinatorLayout getView() { - return this.coordinatorLayout; - } - - @Override - public void applyBrand(int mainColor, int textColor) { - applyBrandToPrimaryToolbar(activityBinding.appBar, activityBinding.toolbar); - applyBrandToFAB(mainColor, textColor, activityBinding.fabCreate); - - binding.headerView.setBackgroundColor(mainColor); - binding.appName.setTextColor(textColor); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activityBinding.progressCircular.getIndeterminateDrawable().setColorFilter(getSecondaryForegroundColorDependingOnTheme(this, mainColor), PorterDuff.Mode.SRC_IN); - } - - // TODO We assume, that the background of the spinner is always white - activityBinding.swiperefreshlayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : Color.BLACK); - binding.appName.setTextColor(textColor); - DrawableCompat.setTint(binding.logo.getDrawable(), textColor); - - adapter.applyBrand(mainColor, textColor); - adapterCategories.applyBrand(mainColor, textColor); - invalidateOptionsMenu(); - } - - @Override - public boolean onSupportNavigateUp() { - if (activityBinding.toolbar.getVisibility() == VISIBLE) { - updateToolbars(true); - return true; - } else { - return super.onSupportNavigateUp(); - } - } - - private class LoadCategoryListTask extends AsyncTask> { - @Override - protected List doInBackground(Void... voids) { - if (localAccount == null) { - return new ArrayList<>(); - } - List categories = db.getCategories(localAccount.getId()); - if (!categories.isEmpty() && categories.get(0).label.isEmpty()) { - itemUncategorized = categories.get(0); - itemUncategorized.label = getString(R.string.action_uncategorized); - itemUncategorized.icon = NavigationAdapter.ICON_NOFOLDER; - } else { - itemUncategorized = null; - } - - Map favorites = db.getFavoritesCount(localAccount.getId()); - //noinspection ConstantConditions - int numFavorites = favorites.containsKey("1") ? favorites.get("1") : 0; - //noinspection ConstantConditions - int numNonFavorites = favorites.containsKey("0") ? favorites.get("0") : 0; - itemFavorites.count = numFavorites; - itemRecent.count = numFavorites + numNonFavorites; - - ArrayList items = new ArrayList<>(); - items.add(itemRecent); - items.add(itemFavorites); - NavigationItem lastPrimaryCategory = null; - NavigationItem lastSecondaryCategory = null; - for (NavigationItem item : categories) { - int slashIndex = item.label.indexOf('/'); - String currentPrimaryCategory = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex); - String currentSecondaryCategory = null; - boolean isCategoryOpen = currentPrimaryCategory.equals(navigationOpen); - - if (isCategoryOpen && !currentPrimaryCategory.equals(item.label)) { - String currentCategorySuffix = item.label.substring(navigationOpen.length() + 1); - int subSlashIndex = currentCategorySuffix.indexOf('/'); - currentSecondaryCategory = subSlashIndex < 0 ? currentCategorySuffix : currentCategorySuffix.substring(0, subSlashIndex); - } - - boolean belongsToLastPrimaryCategory = lastPrimaryCategory != null && currentPrimaryCategory.equals(lastPrimaryCategory.label); - boolean belongsToLastSecondaryCategory = belongsToLastPrimaryCategory && lastSecondaryCategory != null && lastSecondaryCategory.label.equals(currentPrimaryCategory + "/" + currentSecondaryCategory); - - if (isCategoryOpen && !belongsToLastPrimaryCategory && currentSecondaryCategory != null) { - lastPrimaryCategory = new NavigationItem("category:" + currentPrimaryCategory, currentPrimaryCategory, 0, NavigationAdapter.ICON_MULTIPLE_OPEN); - items.add(lastPrimaryCategory); - belongsToLastPrimaryCategory = true; - } - - if (belongsToLastPrimaryCategory && belongsToLastSecondaryCategory) { - lastSecondaryCategory.count += item.count; - lastSecondaryCategory.icon = NavigationAdapter.ICON_SUB_MULTIPLE; - } else if (belongsToLastPrimaryCategory) { - if (isCategoryOpen) { - item.label = currentPrimaryCategory + "/" + currentSecondaryCategory; - item.id = "category:" + item.label; - item.icon = NavigationAdapter.ICON_SUB_FOLDER; - items.add(item); - lastSecondaryCategory = item; - } else { - lastPrimaryCategory.count += item.count; - lastPrimaryCategory.icon = NavigationAdapter.ICON_MULTIPLE; - lastSecondaryCategory = null; - } - } else { - if (isCategoryOpen) { - item.icon = NavigationAdapter.ICON_MULTIPLE_OPEN; - } else { - item.label = currentPrimaryCategory; - item.id = "category:" + item.label; - } - items.add(item); - lastPrimaryCategory = item; - lastSecondaryCategory = null; - } - } - return items; - } - - @Override - protected void onPostExecute(List items) { - adapterCategories.setItems(items); - } - } - - private void setupNavigationMenu() { - final NavigationItem itemFormattingHelp = new NavigationItem("formattingHelp", getString(R.string.action_formatting_help), null, R.drawable.ic_baseline_help_outline_24); - final NavigationItem itemTrashbin = new NavigationItem("trashbin", getString(R.string.action_trashbin), null, R.drawable.ic_delete_grey600_24dp); - final NavigationItem itemSettings = new NavigationItem("settings", getString(R.string.action_settings), null, R.drawable.ic_settings_grey600_24dp); - final NavigationItem itemAbout = new NavigationItem("about", getString(R.string.simple_about), null, R.drawable.ic_info_outline_grey600_24dp); - - NavigationAdapter adapterMenu = new NavigationAdapter(this, new NavigationAdapter.ClickListener() { - @Override - public void onItemClick(NavigationItem item) { - if (itemFormattingHelp.equals(item)) { - Intent formattingHelpIntent = new Intent(getApplicationContext(), FormattingHelpActivity.class); - startActivity(formattingHelpIntent); - } else if (itemSettings.equals(item)) { - Intent settingsIntent = new Intent(getApplicationContext(), PreferencesActivity.class); - startActivityForResult(settingsIntent, server_settings); - } else if (itemAbout.equals(item)) { - Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class); - startActivityForResult(aboutIntent, about); - } else if (itemTrashbin.equals(item) && localAccount != null) { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(localAccount.getUrl() + "/index.php/apps/files/?dir=/&view=trashbin"))); - } - } - - @Override - public void onIconClick(NavigationItem item) { - onItemClick(item); - } - }); - adapterMenu.setItems(asList(itemFormattingHelp, itemTrashbin, itemSettings, itemAbout)); - binding.navigationMenu.setAdapter(adapterMenu); - } - - private void initRecyclerView() { - adapter = new ItemAdapter(this, gridView); - listView.setAdapter(adapter); - - if (gridView) { - int spanCount = getResources().getInteger(R.integer.grid_view_span_count); - StaggeredGridLayoutManager gridLayoutManager = new StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL); - listView.setLayoutManager(gridLayoutManager); - listView.addItemDecoration(new GridItemDecoration(adapter, spanCount, - getResources().getDimensionPixelSize(R.dimen.spacer_3x), - getResources().getDimensionPixelSize(R.dimen.spacer_5x), - getResources().getDimensionPixelSize(R.dimen.spacer_3x), - getResources().getDimensionPixelSize(R.dimen.spacer_1x), - getResources().getDimensionPixelSize(R.dimen.spacer_activity_sides) + getResources().getDimensionPixelSize(R.dimen.spacer_1x) - )); - } else { - LinearLayoutManager layoutManager = new LinearLayoutManager(this); - listView.setLayoutManager(layoutManager); - listView.addItemDecoration(new SectionItemDecoration(adapter, - getResources().getDimensionPixelSize(R.dimen.spacer_6x), - getResources().getDimensionPixelSize(R.dimen.spacer_5x), - getResources().getDimensionPixelSize(R.dimen.spacer_1x), - 0 - )); - } - } - - private void refreshLists() { - refreshLists(false); - } - - private void refreshLists(final boolean scrollToTop) { - if (localAccount == null) { - fabCreate.hide(); - adapter.removeAll(); - return; - } - View emptyContentView = binding.activityNotesListView.emptyContentView.getRoot(); - emptyContentView.setVisibility(GONE); - binding.activityNotesListView.progressCircular.setVisibility(VISIBLE); - fabCreate.show(); - String subtitle; - if (navigationSelection.category != null) { - if (navigationSelection.category.isEmpty()) { - subtitle = getString(R.string.search_in_category, getString(R.string.action_uncategorized)); - } else { - subtitle = getString(R.string.search_in_category, NoteUtil.extendCategory(navigationSelection.category)); - } - } else if (navigationSelection.favorite != null && navigationSelection.favorite) { - subtitle = getString(R.string.search_in_category, getString(R.string.label_favorites)); - } else { - subtitle = getString(R.string.search_in_all); - } - activityBinding.searchText.setText(subtitle); - CharSequence query = null; - if (activityBinding.searchView.getQuery().length() != 0) { - query = activityBinding.searchView.getQuery(); - } - - NotesLoadedListener callback = (List notes, boolean showCategory, CharSequence searchQuery) -> { - adapter.setShowCategory(showCategory); - adapter.setHighlightSearchQuery(searchQuery); - adapter.setItemList(notes); - binding.activityNotesListView.progressCircular.setVisibility(GONE); - if (notes.size() > 0) { - emptyContentView.setVisibility(GONE); - } else { - emptyContentView.setVisibility(VISIBLE); - } - if (scrollToTop) { - listView.scrollToPosition(0); - } - }; - new LoadNotesListTask(localAccount.getId(), getApplicationContext(), callback, navigationSelection, query).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - new LoadCategoryListTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - - updateSortMethodIcon(localAccount.getId()); - } - - /** - * Updates sorting method icon. - */ - private void updateSortMethodIcon(long localAccountId) { - CategorySortingMethod method = db.getCategoryOrder(localAccountId, navigationSelection); - if (method == CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC) { - activityBinding.sortingMethod.setImageResource(R.drawable.alphabetical_asc); - activityBinding.sortingMethod.setContentDescription(getString(R.string.sort_last_modified)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activityBinding.sortingMethod.setTooltipText(getString(R.string.sort_last_modified)); - } - } else { - activityBinding.sortingMethod.setImageResource(R.drawable.modification_desc); - activityBinding.sortingMethod.setContentDescription(getString(R.string.sort_alphabetically)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activityBinding.sortingMethod.setTooltipText(getString(R.string.sort_alphabetically)); - } - } - } - - @Override - protected void onNewIntent(Intent intent) { - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - activityBinding.searchView.setQuery(intent.getStringExtra(SearchManager.QUERY), true); - } - super.onNewIntent(intent); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this); - } - - /** - * Handles the Results of started Sub Activities (Created Note, Edited Note) - * - * @param requestCode int to distinguish between the different Sub Activities - * @param resultCode int Return Code - * @param data Intent - */ - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - switch (requestCode) { - case create_note_cmd: { - // Make sure the request was successful - if (resultCode == RESULT_OK) { - //not need because of db.synchronisation in createActivity - - Bundle bundle = data.getExtras(); - if (bundle != null && bundle.containsKey(CREATED_NOTE)) { - DBNote createdNote = (DBNote) bundle.getSerializable(CREATED_NOTE); - if (createdNote != null) { - adapter.add(createdNote); - } else { - Log.w(TAG, "createdNote must not be null"); - } - } else { - Log.w(TAG, "Provide at least " + CREATED_NOTE); - } - } - listView.scrollToPosition(0); - break; - } - case server_settings: { - // Recreate activity completely, because theme switching makes problems when only invalidating the views. - // @see https://github.com/stefan-niedermann/nextcloud-notes/issues/529 - recreate(); - break; - } - case manage_account: { - if (resultCode == RESULT_FIRST_USER) { - selectAccount(null); - } - break; - } - default: { - try { - AccountImporter.onActivityResult(requestCode, resultCode, data, this, (ssoAccount) -> { - CapabilitiesWorker.update(this); - new Thread(() -> { - Log.i(TAG, "Added account: " + "name:" + ssoAccount.name + ", " + ssoAccount.url + ", userId" + ssoAccount.userId); - try { - Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name); - final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null); - db.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities); - Log.i(TAG, capabilities.toString()); - runOnUiThread(() -> selectAccount(ssoAccount.name)); - } catch (SQLiteException e) { - // Happens when upgrading from version ≤ 1.0.1 and importing the account - runOnUiThread(() -> selectAccount(ssoAccount.name)); - } catch (Exception e) { - // Happens when importing an already existing account the second time - if (e instanceof TokenMismatchException && db.getLocalAccountByAccountName(ssoAccount.name) != null) { - Log.w(TAG, "Received " + TokenMismatchException.class.getSimpleName() + " and the given ssoAccount.name (" + ssoAccount.name + ") does already exist in the database. Assume that this account has already been imported."); - runOnUiThread(() -> { - selectAccount(ssoAccount.name); - // TODO there is already a sync in progress and results in displaying a TokenMissMatchException snackbar which conflicts with this one - coordinatorLayout.post(() -> BrandedSnackbar.make(coordinatorLayout, R.string.account_already_imported, Snackbar.LENGTH_LONG).show()); - }); - } else { - e.printStackTrace(); - runOnUiThread(() -> { - binding.activityNotesListView.progressCircular.setVisibility(GONE); - ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - }); - } - } - }).start(); - }); - } catch (AccountImportCancelledException e) { - Log.i(TAG, "AccountImport has been cancelled."); - } - } - } - } - - private void updateCurrentAccountAvatar() { - try { - String url = localAccount.getUrl(); - if (url != null) { - Glide - .with(this) - .load(url + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64") - .placeholder(R.drawable.ic_account_circle_grey_24dp) - .error(R.drawable.ic_account_circle_grey_24dp) - .apply(RequestOptions.circleCropTransform()) - .into(activityBinding.launchAccountSwitcher); - } else { - Log.w(TAG, "url is null"); - } - } catch (NullPointerException e) { // No local account - show generic header - Glide - .with(this) - .load(R.drawable.ic_account_circle_grey_24dp) - .apply(RequestOptions.circleCropTransform()) - .into(activityBinding.launchAccountSwitcher); - Log.w(TAG, "Tried to update username in drawer, but localAccount was null"); - } - } - - @Override - public void onNoteClick(int position, View v) { - boolean hasCheckedItems = adapter.getSelected().size() > 0; - if (hasCheckedItems) { - if (!adapter.select(position)) { - v.setSelected(false); - adapter.deselect(position); - } else { - v.setSelected(true); - } - int size = adapter.getSelected().size(); - if (size > 0) { - mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, size, size)); - } else { - mActionMode.finish(); - } - } else { - DBNote note = (DBNote) adapter.getItem(position); - Intent intent = new Intent(getApplicationContext(), EditNoteActivity.class); - intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()); - startActivityForResult(intent, show_single_note_cmd); - } - } - - @Override - public void onNoteFavoriteClick(int position, View view) { - DBNote note = (DBNote) adapter.getItem(position); - NotesDatabase db = NotesDatabase.getInstance(view.getContext()); - db.toggleFavorite(ssoAccount, note, syncCallBack); - adapter.notifyItemChanged(position); - refreshLists(); - } - - @Override - public boolean onNoteLongClick(int position, View v) { - boolean selected = adapter.select(position); - if (selected) { - v.setSelected(true); - mActionMode = startSupportActionMode(new MultiSelectedActionModeCallback( - this, this, db, mActionMode, adapter, listView, this::refreshLists, getSupportFragmentManager(), activityBinding.searchView - )); - int checkedItemCount = adapter.getSelected().size(); - mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, checkedItemCount, checkedItemCount)); - } - return selected; - } - - @Override - public void onBackPressed() { - if (activityBinding.toolbar.getVisibility() == VISIBLE) { - updateToolbars(true); - } else { - super.onBackPressed(); - } - } - - @SuppressLint("PrivateResource") - private void updateToolbars(boolean disableSearch) { - activityBinding.homeToolbar.setVisibility(disableSearch ? VISIBLE : GONE); - activityBinding.toolbar.setVisibility(disableSearch ? GONE : VISIBLE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activityBinding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator(activityBinding.appBar.getContext(), - disableSearch ? R.animator.appbar_elevation_off : R.animator.appbar_elevation_on)); - } else { - ViewCompat.setElevation(activityBinding.appBar, disableSearch ? 0 : getResources().getDimension(R.dimen.design_appbar_elevation)); - } - if (disableSearch) { - activityBinding.searchView.setQuery(null, true); - } - activityBinding.searchView.setIconified(disableSearch); - } - - private void synchronize() { - NoteServerSyncHelper syncHelper = db.getNoteServerSyncHelper(); - if (syncHelper.isSyncPossible()) { - swipeRefreshLayout.setRefreshing(true); - syncHelper.addCallbackPull(ssoAccount, syncCallBack); - syncHelper.scheduleSync(ssoAccount, false); - } else { // Sync is not possible - swipeRefreshLayout.setRefreshing(false); - if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) { - Log.d(TAG, "Network is connected, but sync is not possible"); - } else { - Log.d(TAG, "Sync is not possible, because network is not connected"); - BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show(); - } - } - } - - @Override - public void addAccount() { - askForNewAccount(this); - } - - @Override - public void onAccountChosen(LocalAccount localAccount) { - binding.drawerLayout.closeDrawer(GravityCompat.START); - selectAccount(localAccount.getAccountName()); - } - - @Override - public void onAccountDeleted(LocalAccount localAccount) { - db.deleteAccount(localAccount); - if (localAccount.getId() == this.localAccount.getId()) { - List remainingAccounts = db.getAccounts(); - if (remainingAccounts.size() > 0) { - this.localAccount = remainingAccounts.get(0); - selectAccount(this.localAccount.getAccountName()); - } else { - selectAccount(null); - askForNewAccount(this); - } - } - } - - @Override - public void moveToAccount(LocalAccount account) { - List selection = new ArrayList<>(adapter.getSelected()); - - adapter.deselect(0); - for (Integer i : selection) { - DBNote note = (DBNote) adapter.getItem(i); - db.moveNoteToAnotherAccount(ssoAccount, note.getAccountId(), db.getNote(note.getAccountId(), note.getId()), account.getId()); - RecyclerView.ViewHolder viewHolder = listView.findViewHolderForAdapterPosition(i); - if (viewHolder != null) { - viewHolder.itemView.setSelected(false); - } else { - Log.w(TAG, "Could not found viewholder to remove selection"); - } - } - - mActionMode.finish(); - refreshLists(); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/PreferencesActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/PreferencesActivity.java deleted file mode 100644 index ba945a6c..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/PreferencesActivity.java +++ /dev/null @@ -1,37 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.os.Bundle; - -import androidx.annotation.Nullable; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.fragment.PreferencesFragment; -import it.niedermann.owncloud.notes.databinding.ActivityPreferencesBinding; - -/** - * Allows to change application settings. - */ - -public class PreferencesActivity extends LockedActivity { - - private ActivityPreferencesBinding binding; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityPreferencesBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setSupportActionBar(binding.toolbar); - setResult(RESULT_CANCELED); - getSupportFragmentManager().beginTransaction() - .replace(R.id.fragment_container_view, new PreferencesFragment()) - .commit(); - } - - @Override - public void applyBrand(int mainColor, int textColor) { - applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SelectSingleNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SelectSingleNoteActivity.java deleted file mode 100644 index 0ad1fa5a..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SelectSingleNoteActivity.java +++ /dev/null @@ -1,76 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.app.Activity; -import android.appwidget.AppWidgetManager; -import android.content.Intent; -import android.database.SQLException; -import android.os.Bundle; -import android.view.Menu; -import android.view.View; -import android.widget.Toast; - -import androidx.appcompat.widget.Toolbar; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import it.niedermann.owncloud.notes.ExceptionHandler; -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.appwidget.SingleNoteWidget; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.SingleNoteWidgetData; -import it.niedermann.owncloud.notes.util.Notes; - -public class SelectSingleNoteActivity extends NotesListViewActivity { - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); - setResult(Activity.RESULT_CANCELED); - - fabCreate.setVisibility(View.GONE); - Toolbar toolbar = binding.activityNotesListView.toolbar; - SwipeRefreshLayout swipeRefreshLayout = binding.activityNotesListView.swiperefreshlayout; - toolbar.setTitle(R.string.activity_select_single_note); - swipeRefreshLayout.setEnabled(false); - swipeRefreshLayout.setRefreshing(false); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return true; - } - - @Override - public void onNoteClick(int position, View v) { - final DBNote note = (DBNote) adapter.getItem(position); - final Bundle extras = getIntent().getExtras(); - - if (extras == null) { - finish(); - return; - } - - int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - - try { - db.createOrUpdateSingleNoteWidgetData( - new SingleNoteWidgetData( - appWidgetId, - note.getAccountId(), - note.getId(), - Notes.getAppTheme(this).getModeId() - ) - ); - final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, - getApplicationContext(), SingleNoteWidget.class) - .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - setResult(RESULT_OK, updateIntent); - getApplicationContext().sendBroadcast(updateIntent); - } catch (SQLException e) { - Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); - } - - finish(); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SplashscreenActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SplashscreenActivity.java deleted file mode 100644 index c05f5b47..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/activity/SplashscreenActivity.java +++ /dev/null @@ -1,25 +0,0 @@ -package it.niedermann.owncloud.notes.android.activity; - -import android.content.Intent; -import android.os.Bundle; - -import androidx.appcompat.app.AppCompatActivity; - -import it.niedermann.owncloud.notes.ExceptionHandler; - - -/** - * Created by stefan on 18.04.17. - */ -public class SplashscreenActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); - - Intent intent = new Intent(this, NotesListViewActivity.class); - startActivity(intent); - finish(); - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/CreateNoteWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/CreateNoteWidget.java deleted file mode 100644 index d025015b..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/CreateNoteWidget.java +++ /dev/null @@ -1,51 +0,0 @@ -package it.niedermann.owncloud.notes.android.appwidget; - -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.Context; -import android.content.Intent; -import android.widget.RemoteViews; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; - -/** - * Implementation of App Widget functionality. - */ -public class CreateNoteWidget extends AppWidgetProvider { - - private static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, - int appWidgetId) { - - // Construct the RemoteViews object - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_create_note); - Intent intent = new Intent(context, EditNoteActivity.class); - - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - views.setOnClickPendingIntent(R.id.widget_create_note, pendingIntent); - - // Instruct the widget manager to update the widget - appWidgetManager.updateAppWidget(appWidgetId, views); - } - - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - - // There may be multiple widgets active, so update all of them - for (int appWidgetId : appWidgetIds) { - updateAppWidget(context, appWidgetManager, appWidgetId); - } - } - - @Override - public void onEnabled(Context context) { - // Enter relevant functionality for when the first widget is created - } - - @Override - public void onDisabled(Context context) { - // Enter relevant functionality for when the last widget is disabled - } -} - diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidget.java deleted file mode 100644 index 2a59aaa8..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidget.java +++ /dev/null @@ -1,194 +0,0 @@ -package it.niedermann.owncloud.notes.android.appwidget; - -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.graphics.Color; -import android.net.Uri; -import android.util.Log; -import android.widget.RemoteViews; - -import java.util.NoSuchElementException; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.DarkModeSetting; -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; -import it.niedermann.owncloud.notes.android.activity.NotesListViewActivity; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.model.Category; -import it.niedermann.owncloud.notes.model.LocalAccount; -import it.niedermann.owncloud.notes.model.NoteListsWidgetData; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.Notes; - -import static it.niedermann.owncloud.notes.android.activity.EditNoteActivity.PARAM_CATEGORY; -import static it.niedermann.owncloud.notes.model.NoteListsWidgetData.MODE_DISPLAY_ALL; -import static it.niedermann.owncloud.notes.model.NoteListsWidgetData.MODE_DISPLAY_CATEGORY; -import static it.niedermann.owncloud.notes.model.NoteListsWidgetData.MODE_DISPLAY_STARRED; - -public class NoteListWidget extends AppWidgetProvider { - private static final String TAG = NoteListWidget.class.getSimpleName(); - - public static final int PENDING_INTENT_NEW_NOTE_RQ = 0; - public static final int PENDING_INTENT_EDIT_NOTE_RQ = 1; - public static final int PENDING_INTENT_OPEN_APP_RQ = 2; - - static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { - final NotesDatabase db = NotesDatabase.getInstance(context); - - RemoteViews views; - DarkModeSetting darkTheme; - - for (int appWidgetId : appWidgetIds) { - try { - final NoteListsWidgetData data = db.getNoteListWidgetData(appWidgetId); - final LocalAccount localAccount = db.getAccount(data.getAccountId()); - - String category = null; - if (data.getCategoryId() != null) { - category = db.getCategoryTitleById(data.getAccountId(), data.getCategoryId()); - } - - darkTheme = DarkModeSetting.fromModeID(data.getThemeMode()); - - Intent serviceIntent = new Intent(context, NoteListWidgetService.class); - serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))); - - // Launch application when user taps the header icon or app title - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setComponent(new ComponentName(context.getPackageName(), - NotesListViewActivity.class.getName())); - - // Open the main app if the user taps the widget header - PendingIntent openAppI = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_APP_RQ, - intent, - PendingIntent.FLAG_UPDATE_CURRENT); - - // Launch create note activity if user taps "+" icon on header - PendingIntent newNoteI = PendingIntent.getActivity(context, (PENDING_INTENT_NEW_NOTE_RQ + appWidgetId), - new Intent(context, EditNoteActivity.class).putExtra(PARAM_CATEGORY, new Category(category, data.getMode() == MODE_DISPLAY_STARRED)), - PendingIntent.FLAG_UPDATE_CURRENT); - - PendingIntent templatePI = PendingIntent.getActivity(context, PENDING_INTENT_EDIT_NOTE_RQ, - new Intent(context, EditNoteActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT); - - Log.v(TAG, "-- data - " + data); - - if (Notes.isDarkThemeActive(context, darkTheme)) { - views = new RemoteViews(context.getPackageName(), R.layout.widget_note_list_dark); - views.setTextViewText(R.id.widget_note_list_title_tv_dark, getWidgetTitle(context, data.getMode(), category)); - views.setOnClickPendingIntent(R.id.widget_note_header_icon_dark, openAppI); - views.setOnClickPendingIntent(R.id.widget_note_list_title_tv_dark, openAppI); - views.setOnClickPendingIntent(R.id.widget_note_list_create_icon_dark, newNoteI); - views.setPendingIntentTemplate(R.id.note_list_widget_lv_dark, templatePI); - views.setRemoteAdapter(appWidgetId, R.id.note_list_widget_lv_dark, serviceIntent); - views.setEmptyView(R.id.note_list_widget_lv_dark, R.id.widget_note_list_placeholder_tv_dark); - awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.note_list_widget_lv_dark); - if (BrandingUtil.isBrandingEnabled(context)) { - views.setInt(R.id.widget_note_header_dark, "setBackgroundColor", localAccount.getColor()); - views.setInt(R.id.widget_note_header_icon_dark, "setColorFilter", localAccount.getTextColor()); - views.setInt(R.id.widget_note_list_create_icon_dark, "setColorFilter", localAccount.getTextColor()); - views.setTextColor(R.id.widget_note_list_title_tv_dark, localAccount.getTextColor()); - } else { - views.setInt(R.id.widget_note_header_dark, "setBackgroundColor", context.getResources().getColor(R.color.defaultBrand)); - views.setInt(R.id.widget_note_header_icon_dark, "setColorFilter", Color.WHITE); - views.setInt(R.id.widget_note_list_create_icon_dark, "setColorFilter", Color.WHITE); - views.setTextColor(R.id.widget_note_list_title_tv_dark, Color.WHITE); - } - } else { - views = new RemoteViews(context.getPackageName(), R.layout.widget_note_list); - views.setTextViewText(R.id.widget_note_list_title_tv, getWidgetTitle(context, data.getMode(), category)); - views.setOnClickPendingIntent(R.id.widget_note_header_icon, openAppI); - views.setOnClickPendingIntent(R.id.widget_note_list_title_tv, openAppI); - views.setOnClickPendingIntent(R.id.widget_note_list_create_icon, newNoteI); - views.setPendingIntentTemplate(R.id.note_list_widget_lv, templatePI); - views.setRemoteAdapter(appWidgetId, R.id.note_list_widget_lv, serviceIntent); - views.setEmptyView(R.id.note_list_widget_lv, R.id.widget_note_list_placeholder_tv); - awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.note_list_widget_lv); - if (BrandingUtil.isBrandingEnabled(context)) { - views.setInt(R.id.widget_note_header, "setBackgroundColor", localAccount.getColor()); - views.setInt(R.id.widget_note_header_icon, "setColorFilter", localAccount.getTextColor()); - views.setInt(R.id.widget_note_list_create_icon, "setColorFilter", localAccount.getTextColor()); - views.setTextColor(R.id.widget_note_list_title_tv, localAccount.getTextColor()); - } else { - views.setInt(R.id.widget_note_header, "setBackgroundColor", context.getResources().getColor(R.color.defaultBrand)); - views.setInt(R.id.widget_note_header_icon, "setColorFilter", Color.WHITE); - views.setInt(R.id.widget_note_list_create_icon, "setColorFilter", Color.WHITE); - views.setTextColor(R.id.widget_note_list_title_tv, Color.WHITE); - } - } - - awm.updateAppWidget(appWidgetId, views); - } catch (NoSuchElementException e) { - Log.i(TAG, "onUpdate has been triggered before the user finished configuring the widget"); - } - } - } - - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - super.onUpdate(context, appWidgetManager, appWidgetIds); - updateAppWidget(context, appWidgetManager, appWidgetIds); - } - - @Override - public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); - AppWidgetManager awm = AppWidgetManager.getInstance(context); - - if (intent.getAction() != null) { - if (intent.getAction().equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)) { - if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) { - if (intent.getExtras() != null) { - updateAppWidget(context, awm, new int[]{intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)}); - } else { - Log.w(TAG, "intent.getExtras() is null"); - } - } else { - updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, NoteListWidget.class))); - } - } - } else { - Log.w(TAG, "intent.getAction() is null"); - } - } - - @Override - public void onDeleted(Context context, int[] appWidgetIds) { - super.onDeleted(context, appWidgetIds); - final NotesDatabase db = NotesDatabase.getInstance(context); - - for (int appWidgetId : appWidgetIds) { - db.removeNoteListWidget(appWidgetId); - } - } - - private static String getWidgetTitle(Context context, int displayMode, String category) { - switch (displayMode) { - case MODE_DISPLAY_ALL: - return context.getString(R.string.app_name); - case MODE_DISPLAY_STARRED: - return context.getString(R.string.label_favorites); - case MODE_DISPLAY_CATEGORY: - if ("".equals(category)) { - return context.getString(R.string.action_uncategorized); - } else { - return category; - } - default: - return null; - } - } - - /** - * Update note list widgets, if the note data was changed. - */ - public static void updateNoteListWidgets(Context context) { - context.sendBroadcast(new Intent(context, NoteListWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidgetFactory.java b/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidgetFactory.java deleted file mode 100644 index 9956ff05..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidgetFactory.java +++ /dev/null @@ -1,148 +0,0 @@ -package it.niedermann.owncloud.notes.android.appwidget; - -import android.appwidget.AppWidgetManager; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; -import android.widget.RemoteViews; -import android.widget.RemoteViewsService; - -import java.util.List; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.DarkModeSetting; -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.NoteListsWidgetData; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.Notes; - -import static it.niedermann.owncloud.notes.model.NoteListsWidgetData.MODE_DISPLAY_ALL; -import static it.niedermann.owncloud.notes.model.NoteListsWidgetData.MODE_DISPLAY_CATEGORY; -import static it.niedermann.owncloud.notes.model.NoteListsWidgetData.MODE_DISPLAY_STARRED; - -public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFactory { - private static final String TAG = NoteListWidgetFactory.class.getSimpleName(); - - private final Context context; - private final NoteListsWidgetData data; - private final boolean darkTheme; - private NotesDatabase db; - private List dbNotes; - - NoteListWidgetFactory(Context context, Intent intent) { - this.context = context; - final int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID); - - db = NotesDatabase.getInstance(context); - data = db.getNoteListWidgetData(appWidgetId); - - darkTheme = Notes.isDarkThemeActive(context, DarkModeSetting.fromModeID(data.getThemeMode())); - } - - @Override - public void onCreate() { - } - - @Override - public void onDataSetChanged() { - try { - Log.v(TAG, "--- data - " + data); - switch (data.getMode()) { - case MODE_DISPLAY_ALL: - dbNotes = db.getNotes(data.getAccountId()); - break; - case MODE_DISPLAY_STARRED: - dbNotes = db.searchNotes(data.getAccountId(), null, null, true); - break; - case MODE_DISPLAY_CATEGORY: - if (data.getCategoryId() != null) { - dbNotes = db.searchNotes(data.getAccountId(), null, db.getCategoryTitleById(data.getAccountId(), data.getCategoryId()), null); - } - break; - } - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - } - - @Override - public void onDestroy() { - //NoOp - } - - /** - * getCount() - * - * @return Total number of entries - */ - @Override - public int getCount() { - if (dbNotes == null) { - return 0; - } - - return dbNotes.size(); - } - - @Override - public RemoteViews getViewAt(int position) { - RemoteViews note_content; - - if (dbNotes == null || position > dbNotes.size() - 1 || dbNotes.get(position) == null) { - Log.e(TAG, "Could not find position \"" + position + "\" in dbNotes list."); - return null; - } - - DBNote note = dbNotes.get(position); - final Intent fillInIntent = new Intent(); - final Bundle extras = new Bundle(); - - extras.putLong(EditNoteActivity.PARAM_NOTE_ID, note.getId()); - extras.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, note.getAccountId()); - fillInIntent.putExtras(extras); - fillInIntent.setData(Uri.parse(fillInIntent.toUri(Intent.URI_INTENT_SCHEME))); - - if (darkTheme) { - note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry_dark); - note_content.setOnClickFillInIntent(R.id.widget_note_list_entry_dark, fillInIntent); - note_content.setTextViewText(R.id.widget_entry_content_tv_dark, note.getTitle()); - note_content.setImageViewResource(R.id.widget_entry_fav_icon_dark, note.isFavorite() - ? R.drawable.ic_star_yellow_24dp - : R.drawable.ic_star_grey_ccc_24dp); - } else { - note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry); - note_content.setOnClickFillInIntent(R.id.widget_note_list_entry, fillInIntent); - note_content.setTextViewText(R.id.widget_entry_content_tv, note.getTitle()); - note_content.setImageViewResource(R.id.widget_entry_fav_icon, note.isFavorite() - ? R.drawable.ic_star_yellow_24dp - : R.drawable.ic_star_grey_ccc_24dp); - } - - return note_content; - - } - - @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; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidgetService.java b/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidgetService.java deleted file mode 100644 index 573ef7ea..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteListWidgetService.java +++ /dev/null @@ -1,11 +0,0 @@ -package it.niedermann.owncloud.notes.android.appwidget; - -import android.content.Intent; -import android.widget.RemoteViewsService; - -public class NoteListWidgetService extends RemoteViewsService { - @Override - public RemoteViewsFactory onGetViewFactory(Intent intent) { - return new NoteListWidgetFactory(this.getApplicationContext(), intent); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteWidgetHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteWidgetHelper.java deleted file mode 100644 index 02382e5c..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/NoteWidgetHelper.java +++ /dev/null @@ -1,23 +0,0 @@ -package it.niedermann.owncloud.notes.android.appwidget; - -import android.content.SharedPreferences; - -import it.niedermann.owncloud.notes.android.DarkModeSetting; - -final class NoteWidgetHelper { - private NoteWidgetHelper() { - // Helper class for static methods - } - - @SuppressWarnings("WeakerAccess") //Making it package-private would generate a warning in PMD - public static DarkModeSetting getDarkThemeSetting(SharedPreferences prefs, String darkModeKey, int appWidgetId) { - try { - String themeName = prefs.getString(darkModeKey + appWidgetId, DarkModeSetting.SYSTEM_DEFAULT.name()); - return DarkModeSetting.valueOf(themeName); - } catch (ClassCastException e) { - //DARK_THEME was a boolean in older versions of the app. We thereofre have to still support the old setting. - boolean isDarkTheme = prefs.getBoolean(darkModeKey + appWidgetId, false); - return isDarkTheme ? DarkModeSetting.DARK : DarkModeSetting.LIGHT; - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidget.java deleted file mode 100644 index 2237b9cb..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidget.java +++ /dev/null @@ -1,97 +0,0 @@ -package it.niedermann.owncloud.notes.android.appwidget; - -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.util.Log; -import android.widget.RemoteViews; - -import java.util.NoSuchElementException; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.DarkModeSetting; -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; -import it.niedermann.owncloud.notes.android.fragment.BaseNoteFragment; -import it.niedermann.owncloud.notes.model.SingleNoteWidgetData; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.Notes; - -public class SingleNoteWidget extends AppWidgetProvider { - - private static final String TAG = SingleNoteWidget.class.getSimpleName(); - - static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { - final Intent templateIntent = new Intent(context, EditNoteActivity.class); - final NotesDatabase db = NotesDatabase.getInstance(context); - - for (int appWidgetId : appWidgetIds) { - try { - final SingleNoteWidgetData data = db.getSingleNoteWidgetData(appWidgetId); - - templateIntent.putExtra(BaseNoteFragment.PARAM_ACCOUNT_ID, data.getAccountId()); - - final PendingIntent templatePendingIntent = PendingIntent.getActivity(context, appWidgetId, templateIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - Intent serviceIntent = new Intent(context, SingleNoteWidgetService.class); - serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))); - - RemoteViews views; - - if (Notes.isDarkThemeActive(context, DarkModeSetting.fromModeID(data.getThemeMode()))) { - views = new RemoteViews(context.getPackageName(), R.layout.widget_single_note_dark); - views.setPendingIntentTemplate(R.id.single_note_widget_lv_dark, templatePendingIntent); - views.setRemoteAdapter(R.id.single_note_widget_lv_dark, serviceIntent); - views.setEmptyView(R.id.single_note_widget_lv_dark, R.id.widget_single_note_placeholder_tv_dark); - awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.single_note_widget_lv_dark); - } else { - views = new RemoteViews(context.getPackageName(), R.layout.widget_single_note); - views.setPendingIntentTemplate(R.id.single_note_widget_lv, templatePendingIntent); - views.setRemoteAdapter(R.id.single_note_widget_lv, serviceIntent); - views.setEmptyView(R.id.single_note_widget_lv, R.id.widget_single_note_placeholder_tv); - awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.single_note_widget_lv); - } - awm.updateAppWidget(appWidgetId, views); - } catch (NoSuchElementException e) { - Log.i(TAG, "onUpdate has been triggered before the user finished configuring the widget"); - } - } - } - - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - super.onUpdate(context, appWidgetManager, appWidgetIds); - updateAppWidget(context, appWidgetManager, appWidgetIds); - } - - @Override - public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); - AppWidgetManager awm = AppWidgetManager.getInstance(context); - - updateAppWidget(context, AppWidgetManager.getInstance(context), - (awm.getAppWidgetIds(new ComponentName(context, SingleNoteWidget.class)))); - } - - @Override - public void onDeleted(Context context, int[] appWidgetIds) { - final NotesDatabase db = NotesDatabase.getInstance(context); - - for (int appWidgetId : appWidgetIds) { - db.removeSingleNoteWidget(appWidgetId); - } - super.onDeleted(context, appWidgetIds); - } - - /** - * Update single note widget, if the note data was changed. - */ - public static void updateSingleNoteWidgets(Context context) { - context.sendBroadcast(new Intent(context, SingleNoteWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidgetFactory.java b/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidgetFactory.java deleted file mode 100644 index b7eee1cc..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidgetFactory.java +++ /dev/null @@ -1,147 +0,0 @@ -package it.niedermann.owncloud.notes.android.appwidget; - -import android.appwidget.AppWidgetManager; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.widget.RemoteViews; -import android.widget.RemoteViewsService; - -import com.yydcdut.markdown.MarkdownProcessor; -import com.yydcdut.markdown.syntax.text.TextFactory; - -import java.util.NoSuchElementException; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.DarkModeSetting; -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.SingleNoteWidgetData; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.MarkDownUtil; -import it.niedermann.owncloud.notes.util.Notes; - -import static it.niedermann.owncloud.notes.util.MarkDownUtil.parseCompat; - -public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFactory { - - private final MarkdownProcessor markdownProcessor; - private final Context context; - private final int appWidgetId; - - private NotesDatabase db; - private DBNote note; - private boolean darkModeActive = false; - - private static final String TAG = SingleNoteWidget.class.getSimpleName(); - - SingleNoteWidgetFactory(Context context, Intent intent) { - this.context = context; - appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID); - db = NotesDatabase.getInstance(context); - markdownProcessor = new MarkdownProcessor(this.context); - markdownProcessor.factory(TextFactory.create()); - try { - SingleNoteWidgetData data = db.getSingleNoteWidgetData(appWidgetId); - darkModeActive = Notes.isDarkThemeActive(context, DarkModeSetting.fromModeID(data.getThemeMode())); - } catch (NoSuchElementException e) { - Log.w(TAG, "Widget with ID " + appWidgetId + " seems to be not configured yet."); - } finally { - markdownProcessor.config(MarkDownUtil.getMarkDownConfiguration(this.context, darkModeActive).build()); - } - } - - @Override - public void onCreate() { - - } - - @Override - public void onDataSetChanged() { - try { - final SingleNoteWidgetData data = db.getSingleNoteWidgetData(appWidgetId); - final long noteId = data.getNoteId(); - Log.v(TAG, "Fetch note with id " + noteId); - note = db.getNote(data.getAccountId(), noteId); - - if (note == null) { - Log.e(TAG, "Error: note not found"); - } - } catch (NoSuchElementException e) { - Log.w(TAG, "Widget with ID " + appWidgetId + " seems to be not configured yet."); - } - } - - @Override - public void onDestroy() { - //NoOp - } - - /** - * Returns the number of items in the data set. In this case, always 1 as a single note is - * being displayed. Will return 0 when the note can't be displayed. - */ - @Override - public int getCount() { - return (note != null) ? 1 : 0; - } - - /** - * Returns a RemoteView containing the note content in a TextView and - * a fillInIntent to handle the user tapping on the item in the list view. - * - * @param position The position of the item in the list - * @return The RemoteView at the specified position in the list - */ - @Override - public RemoteViews getViewAt(int position) { - if (note == null) { - return null; - } - - RemoteViews note_content; - - final Intent fillInIntent = new Intent(); - final Bundle extras = new Bundle(); - - extras.putLong(EditNoteActivity.PARAM_NOTE_ID, note.getId()); - extras.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, note.getAccountId()); - fillInIntent.putExtras(extras); - if (darkModeActive) { - note_content = new RemoteViews(context.getPackageName(), R.layout.widget_single_note_content_dark); - note_content.setOnClickFillInIntent(R.id.single_note_content_tv_dark, fillInIntent); - note_content.setTextViewText(R.id.single_note_content_tv_dark, parseCompat(markdownProcessor, note.getContent())); - - } else { - note_content = new RemoteViews(context.getPackageName(), R.layout.widget_single_note_content); - note_content.setOnClickFillInIntent(R.id.single_note_content_tv, fillInIntent); - note_content.setTextViewText(R.id.single_note_content_tv, parseCompat(markdownProcessor, note.getContent())); - } - - return note_content; - } - - - // TODO Set loading view - @Override - public RemoteViews getLoadingView() { - return null; - } - - @Override - public int getViewTypeCount() { - return 1; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public boolean hasStableIds() { - return true; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidgetService.java b/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidgetService.java deleted file mode 100644 index e194d54d..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/appwidget/SingleNoteWidgetService.java +++ /dev/null @@ -1,11 +0,0 @@ -package it.niedermann.owncloud.notes.android.appwidget; - -import android.content.Intent; -import android.widget.RemoteViewsService; - -public class SingleNoteWidgetService extends RemoteViewsService { - @Override - public RemoteViewsFactory onGetViewFactory(Intent intent) { - return new SingleNoteWidgetFactory(this.getApplicationContext(), intent); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/AccountChooserAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/AccountChooserAdapter.java deleted file mode 100644 index 800660b5..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/AccountChooserAdapter.java +++ /dev/null @@ -1,79 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.net.Uri; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; - -import java.util.List; - -import it.niedermann.android.glidesso.SingleSignOnUrl; -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding; -import it.niedermann.owncloud.notes.model.LocalAccount; - -import static it.niedermann.owncloud.notes.android.fragment.AccountChooserAdapter.AccountChooserViewHolder; - -public class AccountChooserAdapter extends RecyclerView.Adapter { - - @NonNull - private final List localAccounts; - @NonNull - private final MoveAccountListener moveAccountListener; - - AccountChooserAdapter(@NonNull List localAccounts, @NonNull MoveAccountListener moveAccountListener) { - super(); - this.localAccounts = localAccounts; - this.moveAccountListener = moveAccountListener; - } - - @NonNull - @Override - public AccountChooserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_account_choose, parent, false); - return new AccountChooserViewHolder(v); - } - - @Override - public void onBindViewHolder(@NonNull AccountChooserViewHolder holder, int position) { - holder.bind(localAccounts.get(position), moveAccountListener); - } - - @Override - public int getItemCount() { - return localAccounts.size(); - } - - static class AccountChooserViewHolder extends RecyclerView.ViewHolder { - private final ItemAccountChooseBinding binding; - - private AccountChooserViewHolder(View view) { - super(view); - binding = ItemAccountChooseBinding.bind(view); - } - - public void bind(LocalAccount localAccount, MoveAccountListener moveAccountListener) { - Glide - .with(binding.accountItemAvatar.getContext()) - .load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64")) - .placeholder(R.drawable.ic_account_circle_grey_24dp) - .error(R.drawable.ic_account_circle_grey_24dp) - .apply(RequestOptions.circleCropTransform()) - .into(binding.accountItemAvatar); - - binding.accountLayout.setOnClickListener((v) -> moveAccountListener.moveToAccount(localAccount)); - binding.accountName.setText(localAccount.getUserName()); - binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost()); - } - } - - public interface MoveAccountListener { - void moveToAccount(LocalAccount account); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/BaseNoteFragment.java deleted file mode 100644 index c3ccdeb0..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/BaseNoteFragment.java +++ /dev/null @@ -1,411 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.graphics.Color; -import android.graphics.drawable.Icon; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.ScrollView; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; - -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.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; -import it.niedermann.owncloud.notes.android.fragment.CategoryDialogFragment.CategoryDialogListener; -import it.niedermann.owncloud.notes.android.fragment.EditTitleDialogFragment.EditTitleListener; -import it.niedermann.owncloud.notes.branding.BrandedFragment; -import it.niedermann.owncloud.notes.model.ApiVersion; -import it.niedermann.owncloud.notes.model.CloudNote; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.DBStatus; -import it.niedermann.owncloud.notes.model.ISyncCallback; -import it.niedermann.owncloud.notes.model.LocalAccount; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.ColorUtil; -import it.niedermann.owncloud.notes.util.NoteUtil; -import it.niedermann.owncloud.notes.util.ShareUtil; - -import static androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported; -import static it.niedermann.owncloud.notes.android.activity.EditNoteActivity.ACTION_SHORTCUT; -import static it.niedermann.owncloud.notes.branding.BrandingUtil.tintMenuIcon; -import static it.niedermann.owncloud.notes.util.ColorUtil.isColorDark; -import static it.niedermann.owncloud.notes.util.Notes.isDarkThemeActive; - -public abstract class BaseNoteFragment extends BrandedFragment implements CategoryDialogListener, EditTitleListener { - - private static final String TAG = BaseNoteFragment.class.getSimpleName(); - - protected static final int MENU_ID_PIN = -1; - public static final String PARAM_NOTE_ID = "noteId"; - public static final String PARAM_ACCOUNT_ID = "accountId"; - public static final String PARAM_CONTENT = "content"; - public static final String PARAM_NEWNOTE = "newNote"; - private static final String SAVEDKEY_NOTE = "note"; - private static final String SAVEDKEY_ORIGINAL_NOTE = "original_note"; - - private LocalAccount localAccount; - private SingleSignOnAccount ssoAccount; - - protected DBNote note; - // TODO do we really need this? The reference to note is currently the same - @Nullable - private DBNote originalNote; - private int originalScrollY; - protected NotesDatabase db; - private NoteFragmentListener listener; - private boolean titleModified = false; - - protected boolean isNew = true; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - try { - this.ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireActivity().getApplicationContext()); - this.localAccount = db.getLocalAccountByAccountName(ssoAccount.name); - - if (savedInstanceState == null) { - long id = requireArguments().getLong(PARAM_NOTE_ID); - if (id > 0) { - long accountId = requireArguments().getLong(PARAM_ACCOUNT_ID); - if (accountId > 0) { - /* Switch account if account id has been provided */ - this.localAccount = db.getAccount(accountId); - SingleAccountHelper.setCurrentAccount(requireActivity().getApplicationContext(), localAccount.getAccountName()); - } - isNew = false; - note = originalNote = db.getNote(localAccount.getId(), id); - } else { - CloudNote cloudNote = (CloudNote) requireArguments().getSerializable(PARAM_NEWNOTE); - String content = requireArguments().getString(PARAM_CONTENT); - if (cloudNote == null) { - if (content == null) { - throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given, argument " + PARAM_NEWNOTE + " is missing and " + PARAM_CONTENT + " is missing."); - } else { - note = new DBNote(-1, -1, null, NoteUtil.generateNoteTitle(content), content, false, getString(R.string.category_readonly), null, DBStatus.VOID, -1, "", 0); - } - } else { - note = db.getNote(localAccount.getId(), db.addNoteAndSync(ssoAccount, localAccount.getId(), cloudNote)); - originalNote = null; - } - } - } else { - note = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_NOTE); - originalNote = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_ORIGINAL_NOTE); - } - setHasOptionsMenu(true); - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { - e.printStackTrace(); - } - } - - @Nullable - protected abstract ScrollView getScrollView(); - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - final ScrollView scrollView = getScrollView(); - if (scrollView != null) { - scrollView.getViewTreeObserver().addOnScrollChangedListener(() -> { - if (scrollView.getScrollY() > 0) { - note.setScrollY(scrollView.getScrollY()); - } - }); - } - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - final ScrollView scrollView = getScrollView(); - if (scrollView != null) { - this.originalScrollY = note.getScrollY(); - scrollView.post(() -> scrollView.scrollTo(0, originalScrollY)); - } - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - try { - listener = (NoteFragmentListener) context; - } catch (ClassCastException e) { - throw new ClassCastException(context.getClass() + " must implement " + NoteFragmentListener.class); - } - db = NotesDatabase.getInstance(context); - } - - @Override - public void onResume() { - super.onResume(); - listener.onNoteUpdated(note); - } - - @Override - public void onPause() { - super.onPause(); - saveNote(null); - } - - @Override - public void onDetach() { - super.onDetach(); - listener = null; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - saveNote(null); - outState.putSerializable(SAVEDKEY_NOTE, note); - outState.putSerializable(SAVEDKEY_ORIGINAL_NOTE, originalNote); - } - - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - inflater.inflate(R.menu.menu_note_fragment, menu); - - if (isRequestPinShortcutSupported(requireActivity()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - menu.add(Menu.NONE, MENU_ID_PIN, 110, R.string.pin_to_homescreen); - } - - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - MenuItem itemFavorite = menu.findItem(R.id.menu_favorite); - prepareFavoriteOption(itemFavorite); - - menu.findItem(R.id.menu_title).setVisible(localAccount.getPreferredApiVersion() != null && localAccount.getPreferredApiVersion().compareTo(new ApiVersion("1.0", 1, 0)) >= 0); - menu.findItem(R.id.menu_delete).setVisible(!isNew); - } - - private void prepareFavoriteOption(MenuItem item) { - item.setIcon(note.isFavorite() ? R.drawable.ic_star_white_24dp : R.drawable.ic_star_border_white_24dp); - item.setChecked(note.isFavorite()); - tintMenuIcon(item, colorAccent); - } - - /** - * Main-Menu-Handler - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_cancel: - if (originalNote == null) { - db.deleteNoteAndSync(ssoAccount, note.getId()); - } else { - db.updateNoteAndSync(ssoAccount, localAccount, originalNote, null, null); - } - listener.close(); - return true; - case R.id.menu_delete: - db.deleteNoteAndSync(ssoAccount, note.getId()); - listener.close(); - return true; - case R.id.menu_favorite: - db.toggleFavorite(ssoAccount, note, null); - listener.onNoteUpdated(note); - prepareFavoriteOption(item); - return true; - case R.id.menu_category: - showCategorySelector(); - return true; - case R.id.menu_title: - showEditTitleDialog(); - return true; - case R.id.menu_move: - MoveAccountDialogFragment.newInstance().show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName()); - return true; - case R.id.menu_share: - ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent()); - return false; - case MENU_ID_PIN: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ShortcutManager shortcutManager = requireActivity().getSystemService(ShortcutManager.class); - - if (shortcutManager != null) { - if (shortcutManager.isRequestPinShortcutSupported()) { - Intent intent = new Intent(getActivity(), EditNoteActivity.class); - intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()); - intent.setAction(ACTION_SHORTCUT); - - ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(getActivity(), note.getId() + "") - .setShortLabel(note.getTitle()) - .setIcon(Icon.createWithResource(requireActivity().getApplicationContext(), note.isFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp)) - .setIntent(intent) - .build(); - - Intent pinnedShortcutCallbackIntent = - shortcutManager.createShortcutResultIntent(pinShortcutInfo); - - PendingIntent successCallback = PendingIntent.getBroadcast(getActivity(), /* request code */ 0, - pinnedShortcutCallbackIntent, /* flags */ 0); - - shortcutManager.requestPinShortcut(pinShortcutInfo, - successCallback.getIntentSender()); - } else { - Log.i(TAG, "RequestPinShortcut is not supported"); - } - } else { - Log.e(TAG, "ShortcutManager is null"); - } - } - - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - public void onCloseNote() { - if (!titleModified && originalNote == null && getContent().isEmpty()) { - db.deleteNoteAndSync(ssoAccount, note.getId()); - } - } - - /** - * Save the current state in the database and schedule synchronization if needed. - * - * @param callback Observer which is called after save/synchronization - */ - protected void saveNote(@Nullable ISyncCallback callback) { - Log.d(TAG, "saveData()"); - if (note != null) { - String newContent = getContent(); - if (note.getContent().equals(newContent)) { - if (note.getScrollY() != originalScrollY) { - Log.v(TAG, "... only saving new scroll state, since content did not change"); - db.updateScrollY(note.getId(), note.getScrollY()); - } else { - Log.v(TAG, "... not saving, since nothing has changed"); - } - } else { - note = db.updateNoteAndSync(ssoAccount, localAccount, note, newContent, callback); - listener.onNoteUpdated(note); - requireActivity().invalidateOptionsMenu(); - } - } else { - Log.e(TAG, "note is null"); - } - } - - protected abstract String getContent(); - - /** - * Opens a dialog in order to chose a category - */ - private void showCategorySelector() { - final String fragmentId = "fragment_category"; - FragmentManager manager = requireActivity().getSupportFragmentManager(); - Fragment frag = manager.findFragmentByTag(fragmentId); - if (frag != null) { - manager.beginTransaction().remove(frag).commit(); - } - Bundle arguments = new Bundle(); - arguments.putString(CategoryDialogFragment.PARAM_CATEGORY, note.getCategory()); - arguments.putLong(CategoryDialogFragment.PARAM_ACCOUNT_ID, note.getAccountId()); - CategoryDialogFragment categoryFragment = new CategoryDialogFragment(); - categoryFragment.setArguments(arguments); - categoryFragment.setTargetFragment(this, 0); - categoryFragment.show(manager, fragmentId); - } - - /** - * Opens a dialog in order to chose a category - */ - public void showEditTitleDialog() { - saveNote(null); - final String fragmentId = "fragment_edit_title"; - FragmentManager manager = requireActivity().getSupportFragmentManager(); - Fragment frag = manager.findFragmentByTag(fragmentId); - if (frag != null) { - manager.beginTransaction().remove(frag).commit(); - } - DialogFragment editTitleFragment = EditTitleDialogFragment.newInstance(note.getTitle()); - editTitleFragment.setTargetFragment(this, 0); - editTitleFragment.show(manager, fragmentId); - } - - @Override - public void onCategoryChosen(String category) { - db.setCategory(ssoAccount, note, category, null); - listener.onNoteUpdated(note); - } - - @Override - public void onTitleEdited(String newTitle) { - titleModified = true; - note.setTitle(newTitle); - note = db.updateNoteAndSync(ssoAccount, localAccount, note, note.getContent(), newTitle, null); - listener.onNoteUpdated(note); - } - - public void moveNote(LocalAccount account) { - db.moveNoteToAnotherAccount(ssoAccount, note.getAccountId(), note, account.getId()); - listener.close(); - } - - @ColorInt - protected static int getTextHighlightBackgroundColor(@NonNull Context context, @ColorInt int mainColor, @ColorInt int colorPrimary, @ColorInt int colorAccent) { - if (isDarkThemeActive(context)) { // Dark background - if (isColorDark(mainColor)) { // Dark brand color - if (ColorUtil.contrastRatioIsSufficient(mainColor, colorPrimary)) { // But also dark text - return mainColor; - } else { - return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground); - } - } else { // Light brand color - if (ColorUtil.contrastRatioIsSufficient(mainColor, colorAccent)) { // But also dark text - return Color.argb(77, Color.red(mainColor), Color.green(mainColor), Color.blue(mainColor)); - } else { - return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground); - } - } - } else { // Light background - if (isColorDark(mainColor)) { // Dark brand color - if (ColorUtil.contrastRatioIsSufficient(mainColor, colorAccent)) { // But also dark text - return Color.argb(77, Color.red(mainColor), Color.green(mainColor), Color.blue(mainColor)); - } else { - return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground); - } - } else { // Light brand color - if (ColorUtil.contrastRatioIsSufficient(mainColor, colorPrimary)) { // But also dark text - return mainColor; - } else { - return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground); - } - } - } - } - - public interface NoteFragmentListener { - void close(); - - void onNoteUpdated(DBNote note); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/CategoryAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/CategoryAdapter.java deleted file mode 100644 index 5872172a..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/CategoryAdapter.java +++ /dev/null @@ -1,133 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.ItemCategoryBinding; -import it.niedermann.owncloud.notes.model.NavigationAdapter.CategoryNavigationItem; -import it.niedermann.owncloud.notes.model.NavigationAdapter.NavigationItem; -import it.niedermann.owncloud.notes.util.NoteUtil; - -public class CategoryAdapter extends RecyclerView.Adapter { - - private static final String clearItemId = "clear_item"; - private static final String addItemId = "add_item"; - @NonNull - private List categories = new ArrayList<>(); - @NonNull - private final CategoryListener listener; - private final Context context; - - CategoryAdapter(@NonNull Context context, @NonNull CategoryListener categoryListener) { - this.context = context; - this.listener = categoryListener; - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_category, parent, false); - return new CategoryViewHolder(v); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - NavigationItem category = categories.get(position); - CategoryViewHolder categoryViewHolder = (CategoryViewHolder) holder; - - switch (category.id) { - case addItemId: - Drawable wrapDrawable = DrawableCompat.wrap(context.getResources().getDrawable(category.icon)); - DrawableCompat.setTint(wrapDrawable, context.getResources().getColor(R.color.icon_color_default)); - categoryViewHolder.getIcon().setImageDrawable(wrapDrawable); - categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryAdded()); - break; - case clearItemId: - categoryViewHolder.getIcon().setImageDrawable(context.getResources().getDrawable(category.icon)); - categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryCleared()); - break; - default: - categoryViewHolder.getIcon().setImageDrawable(context.getResources().getDrawable(category.icon)); - categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryChosen(category.label)); - break; - } - categoryViewHolder.getCategory().setText(NoteUtil.extendCategory(category.label)); - if (category.count != null && category.count > 0) { - categoryViewHolder.getCount().setText(String.valueOf(category.count)); - } else { - categoryViewHolder.getCount().setVisibility(View.GONE); - } - } - - @Override - public int getItemCount() { - return categories.size(); - } - - static class CategoryViewHolder extends RecyclerView.ViewHolder { - private final ItemCategoryBinding binding; - - private CategoryViewHolder(View view) { - super(view); - binding = ItemCategoryBinding.bind(view); - } - - private View getCategoryWrapper() { - return binding.categoryWrapper; - } - - private AppCompatImageView getIcon() { - return binding.icon; - } - - private TextView getCategory() { - return binding.category; - } - - private TextView getCount() { - return binding.count; - } - } - - void setCategoryList(List categories, String currentSearchString) { - this.categories.clear(); - this.categories.addAll(categories); - final NavigationItem clearItem = new NavigationItem(clearItemId, context.getString(R.string.no_category), 0, R.drawable.ic_clear_grey_24dp); - this.categories.add(0, clearItem); - if (currentSearchString != null && currentSearchString.trim().length() > 0) { - boolean currentSearchStringIsInCategories = false; - for (NavigationItem category : categories) { - if (currentSearchString.equals(category.label)) { - currentSearchStringIsInCategories = true; - break; - } - } - if (!currentSearchStringIsInCategories) { - NavigationItem addItem = new NavigationItem(addItemId, context.getString(R.string.add_category, currentSearchString.trim()), 0, R.drawable.ic_add_blue_24dp); - this.categories.add(addItem); - } - } - notifyDataSetChanged(); - } - - public interface CategoryListener { - void onCategoryChosen(String category); - - void onCategoryAdded(); - - void onCategoryCleared(); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/CategoryDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/CategoryDialogFragment.java deleted file mode 100644 index cd194cc0..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/CategoryDialogFragment.java +++ /dev/null @@ -1,184 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.Context; -import android.os.AsyncTask; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Log; -import android.view.View; -import android.view.WindowManager; -import android.widget.EditText; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; - -import java.util.List; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandedAlertDialogBuilder; -import it.niedermann.owncloud.notes.branding.BrandedDialogFragment; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.databinding.DialogChangeCategoryBinding; -import it.niedermann.owncloud.notes.model.NavigationAdapter; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; - -/** - * This {@link DialogFragment} allows for the selection of a category. - * It targetFragment is set it must implement the interface {@link CategoryDialogListener}. - * The calling Activity must implement the interface {@link CategoryDialogListener}. - */ -public class CategoryDialogFragment extends BrandedDialogFragment { - - private static final String TAG = CategoryDialogFragment.class.getSimpleName(); - private static final String STATE_CATEGORY = "category"; - private DialogChangeCategoryBinding binding; - - private NotesDatabase db; - private CategoryDialogListener listener; - - private EditText editCategory; - - @Override - public void applyBrand(int mainColor, int textColor) { - BrandingUtil.applyBrandToEditText(mainColor, textColor, binding.search); - } - - /** - * Interface that must be implemented by the calling Activity. - */ - public interface CategoryDialogListener { - /** - * This method is called after the user has chosen a category. - * - * @param category Name of the category which was chosen by the user. - */ - void onCategoryChosen(String category); - } - - static final String PARAM_ACCOUNT_ID = "account_id"; - static final String PARAM_CATEGORY = "category"; - - private long accountId; - - private CategoryAdapter adapter; - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - if (getArguments() != null && requireArguments().containsKey(PARAM_ACCOUNT_ID)) { - accountId = requireArguments().getLong(PARAM_ACCOUNT_ID); - } else { - throw new IllegalArgumentException("Provide at least \"" + PARAM_ACCOUNT_ID + "\""); - } - Fragment target = getTargetFragment(); - if (target instanceof CategoryDialogListener) { - listener = (CategoryDialogListener) target; - } else if (getActivity() instanceof CategoryDialogListener) { - listener = (CategoryDialogListener) getActivity(); - } else { - throw new IllegalArgumentException("Calling activity or target fragment must implement " + CategoryDialogListener.class.getSimpleName()); - } - db = NotesDatabase.getInstance(getActivity()); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - View dialogView = View.inflate(getContext(), R.layout.dialog_change_category, null); - binding = DialogChangeCategoryBinding.bind(dialogView); - this.editCategory = binding.search; - - if (savedInstanceState == null) { - if (requireArguments().containsKey(PARAM_CATEGORY)) { - editCategory.setText(requireArguments().getString(PARAM_CATEGORY)); - } - } else if (savedInstanceState.containsKey(STATE_CATEGORY)) { - editCategory.setText(savedInstanceState.getString(STATE_CATEGORY)); - } - - adapter = new CategoryAdapter(requireContext(), new CategoryAdapter.CategoryListener() { - @Override - public void onCategoryChosen(String category) { - listener.onCategoryChosen(category); - dismiss(); - } - - @Override - public void onCategoryAdded() { - listener.onCategoryChosen(editCategory.getText().toString()); - dismiss(); - } - - @Override - public void onCategoryCleared() { - listener.onCategoryChosen(""); - dismiss(); - } - }); - - binding.recyclerView.setAdapter(adapter); - new LoadCategoriesTask().execute(""); - editCategory.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Nothing to do here... - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Nothing to do here... - } - - @Override - public void afterTextChanged(Editable s) { - new LoadCategoriesTask().execute(editCategory.getText().toString()); - } - }); - - return new BrandedAlertDialogBuilder(getActivity()) - .setTitle(R.string.change_category_title) - .setView(dialogView) - .setCancelable(true) - .setPositiveButton(R.string.action_edit_save, (dialog, which) -> listener.onCategoryChosen(editCategory.getText().toString())) - .setNegativeButton(R.string.simple_cancel, null) - .create(); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString(STATE_CATEGORY, editCategory.getText().toString()); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - if (editCategory.getText() == null || editCategory.getText().length() == 0) { - editCategory.requestFocus(); - if (getDialog() != null && getDialog().getWindow() != null) { - getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - } else { - Log.w(TAG, "can not set SOFT_INPUT_STATE_ALWAYAS_VISIBLE because getWindow() == null"); - } - } - } - - - private class LoadCategoriesTask extends AsyncTask> { - String currentSearchString; - - @Override - protected List doInBackground(String... searchText) { - currentSearchString = searchText[0]; - return db.searchCategories(accountId, currentSearchString); - } - - @Override - protected void onPostExecute(List categories) { - adapter.setCategoryList(categories, currentSearchString); - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/EditTitleDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/EditTitleDialogFragment.java deleted file mode 100644 index 1dfd995d..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/EditTitleDialogFragment.java +++ /dev/null @@ -1,96 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.DialogEditTitleBinding; - -public class EditTitleDialogFragment extends DialogFragment { - - private static final String TAG = EditTitleDialogFragment.class.getSimpleName(); - static final String PARAM_OLD_TITLE = "old_title"; - private DialogEditTitleBinding binding; - - private String oldTitle; - private EditTitleListener listener; - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - final Bundle args = getArguments(); - if (args == null) { - throw new IllegalArgumentException("Provide at least " + PARAM_OLD_TITLE); - } - oldTitle = args.getString(PARAM_OLD_TITLE); - - if (getTargetFragment() instanceof EditTitleListener) { - listener = (EditTitleListener) getTargetFragment(); - } else if (getActivity() instanceof EditTitleListener) { - listener = (EditTitleListener) getActivity(); - } else { - throw new IllegalArgumentException("Calling activity or target fragment must implement " + EditTitleListener.class.getSimpleName()); - } - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - View dialogView = View.inflate(getContext(), R.layout.dialog_edit_title, null); - binding = DialogEditTitleBinding.bind(dialogView); - - if (savedInstanceState == null) { - binding.title.setText(oldTitle); - } - - return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.change_note_title) - .setView(dialogView) - .setCancelable(true) - .setPositiveButton(R.string.action_edit_save, (dialog, which) -> listener.onTitleEdited(binding.title.getText().toString())) - .setNegativeButton(R.string.simple_cancel, null) - .create(); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - binding.title.requestFocus(); - Window window = requireDialog().getWindow(); - if (window != null) { - window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - } else { - Log.w(TAG, "can not enable soft keyboard because " + Window.class.getSimpleName() + " is null."); - } - } - - public static DialogFragment newInstance(String title) { - final DialogFragment fragment = new EditTitleDialogFragment(); - final Bundle args = new Bundle(); - args.putString(PARAM_OLD_TITLE, title); - fragment.setArguments(args); - return fragment; - } - - /** - * Interface that must be implemented by the calling Activity. - */ - public interface EditTitleListener { - /** - * This method is called after the user has changed the title of a note manually. - * - * @param newTitle the new title that a user submitted - */ - void onTitleEdited(String newTitle); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/ExceptionDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/ExceptionDialogFragment.java deleted file mode 100644 index 520996ef..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/ExceptionDialogFragment.java +++ /dev/null @@ -1,167 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDialogFragment; -import androidx.fragment.app.DialogFragment; -import androidx.recyclerview.widget.RecyclerView; - -import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotSupportedException; -import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; -import com.nextcloud.android.sso.exceptions.TokenMismatchException; - -import org.json.JSONException; - -import java.net.ConnectException; -import java.net.SocketTimeoutException; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -import it.niedermann.owncloud.notes.ExceptionUtil; -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.DialogExceptionBinding; -import it.niedermann.owncloud.notes.databinding.ItemTipBinding; - -import static it.niedermann.owncloud.notes.util.ClipboardUtil.copyToClipboard; - -public class ExceptionDialogFragment extends AppCompatDialogFragment { - - private static final String KEY_THROWABLES = "throwables"; - - @NonNull - private ArrayList throwables = new ArrayList<>(); - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - final Bundle args = getArguments(); - if (args != null) { - final Object throwablesArgument = args.getSerializable(KEY_THROWABLES); - if (throwablesArgument != null) { - throwables.addAll((ArrayList) throwablesArgument); - } - } - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final View view = View.inflate(getContext(), R.layout.dialog_exception, null); - final DialogExceptionBinding binding = DialogExceptionBinding.bind(view); - - final TipsAdapter adapter = new TipsAdapter(); - - final String debugInfos = ExceptionUtil.getDebugInfos(requireContext(), throwables); - - binding.tips.setAdapter(adapter); - binding.statusMessage.setText(getString(R.string.error_sync, throwables.size() > 0 ? throwables.get(0).getLocalizedMessage() : getString(R.string.error_unknown))); - binding.stacktrace.setText(debugInfos); - - for (Throwable t : throwables) { - if (t instanceof TokenMismatchException) { - adapter.add(R.string.error_dialog_tip_token_mismatch_retry); - adapter.add(R.string.error_dialog_tip_token_mismatch_clear_storage); - adapter.add(R.string.error_dialog_tip_clear_storage); - } else if (t instanceof NextcloudFilesAppNotSupportedException) { - adapter.add(R.string.error_dialog_tip_files_outdated); - } else if (t instanceof NextcloudApiNotRespondingException) { - adapter.add(R.string.error_dialog_tip_files_force_stop); - adapter.add(R.string.error_dialog_tip_files_delete_storage); - } else if (t instanceof SocketTimeoutException || t instanceof ConnectException) { - adapter.add(R.string.error_dialog_timeout_instance); - adapter.add(R.string.error_dialog_timeout_toggle); - } else if (t instanceof JSONException || t instanceof NullPointerException) { - adapter.add(R.string.error_dialog_check_server); - } else if (t instanceof NextcloudHttpRequestFailedException) { - int statusCode = ((NextcloudHttpRequestFailedException) t).getStatusCode(); - switch (statusCode) { - case 302: - adapter.add(R.string.error_dialog_server_app_enabled); - adapter.add(R.string.error_dialog_redirect); - break; - case 500: - adapter.add(R.string.error_dialog_check_server_logs); - break; - case 503: - adapter.add(R.string.error_dialog_check_maintenance); - break; - case 507: - adapter.add(R.string.error_dialog_insufficient_storage); - break; - } - } - } - - return new AlertDialog.Builder(requireActivity()) - .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```")) - .setNegativeButton(R.string.simple_close, null) - .create(); - } - - public static DialogFragment newInstance(ArrayList exceptions) { - final Bundle args = new Bundle(); - args.putSerializable(KEY_THROWABLES, exceptions); - final DialogFragment fragment = new ExceptionDialogFragment(); - fragment.setArguments(args); - return fragment; - } - - public static DialogFragment newInstance(Throwable exception) { - final Bundle args = new Bundle(); - final ArrayList list = new ArrayList<>(1); - list.add(exception); - args.putSerializable(KEY_THROWABLES, list); - final DialogFragment fragment = new ExceptionDialogFragment(); - fragment.setArguments(args); - return fragment; - } - - private static class TipsAdapter extends RecyclerView.Adapter { - - @NonNull - private List tips = new LinkedList<>(); - - @NonNull - @Override - public TipsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_tip, parent, false); - return new TipsViewHolder(v); - } - - @Override - public void onBindViewHolder(@NonNull TipsViewHolder holder, int position) { - holder.binding.tip.setText(tips.get(position)); - } - - @Override - public int getItemCount() { - return tips.size(); - } - - private void add(@StringRes int tip) { - tips.add(tip); - notifyItemInserted(tips.size()); - } - } - - private static class TipsViewHolder extends RecyclerView.ViewHolder { - private final ItemTipBinding binding; - - private TipsViewHolder(@NonNull View itemView) { - super(itemView); - binding = ItemTipBinding.bind(itemView); - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/MoveAccountDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/MoveAccountDialogFragment.java deleted file mode 100644 index 2b959c4b..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/MoveAccountDialogFragment.java +++ /dev/null @@ -1,82 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatDialogFragment; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.List; -import java.util.Objects; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.fragment.AccountChooserAdapter.MoveAccountListener; -import it.niedermann.owncloud.notes.branding.BrandedAlertDialogBuilder; -import it.niedermann.owncloud.notes.databinding.DialogChooseAccountBinding; -import it.niedermann.owncloud.notes.model.LocalAccount; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; - -import static it.niedermann.owncloud.notes.android.fragment.AccountChooserAdapter.AccountChooserViewHolder; - -public class MoveAccountDialogFragment extends AppCompatDialogFragment implements MoveAccountListener { - private MoveAccountListener moveAccountListener; - - /** - * Use newInstance()-Method - */ - public MoveAccountDialogFragment() { - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - if (context instanceof MoveAccountListener) { - this.moveAccountListener = (MoveAccountListener) context; - } else { - throw new ClassCastException("Caller must implement " + MoveAccountListener.class.getSimpleName()); - } - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - View view = View.inflate(getContext(), R.layout.dialog_choose_account, null); - DialogChooseAccountBinding binding = DialogChooseAccountBinding.bind(view); - - NotesDatabase db = NotesDatabase.getInstance(getActivity()); - List accountsList = db.getAccounts(); - - RecyclerView.Adapter adapter = new AccountChooserAdapter(accountsList, this); - binding.accountsList.setAdapter(adapter); - - return new BrandedAlertDialogBuilder(requireActivity()) - .setView(binding.getRoot()) - .setTitle(R.string.simple_move) - .setNegativeButton(android.R.string.cancel, null) - .create(); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - Objects.requireNonNull(requireDialog().getWindow()).setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); - return super.onCreateView(inflater, container, savedInstanceState); - } - - public static MoveAccountDialogFragment newInstance() { - return new MoveAccountDialogFragment(); - } - - @Override - public void moveToAccount(LocalAccount account) { - moveAccountListener.moveToAccount(account); - dismiss(); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NoteEditFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NoteEditFragment.java deleted file mode 100644 index 1a1ab089..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NoteEditFragment.java +++ /dev/null @@ -1,259 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Typeface; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.Editable; -import android.text.Layout; -import android.text.SpannableString; -import android.text.TextWatcher; -import android.util.Log; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; -import android.widget.ScrollView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceManager; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.yydcdut.markdown.MarkdownProcessor; -import com.yydcdut.markdown.syntax.edit.EditFactory; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.FragmentNoteEditBinding; -import it.niedermann.owncloud.notes.model.CloudNote; -import it.niedermann.owncloud.notes.model.ISyncCallback; -import it.niedermann.owncloud.notes.util.MarkDownUtil; -import it.niedermann.owncloud.notes.util.NotesTextWatcher; -import it.niedermann.owncloud.notes.util.format.ContextBasedFormattingCallback; -import it.niedermann.owncloud.notes.util.format.ContextBasedRangeFormattingCallback; - -import static androidx.core.view.ViewCompat.isAttachedToWindow; -import static it.niedermann.owncloud.notes.util.DisplayUtils.searchAndColor; -import static it.niedermann.owncloud.notes.util.NoteUtil.getFontSizeFromPreferences; - -public class NoteEditFragment extends SearchableBaseNoteFragment { - - private static final String TAG = NoteEditFragment.class.getSimpleName(); - - private static final String LOG_TAG_AUTOSAVE = "AutoSave"; - - private static final long DELAY = 2000; // Wait for this time after typing before saving - private static final long DELAY_AFTER_SYNC = 5000; // Wait for this time after saving before checking for next save - - private FragmentNoteEditBinding binding; - - private Handler handler; - private boolean saveActive; - private boolean unsavedEdit; - private final Runnable runAutoSave = new Runnable() { - @Override - public void run() { - if (unsavedEdit) { - Log.d(LOG_TAG_AUTOSAVE, "runAutoSave: start AutoSave"); - autoSave(); - } else { - Log.d(LOG_TAG_AUTOSAVE, "runAutoSave: nothing changed"); - } - } - }; - private TextWatcher textWatcher; - - public static NoteEditFragment newInstance(long accountId, long noteId) { - NoteEditFragment f = new NoteEditFragment(); - Bundle b = new Bundle(); - b.putLong(PARAM_NOTE_ID, noteId); - b.putLong(PARAM_ACCOUNT_ID, accountId); - f.setArguments(b); - return f; - } - - public static NoteEditFragment newInstanceWithNewNote(CloudNote newNote) { - NoteEditFragment f = new NoteEditFragment(); - Bundle b = new Bundle(); - b.putSerializable(PARAM_NEWNOTE, newNote); - f.setArguments(b); - return f; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - handler = new Handler(Looper.getMainLooper()); - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.menu_edit).setVisible(false); - menu.findItem(R.id.menu_preview).setVisible(true); - } - - @Override - public ScrollView getScrollView() { - return binding.scrollView; - } - - @Override - protected Layout getLayout() { - binding.editContent.onPreDraw(); - return binding.editContent.getLayout(); - } - - @Override - protected FloatingActionButton getSearchNextButton() { - return binding.searchNext; - } - - @Override - protected FloatingActionButton getSearchPrevButton() { - return binding.searchPrev; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - binding = FragmentNoteEditBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - textWatcher = new NotesTextWatcher(binding.editContent) { - @Override - public void afterTextChanged(final Editable s) { - super.afterTextChanged(s); - unsavedEdit = true; - if (!saveActive) { - handler.removeCallbacks(runAutoSave); - handler.postDelayed(runAutoSave, DELAY); - } - } - }; - - if (note != null) { - if (note.getContent().isEmpty()) { - binding.editContent.requestFocus(); - - requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); - - final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.showSoftInput(getView(), InputMethodManager.SHOW_IMPLICIT); - } else { - Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); - } - } - - // workaround for issue yydcdut/RxMarkdown#41 - note.setContent(note.getContent().replace("\r\n", "\n")); - - binding.editContent.setText(note.getContent()); - binding.editContent.setEnabled(true); - - final MarkdownProcessor markdownProcessor = new MarkdownProcessor(requireContext()); - markdownProcessor.config(MarkDownUtil.getMarkDownConfiguration(binding.editContent.getContext()).build()); - markdownProcessor.factory(EditFactory.create()); - markdownProcessor.live(binding.editContent); - - binding.editContent.setCustomSelectionActionModeCallback(new ContextBasedRangeFormattingCallback(binding.editContent)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - binding.editContent.setCustomInsertionActionModeCallback(new ContextBasedFormattingCallback(binding.editContent)); - } - final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(requireContext().getApplicationContext()); - binding.editContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp)); - if (sp.getBoolean(getString(R.string.pref_key_font), false)) { - binding.editContent.setTypeface(Typeface.MONOSPACE); - } - } - } - - @Override - public void onResume() { - super.onResume(); - binding.editContent.addTextChangedListener(textWatcher); - } - - @Override - public void onPause() { - super.onPause(); - binding.editContent.removeTextChangedListener(textWatcher); - cancelTimers(); - } - - private void cancelTimers() { - handler.removeCallbacks(runAutoSave); - } - - /** - * Gets the current content of the EditText field in the UI. - * - * @return String of the current content. - */ - @Override - protected String getContent() { - return binding.editContent.getText().toString(); - } - - @Override - protected void saveNote(@Nullable ISyncCallback callback) { - super.saveNote(callback); - unsavedEdit = false; - } - - /** - * Saves the current changes and show the status in the ActionBar - */ - private void autoSave() { - Log.d(LOG_TAG_AUTOSAVE, "STARTAUTOSAVE"); - saveActive = true; - saveNote(new ISyncCallback() { - @Override - public void onFinish() { - onSaved(); - } - - @Override - public void onScheduled() { - onSaved(); - } - - private void onSaved() { - // AFTER SYNCHRONIZATION - Log.d(LOG_TAG_AUTOSAVE, "FINISHED AUTOSAVE"); - saveActive = false; - - // AFTER "DELAY_AFTER_SYNC" SECONDS: allow next auto-save or start it directly - handler.postDelayed(runAutoSave, DELAY_AFTER_SYNC); - - } - }); - } - - @Override - protected void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor) { - if (binding != null && isAttachedToWindow(binding.editContent)) { - binding.editContent.clearFocus(); - binding.editContent.setText(searchAndColor(new SpannableString(getContent()), newText, requireContext(), current, mainColor, textColor), TextView.BufferType.SPANNABLE); - } - } - - @Override - public void applyBrand(int mainColor, int textColor) { - super.applyBrand(mainColor, textColor); - binding.editContent.setHighlightColor(getTextHighlightBackgroundColor(requireContext(), mainColor, colorPrimary, colorAccent)); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NotePreviewFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NotePreviewFragment.java deleted file mode 100644 index 751cf474..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NotePreviewFragment.java +++ /dev/null @@ -1,231 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Typeface; -import android.net.Uri; -import android.os.Bundle; -import android.text.Layout; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.method.LinkMovementMethod; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ScrollView; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.view.ViewCompat; -import androidx.preference.PreferenceManager; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; -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 com.yydcdut.markdown.MarkdownProcessor; -import com.yydcdut.markdown.syntax.text.TextFactory; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; -import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.MarkDownUtil; -import it.niedermann.owncloud.notes.util.NoteLinksUtils; -import it.niedermann.owncloud.notes.util.SSOUtil; - -import static it.niedermann.owncloud.notes.util.DisplayUtils.searchAndColor; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_CHECKED_MINUS; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_CHECKED_STAR; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_UNCHECKED_MINUS; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_UNCHECKED_STAR; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.parseCompat; -import static it.niedermann.owncloud.notes.util.NoteLinksUtils.extractNoteRemoteId; -import static it.niedermann.owncloud.notes.util.NoteLinksUtils.replaceNoteLinksWithDummyUrls; -import static it.niedermann.owncloud.notes.util.NoteUtil.getFontSizeFromPreferences; - -public class NotePreviewFragment extends SearchableBaseNoteFragment implements OnRefreshListener { - - private String changedText; - - private MarkdownProcessor markdownProcessor; - - private FragmentNotePreviewBinding binding; - - public static NotePreviewFragment newInstance(long accountId, long noteId) { - NotePreviewFragment f = new NotePreviewFragment(); - Bundle b = new Bundle(); - b.putLong(PARAM_NOTE_ID, noteId); - b.putLong(PARAM_ACCOUNT_ID, accountId); - f.setArguments(b); - return f; - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.menu_edit).setVisible(true); - menu.findItem(R.id.menu_preview).setVisible(false); - } - - @Override - public ScrollView getScrollView() { - return binding.scrollView; - } - - @Override - protected FloatingActionButton getSearchNextButton() { - return binding.searchNext; - } - - @Override - protected FloatingActionButton getSearchPrevButton() { - return binding.searchPrev; - } - - @Override - protected Layout getLayout() { - binding.singleNoteContent.onPreDraw(); - return binding.singleNoteContent.getLayout(); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup - container, @Nullable Bundle savedInstanceState) { - binding = FragmentNotePreviewBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - markdownProcessor = new MarkdownProcessor(requireContext()); - markdownProcessor.factory(TextFactory.create()); - markdownProcessor.config( - MarkDownUtil.getMarkDownConfiguration(binding.singleNoteContent.getContext()) - .setOnTodoClickCallback((view, line, lineNumber) -> { - try { - String[] lines = TextUtils.split(note.getContent(), "\\r?\\n"); - /* - * Workaround for RxMarkdown-bug: - * When (un)checking a checkbox in a note which contains code-blocks, the "`"-characters get stripped out in the TextView and therefore the given lineNumber is wrong - * Find number of lines starting with ``` before lineNumber - */ - boolean inCodefence = false; - for (int i = 0; i < lines.length; i++) { - if (lines[i].startsWith("```")) { - inCodefence = !inCodefence; - lineNumber++; - } - if (inCodefence && TextUtils.isEmpty(lines[i])) { - lineNumber++; - } - if (i == lineNumber) { - break; - } - } - - /* - * Workaround for multiple RxMarkdown-bugs: - * When (un)checking a checkbox which is in the last line, every time it gets toggled, the last character of the line gets lost. - * When (un)checking a checkbox, every markdown gets stripped in the given line argument - */ - if (lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_MINUS) || lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_STAR)) { - lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_MINUS, CHECKBOX_CHECKED_MINUS); - lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_STAR, CHECKBOX_CHECKED_STAR); - } else { - lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_MINUS, CHECKBOX_UNCHECKED_MINUS); - lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_STAR, CHECKBOX_UNCHECKED_STAR); - } - - changedText = TextUtils.join("\n", lines); - binding.singleNoteContent.setText(parseCompat(markdownProcessor, changedText)); - saveNote(null); - } catch (IndexOutOfBoundsException e) { - Toast.makeText(getActivity(), R.string.checkbox_could_not_be_toggled, Toast.LENGTH_SHORT).show(); - e.printStackTrace(); - } - return line; - } - ) - .setOnLinkClickCallback((view, link) -> { - if (NoteLinksUtils.isNoteLink(link)) { - final Intent intent = new Intent(requireActivity().getApplicationContext(), EditNoteActivity.class) - .putExtra(EditNoteActivity.PARAM_NOTE_ID, db.getLocalIdByRemoteId(this.note.getAccountId(), extractNoteRemoteId(link))); - startActivity(intent); - } else { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); - startActivity(browserIntent); - } - }) - .build()); - try { - binding.singleNoteContent.setText(parseCompat(markdownProcessor, replaceNoteLinksWithDummyUrls(note.getContent(), db.getRemoteIds(note.getAccountId())))); - } catch (StringIndexOutOfBoundsException e) { - // Workaround for RxMarkdown: https://github.com/stefan-niedermann/nextcloud-notes/issues/668 - binding.singleNoteContent.setText(replaceNoteLinksWithDummyUrls(note.getContent(), db.getRemoteIds(note.getAccountId()))); - Toast.makeText(binding.singleNoteContent.getContext(), R.string.could_not_load_preview_two_digit_numbered_list, Toast.LENGTH_LONG).show(); - e.printStackTrace(); - } - changedText = note.getContent(); - binding.singleNoteContent.setMovementMethod(LinkMovementMethod.getInstance()); - - db = NotesDatabase.getInstance(requireContext()); - binding.swiperefreshlayout.setOnRefreshListener(this); - - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(requireActivity().getApplicationContext()); - binding.singleNoteContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp)); - if (sp.getBoolean(getString(R.string.pref_key_font), false)) { - binding.singleNoteContent.setTypeface(Typeface.MONOSPACE); - } - } - - @Override - protected void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor) { - if (binding != null && ViewCompat.isAttachedToWindow(binding.singleNoteContent)) { - binding.singleNoteContent.setText( - searchAndColor(new SpannableString(parseCompat(markdownProcessor, getContent())), newText, requireContext(), current, mainColor, textColor), - TextView.BufferType.SPANNABLE); - } - } - - @Override - protected String getContent() { - return changedText; - } - - @Override - public void onRefresh() { - if (db.getNoteServerSyncHelper().isSyncPossible() && SSOUtil.isConfigured(getContext())) { - binding.swiperefreshlayout.setRefreshing(true); - try { - SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()); - db.getNoteServerSyncHelper().addCallbackPull(ssoAccount, () -> { - note = db.getNote(note.getAccountId(), note.getId()); - changedText = note.getContent(); - binding.singleNoteContent.setText(parseCompat(markdownProcessor, replaceNoteLinksWithDummyUrls(note.getContent(), db.getRemoteIds(note.getAccountId())))); - binding.swiperefreshlayout.setRefreshing(false); - }); - db.getNoteServerSyncHelper().scheduleSync(ssoAccount, false); - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { - e.printStackTrace(); - } - } else { - binding.swiperefreshlayout.setRefreshing(false); - Toast.makeText(requireContext(), getString(R.string.error_sync, getString(R.string.error_no_network)), Toast.LENGTH_LONG).show(); - } - } - - @Override - public void applyBrand(int mainColor, int textColor) { - super.applyBrand(mainColor, textColor); - binding.singleNoteContent.setHighlightColor(getTextHighlightBackgroundColor(requireContext(), mainColor, colorPrimary, colorAccent)); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NoteReadonlyFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NoteReadonlyFragment.java deleted file mode 100644 index 18607c90..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/NoteReadonlyFragment.java +++ /dev/null @@ -1,167 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Typeface; -import android.net.Uri; -import android.os.Bundle; -import android.text.Layout; -import android.text.SpannableString; -import android.text.method.LinkMovementMethod; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ScrollView; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceManager; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.yydcdut.markdown.MarkdownProcessor; -import com.yydcdut.markdown.syntax.text.TextFactory; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; -import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding; -import it.niedermann.owncloud.notes.model.ISyncCallback; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; -import it.niedermann.owncloud.notes.util.MarkDownUtil; -import it.niedermann.owncloud.notes.util.NoteLinksUtils; - -import static androidx.core.view.ViewCompat.isAttachedToWindow; -import static it.niedermann.owncloud.notes.util.DisplayUtils.searchAndColor; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.parseCompat; -import static it.niedermann.owncloud.notes.util.NoteUtil.getFontSizeFromPreferences; - -public class NoteReadonlyFragment extends SearchableBaseNoteFragment { - - private MarkdownProcessor markdownProcessor; - - private FragmentNotePreviewBinding binding; - - public static NoteReadonlyFragment newInstance(String content) { - NoteReadonlyFragment f = new NoteReadonlyFragment(); - Bundle b = new Bundle(); - b.putString(PARAM_CONTENT, content); - f.setArguments(b); - return f; - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.menu_favorite).setVisible(false); - menu.findItem(R.id.menu_edit).setVisible(false); - menu.findItem(R.id.menu_preview).setVisible(false); - menu.findItem(R.id.menu_cancel).setVisible(false); - menu.findItem(R.id.menu_delete).setVisible(false); - menu.findItem(R.id.menu_share).setVisible(false); - menu.findItem(R.id.menu_move).setVisible(false); - menu.findItem(R.id.menu_category).setVisible(false); - if (menu.findItem(MENU_ID_PIN) != null) - menu.findItem(MENU_ID_PIN).setVisible(false); - } - - @Override - public ScrollView getScrollView() { - return binding.scrollView; - } - - @Override - protected FloatingActionButton getSearchNextButton() { - return binding.searchNext; - } - - @Override - protected FloatingActionButton getSearchPrevButton() { - return binding.searchPrev; - } - - @Override - protected Layout getLayout() { - binding.singleNoteContent.onPreDraw(); - return binding.singleNoteContent.getLayout(); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup - container, @Nullable Bundle savedInstanceState) { - binding = FragmentNotePreviewBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - markdownProcessor = new MarkdownProcessor(requireActivity()); - markdownProcessor.factory(TextFactory.create()); - markdownProcessor.config( - MarkDownUtil.getMarkDownConfiguration(binding.singleNoteContent.getContext()) - .setOnLinkClickCallback((view, link) -> { - if (NoteLinksUtils.isNoteLink(link)) { - long noteRemoteId = NoteLinksUtils.extractNoteRemoteId(link); - long noteLocalId = db.getLocalIdByRemoteId(this.note.getAccountId(), noteRemoteId); - Intent intent = new Intent(requireActivity().getApplicationContext(), EditNoteActivity.class); - intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, noteLocalId); - startActivity(intent); - } else { - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); - startActivity(browserIntent); - } - }) - .build()); - try { - binding.singleNoteContent.setText(parseCompat(markdownProcessor, note.getContent())); - onResume(); - } catch (StringIndexOutOfBoundsException e) { - // Workaround for RxMarkdown: https://github.com/stefan-niedermann/nextcloud-notes/issues/668 - binding.singleNoteContent.setText(note.getContent()); - Toast.makeText(binding.singleNoteContent.getContext(), R.string.could_not_load_preview_two_digit_numbered_list, Toast.LENGTH_LONG).show(); - e.printStackTrace(); - } - binding.singleNoteContent.setMovementMethod(LinkMovementMethod.getInstance()); - - db = NotesDatabase.getInstance(getActivity()); - binding.swiperefreshlayout.setEnabled(false); - - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(requireActivity().getApplicationContext()); - binding.singleNoteContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp)); - if (sp.getBoolean(getString(R.string.pref_key_font), false)) { - binding.singleNoteContent.setTypeface(Typeface.MONOSPACE); - } - } - - @Override - public void onCloseNote() { - // Do nothing - } - - @Override - protected void saveNote(@Nullable ISyncCallback callback) { - // Do nothing - } - - @Override - protected void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor) { - if ((binding != null) && isAttachedToWindow(binding.singleNoteContent)) { - binding.singleNoteContent.setText(searchAndColor(new SpannableString(parseCompat(markdownProcessor, getContent())), newText, requireContext(), current, mainColor, textColor), TextView.BufferType.SPANNABLE); - } - } - - @Override - protected String getContent() { - return note.getContent(); - } - - @Override - public void applyBrand(int mainColor, int textColor) { - super.applyBrand(mainColor, textColor); - binding.singleNoteContent.setHighlightColor(getTextHighlightBackgroundColor(requireContext(), mainColor, colorPrimary, colorAccent)); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/PreferencesFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/PreferencesFragment.java deleted file mode 100644 index ff9561e6..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/PreferencesFragment.java +++ /dev/null @@ -1,138 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.app.Activity; -import android.content.Context; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.ColorInt; -import androidx.annotation.Nullable; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.DarkModeSetting; -import it.niedermann.owncloud.notes.branding.Branded; -import it.niedermann.owncloud.notes.branding.BrandedSwitchPreference; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.persistence.SyncWorker; -import it.niedermann.owncloud.notes.util.DeviceCredentialUtil; -import it.niedermann.owncloud.notes.util.Notes; - -import static it.niedermann.owncloud.notes.android.appwidget.NoteListWidget.updateNoteListWidgets; - -public class PreferencesFragment extends PreferenceFragmentCompat implements Branded { - - private static final String TAG = PreferencesFragment.class.getSimpleName(); - - private BrandedSwitchPreference fontPref; - private BrandedSwitchPreference lockPref; - private BrandedSwitchPreference wifiOnlyPref; - private BrandedSwitchPreference brandingPref; - private BrandedSwitchPreference gridViewPref; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences); - - fontPref = findPreference(getString(R.string.pref_key_font)); - - brandingPref = findPreference(getString(R.string.pref_key_branding)); - if (brandingPref != null) { - brandingPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { - updateNoteListWidgets(requireContext()); - final Boolean branding = (Boolean) newValue; - Log.v(TAG, "branding: " + branding); - requireActivity().setResult(Activity.RESULT_OK); - requireActivity().recreate(); - return true; - }); - } else { - Log.e(TAG, "Could not find preference with key: \"" + getString(R.string.pref_key_branding) + "\""); - } - - gridViewPref = findPreference(getString(R.string.pref_key_gridview)); - if (gridViewPref != null) { - gridViewPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { - final Boolean gridView = (Boolean) newValue; - Log.v(TAG, "gridView: " + gridView); - requireActivity().setResult(Activity.RESULT_OK); - Notes.updateGridViewEnabled(gridView); - return true; - }); - } else { - Log.e(TAG, "Could not find preference with key: \"" + getString(R.string.pref_key_branding) + "\""); - } - - lockPref = findPreference(getString(R.string.pref_key_lock)); - if (lockPref != null) { - if (!DeviceCredentialUtil.areCredentialsAvailable(requireContext())) { - lockPref.setVisible(false); - Preference securityCategory = findPreference(getString(R.string.pref_category_security)); - if (securityCategory != null) { - securityCategory.setVisible(false); - } else { - Log.e(TAG, "Could not find preference " + getString(R.string.pref_category_security)); - } - } else { - lockPref.setOnPreferenceChangeListener((preference, newValue) -> { - Notes.setLockedPreference((Boolean) newValue); - return true; - }); - } - } else { - Log.e(TAG, "Could not find \"" + getString(R.string.pref_key_lock) + "\"-preference."); - } - - final ListPreference themePref = findPreference(getString(R.string.pref_key_theme)); - assert themePref != null; - themePref.setOnPreferenceChangeListener((preference, newValue) -> { - Notes.setAppTheme(DarkModeSetting.valueOf((String) newValue)); - requireActivity().setResult(Activity.RESULT_OK); - requireActivity().recreate(); - return true; - }); - - wifiOnlyPref = findPreference(getString(R.string.pref_key_wifi_only)); - assert wifiOnlyPref != null; - wifiOnlyPref.setOnPreferenceChangeListener((preference, newValue) -> { - Log.i(TAG, "syncOnWifiOnly: " + newValue); - return true; - }); - - final ListPreference syncPref = findPreference(getString(R.string.pref_key_background_sync)); - assert syncPref != null; - syncPref.setOnPreferenceChangeListener((preference, newValue) -> { - Log.i(TAG, "syncPref: " + preference + " - newValue: " + newValue); - SyncWorker.update(requireContext(), newValue.toString()); - return true; - }); - } - - - @Override - public void onStart() { - super.onStart(); - @Nullable Context context = getContext(); - if (context != null) { - @ColorInt final int mainColor = BrandingUtil.readBrandMainColor(context); - @ColorInt final int textColor = BrandingUtil.readBrandTextColor(context); - applyBrand(mainColor, textColor); - } - } - - @Override - public void applyBrand(int mainColor, int textColor) { - fontPref.applyBrand(mainColor, textColor); - lockPref.applyBrand(mainColor, textColor); - wifiOnlyPref.applyBrand(mainColor, textColor); - brandingPref.applyBrand(mainColor, textColor); - gridViewPref.applyBrand(mainColor, textColor); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/SearchableBaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/SearchableBaseNoteFragment.java deleted file mode 100644 index 3aa2da79..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/SearchableBaseNoteFragment.java +++ /dev/null @@ -1,302 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment; - -import android.graphics.Color; -import android.os.Bundle; -import android.os.Handler; -import android.text.Layout; -import android.text.TextUtils; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewTreeObserver; -import android.widget.LinearLayout; -import android.widget.ScrollView; - -import androidx.annotation.CallSuper; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.SearchView; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandedActivity; - -public abstract class SearchableBaseNoteFragment extends BaseNoteFragment { - - private static final String TAG = SearchableBaseNoteFragment.class.getSimpleName(); - private static final String saved_instance_key_searchQuery = "searchQuery"; - private static final String saved_instance_key_currentOccurrence = "currentOccurrence"; - - private int currentOccurrence = 1; - private int occurrenceCount = 0; - private SearchView searchView; - private String searchQuery = null; - private static final int delay = 50; // If the search string does not change after $delay ms, then the search task starts. - - @ColorInt - private int mainColor; - @ColorInt - private int textColor; - - @Override - public void onStart() { - this.mainColor = getResources().getColor(R.color.defaultBrand); - this.textColor = Color.WHITE; - super.onStart(); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - if (savedInstanceState != null) { - searchQuery = savedInstanceState.getString(saved_instance_key_searchQuery, ""); - currentOccurrence = savedInstanceState.getInt(saved_instance_key_currentOccurrence, 1); - } - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - - MenuItem searchMenuItem = menu.findItem(R.id.search); - searchView = (SearchView) searchMenuItem.getActionView(); - - if (!TextUtils.isEmpty(searchQuery) && isNew) { - searchMenuItem.expandActionView(); - searchView.setQuery(searchQuery, true); - searchView.clearFocus(); - } else { - searchMenuItem.collapseActionView(); - } - - - final LinearLayout searchEditFrame = searchView.findViewById(R.id - .search_edit_frame); - - searchEditFrame.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - int oldVisibility = -1; - - @Override - public void onGlobalLayout() { - int currentVisibility = searchEditFrame.getVisibility(); - - if (currentVisibility != oldVisibility) { - if (currentVisibility != View.VISIBLE) { - colorWithText("", null, mainColor, textColor); - searchQuery = ""; - hideSearchFabs(); - } else { - jumpToOccurrence(); - colorWithText(searchQuery, null, mainColor, textColor); - occurrenceCount = countOccurrences(getContent(), searchQuery); - showSearchFabs(); - } - - oldVisibility = currentVisibility; - } - } - - }); - - FloatingActionButton next = getSearchNextButton(); - FloatingActionButton prev = getSearchPrevButton(); - - if (next != null) { - next.setOnClickListener(v -> { - currentOccurrence++; - jumpToOccurrence(); - colorWithText(searchView.getQuery().toString(), currentOccurrence, mainColor, textColor); - }); - } - - if (prev != null) { - prev.setOnClickListener(v -> { - occurrenceCount = countOccurrences(getContent(), searchView.getQuery().toString()); - currentOccurrence--; - jumpToOccurrence(); - colorWithText(searchView.getQuery().toString(), currentOccurrence, mainColor, textColor); - }); - } - - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - private DelayQueryRunnable delayQueryTask; - private Handler handler = new Handler(); - - @Override - public boolean onQueryTextSubmit(@NonNull String query) { - currentOccurrence++; - jumpToOccurrence(); - colorWithText(query, currentOccurrence, mainColor, textColor); - return true; - } - - @Override - public boolean onQueryTextChange(@NonNull String newText) { - queryWithHandler(newText); - return true; - } - - private void queryMatch(@NonNull String newText) { - searchQuery = newText; - occurrenceCount = countOccurrences(getContent(), searchQuery); - if (occurrenceCount > 1) { - showSearchFabs(); - } else { - hideSearchFabs(); - } - currentOccurrence = 1; - jumpToOccurrence(); - colorWithText(searchQuery, currentOccurrence, mainColor, textColor); - } - - private void queryWithHandler(@NonNull String newText) { - if (delayQueryTask != null) { - delayQueryTask.cancel(); - handler.removeCallbacksAndMessages(null); - } - delayQueryTask = new DelayQueryRunnable(newText); - // If there is only one char in the search pattern, we should start the search immediately. - handler.postDelayed(delayQueryTask, newText.length() > 1 ? delay : 0); - } - - class DelayQueryRunnable implements Runnable { - private String text; - private boolean canceled = false; - - public DelayQueryRunnable(String text) { - this.text = text; - } - - @Override - public void run() { - if (canceled) { - return; - } - queryMatch(text); - } - - public void cancel() { - canceled = true; - } - } - }); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - - if (searchView != null && !TextUtils.isEmpty(searchView.getQuery().toString())) { - outState.putString(saved_instance_key_searchQuery, searchView.getQuery().toString()); - outState.putInt(saved_instance_key_currentOccurrence, currentOccurrence); - } - } - - protected abstract void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor); - - protected abstract Layout getLayout(); - - protected abstract FloatingActionButton getSearchNextButton(); - - protected abstract FloatingActionButton getSearchPrevButton(); - - private void showSearchFabs() { - FloatingActionButton next = getSearchNextButton(); - FloatingActionButton prev = getSearchPrevButton(); - if (prev != null) { - prev.show(); - } - if (next != null) { - next.show(); - } - } - - private void hideSearchFabs() { - FloatingActionButton next = getSearchNextButton(); - FloatingActionButton prev = getSearchPrevButton(); - if (prev != null) { - prev.hide(); - } - if (next != null) { - next.hide(); - } - } - - private void jumpToOccurrence() { - Layout layout = getLayout(); - if (layout == null) { - Log.w(TAG, "getLayout() is null"); - } else if (getContent() == null || getContent().isEmpty()) { - Log.w(TAG, "getContent is null or empty"); - } else if (currentOccurrence < 1) { - // if currentOccurrence is lower than 1, jump to last occurrence - currentOccurrence = occurrenceCount; - jumpToOccurrence(); - } else if (searchQuery != null && !searchQuery.isEmpty()) { - String currentContent = getContent().toLowerCase(); - int indexOfNewText = indexOfNth(currentContent, searchQuery.toLowerCase(), 0, currentOccurrence); - if (indexOfNewText <= 0) { - // Search term is not n times in text - // Go back to first search result - if (currentOccurrence != 1) { - currentOccurrence = 1; - jumpToOccurrence(); - } - return; - } - String textUntilFirstOccurrence = currentContent.substring(0, indexOfNewText); - int numberLine = layout.getLineForOffset(textUntilFirstOccurrence.length()); - - if (numberLine >= 0) { - ScrollView scrollView = getScrollView(); - if (scrollView != null) { - scrollView.post(() -> scrollView.smoothScrollTo(0, layout.getLineTop(numberLine))); - } - } - } - } - - private static int indexOfNth(String input, String value, int startIndex, int nth) { - if (nth < 1) - throw new IllegalArgumentException("Param 'nth' must be greater than 0!"); - if (nth == 1) - return input.indexOf(value, startIndex); - int idx = input.indexOf(value, startIndex); - if (idx == -1) - return -1; - return indexOfNth(input, value, idx + 1, nth - 1); - } - - private static int countOccurrences(String haystack, String needle) { - if (haystack == null || haystack.isEmpty() || needle == null || needle.isEmpty()) { - return 0; - } - // Use regrex which is faster before. - // Such that the main thread will not stop for a long tilme - // And so there will not an ANR problem - Matcher m = Pattern.compile(needle, Pattern.CASE_INSENSITIVE | Pattern.LITERAL) - .matcher(haystack); - - int count = 0; - while (m.find()) { - count++; - } - return count; - } - - @CallSuper - @Override - public void applyBrand(int mainColor, int textColor) { - this.mainColor = mainColor; - this.textColor = textColor; - BrandedActivity.applyBrandToFAB(mainColor, textColor, getSearchPrevButton()); - BrandedActivity.applyBrandToFAB(mainColor, textColor, getSearchNextButton()); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentContributingTab.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentContributingTab.java deleted file mode 100644 index 21c6e2e7..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentContributingTab.java +++ /dev/null @@ -1,25 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment.about; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.FragmentAboutContributionTabBinding; -import it.niedermann.owncloud.notes.util.SupportUtil; - -public class AboutFragmentContributingTab extends Fragment { - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - FragmentAboutContributionTabBinding b = FragmentAboutContributionTabBinding.inflate(inflater, container, false); - SupportUtil.setHtml(b.aboutSource, R.string.about_source, getString(R.string.url_source)); - SupportUtil.setHtml(b.aboutIssues, R.string.about_issues, getString(R.string.url_issues)); - SupportUtil.setHtml(b.aboutTranslate, R.string.about_translate, getString(R.string.url_translations)); - return b.getRoot(); - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentCreditsTab.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentCreditsTab.java deleted file mode 100644 index 52aadbbb..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentCreditsTab.java +++ /dev/null @@ -1,26 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment.about; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; - -import it.niedermann.owncloud.notes.BuildConfig; -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.FragmentAboutCreditsTabBinding; -import it.niedermann.owncloud.notes.util.SupportUtil; - -public class AboutFragmentCreditsTab extends Fragment { - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - FragmentAboutCreditsTabBinding binding = FragmentAboutCreditsTabBinding.inflate(inflater, container, false); - SupportUtil.setHtml(binding.aboutVersion, R.string.about_version, "v" + BuildConfig.VERSION_NAME); - SupportUtil.setHtml(binding.aboutMaintainer, R.string.about_maintainer); - SupportUtil.setHtml(binding.aboutTranslators, R.string.about_translators_transifex, getString(R.string.url_translations)); - return binding.getRoot(); - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentLicenseTab.java b/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentLicenseTab.java deleted file mode 100644 index b5b984cd..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/fragment/about/AboutFragmentLicenseTab.java +++ /dev/null @@ -1,44 +0,0 @@ -package it.niedermann.owncloud.notes.android.fragment.about; - -import android.content.Intent; -import android.content.res.ColorStateList; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.core.graphics.drawable.DrawableCompat; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandedFragment; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.databinding.FragmentAboutLicenseTabBinding; -import it.niedermann.owncloud.notes.util.ColorUtil; -import it.niedermann.owncloud.notes.util.SupportUtil; - -public class AboutFragmentLicenseTab extends BrandedFragment { - - private FragmentAboutLicenseTabBinding binding; - - private void openLicense() { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_license)))); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentAboutLicenseTabBinding.inflate(inflater, container, false); - binding.aboutAppLicenseButton.setOnClickListener((v) -> openLicense()); - SupportUtil.setHtml(binding.aboutIconsDisclaimer, R.string.about_icons_disclaimer, getString(R.string.about_app_icon_author)); - return binding.getRoot(); - } - - @Override - public void applyBrand(int mainColor, int textColor) { - @ColorInt final int finalMainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor); - DrawableCompat.setTintList(binding.aboutAppLicenseButton.getBackground(), ColorStateList.valueOf(finalMainColor)); - binding.aboutAppLicenseButton.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor)); - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/android/quicksettings/NewNoteTileService.java b/app/src/main/java/it/niedermann/owncloud/notes/android/quicksettings/NewNoteTileService.java deleted file mode 100644 index ca101c88..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/android/quicksettings/NewNoteTileService.java +++ /dev/null @@ -1,34 +0,0 @@ -package it.niedermann.owncloud.notes.android.quicksettings; - -import android.annotation.TargetApi; -import android.content.Intent; -import android.os.Build; -import android.service.quicksettings.Tile; -import android.service.quicksettings.TileService; - -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; - -/** - * This {@link TileService} adds a quick settings tile that leads to the new note view. - */ -@TargetApi(Build.VERSION_CODES.N) -public class NewNoteTileService extends TileService { - - @Override - public void onStartListening() { - Tile tile = getQsTile(); - tile.setState(Tile.STATE_ACTIVE); - - tile.updateTile(); - } - - @Override - public void onClick() { - // create new note intent - final Intent newNoteIntent = new Intent(getApplicationContext(), EditNoteActivity.class); - // ensure it won't open twice if already running - newNoteIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - // ask to unlock the screen if locked, then start new note intent - unlockAndRun(() -> startActivityAndCollapse(newNoteIntent)); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java index 42b9c5cb..b4c534f6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java @@ -9,7 +9,7 @@ import androidx.annotation.StringRes; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; -import it.niedermann.owncloud.notes.util.ColorUtil; +import it.niedermann.owncloud.notes.shared.util.ColorUtil; public class BrandedSnackbar { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java index 878917fa..d22fb4a3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java @@ -17,9 +17,9 @@ import androidx.core.graphics.drawable.DrawableCompat; import androidx.preference.PreferenceManager; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.util.Notes; +import it.niedermann.owncloud.notes.NotesApplication; -import static it.niedermann.owncloud.notes.util.ColorUtil.contrastRatioIsSufficient; +import static it.niedermann.owncloud.notes.shared.util.ColorUtil.contrastRatioIsSufficient; public class BrandingUtil { @@ -81,7 +81,7 @@ public class BrandingUtil { @ColorInt public static int getSecondaryForegroundColorDependingOnTheme(@NonNull Context context, @ColorInt int mainColor) { final int primaryColor = context.getResources().getColor(R.color.primary); - final boolean isDarkTheme = Notes.isDarkThemeActive(context); + final boolean isDarkTheme = NotesApplication.isDarkThemeActive(context); if (isDarkTheme && !contrastRatioIsSufficient(mainColor, primaryColor)) { Log.v(TAG, "Contrast ratio between brand color " + String.format("#%06X", (0xFFFFFF & mainColor)) + " and dark theme is too low. Falling back to WHITE as brand color."); return Color.WHITE; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java new file mode 100644 index 00000000..f9579c86 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java @@ -0,0 +1,413 @@ +package it.niedermann.owncloud.notes.edit; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.Color; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ScrollView; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +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.owncloud.notes.R; +import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment; +import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment; +import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment; +import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment.CategoryDialogListener; +import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment.EditTitleListener; +import it.niedermann.owncloud.notes.branding.BrandedFragment; +import it.niedermann.owncloud.notes.shared.model.ApiVersion; +import it.niedermann.owncloud.notes.shared.model.CloudNote; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.DBStatus; +import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.shared.util.ColorUtil; +import it.niedermann.owncloud.notes.shared.util.NoteUtil; +import it.niedermann.owncloud.notes.shared.util.ShareUtil; + +import static androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported; +import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT; +import static it.niedermann.owncloud.notes.branding.BrandingUtil.tintMenuIcon; +import static it.niedermann.owncloud.notes.shared.util.ColorUtil.isColorDark; +import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive; + +public abstract class BaseNoteFragment extends BrandedFragment implements CategoryDialogListener, EditTitleListener { + + private static final String TAG = BaseNoteFragment.class.getSimpleName(); + + protected static final int MENU_ID_PIN = -1; + public static final String PARAM_NOTE_ID = "noteId"; + public static final String PARAM_ACCOUNT_ID = "accountId"; + public static final String PARAM_CONTENT = "content"; + public static final String PARAM_NEWNOTE = "newNote"; + private static final String SAVEDKEY_NOTE = "note"; + private static final String SAVEDKEY_ORIGINAL_NOTE = "original_note"; + + private LocalAccount localAccount; + private SingleSignOnAccount ssoAccount; + + protected DBNote note; + // TODO do we really need this? The reference to note is currently the same + @Nullable + private DBNote originalNote; + private int originalScrollY; + protected NotesDatabase db; + private NoteFragmentListener listener; + private boolean titleModified = false; + + protected boolean isNew = true; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + this.ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireActivity().getApplicationContext()); + this.localAccount = db.getLocalAccountByAccountName(ssoAccount.name); + + if (savedInstanceState == null) { + long id = requireArguments().getLong(PARAM_NOTE_ID); + if (id > 0) { + long accountId = requireArguments().getLong(PARAM_ACCOUNT_ID); + if (accountId > 0) { + /* Switch account if account id has been provided */ + this.localAccount = db.getAccount(accountId); + SingleAccountHelper.setCurrentAccount(requireActivity().getApplicationContext(), localAccount.getAccountName()); + } + isNew = false; + note = originalNote = db.getNote(localAccount.getId(), id); + } else { + CloudNote cloudNote = (CloudNote) requireArguments().getSerializable(PARAM_NEWNOTE); + String content = requireArguments().getString(PARAM_CONTENT); + if (cloudNote == null) { + if (content == null) { + throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given, argument " + PARAM_NEWNOTE + " is missing and " + PARAM_CONTENT + " is missing."); + } else { + note = new DBNote(-1, -1, null, NoteUtil.generateNoteTitle(content), content, false, getString(R.string.category_readonly), null, DBStatus.VOID, -1, "", 0); + } + } else { + note = db.getNote(localAccount.getId(), db.addNoteAndSync(ssoAccount, localAccount.getId(), cloudNote)); + originalNote = null; + } + } + } else { + note = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_NOTE); + originalNote = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_ORIGINAL_NOTE); + } + setHasOptionsMenu(true); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + e.printStackTrace(); + } + } + + @Nullable + protected abstract ScrollView getScrollView(); + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + final ScrollView scrollView = getScrollView(); + if (scrollView != null) { + scrollView.getViewTreeObserver().addOnScrollChangedListener(() -> { + if (scrollView.getScrollY() > 0) { + note.setScrollY(scrollView.getScrollY()); + } + }); + } + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + final ScrollView scrollView = getScrollView(); + if (scrollView != null) { + this.originalScrollY = note.getScrollY(); + scrollView.post(() -> scrollView.scrollTo(0, originalScrollY)); + } + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + try { + listener = (NoteFragmentListener) context; + } catch (ClassCastException e) { + throw new ClassCastException(context.getClass() + " must implement " + NoteFragmentListener.class); + } + db = NotesDatabase.getInstance(context); + } + + @Override + public void onResume() { + super.onResume(); + listener.onNoteUpdated(note); + } + + @Override + public void onPause() { + super.onPause(); + saveNote(null); + } + + @Override + public void onDetach() { + super.onDetach(); + listener = null; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + saveNote(null); + outState.putSerializable(SAVEDKEY_NOTE, note); + outState.putSerializable(SAVEDKEY_ORIGINAL_NOTE, originalNote); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.menu_note_fragment, menu); + + if (isRequestPinShortcutSupported(requireActivity()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + menu.add(Menu.NONE, MENU_ID_PIN, 110, R.string.pin_to_homescreen); + } + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + MenuItem itemFavorite = menu.findItem(R.id.menu_favorite); + prepareFavoriteOption(itemFavorite); + + menu.findItem(R.id.menu_title).setVisible(localAccount.getPreferredApiVersion() != null && localAccount.getPreferredApiVersion().compareTo(new ApiVersion("1.0", 1, 0)) >= 0); + menu.findItem(R.id.menu_delete).setVisible(!isNew); + } + + private void prepareFavoriteOption(MenuItem item) { + item.setIcon(note.isFavorite() ? R.drawable.ic_star_white_24dp : R.drawable.ic_star_border_white_24dp); + item.setChecked(note.isFavorite()); + tintMenuIcon(item, colorAccent); + } + + /** + * Main-Menu-Handler + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_cancel: + if (originalNote == null) { + db.deleteNoteAndSync(ssoAccount, note.getId()); + } else { + db.updateNoteAndSync(ssoAccount, localAccount, originalNote, null, null); + } + listener.close(); + return true; + case R.id.menu_delete: + db.deleteNoteAndSync(ssoAccount, note.getId()); + listener.close(); + return true; + case R.id.menu_favorite: + db.toggleFavorite(ssoAccount, note, null); + listener.onNoteUpdated(note); + prepareFavoriteOption(item); + return true; + case R.id.menu_category: + showCategorySelector(); + return true; + case R.id.menu_title: + showEditTitleDialog(); + return true; + case R.id.menu_move: + AccountPickerDialogFragment.newInstance(note.getAccountId()).show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName()); + return true; + case R.id.menu_share: + ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent()); + return false; + case MENU_ID_PIN: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ShortcutManager shortcutManager = requireActivity().getSystemService(ShortcutManager.class); + + if (shortcutManager != null) { + if (shortcutManager.isRequestPinShortcutSupported()) { + Intent intent = new Intent(getActivity(), EditNoteActivity.class); + intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()); + intent.setAction(ACTION_SHORTCUT); + + ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(getActivity(), note.getId() + "") + .setShortLabel(note.getTitle()) + .setIcon(Icon.createWithResource(requireActivity().getApplicationContext(), note.isFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp)) + .setIntent(intent) + .build(); + + Intent pinnedShortcutCallbackIntent = + shortcutManager.createShortcutResultIntent(pinShortcutInfo); + + PendingIntent successCallback = PendingIntent.getBroadcast(getActivity(), /* request code */ 0, + pinnedShortcutCallbackIntent, /* flags */ 0); + + shortcutManager.requestPinShortcut(pinShortcutInfo, + successCallback.getIntentSender()); + } else { + Log.i(TAG, "RequestPinShortcut is not supported"); + } + } else { + Log.e(TAG, "ShortcutManager is null"); + } + } + + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + public void onCloseNote() { + if (!titleModified && originalNote == null && getContent().isEmpty()) { + db.deleteNoteAndSync(ssoAccount, note.getId()); + } + } + + /** + * Save the current state in the database and schedule synchronization if needed. + * + * @param callback Observer which is called after save/synchronization + */ + protected void saveNote(@Nullable ISyncCallback callback) { + Log.d(TAG, "saveData()"); + if (note != null) { + String newContent = getContent(); + if (note.getContent().equals(newContent)) { + if (note.getScrollY() != originalScrollY) { + Log.v(TAG, "... only saving new scroll state, since content did not change"); + db.updateScrollY(note.getId(), note.getScrollY()); + } else { + Log.v(TAG, "... not saving, since nothing has changed"); + } + } else { + note = db.updateNoteAndSync(ssoAccount, localAccount, note, newContent, callback); + listener.onNoteUpdated(note); + requireActivity().invalidateOptionsMenu(); + } + } else { + Log.e(TAG, "note is null"); + } + } + + protected abstract String getContent(); + + /** + * Opens a dialog in order to chose a category + */ + private void showCategorySelector() { + final String fragmentId = "fragment_category"; + FragmentManager manager = requireActivity().getSupportFragmentManager(); + Fragment frag = manager.findFragmentByTag(fragmentId); + if (frag != null) { + manager.beginTransaction().remove(frag).commit(); + } + Bundle arguments = new Bundle(); + arguments.putString(CategoryDialogFragment.PARAM_CATEGORY, note.getCategory()); + arguments.putLong(CategoryDialogFragment.PARAM_ACCOUNT_ID, note.getAccountId()); + CategoryDialogFragment categoryFragment = new CategoryDialogFragment(); + categoryFragment.setArguments(arguments); + categoryFragment.setTargetFragment(this, 0); + categoryFragment.show(manager, fragmentId); + } + + /** + * Opens a dialog in order to chose a category + */ + public void showEditTitleDialog() { + saveNote(null); + final String fragmentId = "fragment_edit_title"; + FragmentManager manager = requireActivity().getSupportFragmentManager(); + Fragment frag = manager.findFragmentByTag(fragmentId); + if (frag != null) { + manager.beginTransaction().remove(frag).commit(); + } + DialogFragment editTitleFragment = EditTitleDialogFragment.newInstance(note.getTitle()); + editTitleFragment.setTargetFragment(this, 0); + editTitleFragment.show(manager, fragmentId); + } + + @Override + public void onCategoryChosen(String category) { + db.setCategory(ssoAccount, note, category, null); + listener.onNoteUpdated(note); + } + + @Override + public void onTitleEdited(String newTitle) { + titleModified = true; + note.setTitle(newTitle); + note = db.updateNoteAndSync(ssoAccount, localAccount, note, note.getContent(), newTitle, null); + listener.onNoteUpdated(note); + } + + public void moveNote(LocalAccount account) { + db.moveNoteToAnotherAccount(ssoAccount, note.getAccountId(), note, account.getId()); + listener.close(); + } + + @ColorInt + protected static int getTextHighlightBackgroundColor(@NonNull Context context, @ColorInt int mainColor, @ColorInt int colorPrimary, @ColorInt int colorAccent) { + if (isDarkThemeActive(context)) { // Dark background + if (isColorDark(mainColor)) { // Dark brand color + if (ColorUtil.contrastRatioIsSufficient(mainColor, colorPrimary)) { // But also dark text + return mainColor; + } else { + return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground); + } + } else { // Light brand color + if (ColorUtil.contrastRatioIsSufficient(mainColor, colorAccent)) { // But also dark text + return Color.argb(77, Color.red(mainColor), Color.green(mainColor), Color.blue(mainColor)); + } else { + return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground); + } + } + } else { // Light background + if (isColorDark(mainColor)) { // Dark brand color + if (ColorUtil.contrastRatioIsSufficient(mainColor, colorAccent)) { // But also dark text + return Color.argb(77, Color.red(mainColor), Color.green(mainColor), Color.blue(mainColor)); + } else { + return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground); + } + } else { // Light brand color + if (ColorUtil.contrastRatioIsSufficient(mainColor, colorPrimary)) { // But also dark text + return mainColor; + } else { + return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground); + } + } + } + } + + public interface NoteFragmentListener { + void close(); + + void onNoteUpdated(DBNote note); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java new file mode 100644 index 00000000..14fea067 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java @@ -0,0 +1,280 @@ +package it.niedermann.owncloud.notes.edit; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Calendar; +import java.util.Objects; + +import it.niedermann.owncloud.notes.LockedActivity; +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.ActivityEditBinding; +import it.niedermann.owncloud.notes.accountpicker.AccountPickerListener; +import it.niedermann.owncloud.notes.main.MainActivity; +import it.niedermann.owncloud.notes.shared.model.Category; +import it.niedermann.owncloud.notes.shared.model.CloudNote; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.util.NoteUtil; + +public class EditNoteActivity extends LockedActivity implements BaseNoteFragment.NoteFragmentListener, AccountPickerListener { + + private static final String TAG = EditNoteActivity.class.getSimpleName(); + + public static final String ACTION_SHORTCUT = "it.niedermann.owncloud.notes.shortcut"; + private static final String INTENT_GOOGLE_ASSISTANT = "com.google.android.gm.action.AUTO_SEND"; + private static final String MIMETYPE_TEXT_PLAIN = "text/plain"; + public static final String PARAM_NOTE_ID = "noteId"; + public static final String PARAM_ACCOUNT_ID = "accountId"; + public static final String PARAM_CATEGORY = "category"; + public static final String PARAM_CONTENT = "content"; + public static final String PARAM_FAVORITE = "favorite"; + + private ActivityEditBinding binding; + + private BaseNoteFragment fragment; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityEditBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + + if (savedInstanceState == null) { + launchNoteFragment(); + } else { + fragment = (BaseNoteFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container_view); + } + + setSupportActionBar(binding.toolbar); + if (!(fragment instanceof NoteReadonlyFragment)) { + binding.toolbar.setOnClickListener((v) -> fragment.showEditTitleDialog()); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Log.d(TAG, "onNewIntent: " + intent.getLongExtra(PARAM_NOTE_ID, 0)); + setIntent(intent); + if (fragment != null) { + getSupportFragmentManager().beginTransaction().detach(fragment).commit(); + fragment = null; + } + launchNoteFragment(); + } + + private long getNoteId() { + return getIntent().getLongExtra(PARAM_NOTE_ID, 0); + } + + private long getAccountId() { + return getIntent().getLongExtra(PARAM_ACCOUNT_ID, 0); + } + + /** + * Starts the note fragment for an existing note or a new note. + * The actual behavior is triggered by the activity's intent. + */ + private void launchNoteFragment() { + long noteId = getNoteId(); + if (noteId > 0) { + launchExistingNote(getAccountId(), noteId); + } else { + if (Intent.ACTION_VIEW.equals(getIntent().getAction())) { + launchReadonlyNote(); + } else { + launchNewNote(); + } + } + } + + /** + * Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note. + * The type of fragment (view-mode) is chosen based on the user preferences. + * + * @param noteId ID of the existing note. + */ + private void launchExistingNote(long accountId, long noteId) { + final String prefKeyNoteMode = getString(R.string.pref_key_note_mode); + final String prefKeyLastMode = getString(R.string.pref_key_last_note_mode); + final String prefValueEdit = getString(R.string.pref_value_mode_edit); + final String prefValuePreview = getString(R.string.pref_value_mode_preview); + final String prefValueLast = getString(R.string.pref_value_mode_last); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + String mode = preferences.getString(prefKeyNoteMode, prefValueEdit); + String lastMode = preferences.getString(prefKeyLastMode, prefValueEdit); + boolean editMode = true; + if (prefValuePreview.equals(mode) || (prefValueLast.equals(mode) && prefValuePreview.equals(lastMode))) { + editMode = false; + } + launchExistingNote(accountId, noteId, editMode); + } + + /** + * Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note. + * + * @param noteId ID of the existing note. + * @param edit View-mode of the fragment: + * true for {@link NoteEditFragment}, + * false for {@link NotePreviewFragment}. + */ + private void launchExistingNote(long accountId, long noteId, boolean edit) { + // save state of the fragment in order to resume with the same note and originalNote + Fragment.SavedState savedState = null; + if (fragment != null) { + savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment); + } + fragment = edit + ? NoteEditFragment.newInstance(accountId, noteId) + : NotePreviewFragment.newInstance(accountId, noteId); + + if (savedState != null) { + fragment.setInitialSavedState(savedState); + } + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit(); + } + + /** + * Starts the {@link NoteEditFragment} with a new note. + * Content ("share" functionality), category and favorite attribute can be preset. + */ + private void launchNewNote() { + Intent intent = getIntent(); + + String category = null; + boolean favorite = false; + if (intent.hasExtra(PARAM_CATEGORY)) { + Category categoryPreselection = (Category) Objects.requireNonNull(intent.getSerializableExtra(PARAM_CATEGORY)); + category = categoryPreselection.category; + favorite = categoryPreselection.favorite != null ? categoryPreselection.favorite : false; + } + + String content = ""; + if ( + intent.hasExtra(Intent.EXTRA_TEXT) && + MIMETYPE_TEXT_PLAIN.equals(intent.getType()) && + (Intent.ACTION_SEND.equals(intent.getAction()) || + INTENT_GOOGLE_ASSISTANT.equals(intent.getAction())) + ) { + content = intent.getStringExtra(Intent.EXTRA_TEXT); + } else if (intent.hasExtra(PARAM_CONTENT)) { + content = intent.getStringExtra(PARAM_CONTENT); + } + + if (content == null) { + content = ""; + } + CloudNote newNote = new CloudNote(0, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, favorite, category, null); + fragment = NoteEditFragment.newInstanceWithNewNote(newNote); + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit(); + } + + private void launchReadonlyNote() { + Intent intent = getIntent(); + StringBuilder content = new StringBuilder(); + try { + InputStream inputStream = getContentResolver().openInputStream(Objects.requireNonNull(intent.getData())); + BufferedReader r = new BufferedReader(new InputStreamReader(Objects.requireNonNull(inputStream))); + String line; + while ((line = r.readLine()) != null) { + content.append(line).append('\n'); + } + } catch (IOException e) { + e.printStackTrace(); + } + + fragment = NoteReadonlyFragment.newInstance(content.toString()); + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit(); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + close(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_note_activity, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + close(); + return true; + case R.id.menu_preview: + launchExistingNote(getAccountId(), getNoteId(), false); + return true; + case R.id.menu_edit: + launchExistingNote(getAccountId(), getNoteId(), true); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + + /** + * Send result and closes the Activity + */ + public void close() { + /* TODO enhancement: store last mode in note + * for cross device functionality per note mode should be stored on the server. + */ + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + final String prefKeyLastMode = getString(R.string.pref_key_last_note_mode); + if (fragment instanceof NoteEditFragment) { + preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_edit)).apply(); + } else { + preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_preview)).apply(); + } + fragment.onCloseNote(); + finish(); + } + + @Override + public void onNoteUpdated(DBNote note) { + if (note != null) { + binding.toolbar.setTitle(note.getTitle()); + if (note.getCategory().isEmpty()) { + binding.toolbar.setSubtitle(null); + } else { + binding.toolbar.setSubtitle(NoteUtil.extendCategory(note.getCategory())); + } + } else { + // Maybe account is not authenticated -> note == null + Log.e(TAG, "note is null, start " + MainActivity.class.getSimpleName()); + startActivity(new Intent(this, MainActivity.class)); + finish(); + } + } + + @Override + public void onAccountPicked(@NonNull LocalAccount account) { + fragment.moveNote(account); + } + + @Override + public void applyBrand(int mainColor, int textColor) { + applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar); + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java new file mode 100644 index 00000000..ba297b95 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java @@ -0,0 +1,258 @@ +package it.niedermann.owncloud.notes.edit; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.Layout; +import android.text.SpannableString; +import android.text.TextWatcher; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.yydcdut.markdown.MarkdownProcessor; +import com.yydcdut.markdown.syntax.edit.EditFactory; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.FragmentNoteEditBinding; +import it.niedermann.owncloud.notes.shared.model.CloudNote; +import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.shared.util.MarkDownUtil; +import it.niedermann.owncloud.notes.edit.format.ContextBasedFormattingCallback; +import it.niedermann.owncloud.notes.edit.format.ContextBasedRangeFormattingCallback; + +import static androidx.core.view.ViewCompat.isAttachedToWindow; +import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.searchAndColor; +import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences; + +public class NoteEditFragment extends SearchableBaseNoteFragment { + + private static final String TAG = NoteEditFragment.class.getSimpleName(); + + private static final String LOG_TAG_AUTOSAVE = "AutoSave"; + + private static final long DELAY = 2000; // Wait for this time after typing before saving + private static final long DELAY_AFTER_SYNC = 5000; // Wait for this time after saving before checking for next save + + private FragmentNoteEditBinding binding; + + private Handler handler; + private boolean saveActive; + private boolean unsavedEdit; + private final Runnable runAutoSave = new Runnable() { + @Override + public void run() { + if (unsavedEdit) { + Log.d(LOG_TAG_AUTOSAVE, "runAutoSave: start AutoSave"); + autoSave(); + } else { + Log.d(LOG_TAG_AUTOSAVE, "runAutoSave: nothing changed"); + } + } + }; + private TextWatcher textWatcher; + + public static NoteEditFragment newInstance(long accountId, long noteId) { + NoteEditFragment f = new NoteEditFragment(); + Bundle b = new Bundle(); + b.putLong(PARAM_NOTE_ID, noteId); + b.putLong(PARAM_ACCOUNT_ID, accountId); + f.setArguments(b); + return f; + } + + public static NoteEditFragment newInstanceWithNewNote(CloudNote newNote) { + NoteEditFragment f = new NoteEditFragment(); + Bundle b = new Bundle(); + b.putSerializable(PARAM_NEWNOTE, newNote); + f.setArguments(b); + return f; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + handler = new Handler(Looper.getMainLooper()); + } + + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + menu.findItem(R.id.menu_edit).setVisible(false); + menu.findItem(R.id.menu_preview).setVisible(true); + } + + @Override + public ScrollView getScrollView() { + return binding.scrollView; + } + + @Override + protected Layout getLayout() { + binding.editContent.onPreDraw(); + return binding.editContent.getLayout(); + } + + @Override + protected FloatingActionButton getSearchNextButton() { + return binding.searchNext; + } + + @Override + protected FloatingActionButton getSearchPrevButton() { + return binding.searchPrev; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + binding = FragmentNoteEditBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + textWatcher = new NotesTextWatcher(binding.editContent) { + @Override + public void afterTextChanged(final Editable s) { + super.afterTextChanged(s); + unsavedEdit = true; + if (!saveActive) { + handler.removeCallbacks(runAutoSave); + handler.postDelayed(runAutoSave, DELAY); + } + } + }; + + if (note != null) { + if (note.getContent().isEmpty()) { + binding.editContent.requestFocus(); + + requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + + final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(getView(), InputMethodManager.SHOW_IMPLICIT); + } else { + Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); + } + } + + // workaround for issue yydcdut/RxMarkdown#41 + note.setContent(note.getContent().replace("\r\n", "\n")); + + binding.editContent.setText(note.getContent()); + binding.editContent.setEnabled(true); + + final MarkdownProcessor markdownProcessor = new MarkdownProcessor(requireContext()); + markdownProcessor.config(MarkDownUtil.getMarkDownConfiguration(binding.editContent.getContext()).build()); + markdownProcessor.factory(EditFactory.create()); + markdownProcessor.live(binding.editContent); + + binding.editContent.setCustomSelectionActionModeCallback(new ContextBasedRangeFormattingCallback(binding.editContent)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + binding.editContent.setCustomInsertionActionModeCallback(new ContextBasedFormattingCallback(binding.editContent)); + } + final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(requireContext().getApplicationContext()); + binding.editContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp)); + if (sp.getBoolean(getString(R.string.pref_key_font), false)) { + binding.editContent.setTypeface(Typeface.MONOSPACE); + } + } + } + + @Override + public void onResume() { + super.onResume(); + binding.editContent.addTextChangedListener(textWatcher); + } + + @Override + public void onPause() { + super.onPause(); + binding.editContent.removeTextChangedListener(textWatcher); + cancelTimers(); + } + + private void cancelTimers() { + handler.removeCallbacks(runAutoSave); + } + + /** + * Gets the current content of the EditText field in the UI. + * + * @return String of the current content. + */ + @Override + protected String getContent() { + return binding.editContent.getText().toString(); + } + + @Override + protected void saveNote(@Nullable ISyncCallback callback) { + super.saveNote(callback); + unsavedEdit = false; + } + + /** + * Saves the current changes and show the status in the ActionBar + */ + private void autoSave() { + Log.d(LOG_TAG_AUTOSAVE, "STARTAUTOSAVE"); + saveActive = true; + saveNote(new ISyncCallback() { + @Override + public void onFinish() { + onSaved(); + } + + @Override + public void onScheduled() { + onSaved(); + } + + private void onSaved() { + // AFTER SYNCHRONIZATION + Log.d(LOG_TAG_AUTOSAVE, "FINISHED AUTOSAVE"); + saveActive = false; + + // AFTER "DELAY_AFTER_SYNC" SECONDS: allow next auto-save or start it directly + handler.postDelayed(runAutoSave, DELAY_AFTER_SYNC); + + } + }); + } + + @Override + protected void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor) { + if (binding != null && isAttachedToWindow(binding.editContent)) { + binding.editContent.clearFocus(); + binding.editContent.setText(searchAndColor(new SpannableString(getContent()), newText, requireContext(), current, mainColor, textColor), TextView.BufferType.SPANNABLE); + } + } + + @Override + public void applyBrand(int mainColor, int textColor) { + super.applyBrand(mainColor, textColor); + binding.editContent.setHighlightColor(getTextHighlightBackgroundColor(requireContext(), mainColor, colorPrimary, colorAccent)); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java new file mode 100644 index 00000000..1eb90c43 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java @@ -0,0 +1,230 @@ +package it.niedermann.owncloud.notes.edit; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; +import android.text.Layout; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.preference.PreferenceManager; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +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 com.yydcdut.markdown.MarkdownProcessor; +import com.yydcdut.markdown.syntax.text.TextFactory; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.shared.util.MarkDownUtil; +import it.niedermann.owncloud.notes.shared.util.NoteLinksUtils; +import it.niedermann.owncloud.notes.shared.util.SSOUtil; + +import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.searchAndColor; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_CHECKED_MINUS; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_CHECKED_STAR; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_UNCHECKED_MINUS; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_UNCHECKED_STAR; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.parseCompat; +import static it.niedermann.owncloud.notes.shared.util.NoteLinksUtils.extractNoteRemoteId; +import static it.niedermann.owncloud.notes.shared.util.NoteLinksUtils.replaceNoteLinksWithDummyUrls; +import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences; + +public class NotePreviewFragment extends SearchableBaseNoteFragment implements OnRefreshListener { + + private String changedText; + + private MarkdownProcessor markdownProcessor; + + private FragmentNotePreviewBinding binding; + + public static NotePreviewFragment newInstance(long accountId, long noteId) { + NotePreviewFragment f = new NotePreviewFragment(); + Bundle b = new Bundle(); + b.putLong(PARAM_NOTE_ID, noteId); + b.putLong(PARAM_ACCOUNT_ID, accountId); + f.setArguments(b); + return f; + } + + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + menu.findItem(R.id.menu_edit).setVisible(true); + menu.findItem(R.id.menu_preview).setVisible(false); + } + + @Override + public ScrollView getScrollView() { + return binding.scrollView; + } + + @Override + protected FloatingActionButton getSearchNextButton() { + return binding.searchNext; + } + + @Override + protected FloatingActionButton getSearchPrevButton() { + return binding.searchPrev; + } + + @Override + protected Layout getLayout() { + binding.singleNoteContent.onPreDraw(); + return binding.singleNoteContent.getLayout(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup + container, @Nullable Bundle savedInstanceState) { + binding = FragmentNotePreviewBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + markdownProcessor = new MarkdownProcessor(requireContext()); + markdownProcessor.factory(TextFactory.create()); + markdownProcessor.config( + MarkDownUtil.getMarkDownConfiguration(binding.singleNoteContent.getContext()) + .setOnTodoClickCallback((view, line, lineNumber) -> { + try { + String[] lines = TextUtils.split(note.getContent(), "\\r?\\n"); + /* + * Workaround for RxMarkdown-bug: + * When (un)checking a checkbox in a note which contains code-blocks, the "`"-characters get stripped out in the TextView and therefore the given lineNumber is wrong + * Find number of lines starting with ``` before lineNumber + */ + boolean inCodefence = false; + for (int i = 0; i < lines.length; i++) { + if (lines[i].startsWith("```")) { + inCodefence = !inCodefence; + lineNumber++; + } + if (inCodefence && TextUtils.isEmpty(lines[i])) { + lineNumber++; + } + if (i == lineNumber) { + break; + } + } + + /* + * Workaround for multiple RxMarkdown-bugs: + * When (un)checking a checkbox which is in the last line, every time it gets toggled, the last character of the line gets lost. + * When (un)checking a checkbox, every markdown gets stripped in the given line argument + */ + if (lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_MINUS) || lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_STAR)) { + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_MINUS, CHECKBOX_CHECKED_MINUS); + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_STAR, CHECKBOX_CHECKED_STAR); + } else { + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_MINUS, CHECKBOX_UNCHECKED_MINUS); + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_STAR, CHECKBOX_UNCHECKED_STAR); + } + + changedText = TextUtils.join("\n", lines); + binding.singleNoteContent.setText(parseCompat(markdownProcessor, changedText)); + saveNote(null); + } catch (IndexOutOfBoundsException e) { + Toast.makeText(getActivity(), R.string.checkbox_could_not_be_toggled, Toast.LENGTH_SHORT).show(); + e.printStackTrace(); + } + return line; + } + ) + .setOnLinkClickCallback((view, link) -> { + if (NoteLinksUtils.isNoteLink(link)) { + final Intent intent = new Intent(requireActivity().getApplicationContext(), EditNoteActivity.class) + .putExtra(EditNoteActivity.PARAM_NOTE_ID, db.getLocalIdByRemoteId(this.note.getAccountId(), extractNoteRemoteId(link))); + startActivity(intent); + } else { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); + startActivity(browserIntent); + } + }) + .build()); + try { + binding.singleNoteContent.setText(parseCompat(markdownProcessor, replaceNoteLinksWithDummyUrls(note.getContent(), db.getRemoteIds(note.getAccountId())))); + } catch (StringIndexOutOfBoundsException e) { + // Workaround for RxMarkdown: https://github.com/stefan-niedermann/nextcloud-notes/issues/668 + binding.singleNoteContent.setText(replaceNoteLinksWithDummyUrls(note.getContent(), db.getRemoteIds(note.getAccountId()))); + Toast.makeText(binding.singleNoteContent.getContext(), R.string.could_not_load_preview_two_digit_numbered_list, Toast.LENGTH_LONG).show(); + e.printStackTrace(); + } + changedText = note.getContent(); + binding.singleNoteContent.setMovementMethod(LinkMovementMethod.getInstance()); + + db = NotesDatabase.getInstance(requireContext()); + binding.swiperefreshlayout.setOnRefreshListener(this); + + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(requireActivity().getApplicationContext()); + binding.singleNoteContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp)); + if (sp.getBoolean(getString(R.string.pref_key_font), false)) { + binding.singleNoteContent.setTypeface(Typeface.MONOSPACE); + } + } + + @Override + protected void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor) { + if (binding != null && ViewCompat.isAttachedToWindow(binding.singleNoteContent)) { + binding.singleNoteContent.setText( + searchAndColor(new SpannableString(parseCompat(markdownProcessor, getContent())), newText, requireContext(), current, mainColor, textColor), + TextView.BufferType.SPANNABLE); + } + } + + @Override + protected String getContent() { + return changedText; + } + + @Override + public void onRefresh() { + if (db.getNoteServerSyncHelper().isSyncPossible() && SSOUtil.isConfigured(getContext())) { + binding.swiperefreshlayout.setRefreshing(true); + try { + SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()); + db.getNoteServerSyncHelper().addCallbackPull(ssoAccount, () -> { + note = db.getNote(note.getAccountId(), note.getId()); + changedText = note.getContent(); + binding.singleNoteContent.setText(parseCompat(markdownProcessor, replaceNoteLinksWithDummyUrls(note.getContent(), db.getRemoteIds(note.getAccountId())))); + binding.swiperefreshlayout.setRefreshing(false); + }); + db.getNoteServerSyncHelper().scheduleSync(ssoAccount, false); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + e.printStackTrace(); + } + } else { + binding.swiperefreshlayout.setRefreshing(false); + Toast.makeText(requireContext(), getString(R.string.error_sync, getString(R.string.error_no_network)), Toast.LENGTH_LONG).show(); + } + } + + @Override + public void applyBrand(int mainColor, int textColor) { + super.applyBrand(mainColor, textColor); + binding.singleNoteContent.setHighlightColor(getTextHighlightBackgroundColor(requireContext(), mainColor, colorPrimary, colorAccent)); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java new file mode 100644 index 00000000..3b5e4c6e --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java @@ -0,0 +1,166 @@ +package it.niedermann.owncloud.notes.edit; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; +import android.text.Layout; +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.yydcdut.markdown.MarkdownProcessor; +import com.yydcdut.markdown.syntax.text.TextFactory; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding; +import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.shared.util.MarkDownUtil; +import it.niedermann.owncloud.notes.shared.util.NoteLinksUtils; + +import static androidx.core.view.ViewCompat.isAttachedToWindow; +import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.searchAndColor; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.parseCompat; +import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences; + +public class NoteReadonlyFragment extends SearchableBaseNoteFragment { + + private MarkdownProcessor markdownProcessor; + + private FragmentNotePreviewBinding binding; + + public static NoteReadonlyFragment newInstance(String content) { + NoteReadonlyFragment f = new NoteReadonlyFragment(); + Bundle b = new Bundle(); + b.putString(PARAM_CONTENT, content); + f.setArguments(b); + return f; + } + + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + menu.findItem(R.id.menu_favorite).setVisible(false); + menu.findItem(R.id.menu_edit).setVisible(false); + menu.findItem(R.id.menu_preview).setVisible(false); + menu.findItem(R.id.menu_cancel).setVisible(false); + menu.findItem(R.id.menu_delete).setVisible(false); + menu.findItem(R.id.menu_share).setVisible(false); + menu.findItem(R.id.menu_move).setVisible(false); + menu.findItem(R.id.menu_category).setVisible(false); + if (menu.findItem(MENU_ID_PIN) != null) + menu.findItem(MENU_ID_PIN).setVisible(false); + } + + @Override + public ScrollView getScrollView() { + return binding.scrollView; + } + + @Override + protected FloatingActionButton getSearchNextButton() { + return binding.searchNext; + } + + @Override + protected FloatingActionButton getSearchPrevButton() { + return binding.searchPrev; + } + + @Override + protected Layout getLayout() { + binding.singleNoteContent.onPreDraw(); + return binding.singleNoteContent.getLayout(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup + container, @Nullable Bundle savedInstanceState) { + binding = FragmentNotePreviewBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + markdownProcessor = new MarkdownProcessor(requireActivity()); + markdownProcessor.factory(TextFactory.create()); + markdownProcessor.config( + MarkDownUtil.getMarkDownConfiguration(binding.singleNoteContent.getContext()) + .setOnLinkClickCallback((view, link) -> { + if (NoteLinksUtils.isNoteLink(link)) { + long noteRemoteId = NoteLinksUtils.extractNoteRemoteId(link); + long noteLocalId = db.getLocalIdByRemoteId(this.note.getAccountId(), noteRemoteId); + Intent intent = new Intent(requireActivity().getApplicationContext(), EditNoteActivity.class); + intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, noteLocalId); + startActivity(intent); + } else { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); + startActivity(browserIntent); + } + }) + .build()); + try { + binding.singleNoteContent.setText(parseCompat(markdownProcessor, note.getContent())); + onResume(); + } catch (StringIndexOutOfBoundsException e) { + // Workaround for RxMarkdown: https://github.com/stefan-niedermann/nextcloud-notes/issues/668 + binding.singleNoteContent.setText(note.getContent()); + Toast.makeText(binding.singleNoteContent.getContext(), R.string.could_not_load_preview_two_digit_numbered_list, Toast.LENGTH_LONG).show(); + e.printStackTrace(); + } + binding.singleNoteContent.setMovementMethod(LinkMovementMethod.getInstance()); + + db = NotesDatabase.getInstance(getActivity()); + binding.swiperefreshlayout.setEnabled(false); + + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(requireActivity().getApplicationContext()); + binding.singleNoteContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp)); + if (sp.getBoolean(getString(R.string.pref_key_font), false)) { + binding.singleNoteContent.setTypeface(Typeface.MONOSPACE); + } + } + + @Override + public void onCloseNote() { + // Do nothing + } + + @Override + protected void saveNote(@Nullable ISyncCallback callback) { + // Do nothing + } + + @Override + protected void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor) { + if ((binding != null) && isAttachedToWindow(binding.singleNoteContent)) { + binding.singleNoteContent.setText(searchAndColor(new SpannableString(parseCompat(markdownProcessor, getContent())), newText, requireContext(), current, mainColor, textColor), TextView.BufferType.SPANNABLE); + } + } + + @Override + protected String getContent() { + return note.getContent(); + } + + @Override + public void applyBrand(int mainColor, int textColor) { + super.applyBrand(mainColor, textColor); + binding.singleNoteContent.setHighlightColor(getTextHighlightBackgroundColor(requireContext(), mainColor, colorPrimary, colorAccent)); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotesTextWatcher.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotesTextWatcher.java new file mode 100644 index 00000000..f9ced1ae --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotesTextWatcher.java @@ -0,0 +1,100 @@ +package it.niedermann.owncloud.notes.edit; + +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.widget.EditText; + +import androidx.annotation.NonNull; + +import it.niedermann.owncloud.notes.shared.util.MarkDownUtil; + +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_UNCHECKED_STAR_TRAILING_SPACE; + +/** + * Implements auto-continuation for checked-lists + */ +public abstract class NotesTextWatcher implements TextWatcher { + + private static final String TAG = NotesTextWatcher.class.getSimpleName(); + + private static final String codeBlock = "```"; + + private static final int lengthCheckbox = 6; + + private boolean resetSelection = false; + private boolean afterTextChangedHandeled = false; + private int resetSelectionTo = -1; + + private final EditText editText; + + protected NotesTextWatcher(EditText editText) { + this.editText = editText; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Nothing to do here... + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // https://github.com/stefan-niedermann/nextcloud-notes/issues/608 + if (count == 1 && s.charAt(start) == '\n') { // 'Enter' was pressed + autoContinueCheckboxListsOnEnter(s, start, count); + } + // https://github.com/stefan-niedermann/nextcloud-notes/issues/558 + if (s.toString().contains(codeBlock)) { + preventCursorJumpToTopWithinCodeBlock(s, start, count); + } + } + + @Override + public void afterTextChanged(Editable s) { + if (resetSelection && !afterTextChangedHandeled) { + Log.v(TAG, "Resetting selection to " + resetSelectionTo); + afterTextChangedHandeled = true; + setNewText(new StringBuilder(s), resetSelectionTo); + afterTextChangedHandeled = false; + resetSelection = false; + resetSelectionTo = -1; + } + } + + private void autoContinueCheckboxListsOnEnter(@NonNull CharSequence s, int start, int count) { + // Find start of line + int startOfLine = MarkDownUtil.getStartOfLine(s, start); + String line = s.subSequence(startOfLine, start).toString(); + + if (line.equals(CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE) || line.equals(CHECKBOX_UNCHECKED_STAR_TRAILING_SPACE)) { + editText.setSelection(startOfLine + 1); + setNewText(new StringBuilder(s).replace(startOfLine, startOfLine + lengthCheckbox + 1, "\n"), startOfLine + 1); + } else if (MarkDownUtil.lineStartsWithCheckbox(line, false)) { + setNewText(new StringBuilder(s).insert(start + count, CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE), start + lengthCheckbox + 1); + } else if (MarkDownUtil.lineStartsWithCheckbox(line, true)) { + setNewText(new StringBuilder(s).insert(start + count, CHECKBOX_UNCHECKED_STAR_TRAILING_SPACE), start + lengthCheckbox + 1); + } + } + + private void preventCursorJumpToTopWithinCodeBlock(@NonNull CharSequence s, int start, int count) { + // Find start of line + int startOfLine = MarkDownUtil.getStartOfLine(s, start); + String line = s.subSequence(startOfLine, start).toString(); + // "start" is the direct sibling of the codeBlock + if (line.startsWith(codeBlock) && start - startOfLine == codeBlock.length() && !resetSelection) { + resetSelectionTo = editText.getSelectionEnd(); + resetSelection = true; + Log.v(TAG, "Entered a character directly behind a codeBlock - prepare selection reset to " + resetSelectionTo); + } else if (s.subSequence(startOfLine, start + count).toString().startsWith(codeBlock) && !resetSelection) { + resetSelectionTo = editText.getSelectionEnd(); + resetSelection = true; + Log.v(TAG, "One completed a ``-codeBlock with the third `-character - prepare selection reset to " + resetSelectionTo); + } + } + + private void setNewText(@NonNull StringBuilder newText, int selection) { + editText.setText(newText); + editText.setSelection(selection); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java new file mode 100644 index 00000000..5ae9b22a --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java @@ -0,0 +1,302 @@ +package it.niedermann.owncloud.notes.edit; + +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.text.Layout; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.ScrollView; + +import androidx.annotation.CallSuper; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandedActivity; + +public abstract class SearchableBaseNoteFragment extends BaseNoteFragment { + + private static final String TAG = SearchableBaseNoteFragment.class.getSimpleName(); + private static final String saved_instance_key_searchQuery = "searchQuery"; + private static final String saved_instance_key_currentOccurrence = "currentOccurrence"; + + private int currentOccurrence = 1; + private int occurrenceCount = 0; + private SearchView searchView; + private String searchQuery = null; + private static final int delay = 50; // If the search string does not change after $delay ms, then the search task starts. + + @ColorInt + private int mainColor; + @ColorInt + private int textColor; + + @Override + public void onStart() { + this.mainColor = getResources().getColor(R.color.defaultBrand); + this.textColor = Color.WHITE; + super.onStart(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (savedInstanceState != null) { + searchQuery = savedInstanceState.getString(saved_instance_key_searchQuery, ""); + currentOccurrence = savedInstanceState.getInt(saved_instance_key_currentOccurrence, 1); + } + } + + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + + MenuItem searchMenuItem = menu.findItem(R.id.search); + searchView = (SearchView) searchMenuItem.getActionView(); + + if (!TextUtils.isEmpty(searchQuery) && isNew) { + searchMenuItem.expandActionView(); + searchView.setQuery(searchQuery, true); + searchView.clearFocus(); + } else { + searchMenuItem.collapseActionView(); + } + + + final LinearLayout searchEditFrame = searchView.findViewById(R.id + .search_edit_frame); + + searchEditFrame.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + int oldVisibility = -1; + + @Override + public void onGlobalLayout() { + int currentVisibility = searchEditFrame.getVisibility(); + + if (currentVisibility != oldVisibility) { + if (currentVisibility != View.VISIBLE) { + colorWithText("", null, mainColor, textColor); + searchQuery = ""; + hideSearchFabs(); + } else { + jumpToOccurrence(); + colorWithText(searchQuery, null, mainColor, textColor); + occurrenceCount = countOccurrences(getContent(), searchQuery); + showSearchFabs(); + } + + oldVisibility = currentVisibility; + } + } + + }); + + FloatingActionButton next = getSearchNextButton(); + FloatingActionButton prev = getSearchPrevButton(); + + if (next != null) { + next.setOnClickListener(v -> { + currentOccurrence++; + jumpToOccurrence(); + colorWithText(searchView.getQuery().toString(), currentOccurrence, mainColor, textColor); + }); + } + + if (prev != null) { + prev.setOnClickListener(v -> { + occurrenceCount = countOccurrences(getContent(), searchView.getQuery().toString()); + currentOccurrence--; + jumpToOccurrence(); + colorWithText(searchView.getQuery().toString(), currentOccurrence, mainColor, textColor); + }); + } + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + private DelayQueryRunnable delayQueryTask; + private Handler handler = new Handler(); + + @Override + public boolean onQueryTextSubmit(@NonNull String query) { + currentOccurrence++; + jumpToOccurrence(); + colorWithText(query, currentOccurrence, mainColor, textColor); + return true; + } + + @Override + public boolean onQueryTextChange(@NonNull String newText) { + queryWithHandler(newText); + return true; + } + + private void queryMatch(@NonNull String newText) { + searchQuery = newText; + occurrenceCount = countOccurrences(getContent(), searchQuery); + if (occurrenceCount > 1) { + showSearchFabs(); + } else { + hideSearchFabs(); + } + currentOccurrence = 1; + jumpToOccurrence(); + colorWithText(searchQuery, currentOccurrence, mainColor, textColor); + } + + private void queryWithHandler(@NonNull String newText) { + if (delayQueryTask != null) { + delayQueryTask.cancel(); + handler.removeCallbacksAndMessages(null); + } + delayQueryTask = new DelayQueryRunnable(newText); + // If there is only one char in the search pattern, we should start the search immediately. + handler.postDelayed(delayQueryTask, newText.length() > 1 ? delay : 0); + } + + class DelayQueryRunnable implements Runnable { + private String text; + private boolean canceled = false; + + public DelayQueryRunnable(String text) { + this.text = text; + } + + @Override + public void run() { + if (canceled) { + return; + } + queryMatch(text); + } + + public void cancel() { + canceled = true; + } + } + }); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + if (searchView != null && !TextUtils.isEmpty(searchView.getQuery().toString())) { + outState.putString(saved_instance_key_searchQuery, searchView.getQuery().toString()); + outState.putInt(saved_instance_key_currentOccurrence, currentOccurrence); + } + } + + protected abstract void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor); + + protected abstract Layout getLayout(); + + protected abstract FloatingActionButton getSearchNextButton(); + + protected abstract FloatingActionButton getSearchPrevButton(); + + private void showSearchFabs() { + FloatingActionButton next = getSearchNextButton(); + FloatingActionButton prev = getSearchPrevButton(); + if (prev != null) { + prev.show(); + } + if (next != null) { + next.show(); + } + } + + private void hideSearchFabs() { + FloatingActionButton next = getSearchNextButton(); + FloatingActionButton prev = getSearchPrevButton(); + if (prev != null) { + prev.hide(); + } + if (next != null) { + next.hide(); + } + } + + private void jumpToOccurrence() { + Layout layout = getLayout(); + if (layout == null) { + Log.w(TAG, "getLayout() is null"); + } else if (getContent() == null || getContent().isEmpty()) { + Log.w(TAG, "getContent is null or empty"); + } else if (currentOccurrence < 1) { + // if currentOccurrence is lower than 1, jump to last occurrence + currentOccurrence = occurrenceCount; + jumpToOccurrence(); + } else if (searchQuery != null && !searchQuery.isEmpty()) { + String currentContent = getContent().toLowerCase(); + int indexOfNewText = indexOfNth(currentContent, searchQuery.toLowerCase(), 0, currentOccurrence); + if (indexOfNewText <= 0) { + // Search term is not n times in text + // Go back to first search result + if (currentOccurrence != 1) { + currentOccurrence = 1; + jumpToOccurrence(); + } + return; + } + String textUntilFirstOccurrence = currentContent.substring(0, indexOfNewText); + int numberLine = layout.getLineForOffset(textUntilFirstOccurrence.length()); + + if (numberLine >= 0) { + ScrollView scrollView = getScrollView(); + if (scrollView != null) { + scrollView.post(() -> scrollView.smoothScrollTo(0, layout.getLineTop(numberLine))); + } + } + } + } + + private static int indexOfNth(String input, String value, int startIndex, int nth) { + if (nth < 1) + throw new IllegalArgumentException("Param 'nth' must be greater than 0!"); + if (nth == 1) + return input.indexOf(value, startIndex); + int idx = input.indexOf(value, startIndex); + if (idx == -1) + return -1; + return indexOfNth(input, value, idx + 1, nth - 1); + } + + private static int countOccurrences(String haystack, String needle) { + if (haystack == null || haystack.isEmpty() || needle == null || needle.isEmpty()) { + return 0; + } + // Use regrex which is faster before. + // Such that the main thread will not stop for a long tilme + // And so there will not an ANR problem + Matcher m = Pattern.compile(needle, Pattern.CASE_INSENSITIVE | Pattern.LITERAL) + .matcher(haystack); + + int count = 0; + while (m.find()) { + count++; + } + return count; + } + + @CallSuper + @Override + public void applyBrand(int mainColor, int textColor) { + this.mainColor = mainColor; + this.textColor = textColor; + BrandedActivity.applyBrandToFAB(mainColor, textColor, getSearchPrevButton()); + BrandedActivity.applyBrandToFAB(mainColor, textColor, getSearchNextButton()); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java new file mode 100644 index 00000000..d938cb53 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java @@ -0,0 +1,133 @@ +package it.niedermann.owncloud.notes.edit.category; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.ItemCategoryBinding; +import it.niedermann.owncloud.notes.main.NavigationAdapter.CategoryNavigationItem; +import it.niedermann.owncloud.notes.main.NavigationAdapter.NavigationItem; +import it.niedermann.owncloud.notes.shared.util.NoteUtil; + +public class CategoryAdapter extends RecyclerView.Adapter { + + private static final String clearItemId = "clear_item"; + private static final String addItemId = "add_item"; + @NonNull + private List categories = new ArrayList<>(); + @NonNull + private final CategoryListener listener; + private final Context context; + + CategoryAdapter(@NonNull Context context, @NonNull CategoryListener categoryListener) { + this.context = context; + this.listener = categoryListener; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_category, parent, false); + return new CategoryViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + NavigationItem category = categories.get(position); + CategoryViewHolder categoryViewHolder = (CategoryViewHolder) holder; + + switch (category.id) { + case addItemId: + Drawable wrapDrawable = DrawableCompat.wrap(context.getResources().getDrawable(category.icon)); + DrawableCompat.setTint(wrapDrawable, context.getResources().getColor(R.color.icon_color_default)); + categoryViewHolder.getIcon().setImageDrawable(wrapDrawable); + categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryAdded()); + break; + case clearItemId: + categoryViewHolder.getIcon().setImageDrawable(context.getResources().getDrawable(category.icon)); + categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryCleared()); + break; + default: + categoryViewHolder.getIcon().setImageDrawable(context.getResources().getDrawable(category.icon)); + categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryChosen(category.label)); + break; + } + categoryViewHolder.getCategory().setText(NoteUtil.extendCategory(category.label)); + if (category.count != null && category.count > 0) { + categoryViewHolder.getCount().setText(String.valueOf(category.count)); + } else { + categoryViewHolder.getCount().setVisibility(View.GONE); + } + } + + @Override + public int getItemCount() { + return categories.size(); + } + + static class CategoryViewHolder extends RecyclerView.ViewHolder { + private final ItemCategoryBinding binding; + + private CategoryViewHolder(View view) { + super(view); + binding = ItemCategoryBinding.bind(view); + } + + private View getCategoryWrapper() { + return binding.categoryWrapper; + } + + private AppCompatImageView getIcon() { + return binding.icon; + } + + private TextView getCategory() { + return binding.category; + } + + private TextView getCount() { + return binding.count; + } + } + + void setCategoryList(List categories, String currentSearchString) { + this.categories.clear(); + this.categories.addAll(categories); + final NavigationItem clearItem = new NavigationItem(clearItemId, context.getString(R.string.no_category), 0, R.drawable.ic_clear_grey_24dp); + this.categories.add(0, clearItem); + if (currentSearchString != null && currentSearchString.trim().length() > 0) { + boolean currentSearchStringIsInCategories = false; + for (NavigationItem category : categories) { + if (currentSearchString.equals(category.label)) { + currentSearchStringIsInCategories = true; + break; + } + } + if (!currentSearchStringIsInCategories) { + NavigationItem addItem = new NavigationItem(addItemId, context.getString(R.string.add_category, currentSearchString.trim()), 0, R.drawable.ic_add_blue_24dp); + this.categories.add(addItem); + } + } + notifyDataSetChanged(); + } + + public interface CategoryListener { + void onCategoryChosen(String category); + + void onCategoryAdded(); + + void onCategoryCleared(); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java new file mode 100644 index 00000000..881e8478 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java @@ -0,0 +1,184 @@ +package it.niedermann.owncloud.notes.edit.category; + +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import java.util.List; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandedAlertDialogBuilder; +import it.niedermann.owncloud.notes.branding.BrandedDialogFragment; +import it.niedermann.owncloud.notes.branding.BrandingUtil; +import it.niedermann.owncloud.notes.databinding.DialogChangeCategoryBinding; +import it.niedermann.owncloud.notes.main.NavigationAdapter; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; + +/** + * This {@link DialogFragment} allows for the selection of a category. + * It targetFragment is set it must implement the interface {@link CategoryDialogListener}. + * The calling Activity must implement the interface {@link CategoryDialogListener}. + */ +public class CategoryDialogFragment extends BrandedDialogFragment { + + private static final String TAG = CategoryDialogFragment.class.getSimpleName(); + private static final String STATE_CATEGORY = "category"; + private DialogChangeCategoryBinding binding; + + private NotesDatabase db; + private CategoryDialogListener listener; + + private EditText editCategory; + + @Override + public void applyBrand(int mainColor, int textColor) { + BrandingUtil.applyBrandToEditText(mainColor, textColor, binding.search); + } + + /** + * Interface that must be implemented by the calling Activity. + */ + public interface CategoryDialogListener { + /** + * This method is called after the user has chosen a category. + * + * @param category Name of the category which was chosen by the user. + */ + void onCategoryChosen(String category); + } + + public static final String PARAM_ACCOUNT_ID = "account_id"; + public static final String PARAM_CATEGORY = "category"; + + private long accountId; + + private CategoryAdapter adapter; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (getArguments() != null && requireArguments().containsKey(PARAM_ACCOUNT_ID)) { + accountId = requireArguments().getLong(PARAM_ACCOUNT_ID); + } else { + throw new IllegalArgumentException("Provide at least \"" + PARAM_ACCOUNT_ID + "\""); + } + Fragment target = getTargetFragment(); + if (target instanceof CategoryDialogListener) { + listener = (CategoryDialogListener) target; + } else if (getActivity() instanceof CategoryDialogListener) { + listener = (CategoryDialogListener) getActivity(); + } else { + throw new IllegalArgumentException("Calling activity or target fragment must implement " + CategoryDialogListener.class.getSimpleName()); + } + db = NotesDatabase.getInstance(getActivity()); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + View dialogView = View.inflate(getContext(), R.layout.dialog_change_category, null); + binding = DialogChangeCategoryBinding.bind(dialogView); + this.editCategory = binding.search; + + if (savedInstanceState == null) { + if (requireArguments().containsKey(PARAM_CATEGORY)) { + editCategory.setText(requireArguments().getString(PARAM_CATEGORY)); + } + } else if (savedInstanceState.containsKey(STATE_CATEGORY)) { + editCategory.setText(savedInstanceState.getString(STATE_CATEGORY)); + } + + adapter = new CategoryAdapter(requireContext(), new CategoryAdapter.CategoryListener() { + @Override + public void onCategoryChosen(String category) { + listener.onCategoryChosen(category); + dismiss(); + } + + @Override + public void onCategoryAdded() { + listener.onCategoryChosen(editCategory.getText().toString()); + dismiss(); + } + + @Override + public void onCategoryCleared() { + listener.onCategoryChosen(""); + dismiss(); + } + }); + + binding.recyclerView.setAdapter(adapter); + new LoadCategoriesTask().execute(""); + editCategory.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Nothing to do here... + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Nothing to do here... + } + + @Override + public void afterTextChanged(Editable s) { + new LoadCategoriesTask().execute(editCategory.getText().toString()); + } + }); + + return new BrandedAlertDialogBuilder(getActivity()) + .setTitle(R.string.change_category_title) + .setView(dialogView) + .setCancelable(true) + .setPositiveButton(R.string.action_edit_save, (dialog, which) -> listener.onCategoryChosen(editCategory.getText().toString())) + .setNegativeButton(R.string.simple_cancel, null) + .create(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(STATE_CATEGORY, editCategory.getText().toString()); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (editCategory.getText() == null || editCategory.getText().length() == 0) { + editCategory.requestFocus(); + if (getDialog() != null && getDialog().getWindow() != null) { + getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } else { + Log.w(TAG, "can not set SOFT_INPUT_STATE_ALWAYAS_VISIBLE because getWindow() == null"); + } + } + } + + + private class LoadCategoriesTask extends AsyncTask> { + String currentSearchString; + + @Override + protected List doInBackground(String... searchText) { + currentSearchString = searchText[0]; + return db.searchCategories(accountId, currentSearchString); + } + + @Override + protected void onPostExecute(List categories) { + adapter.setCategoryList(categories, currentSearchString); + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/format/ContextBasedFormattingCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/format/ContextBasedFormattingCallback.java new file mode 100644 index 00000000..a3789dbd --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/format/ContextBasedFormattingCallback.java @@ -0,0 +1,115 @@ +package it.niedermann.owncloud.notes.edit.format; + +import android.graphics.Typeface; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.EditText; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.shared.util.MarkDownUtil; + +import static it.niedermann.owncloud.notes.shared.util.ClipboardUtil.getClipboardURLorNull; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.getEndOfLine; +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.getStartOfLine; + +public class ContextBasedFormattingCallback implements ActionMode.Callback { + + private static final String TAG = ContextBasedFormattingCallback.class.getSimpleName(); + + private final EditText editText; + + public ContextBasedFormattingCallback(EditText editText) { + this.editText = editText; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.getMenuInflater().inflate(R.menu.context_based_formatting, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + CharSequence text = editText.getText(); + int originalCursorPosition = editText.getSelectionStart(); + if (originalCursorPosition >= 0 && originalCursorPosition <= text.length()) { + int startOfLine = getStartOfLine(text, originalCursorPosition); + int endOfLine = getEndOfLine(text, startOfLine); + String line = text.subSequence(startOfLine, endOfLine).toString(); + if (MarkDownUtil.lineStartsWithCheckbox(line)) { + menu.findItem(R.id.checkbox).setVisible(false); + Log.i(TAG, "Hide checkbox menu item because line starts already with checkbox"); + } + } else { + Log.e(TAG, "SelectionStart is " + originalCursorPosition + ". Expected to be between 0 and " + text.length()); + } + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.checkbox: + insertCheckbox(); + return true; + case R.id.link: + insertLink(); + return true; + default: + return false; + } + } + + private void insertCheckbox() { + CharSequence text = editText.getText(); + int originalCursorPosition = editText.getSelectionStart(); + int startOfLine = getStartOfLine(text, originalCursorPosition); + Log.i(TAG, "Inserting checkbox at position " + startOfLine); + CharSequence part1 = text.subSequence(0, startOfLine); + CharSequence part2 = text.subSequence(startOfLine, text.length()); + editText.setText(TextUtils.concat(part1, CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE, part2)); + editText.setSelection(originalCursorPosition + CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE.length()); + } + + private void insertLink() { + SpannableStringBuilder ssb = new SpannableStringBuilder(editText.getText()); + int start = editText.getText().length(); + int end = start; + boolean textToFormatIsLink = TextUtils.indexOf(editText.getText().subSequence(start, end), "http") == 0; + if (textToFormatIsLink) { + Log.i(TAG, "Inserting link description for position " + start + " to " + end); + ssb.insert(end, ")"); + ssb.insert(start, "[]("); + } else { + String clipboardURL = getClipboardURLorNull(editText.getContext()); + if (clipboardURL != null) { + Log.i(TAG, "Inserting link from clipboard at position " + start + " to " + end + ": " + clipboardURL); + ssb.insert(end, "](" + clipboardURL + ")"); + end += clipboardURL.length(); + } else { + Log.i(TAG, "Inserting empty link for position " + start + " to " + end); + ssb.insert(end, "]()"); + } + ssb.insert(start, "["); + } + end++; + ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1); + editText.setText(ssb); + if (textToFormatIsLink) { + editText.setSelection(start + 1); + } else { + editText.setSelection(end + 2); // after ]( + } + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + // Nothing to do here... + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/format/ContextBasedRangeFormattingCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/format/ContextBasedRangeFormattingCallback.java new file mode 100644 index 00000000..91f3b9f3 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/format/ContextBasedRangeFormattingCallback.java @@ -0,0 +1,150 @@ +package it.niedermann.owncloud.notes.edit.format; + +import android.graphics.Typeface; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.StyleSpan; +import android.util.SparseIntArray; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.EditText; + +import it.niedermann.owncloud.notes.R; + +import static it.niedermann.owncloud.notes.shared.util.ClipboardUtil.getClipboardURLorNull; + +public class ContextBasedRangeFormattingCallback implements ActionMode.Callback { + + private static final String TAG = ContextBasedRangeFormattingCallback.class.getSimpleName(); + + private final EditText editText; + + public ContextBasedRangeFormattingCallback(EditText editText) { + this.editText = editText; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.getMenuInflater().inflate(R.menu.context_based_range_formatting, menu); + + SparseIntArray styleFormatMap = new SparseIntArray(); + styleFormatMap.append(R.id.bold, Typeface.BOLD); + styleFormatMap.append(R.id.italic, Typeface.ITALIC); + + MenuItem item; + CharSequence title; + SpannableStringBuilder ssb; + + for (int i = 0; i < styleFormatMap.size(); i++) { + item = menu.findItem(styleFormatMap.keyAt(i)); + title = item.getTitle(); + ssb = new SpannableStringBuilder(title); + ssb.setSpan(new StyleSpan(styleFormatMap.valueAt(i)), 0, title.length(), 0); + item.setTitle(ssb); + } + + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO hide actions if not available? + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + int start = editText.getSelectionStart(); + int end = editText.getSelectionEnd(); + SpannableStringBuilder ssb = new SpannableStringBuilder(editText.getText()); + final String markdown; + + + switch (item.getItemId()) { + case R.id.bold: + markdown = "**"; + if (hasAlreadyMarkdown(start, end, markdown)) { + this.removeMarkdown(ssb, start, end, markdown); + } else { + this.addMarkdown(ssb, start, end, markdown, Typeface.BOLD); + } + editText.setText(ssb); + editText.setSelection(end + markdown.length() * 2); + return true; + case R.id.italic: + markdown = "*"; + if (hasAlreadyMarkdown(start, end, markdown)) { + this.removeMarkdown(ssb, start, end, markdown); + } else { + this.addMarkdown(ssb, start, end, markdown, Typeface.ITALIC); + } + editText.setText(ssb); + editText.setSelection(end + markdown.length() * 2); + return true; + case R.id.link: + boolean textToFormatIsLink = TextUtils.indexOf(editText.getText().subSequence(start, end), "http") == 0; + if (textToFormatIsLink) { + ssb.insert(end, ")"); + ssb.insert(start, "[]("); + } else { + String clipboardURL = getClipboardURLorNull(editText.getContext()); + if (clipboardURL != null) { + ssb.insert(end, "](" + clipboardURL + ")"); + end += clipboardURL.length(); + } else { + ssb.insert(end, "]()"); + } + ssb.insert(start, "["); + } + end++; + ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1); + editText.setText(ssb); + if (textToFormatIsLink) { + editText.setSelection(start + 1); + } else { + editText.setSelection(end + 2); // after ]( + } + return true; + case android.R.id.cut: + // https://github.com/stefan-niedermann/nextcloud-notes/issues/604 + // https://github.com/stefan-niedermann/nextcloud-notes/issues/477 + try { + editText.onTextContextMenuItem(item.getItemId()); + return true; + } catch (IndexOutOfBoundsException e) { + e.printStackTrace(); + editText.setSelection(0, 0); + editText.clearFocus(); + return true; + } + default: + return false; + } + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + // Nothing to do here... + } + + private boolean hasAlreadyMarkdown(int start, int end, String markdown) { + return start > markdown.length() && markdown.contentEquals(editText.getText().subSequence(start - markdown.length(), start)) && + editText.getText().length() > end + markdown.length() && markdown.contentEquals(editText.getText().subSequence(end, end + markdown.length())); + } + + private void removeMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown) { + // FIXME disabled, because it does not work properly and might cause data loss + // ssb.delete(start - markdown.length(), start); + // ssb.delete(end - markdown.length(), end); + // ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1); + } + + private void addMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown, int typeface) { + ssb.insert(end, markdown); + ssb.insert(start, markdown); + editText.getText().charAt(start); + editText.getText().charAt(start + 1); + ssb.setSpan(new StyleSpan(typeface), start, end + markdown.length() * 2, 1); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java new file mode 100644 index 00000000..e9473399 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java @@ -0,0 +1,96 @@ +package it.niedermann.owncloud.notes.edit.title; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.DialogEditTitleBinding; + +public class EditTitleDialogFragment extends DialogFragment { + + private static final String TAG = EditTitleDialogFragment.class.getSimpleName(); + static final String PARAM_OLD_TITLE = "old_title"; + private DialogEditTitleBinding binding; + + private String oldTitle; + private EditTitleListener listener; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + final Bundle args = getArguments(); + if (args == null) { + throw new IllegalArgumentException("Provide at least " + PARAM_OLD_TITLE); + } + oldTitle = args.getString(PARAM_OLD_TITLE); + + if (getTargetFragment() instanceof EditTitleListener) { + listener = (EditTitleListener) getTargetFragment(); + } else if (getActivity() instanceof EditTitleListener) { + listener = (EditTitleListener) getActivity(); + } else { + throw new IllegalArgumentException("Calling activity or target fragment must implement " + EditTitleListener.class.getSimpleName()); + } + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + View dialogView = View.inflate(getContext(), R.layout.dialog_edit_title, null); + binding = DialogEditTitleBinding.bind(dialogView); + + if (savedInstanceState == null) { + binding.title.setText(oldTitle); + } + + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.change_note_title) + .setView(dialogView) + .setCancelable(true) + .setPositiveButton(R.string.action_edit_save, (dialog, which) -> listener.onTitleEdited(binding.title.getText().toString())) + .setNegativeButton(R.string.simple_cancel, null) + .create(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + binding.title.requestFocus(); + Window window = requireDialog().getWindow(); + if (window != null) { + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } else { + Log.w(TAG, "can not enable soft keyboard because " + Window.class.getSimpleName() + " is null."); + } + } + + public static DialogFragment newInstance(String title) { + final DialogFragment fragment = new EditTitleDialogFragment(); + final Bundle args = new Bundle(); + args.putString(PARAM_OLD_TITLE, title); + fragment.setArguments(args); + return fragment; + } + + /** + * Interface that must be implemented by the calling Activity. + */ + public interface EditTitleListener { + /** + * This method is called after the user has changed the title of a note manually. + * + * @param newTitle the new title that a user submitted + */ + void onTitleEdited(String newTitle); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionActivity.java new file mode 100644 index 00000000..4999a30e --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionActivity.java @@ -0,0 +1,54 @@ +package it.niedermann.owncloud.notes.exception; + +import android.annotation.SuppressLint; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import java.util.Objects; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.ActivityExceptionBinding; + +import static it.niedermann.owncloud.notes.exception.ExceptionHandler.KEY_THROWABLE; + + +public class ExceptionActivity extends AppCompatActivity { + + private ActivityExceptionBinding binding; + + @SuppressLint("SetTextI18n") // only used for logging + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityExceptionBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + binding.copy.setOnClickListener((v) -> copyStacktraceToClipboard()); + binding.close.setOnClickListener((v) -> close()); + + setSupportActionBar(binding.toolbar); + Throwable throwable = (Throwable) Objects.requireNonNull(getIntent().getSerializableExtra(KEY_THROWABLE)); + throwable.printStackTrace(); + binding.toolbar.setTitle(getString(R.string.simple_error)); + binding.message.setText(throwable.getMessage()); + binding.stacktrace.setText(ExceptionUtil.getDebugInfos(this, throwable)); + } + + + private void copyStacktraceToClipboard() { + final ClipboardManager clipboardManager = (ClipboardManager) Objects.requireNonNull(getSystemService(CLIPBOARD_SERVICE)); + ClipData clipData = ClipData.newPlainText(getString(R.string.simple_exception), "```\n" + binding.stacktrace.getText() + "\n```"); + clipboardManager.setPrimaryClip(clipData); + Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); + } + + private void close() { + finish(); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java new file mode 100644 index 00000000..4760c07d --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java @@ -0,0 +1,166 @@ +package it.niedermann.owncloud.notes.exception; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotSupportedException; +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; +import com.nextcloud.android.sso.exceptions.TokenMismatchException; + +import org.json.JSONException; + +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.DialogExceptionBinding; +import it.niedermann.owncloud.notes.databinding.ItemTipBinding; + +import static it.niedermann.owncloud.notes.shared.util.ClipboardUtil.copyToClipboard; + +public class ExceptionDialogFragment extends AppCompatDialogFragment { + + private static final String KEY_THROWABLES = "throwables"; + + @NonNull + private ArrayList throwables = new ArrayList<>(); + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + final Bundle args = getArguments(); + if (args != null) { + final Object throwablesArgument = args.getSerializable(KEY_THROWABLES); + if (throwablesArgument != null) { + throwables.addAll((ArrayList) throwablesArgument); + } + } + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final View view = View.inflate(getContext(), R.layout.dialog_exception, null); + final DialogExceptionBinding binding = DialogExceptionBinding.bind(view); + + final TipsAdapter adapter = new TipsAdapter(); + + final String debugInfos = ExceptionUtil.getDebugInfos(requireContext(), throwables); + + binding.tips.setAdapter(adapter); + binding.statusMessage.setText(getString(R.string.error_sync, throwables.size() > 0 ? throwables.get(0).getLocalizedMessage() : getString(R.string.error_unknown))); + binding.stacktrace.setText(debugInfos); + + for (Throwable t : throwables) { + if (t instanceof TokenMismatchException) { + adapter.add(R.string.error_dialog_tip_token_mismatch_retry); + adapter.add(R.string.error_dialog_tip_token_mismatch_clear_storage); + adapter.add(R.string.error_dialog_tip_clear_storage); + } else if (t instanceof NextcloudFilesAppNotSupportedException) { + adapter.add(R.string.error_dialog_tip_files_outdated); + } else if (t instanceof NextcloudApiNotRespondingException) { + adapter.add(R.string.error_dialog_tip_files_force_stop); + adapter.add(R.string.error_dialog_tip_files_delete_storage); + } else if (t instanceof SocketTimeoutException || t instanceof ConnectException) { + adapter.add(R.string.error_dialog_timeout_instance); + adapter.add(R.string.error_dialog_timeout_toggle); + } else if (t instanceof JSONException || t instanceof NullPointerException) { + adapter.add(R.string.error_dialog_check_server); + } else if (t instanceof NextcloudHttpRequestFailedException) { + int statusCode = ((NextcloudHttpRequestFailedException) t).getStatusCode(); + switch (statusCode) { + case 302: + adapter.add(R.string.error_dialog_server_app_enabled); + adapter.add(R.string.error_dialog_redirect); + break; + case 500: + adapter.add(R.string.error_dialog_check_server_logs); + break; + case 503: + adapter.add(R.string.error_dialog_check_maintenance); + break; + case 507: + adapter.add(R.string.error_dialog_insufficient_storage); + break; + } + } + } + + return new AlertDialog.Builder(requireActivity()) + .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```")) + .setNegativeButton(R.string.simple_close, null) + .create(); + } + + public static DialogFragment newInstance(ArrayList exceptions) { + final Bundle args = new Bundle(); + args.putSerializable(KEY_THROWABLES, exceptions); + final DialogFragment fragment = new ExceptionDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + public static DialogFragment newInstance(Throwable exception) { + final Bundle args = new Bundle(); + final ArrayList list = new ArrayList<>(1); + list.add(exception); + args.putSerializable(KEY_THROWABLES, list); + final DialogFragment fragment = new ExceptionDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + private static class TipsAdapter extends RecyclerView.Adapter { + + @NonNull + private List tips = new LinkedList<>(); + + @NonNull + @Override + public TipsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_tip, parent, false); + return new TipsViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull TipsViewHolder holder, int position) { + holder.binding.tip.setText(tips.get(position)); + } + + @Override + public int getItemCount() { + return tips.size(); + } + + private void add(@StringRes int tip) { + tips.add(tip); + notifyItemInserted(tips.size()); + } + } + + private static class TipsViewHolder extends RecyclerView.ViewHolder { + private final ItemTipBinding binding; + + private TipsViewHolder(@NonNull View itemView) { + super(itemView); + binding = ItemTipBinding.bind(itemView); + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionHandler.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionHandler.java new file mode 100644 index 00000000..d4fa7986 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionHandler.java @@ -0,0 +1,46 @@ +package it.niedermann.owncloud.notes.exception; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.NonNull; + + +public class ExceptionHandler implements Thread.UncaughtExceptionHandler { + + private static final String TAG = ExceptionHandler.class.getSimpleName(); + private Context context; + private Class errorActivity; + public static final String KEY_THROWABLE = "T"; + + public ExceptionHandler(Context context) { + super(); + this.context = context; + this.errorActivity = ExceptionActivity.class; + } + + public ExceptionHandler(Context context, Class errorActivity) { + super(); + this.context = context; + this.errorActivity = errorActivity; + } + + @Override + public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { + Log.e(TAG, e.getMessage(), e); + Intent intent = new Intent(context.getApplicationContext(), errorActivity); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + Bundle extras = new Bundle(); + intent.putExtra(KEY_THROWABLE, e); + extras.putSerializable(KEY_THROWABLE, e); + intent.putExtras(extras); + context.getApplicationContext().startActivity(intent); + if (context instanceof Activity) { + ((Activity) context).finish(); + } + Runtime.getRuntime().exit(0); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionUtil.java new file mode 100644 index 00000000..23f11ee0 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionUtil.java @@ -0,0 +1,71 @@ +package it.niedermann.owncloud.notes.exception; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.annotation.NonNull; + +import com.nextcloud.android.sso.helper.VersionCheckHelper; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.owncloud.notes.BuildConfig; + +public class ExceptionUtil { + + private ExceptionUtil() { + + } + + public static String getDebugInfos(Context context, Throwable throwable) { + List throwables = new ArrayList<>(); + throwables.add(throwable); + return getDebugInfos(context, throwables); + } + + public static String getDebugInfos(@NonNull Context context, List throwables) { + StringBuilder debugInfos = new StringBuilder() + .append(getAppVersions(context)) + .append("\n\n---\n") + .append(getDeviceInfos()) + .append("\n\n---"); + for (Throwable throwable : throwables) { + debugInfos.append("\n\n").append(getStacktraceOf(throwable)); + } + return debugInfos.toString(); + } + + private static String getAppVersions(Context context) { + String versions = "" + + "App Version: " + BuildConfig.VERSION_NAME + "\n" + + "App Version Code: " + BuildConfig.VERSION_CODE + "\n" + + "App Flavor: " + BuildConfig.FLAVOR + "\n"; + + try { + versions += "\nFiles App Version Code: " + VersionCheckHelper.getNextcloudFilesVersionCode(context); + } catch (PackageManager.NameNotFoundException e) { + versions += "\nFiles 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(); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/formattinghelp/FormattingHelpActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/formattinghelp/FormattingHelpActivity.java deleted file mode 100644 index 10bfb6e1..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/formattinghelp/FormattingHelpActivity.java +++ /dev/null @@ -1,252 +0,0 @@ -package it.niedermann.owncloud.notes.formattinghelp; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Typeface; -import android.net.Uri; -import android.os.Bundle; -import android.text.TextUtils; -import android.text.method.LinkMovementMethod; -import android.util.TypedValue; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceManager; - -import com.yydcdut.markdown.MarkdownProcessor; -import com.yydcdut.markdown.syntax.text.TextFactory; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandedActivity; -import it.niedermann.owncloud.notes.databinding.ActivityFormattingHelpBinding; - -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_CHECKED_MINUS; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_CHECKED_STAR; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_UNCHECKED_MINUS; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_UNCHECKED_STAR; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.getMarkDownConfiguration; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.parseCompat; -import static it.niedermann.owncloud.notes.util.NoteUtil.getFontSizeFromPreferences; - -public class FormattingHelpActivity extends BrandedActivity { - - private ActivityFormattingHelpBinding binding; - private String content; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityFormattingHelpBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setSupportActionBar(binding.toolbar); - - content = buildFormattingHelp(); - - final MarkdownProcessor markdownProcessor = new MarkdownProcessor(this); - markdownProcessor.factory(TextFactory.create()); - markdownProcessor.config(getMarkDownConfiguration(binding.content.getContext()) - .setOnTodoClickCallback((view, line, lineNumber) -> { - try { - String[] lines = TextUtils.split(content, "\\r?\\n"); - /* - * Workaround for RxMarkdown-bug: - * When (un)checking a checkbox in a note which contains code-blocks, the "`"-characters get stripped out in the TextView and therefore the given lineNumber is wrong - * Find number of lines starting with ``` before lineNumber - */ - boolean inCodefence = false; - for (int i = 0; i < lines.length; i++) { - if (lines[i].startsWith("```")) { - inCodefence = !inCodefence; - lineNumber++; - } - if (inCodefence && TextUtils.isEmpty(lines[i])) { - lineNumber++; - } - if (i == lineNumber) { - break; - } - } - - /* - * Workaround for multiple RxMarkdown-bugs: - * When (un)checking a checkbox which is in the last line, every time it gets toggled, the last character of the line gets lost. - * When (un)checking a checkbox, every markdown gets stripped in the given line argument - */ - if (lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_MINUS) || lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_STAR)) { - lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_MINUS, CHECKBOX_CHECKED_MINUS); - lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_STAR, CHECKBOX_CHECKED_STAR); - } else { - lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_MINUS, CHECKBOX_UNCHECKED_MINUS); - lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_STAR, CHECKBOX_UNCHECKED_STAR); - } - - content = TextUtils.join("\n", lines); - binding.content.setText(parseCompat(markdownProcessor, content)); - } catch (IndexOutOfBoundsException e) { - Toast.makeText(this, R.string.checkbox_could_not_be_toggled, Toast.LENGTH_SHORT).show(); - e.printStackTrace(); - } - return line; - } - ) - .setOnLinkClickCallback((view, link) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(link)))) - .build()); - binding.content.setMovementMethod(LinkMovementMethod.getInstance()); - binding.content.setText(parseCompat(markdownProcessor, content)); - - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - binding.content.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(this, sp)); - if (sp.getBoolean(getString(R.string.pref_key_font), false)) { - binding.content.setTypeface(Typeface.MONOSPACE); - } - } - - @NonNull - private String buildFormattingHelp() { - final String lineBreak = "\n"; - final String indention = " "; - final String divider = getString(R.string.formatting_help_divider); - final String codefence = getString(R.string.formatting_help_codefence); - - int numberedListItem = 1; - final String lists = getString(R.string.formatting_help_lists_body_1) + lineBreak + - lineBreak + - getString(R.string.formatting_help_ol, numberedListItem++, getString(R.string.formatting_help_lists_body_2)) + lineBreak + - getString(R.string.formatting_help_ol, numberedListItem++, getString(R.string.formatting_help_lists_body_3)) + lineBreak + - getString(R.string.formatting_help_ol, numberedListItem, getString(R.string.formatting_help_lists_body_4)) + lineBreak + - lineBreak + - getString(R.string.formatting_help_lists_body_5) + lineBreak + - lineBreak + - getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_6)) + lineBreak + - getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_7)) + lineBreak + - indention + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_8)) + lineBreak + - indention + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_9)) + lineBreak; - - final String checkboxes = getString(R.string.formatting_help_checkboxes_body_1) + lineBreak + - lineBreak + - getString(R.string.formatting_help_checkbox_checked, getString(R.string.formatting_help_checkboxes_body_2)) + lineBreak + - getString(R.string.formatting_help_checkbox_unchecked, getString(R.string.formatting_help_checkboxes_body_3)) + lineBreak; - - final String structuredDocuments = getString(R.string.formatting_help_structured_documents_body_1, "`#`", "`##`") + lineBreak + - lineBreak + - getString(R.string.formatting_help_title_level_3, getString(R.string.formatting_help_structured_documents_body_2)) + lineBreak + - lineBreak + - getString(R.string.formatting_help_structured_documents_body_3, "`#`", "`######`") + lineBreak + - lineBreak + - getString(R.string.formatting_help_structured_documents_body_4, getString(R.string.formatting_help_quote_keyword)) + lineBreak + - lineBreak + - getString(R.string.formatting_help_quote, getString(R.string.formatting_help_structured_documents_body_5)) + lineBreak + - getString(R.string.formatting_help_quote, getString(R.string.formatting_help_structured_documents_body_6)) + lineBreak; - - final String javascript = getString(R.string.formatting_help_javascript_1) + lineBreak + - indention + indention + getString(R.string.formatting_help_javascript_2) + lineBreak + - getString(R.string.formatting_help_javascript_3) + lineBreak; - - return getString(R.string.formatting_help_title, getString(R.string.formatting_help_cbf_title)) + lineBreak + - lineBreak + - getString(R.string.formatting_help_cbf_body_1) + lineBreak + - getString(R.string.formatting_help_cbf_body_2, - getString(R.string.formatting_help_codefence_inline, getString(android.R.string.cut)), - getString(R.string.formatting_help_codefence_inline, getString(android.R.string.copy)), - getString(R.string.formatting_help_codefence_inline, getString(android.R.string.selectAll)), - getString(R.string.formatting_help_codefence_inline, getString(R.string.simple_link)), - getString(R.string.formatting_help_codefence_inline, getString(R.string.simple_checkbox)) - ) + lineBreak + - lineBreak + - divider + lineBreak + - lineBreak + - getString(R.string.formatting_help_title, getString(R.string.formatting_help_text_title)) + lineBreak + - lineBreak + - getString(R.string.formatting_help_text_body, - getString(R.string.formatting_help_bold), - getString(R.string.formatting_help_italic), - getString(R.string.formatting_help_strike_through) - ) + lineBreak + - lineBreak + - codefence + lineBreak + - getString(R.string.formatting_help_text_body, - getString(R.string.formatting_help_bold), - getString(R.string.formatting_help_italic), - getString(R.string.formatting_help_strike_through) - ) + lineBreak + - codefence + lineBreak + - lineBreak + - divider + lineBreak + - lineBreak + - getString(R.string.formatting_help_title, getString(R.string.formatting_help_lists_title)) + lineBreak + - lineBreak + - lists + - lineBreak + - codefence + lineBreak + - lists + - codefence + lineBreak + - lineBreak + - divider + lineBreak + - lineBreak + - getString(R.string.formatting_help_title, getString(R.string.formatting_help_checkboxes_title)) + lineBreak + - lineBreak + - checkboxes + - lineBreak + - codefence + lineBreak + - checkboxes + - codefence + lineBreak + - lineBreak + - divider + lineBreak + - lineBreak + - getString(R.string.formatting_help_title, getString(R.string.formatting_help_structured_documents_title)) + lineBreak + - lineBreak + - structuredDocuments + - lineBreak + - codefence + lineBreak + - structuredDocuments + - codefence + lineBreak + - lineBreak + - divider + lineBreak + - lineBreak + - getString(R.string.formatting_help_title, getString(R.string.formatting_help_code_title)) + lineBreak + - lineBreak + - getString(R.string.formatting_help_code_body_1) + lineBreak + - lineBreak + - getString(R.string.formatting_help_codefence_inline_escaped, getString(R.string.formatting_help_code_javascript_inline)) + lineBreak + - getString(R.string.formatting_help_codefence_inline, getString(R.string.formatting_help_code_javascript_inline)) + lineBreak + - lineBreak + - getString(R.string.formatting_help_code_body_2) + lineBreak + - lineBreak + - getString(R.string.formatting_help_codefence_escaped) + lineBreak + - javascript + - getString(R.string.formatting_help_codefence_escaped) + lineBreak + - lineBreak + - codefence + lineBreak + - javascript + - codefence + lineBreak + - lineBreak + - getString(R.string.formatting_help_code_body_3) + lineBreak + - lineBreak + - getString(R.string.formatting_help_codefence_javascript_escaped) + lineBreak + - javascript + - getString(R.string.formatting_help_codefence_escaped) + lineBreak + - lineBreak + - getString(R.string.formatting_help_codefence_javascript) + lineBreak + - javascript + - codefence + lineBreak + - lineBreak + - divider + lineBreak + - lineBreak + - getString(R.string.formatting_help_title, getString(R.string.formatting_help_unsupported_title)) + lineBreak + - lineBreak + - getString(R.string.formatting_help_unsupported_body_1) + lineBreak + - lineBreak + - getString(R.string.formatting_help_ul, getString(R.string.formatting_help_unsupported_body_2)) + lineBreak + - getString(R.string.formatting_help_ul, getString(R.string.formatting_help_unsupported_body_3)) + lineBreak + - lineBreak + - getString(R.string.formatting_help_unsupported_body_4) + lineBreak; - } - - @Override - public void applyBrand(int mainColor, int textColor) { - applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java new file mode 100644 index 00000000..1163c053 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -0,0 +1,978 @@ +package it.niedermann.owncloud.notes.main; + +import android.animation.AnimatorInflater; +import android.annotation.SuppressLint; +import android.app.SearchManager; +import android.content.Intent; +import android.database.sqlite.SQLiteException; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.appcompat.view.ActionMode; +import androidx.appcompat.widget.SearchView; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; +import com.nextcloud.android.sso.AccountImporter; +import com.nextcloud.android.sso.exceptions.AccountImportCancelledException; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; +import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; +import com.nextcloud.android.sso.exceptions.TokenMismatchException; +import com.nextcloud.android.sso.helper.SingleAccountHelper; +import com.nextcloud.android.sso.model.SingleSignOnAccount; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.about.AboutActivity; +import it.niedermann.owncloud.notes.accountswitcher.AccountSwitcherDialog; +import it.niedermann.owncloud.notes.accountswitcher.AccountSwitcherListener; +import it.niedermann.owncloud.notes.LockedActivity; +import it.niedermann.owncloud.notes.edit.EditNoteActivity; +import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment; +import it.niedermann.owncloud.notes.branding.BrandedSnackbar; +import it.niedermann.owncloud.notes.branding.BrandingUtil; +import it.niedermann.owncloud.notes.databinding.ActivityNotesListViewBinding; +import it.niedermann.owncloud.notes.databinding.DrawerLayoutBinding; +import it.niedermann.owncloud.notes.FormattingHelpActivity; +import it.niedermann.owncloud.notes.accountpicker.AccountPickerListener; +import it.niedermann.owncloud.notes.shared.model.Capabilities; +import it.niedermann.owncloud.notes.shared.model.Category; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.shared.model.Item; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; +import it.niedermann.owncloud.notes.main.NavigationAdapter.CategoryNavigationItem; +import it.niedermann.owncloud.notes.main.NavigationAdapter.NavigationItem; +import it.niedermann.owncloud.notes.shared.model.NoteClickListener; +import it.niedermann.owncloud.notes.main.items.ItemAdapter; +import it.niedermann.owncloud.notes.main.items.section.SectionItemDecoration; +import it.niedermann.owncloud.notes.main.items.grid.GridItemDecoration; +import it.niedermann.owncloud.notes.main.items.list.NotesListViewItemTouchHelper; +import it.niedermann.owncloud.notes.persistence.CapabilitiesClient; +import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker; +import it.niedermann.owncloud.notes.persistence.LoadNotesListTask; +import it.niedermann.owncloud.notes.persistence.LoadNotesListTask.NotesLoadedListener; +import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper; +import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper.ViewProvider; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.preferences.PreferencesActivity; +import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; +import it.niedermann.owncloud.notes.shared.util.NoteUtil; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; +import static it.niedermann.owncloud.notes.shared.util.ColorUtil.contrastRatioIsSufficient; +import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive; +import static it.niedermann.owncloud.notes.NotesApplication.isGridViewEnabled; +import static it.niedermann.owncloud.notes.shared.util.SSOUtil.askForNewAccount; +import static java.util.Arrays.asList; + +public class MainActivity extends LockedActivity implements NoteClickListener, ViewProvider, AccountPickerListener, AccountSwitcherListener { + + private static final String TAG = MainActivity.class.getSimpleName(); + + private boolean gridView = true; + + public static final String CREATED_NOTE = "it.niedermann.owncloud.notes.created_notes"; + public static final String ADAPTER_KEY_RECENT = "recent"; + public static final String ADAPTER_KEY_STARRED = "starred"; + public static final String ACTION_FAVORITES = "it.niedermann.owncloud.notes.favorites"; + public static final String ACTION_RECENT = "it.niedermann.owncloud.notes.recent"; + + private static final String SAVED_STATE_NAVIGATION_SELECTION = "navigationSelection"; + private static final String SAVED_STATE_NAVIGATION_ADAPTER_SLECTION = "navigationAdapterSelection"; + private static final String SAVED_STATE_NAVIGATION_OPEN = "navigationOpen"; + + private final static int create_note_cmd = 0; + private final static int show_single_note_cmd = 1; + private final static int server_settings = 2; + private final static int about = 3; + public final static int manage_account = 4; + + /** + * Used to detect the onResume() call after the import dialog has been displayed. + * https://github.com/stefan-niedermann/nextcloud-notes/pull/599/commits/f40eab402d122f113020200751894fa39c8b9fcc#r334239634 + */ + private boolean notAuthorizedAccountHandled = false; + + protected SingleSignOnAccount ssoAccount; + protected LocalAccount localAccount; + + protected DrawerLayoutBinding binding; + protected ActivityNotesListViewBinding activityBinding; + + private CoordinatorLayout coordinatorLayout; + private SwipeRefreshLayout swipeRefreshLayout; + protected FloatingActionButton fabCreate; + private RecyclerView listView; + + protected ItemAdapter adapter; + + protected NotesDatabase db = null; + private NavigationAdapter adapterCategories; + private NavigationItem itemRecent; + private NavigationItem itemFavorites; + private NavigationItem itemUncategorized; + @NonNull + private Category navigationSelection = new Category(null, null); + private String navigationOpen = ""; + private ActionMode mActionMode; + private final ISyncCallback syncCallBack = () -> { + adapter.clearSelection(listView); + if (mActionMode != null) { + mActionMode.finish(); + } + refreshLists(); + swipeRefreshLayout.setRefreshing(false); + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + CapabilitiesWorker.update(this); + binding = DrawerLayoutBinding.inflate(getLayoutInflater()); + activityBinding = ActivityNotesListViewBinding.bind(binding.activityNotesListView.getRoot()); + + setContentView(binding.getRoot()); + + this.coordinatorLayout = binding.activityNotesListView.activityNotesListView; + this.swipeRefreshLayout = binding.activityNotesListView.swiperefreshlayout; + this.fabCreate = binding.activityNotesListView.fabCreate; + this.listView = binding.activityNotesListView.recyclerView; + + String categoryAdapterSelectedItem = ADAPTER_KEY_RECENT; + if (savedInstanceState == null) { + if (ACTION_RECENT.equals(getIntent().getAction())) { + categoryAdapterSelectedItem = ADAPTER_KEY_RECENT; + } else if (ACTION_FAVORITES.equals(getIntent().getAction())) { + categoryAdapterSelectedItem = ADAPTER_KEY_STARRED; + navigationSelection = new Category(null, true); + } + } else { + Object savedCategory = savedInstanceState.getSerializable(SAVED_STATE_NAVIGATION_SELECTION); + if (savedCategory != null) { + navigationSelection = (Category) savedCategory; + } + navigationOpen = savedInstanceState.getString(SAVED_STATE_NAVIGATION_OPEN); + categoryAdapterSelectedItem = savedInstanceState.getString(SAVED_STATE_NAVIGATION_ADAPTER_SLECTION); + } + + db = NotesDatabase.getInstance(this); + + gridView = isGridViewEnabled(); + if (!gridView || isDarkThemeActive(this)) { + activityBinding.activityNotesListView.setBackgroundColor(ContextCompat.getColor(this, R.color.primary)); + } + + setupToolbars(); + setupNavigationList(categoryAdapterSelectedItem); + setupNavigationMenu(); + setupNotesList(); + } + + @Override + protected void onResume() { + try { + ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()); + if (localAccount == null || !localAccount.getAccountName().equals(ssoAccount.name)) { + selectAccount(ssoAccount.name); + } + } catch (NoCurrentAccountSelectedException | NextcloudFilesAppAccountNotFoundException e) { + if (localAccount == null) { + List localAccounts = db.getAccounts(); + if (localAccounts.size() > 0) { + localAccount = localAccounts.get(0); + } + } + if (!notAuthorizedAccountHandled) { + handleNotAuthorizedAccount(); + } + } + + // refresh and sync every time the activity gets + refreshLists(); + if (localAccount != null) { + synchronize(); + db.getNoteServerSyncHelper().addCallbackPull(ssoAccount, syncCallBack); + } + super.onResume(); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (localAccount != null) { + outState.putSerializable(SAVED_STATE_NAVIGATION_SELECTION, navigationSelection); + outState.putString(SAVED_STATE_NAVIGATION_ADAPTER_SLECTION, adapterCategories.getSelectedItem()); + outState.putString(SAVED_STATE_NAVIGATION_OPEN, navigationOpen); + } + } + + private void selectAccount(String accountName) { + fabCreate.hide(); + SingleAccountHelper.setCurrentAccount(getApplicationContext(), accountName); + localAccount = db.getLocalAccountByAccountName(accountName); + if (localAccount != null) { + try { + BrandingUtil.saveBrandColors(this, localAccount.getColor(), localAccount.getTextColor()); + ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()); + new NotesListViewItemTouchHelper(ssoAccount, this, db, adapter, syncCallBack, this::refreshLists, swipeRefreshLayout, this, gridView) + .attachToRecyclerView(listView); + synchronize(); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + Log.i(TAG, "Tried to select account, but got an " + e.getClass().getSimpleName() + ". Asking for importing an account..."); + handleNotAuthorizedAccount(); + } + refreshLists(); + fabCreate.show(); + activityBinding.launchAccountSwitcher.setOnClickListener((v) -> { + if (localAccount == null) { + handleNotAuthorizedAccount(); + } else { + AccountSwitcherDialog.newInstance(localAccount.getId()).show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName()); + } + }); + setupNavigationList(ADAPTER_KEY_RECENT); + } else { + if (!notAuthorizedAccountHandled) { + handleNotAuthorizedAccount(); + } + binding.navigationList.setAdapter(null); + } + updateCurrentAccountAvatar(); + } + + private void handleNotAuthorizedAccount() { + fabCreate.hide(); + swipeRefreshLayout.setRefreshing(false); + askForNewAccount(this); + notAuthorizedAccountHandled = true; + } + + private void setupToolbars() { + setSupportActionBar(binding.activityNotesListView.toolbar); + updateCurrentAccountAvatar(); + activityBinding.homeToolbar.setOnClickListener((v) -> { + if (activityBinding.toolbar.getVisibility() == GONE) { + updateToolbars(false); + } + }); + + activityBinding.launchAccountSwitcher.setOnClickListener((v) -> askForNewAccount(this)); + activityBinding.menuButton.setOnClickListener((v) -> binding.drawerLayout.openDrawer(GravityCompat.START)); + + final LinearLayout searchEditFrame = activityBinding.searchView.findViewById(R.id + .search_edit_frame); + + searchEditFrame.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + int oldVisibility = -1; + + @Override + public void onGlobalLayout() { + int currentVisibility = searchEditFrame.getVisibility(); + + if (currentVisibility != oldVisibility) { + if (currentVisibility == VISIBLE) { + fabCreate.hide(); + } else { + new Handler().postDelayed(() -> fabCreate.show(), 150); + } + + oldVisibility = currentVisibility; + } + } + + }); + activityBinding.searchView.setOnCloseListener(() -> { + if (activityBinding.toolbar.getVisibility() == VISIBLE && TextUtils.isEmpty(activityBinding.searchView.getQuery())) { + updateToolbars(true); + return true; + } + return false; + }); + activityBinding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + refreshLists(); + return true; + } + }); + } + + private void setupNotesList() { + initRecyclerView(); + + ((RecyclerView) findViewById(R.id.recycler_view)).addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0) + fabCreate.hide(); + else if (dy < 0) + fabCreate.show(); + } + }); + + swipeRefreshLayout.setOnRefreshListener(() -> { + if (ssoAccount == null) { + swipeRefreshLayout.setRefreshing(false); + askForNewAccount(this); + } else { + Log.i(TAG, "Clearing Glide memory cache"); + Glide.get(this).clearMemory(); + new Thread(() -> { + Log.i(TAG, "Clearing Glide disk cache"); + Glide.get(getApplicationContext()).clearDiskCache(); + }).start(); + new Thread(() -> { + Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name); + final Capabilities capabilities; + try { + capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, localAccount.getCapabilitiesETag()); + db.updateCapabilitiesETag(localAccount.getId(), capabilities.getETag()); + db.updateBrand(localAccount.getId(), capabilities); + db.updateBrand(localAccount.getId(), capabilities); + localAccount.setColor(Color.parseColor(capabilities.getColor())); + localAccount.setTextColor(Color.parseColor(capabilities.getTextColor())); + BrandingUtil.saveBrandColors(this, localAccount.getColor(), localAccount.getTextColor()); + db.updateApiVersion(localAccount.getId(), capabilities.getApiVersion()); + Log.i(TAG, capabilities.toString()); + } catch (Exception e) { + if (e instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + Log.i(TAG, "Capabilities not modified."); + } else { + e.printStackTrace(); + } + } finally { + // Even if the capabilities endpoint makes trouble, we can still try to synchronize the notes + synchronize(); + } + }).start(); + } + }); + + // Floating Action Button + fabCreate.setOnClickListener((View view) -> { + Intent createIntent = new Intent(getApplicationContext(), EditNoteActivity.class); + createIntent.putExtra(EditNoteActivity.PARAM_CATEGORY, navigationSelection); + if (activityBinding.searchView.getQuery().length() > 0) { + createIntent.putExtra(EditNoteActivity.PARAM_CONTENT, activityBinding.searchView.getQuery().toString()); + invalidateOptionsMenu(); + } + startActivityForResult(createIntent, create_note_cmd); + }); + + activityBinding.sortingMethod.setOnClickListener((v) -> { + CategorySortingMethod method; + + method = db.getCategoryOrder(localAccount.getId(), navigationSelection); + + if (method == CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC) { + method = CategorySortingMethod.SORT_MODIFIED_DESC; + } else { + method = CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC; + } + db.modifyCategoryOrder(localAccount.getId(), navigationSelection, method); + refreshLists(); + updateSortMethodIcon(localAccount.getId()); + }); + } + + private void setupNavigationList(final String selectedItem) { + itemRecent = new NavigationItem(ADAPTER_KEY_RECENT, getString(R.string.label_all_notes), null, R.drawable.ic_access_time_grey600_24dp); + itemFavorites = new NavigationItem(ADAPTER_KEY_STARRED, getString(R.string.label_favorites), null, R.drawable.ic_star_yellow_24dp); + adapterCategories = new NavigationAdapter(this, new NavigationAdapter.ClickListener() { + @Override + public void onItemClick(NavigationItem item) { + selectItem(item, true); + } + + private void selectItem(NavigationItem item, boolean closeNavigation) { + adapterCategories.setSelectedItem(item.id); + // update current selection + if (itemRecent.equals(item)) { + navigationSelection = new Category(null, null); + } else if (itemFavorites.equals(item)) { + navigationSelection = new Category(null, true); + } else if (itemUncategorized != null && itemUncategorized.equals(item)) { + navigationSelection = new Category("", null); + } else { + navigationSelection = new Category(item.label, null); + } + + // auto-close sub-folder in Navigation if selection is outside of that folder + if (navigationOpen != null) { + int slashIndex = navigationSelection.category == null ? -1 : navigationSelection.category.indexOf('/'); + String rootCategory = slashIndex < 0 ? navigationSelection.category : navigationSelection.category.substring(0, slashIndex); + if (!navigationOpen.equals(rootCategory)) { + navigationOpen = null; + } + } + + // update views + if (closeNavigation) { + binding.drawerLayout.closeDrawer(GravityCompat.START); + } + refreshLists(true); + } + + @Override + public void onIconClick(NavigationItem item) { + if (item.icon == NavigationAdapter.ICON_MULTIPLE && !item.label.equals(navigationOpen)) { + navigationOpen = item.label; + selectItem(item, false); + } else if (item.icon == NavigationAdapter.ICON_MULTIPLE || item.icon == NavigationAdapter.ICON_MULTIPLE_OPEN && item.label.equals(navigationOpen)) { + navigationOpen = null; + refreshLists(); + } else { + onItemClick(item); + } + } + }); + adapterCategories.setSelectedItem(selectedItem); + binding.navigationList.setAdapter(adapterCategories); + } + + @Override + public CoordinatorLayout getView() { + return this.coordinatorLayout; + } + + @Override + public void applyBrand(int mainColor, int textColor) { + applyBrandToPrimaryToolbar(activityBinding.appBar, activityBinding.toolbar); + applyBrandToFAB(mainColor, textColor, activityBinding.fabCreate); + + binding.headerView.setBackgroundColor(mainColor); + binding.appName.setTextColor(textColor); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activityBinding.progressCircular.getIndeterminateDrawable().setColorFilter(getSecondaryForegroundColorDependingOnTheme(this, mainColor), PorterDuff.Mode.SRC_IN); + } + + // TODO We assume, that the background of the spinner is always white + activityBinding.swiperefreshlayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : Color.BLACK); + binding.appName.setTextColor(textColor); + DrawableCompat.setTint(binding.logo.getDrawable(), textColor); + + adapter.applyBrand(mainColor, textColor); + adapterCategories.applyBrand(mainColor, textColor); + invalidateOptionsMenu(); + } + + @Override + public boolean onSupportNavigateUp() { + if (activityBinding.toolbar.getVisibility() == VISIBLE) { + updateToolbars(true); + return true; + } else { + return super.onSupportNavigateUp(); + } + } + + private class LoadCategoryListTask extends AsyncTask> { + @Override + protected List doInBackground(Void... voids) { + if (localAccount == null) { + return new ArrayList<>(); + } + List categories = db.getCategories(localAccount.getId()); + if (!categories.isEmpty() && categories.get(0).label.isEmpty()) { + itemUncategorized = categories.get(0); + itemUncategorized.label = getString(R.string.action_uncategorized); + itemUncategorized.icon = NavigationAdapter.ICON_NOFOLDER; + } else { + itemUncategorized = null; + } + + Map favorites = db.getFavoritesCount(localAccount.getId()); + //noinspection ConstantConditions + int numFavorites = favorites.containsKey("1") ? favorites.get("1") : 0; + //noinspection ConstantConditions + int numNonFavorites = favorites.containsKey("0") ? favorites.get("0") : 0; + itemFavorites.count = numFavorites; + itemRecent.count = numFavorites + numNonFavorites; + + ArrayList items = new ArrayList<>(); + items.add(itemRecent); + items.add(itemFavorites); + NavigationItem lastPrimaryCategory = null; + NavigationItem lastSecondaryCategory = null; + for (NavigationItem item : categories) { + int slashIndex = item.label.indexOf('/'); + String currentPrimaryCategory = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex); + String currentSecondaryCategory = null; + boolean isCategoryOpen = currentPrimaryCategory.equals(navigationOpen); + + if (isCategoryOpen && !currentPrimaryCategory.equals(item.label)) { + String currentCategorySuffix = item.label.substring(navigationOpen.length() + 1); + int subSlashIndex = currentCategorySuffix.indexOf('/'); + currentSecondaryCategory = subSlashIndex < 0 ? currentCategorySuffix : currentCategorySuffix.substring(0, subSlashIndex); + } + + boolean belongsToLastPrimaryCategory = lastPrimaryCategory != null && currentPrimaryCategory.equals(lastPrimaryCategory.label); + boolean belongsToLastSecondaryCategory = belongsToLastPrimaryCategory && lastSecondaryCategory != null && lastSecondaryCategory.label.equals(currentPrimaryCategory + "/" + currentSecondaryCategory); + + if (isCategoryOpen && !belongsToLastPrimaryCategory && currentSecondaryCategory != null) { + lastPrimaryCategory = new NavigationItem("category:" + currentPrimaryCategory, currentPrimaryCategory, 0, NavigationAdapter.ICON_MULTIPLE_OPEN); + items.add(lastPrimaryCategory); + belongsToLastPrimaryCategory = true; + } + + if (belongsToLastPrimaryCategory && belongsToLastSecondaryCategory) { + lastSecondaryCategory.count += item.count; + lastSecondaryCategory.icon = NavigationAdapter.ICON_SUB_MULTIPLE; + } else if (belongsToLastPrimaryCategory) { + if (isCategoryOpen) { + item.label = currentPrimaryCategory + "/" + currentSecondaryCategory; + item.id = "category:" + item.label; + item.icon = NavigationAdapter.ICON_SUB_FOLDER; + items.add(item); + lastSecondaryCategory = item; + } else { + lastPrimaryCategory.count += item.count; + lastPrimaryCategory.icon = NavigationAdapter.ICON_MULTIPLE; + lastSecondaryCategory = null; + } + } else { + if (isCategoryOpen) { + item.icon = NavigationAdapter.ICON_MULTIPLE_OPEN; + } else { + item.label = currentPrimaryCategory; + item.id = "category:" + item.label; + } + items.add(item); + lastPrimaryCategory = item; + lastSecondaryCategory = null; + } + } + return items; + } + + @Override + protected void onPostExecute(List items) { + adapterCategories.setItems(items); + } + } + + private void setupNavigationMenu() { + final NavigationItem itemFormattingHelp = new NavigationItem("formattingHelp", getString(R.string.action_formatting_help), null, R.drawable.ic_baseline_help_outline_24); + final NavigationItem itemTrashbin = new NavigationItem("trashbin", getString(R.string.action_trashbin), null, R.drawable.ic_delete_grey600_24dp); + final NavigationItem itemSettings = new NavigationItem("settings", getString(R.string.action_settings), null, R.drawable.ic_settings_grey600_24dp); + final NavigationItem itemAbout = new NavigationItem("about", getString(R.string.simple_about), null, R.drawable.ic_info_outline_grey600_24dp); + + NavigationAdapter adapterMenu = new NavigationAdapter(this, new NavigationAdapter.ClickListener() { + @Override + public void onItemClick(NavigationItem item) { + if (itemFormattingHelp.equals(item)) { + Intent formattingHelpIntent = new Intent(getApplicationContext(), FormattingHelpActivity.class); + startActivity(formattingHelpIntent); + } else if (itemSettings.equals(item)) { + Intent settingsIntent = new Intent(getApplicationContext(), PreferencesActivity.class); + startActivityForResult(settingsIntent, server_settings); + } else if (itemAbout.equals(item)) { + Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class); + startActivityForResult(aboutIntent, about); + } else if (itemTrashbin.equals(item) && localAccount != null) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(localAccount.getUrl() + "/index.php/apps/files/?dir=/&view=trashbin"))); + } + } + + @Override + public void onIconClick(NavigationItem item) { + onItemClick(item); + } + }); + adapterMenu.setItems(asList(itemFormattingHelp, itemTrashbin, itemSettings, itemAbout)); + binding.navigationMenu.setAdapter(adapterMenu); + } + + private void initRecyclerView() { + adapter = new ItemAdapter(this, gridView); + listView.setAdapter(adapter); + + if (gridView) { + int spanCount = getResources().getInteger(R.integer.grid_view_span_count); + StaggeredGridLayoutManager gridLayoutManager = new StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL); + listView.setLayoutManager(gridLayoutManager); + listView.addItemDecoration(new GridItemDecoration(adapter, spanCount, + getResources().getDimensionPixelSize(R.dimen.spacer_3x), + getResources().getDimensionPixelSize(R.dimen.spacer_5x), + getResources().getDimensionPixelSize(R.dimen.spacer_3x), + getResources().getDimensionPixelSize(R.dimen.spacer_1x), + getResources().getDimensionPixelSize(R.dimen.spacer_activity_sides) + getResources().getDimensionPixelSize(R.dimen.spacer_1x) + )); + } else { + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + listView.setLayoutManager(layoutManager); + listView.addItemDecoration(new SectionItemDecoration(adapter, + getResources().getDimensionPixelSize(R.dimen.spacer_6x), + getResources().getDimensionPixelSize(R.dimen.spacer_5x), + getResources().getDimensionPixelSize(R.dimen.spacer_1x), + 0 + )); + } + } + + private void refreshLists() { + refreshLists(false); + } + + private void refreshLists(final boolean scrollToTop) { + if (localAccount == null) { + fabCreate.hide(); + adapter.removeAll(); + return; + } + View emptyContentView = binding.activityNotesListView.emptyContentView.getRoot(); + emptyContentView.setVisibility(GONE); + binding.activityNotesListView.progressCircular.setVisibility(VISIBLE); + fabCreate.show(); + String subtitle; + if (navigationSelection.category != null) { + if (navigationSelection.category.isEmpty()) { + subtitle = getString(R.string.search_in_category, getString(R.string.action_uncategorized)); + } else { + subtitle = getString(R.string.search_in_category, NoteUtil.extendCategory(navigationSelection.category)); + } + } else if (navigationSelection.favorite != null && navigationSelection.favorite) { + subtitle = getString(R.string.search_in_category, getString(R.string.label_favorites)); + } else { + subtitle = getString(R.string.search_in_all); + } + activityBinding.searchText.setText(subtitle); + CharSequence query = null; + if (activityBinding.searchView.getQuery().length() != 0) { + query = activityBinding.searchView.getQuery(); + } + + NotesLoadedListener callback = (List notes, boolean showCategory, CharSequence searchQuery) -> { + adapter.setShowCategory(showCategory); + adapter.setHighlightSearchQuery(searchQuery); + adapter.setItemList(notes); + binding.activityNotesListView.progressCircular.setVisibility(GONE); + if (notes.size() > 0) { + emptyContentView.setVisibility(GONE); + } else { + emptyContentView.setVisibility(VISIBLE); + } + if (scrollToTop) { + listView.scrollToPosition(0); + } + }; + new LoadNotesListTask(localAccount.getId(), getApplicationContext(), callback, navigationSelection, query).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + new LoadCategoryListTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + updateSortMethodIcon(localAccount.getId()); + } + + /** + * Updates sorting method icon. + */ + private void updateSortMethodIcon(long localAccountId) { + CategorySortingMethod method = db.getCategoryOrder(localAccountId, navigationSelection); + if (method == CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC) { + activityBinding.sortingMethod.setImageResource(R.drawable.alphabetical_asc); + activityBinding.sortingMethod.setContentDescription(getString(R.string.sort_last_modified)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activityBinding.sortingMethod.setTooltipText(getString(R.string.sort_last_modified)); + } + } else { + activityBinding.sortingMethod.setImageResource(R.drawable.modification_desc); + activityBinding.sortingMethod.setContentDescription(getString(R.string.sort_alphabetically)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + activityBinding.sortingMethod.setTooltipText(getString(R.string.sort_alphabetically)); + } + } + } + + @Override + protected void onNewIntent(Intent intent) { + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + activityBinding.searchView.setQuery(intent.getStringExtra(SearchManager.QUERY), true); + } + super.onNewIntent(intent); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this); + } + + /** + * Handles the Results of started Sub Activities (Created Note, Edited Note) + * + * @param requestCode int to distinguish between the different Sub Activities + * @param resultCode int Return Code + * @param data Intent + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + switch (requestCode) { + case create_note_cmd: { + // Make sure the request was successful + if (resultCode == RESULT_OK) { + //not need because of db.synchronisation in createActivity + + Bundle bundle = data.getExtras(); + if (bundle != null && bundle.containsKey(CREATED_NOTE)) { + DBNote createdNote = (DBNote) bundle.getSerializable(CREATED_NOTE); + if (createdNote != null) { + adapter.add(createdNote); + } else { + Log.w(TAG, "createdNote must not be null"); + } + } else { + Log.w(TAG, "Provide at least " + CREATED_NOTE); + } + } + listView.scrollToPosition(0); + break; + } + case server_settings: { + // Recreate activity completely, because theme switching makes problems when only invalidating the views. + // @see https://github.com/stefan-niedermann/nextcloud-notes/issues/529 + recreate(); + break; + } + case manage_account: { + if (resultCode == RESULT_FIRST_USER) { + selectAccount(null); + } + break; + } + default: { + try { + AccountImporter.onActivityResult(requestCode, resultCode, data, this, (ssoAccount) -> { + CapabilitiesWorker.update(this); + new Thread(() -> { + Log.i(TAG, "Added account: " + "name:" + ssoAccount.name + ", " + ssoAccount.url + ", userId" + ssoAccount.userId); + try { + Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name); + final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null); + db.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities); + Log.i(TAG, capabilities.toString()); + runOnUiThread(() -> selectAccount(ssoAccount.name)); + } catch (SQLiteException e) { + // Happens when upgrading from version ≤ 1.0.1 and importing the account + runOnUiThread(() -> selectAccount(ssoAccount.name)); + } catch (Exception e) { + // Happens when importing an already existing account the second time + if (e instanceof TokenMismatchException && db.getLocalAccountByAccountName(ssoAccount.name) != null) { + Log.w(TAG, "Received " + TokenMismatchException.class.getSimpleName() + " and the given ssoAccount.name (" + ssoAccount.name + ") does already exist in the database. Assume that this account has already been imported."); + runOnUiThread(() -> { + selectAccount(ssoAccount.name); + // TODO there is already a sync in progress and results in displaying a TokenMissMatchException snackbar which conflicts with this one + coordinatorLayout.post(() -> BrandedSnackbar.make(coordinatorLayout, R.string.account_already_imported, Snackbar.LENGTH_LONG).show()); + }); + } else { + e.printStackTrace(); + runOnUiThread(() -> { + binding.activityNotesListView.progressCircular.setVisibility(GONE); + ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + }); + } + } + }).start(); + }); + } catch (AccountImportCancelledException e) { + Log.i(TAG, "AccountImport has been cancelled."); + } + } + } + } + + private void updateCurrentAccountAvatar() { + try { + String url = localAccount.getUrl(); + if (url != null) { + Glide + .with(this) + .load(url + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64") + .placeholder(R.drawable.ic_account_circle_grey_24dp) + .error(R.drawable.ic_account_circle_grey_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(activityBinding.launchAccountSwitcher); + } else { + Log.w(TAG, "url is null"); + } + } catch (NullPointerException e) { // No local account - show generic header + Glide + .with(this) + .load(R.drawable.ic_account_circle_grey_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(activityBinding.launchAccountSwitcher); + Log.w(TAG, "Tried to update username in drawer, but localAccount was null"); + } + } + + @Override + public void onNoteClick(int position, View v) { + boolean hasCheckedItems = adapter.getSelected().size() > 0; + if (hasCheckedItems) { + if (!adapter.select(position)) { + v.setSelected(false); + adapter.deselect(position); + } else { + v.setSelected(true); + } + int size = adapter.getSelected().size(); + if (size > 0) { + mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, size, size)); + } else { + mActionMode.finish(); + } + } else { + DBNote note = (DBNote) adapter.getItem(position); + Intent intent = new Intent(getApplicationContext(), EditNoteActivity.class); + intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()); + startActivityForResult(intent, show_single_note_cmd); + } + } + + @Override + public void onNoteFavoriteClick(int position, View view) { + DBNote note = (DBNote) adapter.getItem(position); + NotesDatabase db = NotesDatabase.getInstance(view.getContext()); + db.toggleFavorite(ssoAccount, note, syncCallBack); + adapter.notifyItemChanged(position); + refreshLists(); + } + + @Override + public boolean onNoteLongClick(int position, View v) { + boolean selected = adapter.select(position); + if (selected) { + v.setSelected(true); + mActionMode = startSupportActionMode(new MultiSelectedActionModeCallback( + this, this, db, localAccount.getId(), adapter, listView, this::refreshLists, getSupportFragmentManager(), activityBinding.searchView + )); + int checkedItemCount = adapter.getSelected().size(); + mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, checkedItemCount, checkedItemCount)); + } + return selected; + } + + @Override + public void onBackPressed() { + if (activityBinding.toolbar.getVisibility() == VISIBLE) { + updateToolbars(true); + } else { + super.onBackPressed(); + } + } + + @SuppressLint("PrivateResource") + private void updateToolbars(boolean disableSearch) { + activityBinding.homeToolbar.setVisibility(disableSearch ? VISIBLE : GONE); + activityBinding.toolbar.setVisibility(disableSearch ? GONE : VISIBLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activityBinding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator(activityBinding.appBar.getContext(), + disableSearch ? R.animator.appbar_elevation_off : R.animator.appbar_elevation_on)); + } else { + ViewCompat.setElevation(activityBinding.appBar, disableSearch ? 0 : getResources().getDimension(R.dimen.design_appbar_elevation)); + } + if (disableSearch) { + activityBinding.searchView.setQuery(null, true); + } + activityBinding.searchView.setIconified(disableSearch); + } + + private void synchronize() { + NoteServerSyncHelper syncHelper = db.getNoteServerSyncHelper(); + if (syncHelper.isSyncPossible()) { + swipeRefreshLayout.setRefreshing(true); + syncHelper.addCallbackPull(ssoAccount, syncCallBack); + syncHelper.scheduleSync(ssoAccount, false); + } else { // Sync is not possible + swipeRefreshLayout.setRefreshing(false); + if (syncHelper.isNetworkConnected() && syncHelper.isSyncOnlyOnWifi()) { + Log.d(TAG, "Network is connected, but sync is not possible"); + } else { + Log.d(TAG, "Sync is not possible, because network is not connected"); + BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show(); + } + } + } + + @Override + public void addAccount() { + askForNewAccount(this); + } + + @Override + public void onAccountChosen(LocalAccount localAccount) { + binding.drawerLayout.closeDrawer(GravityCompat.START); + selectAccount(localAccount.getAccountName()); + } + + @Override + public void onAccountDeleted(LocalAccount localAccount) { + db.deleteAccount(localAccount); + if (localAccount.getId() == this.localAccount.getId()) { + List remainingAccounts = db.getAccounts(); + if (remainingAccounts.size() > 0) { + this.localAccount = remainingAccounts.get(0); + selectAccount(this.localAccount.getAccountName()); + } else { + selectAccount(null); + askForNewAccount(this); + } + } + } + + @Override + public void onAccountPicked(@NonNull LocalAccount account) { + List selection = new ArrayList<>(adapter.getSelected()); + + adapter.deselect(0); + for (Integer i : selection) { + DBNote note = (DBNote) adapter.getItem(i); + db.moveNoteToAnotherAccount(ssoAccount, note.getAccountId(), db.getNote(note.getAccountId(), note.getId()), account.getId()); + RecyclerView.ViewHolder viewHolder = listView.findViewHolderForAdapterPosition(i); + if (viewHolder != null) { + viewHolder.itemView.setSelected(false); + } else { + Log.w(TAG, "Could not found viewholder to remove selection"); + } + } + + mActionMode.finish(); + refreshLists(); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java new file mode 100644 index 00000000..8a7d9ecb --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -0,0 +1,162 @@ +package it.niedermann.owncloud.notes.main; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.appcompat.view.ActionMode; +import androidx.appcompat.view.ActionMode.Callback; +import androidx.appcompat.widget.SearchView; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.snackbar.Snackbar; +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 java.util.ArrayList; +import java.util.List; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandedSnackbar; +import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment; +import it.niedermann.owncloud.notes.main.items.ItemAdapter; +import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper.ViewProvider; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.util.ShareUtil; + +public class MultiSelectedActionModeCallback implements Callback { + + @ColorInt + private int colorAccent; + + private final Context context; + private final ViewProvider viewProvider; + private final NotesDatabase db; + private final long currentLocalAccountId; + private final ItemAdapter adapter; + private final RecyclerView recyclerView; + private final Runnable refreshLists; + private final FragmentManager fragmentManager; + private final SearchView searchView; + + public MultiSelectedActionModeCallback( + Context context, ViewProvider viewProvider, NotesDatabase db, long currentLocalAccountId, ItemAdapter adapter, RecyclerView recyclerView, Runnable refreshLists, FragmentManager fragmentManager, SearchView searchView) { + this.context = context; + this.viewProvider = viewProvider; + this.db = db; + this.currentLocalAccountId = currentLocalAccountId; + this.adapter = adapter; + this.recyclerView = recyclerView; + this.refreshLists = refreshLists; + this.fragmentManager = fragmentManager; + this.searchView = searchView; + + final TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorAccent, typedValue, true); + colorAccent = typedValue.data; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // inflate contextual menu + mode.getMenuInflater().inflate(R.menu.menu_list_context_multiple, menu); + for (int i = 0; i < menu.size(); i++) { + Drawable drawable = menu.getItem(i).getIcon(); + if (drawable != null) { + drawable = DrawableCompat.wrap(drawable); + DrawableCompat.setTint(drawable, colorAccent); + menu.getItem(i).setIcon(drawable); + } + } + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + /** + * @param mode ActionMode - used to close the Action Bar after all work is done. + * @param item MenuItem - the item in the List that contains the Node + * @return boolean + */ + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_delete: + try { + SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context); + List deletedNotes = new ArrayList<>(); + List selection = adapter.getSelected(); + for (Integer i : selection) { + DBNote note = (DBNote) adapter.getItem(i); + deletedNotes.add(db.getNote(note.getAccountId(), note.getId())); + db.deleteNoteAndSync(ssoAccount, note.getId()); + } + mode.finish(); // Action picked, so close the CAB + //after delete selection has to be cleared + searchView.setIconified(true); + refreshLists.run(); + String deletedSnackbarTitle = deletedNotes.size() == 1 + ? context.getString(R.string.action_note_deleted, deletedNotes.get(0).getTitle()) + : context.getString(R.string.bulk_notes_deleted, deletedNotes.size()); + BrandedSnackbar.make(viewProvider.getView(), deletedSnackbarTitle, Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo, (View v) -> { + db.getNoteServerSyncHelper().addCallbackPush(ssoAccount, refreshLists::run); + for (DBNote deletedNote : deletedNotes) { + db.addNoteAndSync(ssoAccount, deletedNote.getAccountId(), deletedNote); + } + refreshLists.run(); + String restoreSnackbarTitle = deletedNotes.size() == 1 + ? context.getString(R.string.action_note_restored, deletedNotes.get(0).getTitle()) + : context.getString(R.string.bulk_notes_restored, deletedNotes.size()); + BrandedSnackbar.make(viewProvider.getView(), restoreSnackbarTitle, Snackbar.LENGTH_SHORT) + .show(); + }) + .show(); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + e.printStackTrace(); + } + return true; + case R.id.menu_move: + AccountPickerDialogFragment.newInstance(currentLocalAccountId).show(fragmentManager, MainActivity.class.getSimpleName()); + return true; + case R.id.menu_share: + final String subject = (adapter.getSelected().size() == 1) + ? ((DBNote) adapter.getItem(adapter.getSelected().get(0))).getTitle() + : context.getString(R.string.share_multiple, adapter.getSelected().size()); + final StringBuilder noteContents = new StringBuilder(); + for (Integer i : adapter.getSelected()) { + final DBNote noteWithoutContent = (DBNote) adapter.getItem(i); + final String tempFullNote = db.getNote(noteWithoutContent.getAccountId(), noteWithoutContent.getId()).getContent(); + if (!TextUtils.isEmpty(tempFullNote)) { + if (noteContents.length() > 0) { + noteContents.append("\n\n"); + } + noteContents.append(tempFullNote); + } + } + ShareUtil.openShareDialog(context, subject, noteContents.toString()); + return true; + default: + return false; + } + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + adapter.clearSelection(recyclerView); + adapter.notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/NavigationAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/NavigationAdapter.java new file mode 100644 index 00000000..d12a92e7 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/NavigationAdapter.java @@ -0,0 +1,171 @@ +package it.niedermann.owncloud.notes.main; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandingUtil; +import it.niedermann.owncloud.notes.databinding.ItemNavigationBinding; +import it.niedermann.owncloud.notes.shared.util.NoteUtil; + +public class NavigationAdapter extends RecyclerView.Adapter { + + @NonNull + private final Context context; + @ColorInt + private int mainColor; + @DrawableRes + public static final int ICON_FOLDER = R.drawable.ic_folder_grey600_24dp; + @DrawableRes + public static final int ICON_NOFOLDER = R.drawable.ic_folder_open_grey600_24dp; + @DrawableRes + public static final int ICON_SUB_FOLDER = R.drawable.ic_folder_grey600_18dp; + @DrawableRes + public static final int ICON_MULTIPLE = R.drawable.ic_create_new_folder_grey600_24dp; + @DrawableRes + public static final int ICON_MULTIPLE_OPEN = R.drawable.ic_folder_grey600_24dp; + @DrawableRes + public static final int ICON_SUB_MULTIPLE = R.drawable.ic_create_new_folder_grey600_18dp; + + public void applyBrand(int mainColor, int textColor) { + this.mainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, mainColor); + notifyDataSetChanged(); + } + + public static class NavigationItem { + @NonNull + public String id; + @NonNull + public String label; + @DrawableRes + public int icon; + @Nullable + public Integer count; + + public NavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon) { + this.id = id; + this.label = label; + this.count = count; + this.icon = icon; + } + } + + public static class CategoryNavigationItem extends NavigationItem { + @NonNull + public Long categoryId; + + public CategoryNavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon, @NonNull Long categoryId) { + super(id, label, count, icon); + this.categoryId = categoryId; + } + } + + class ViewHolder extends RecyclerView.ViewHolder { + @NonNull + private final View view; + + @NonNull + private final TextView name; + @NonNull + private final TextView count; + @NonNull + private final ImageView icon; + + private NavigationItem currentItem; + + ViewHolder(@NonNull View itemView, @NonNull final ClickListener clickListener) { + super(itemView); + view = itemView; + ItemNavigationBinding binding = ItemNavigationBinding.bind(view); + this.name = binding.navigationItemLabel; + this.count = binding.navigationItemCount; + this.icon = binding.navigationItemIcon; + icon.setOnClickListener(view -> clickListener.onIconClick(currentItem)); + itemView.setOnClickListener(view -> clickListener.onItemClick(currentItem)); + } + + private void bind(@NonNull NavigationItem item) { + currentItem = item; + boolean isSelected = item.id.equals(selectedItem); + name.setText(NoteUtil.extendCategory(item.label)); + count.setVisibility(item.count == null ? View.GONE : View.VISIBLE); + count.setText(String.valueOf(item.count)); + if (item.icon > 0) { + icon.setImageDrawable(DrawableCompat.wrap(icon.getResources().getDrawable(item.icon))); + icon.setVisibility(View.VISIBLE); + } else { + icon.setVisibility(View.GONE); + } + int textColor = isSelected ? mainColor : view.getResources().getColor(R.color.fg_default); + + name.setTextColor(textColor); + count.setTextColor(textColor); + icon.setColorFilter(isSelected ? textColor : 0); + + view.setSelected(isSelected); + } + } + + public interface ClickListener { + void onItemClick(NavigationItem item); + + void onIconClick(NavigationItem item); + } + + @NonNull + private List items = new ArrayList<>(); + private String selectedItem = null; + @NonNull + private final ClickListener clickListener; + + public NavigationAdapter(@NonNull Context context, @NonNull ClickListener clickListener) { + this.context = context; + this.mainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, BrandingUtil.readBrandMainColor(context)); + this.clickListener = clickListener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_navigation, parent, false); + return new ViewHolder(v, clickListener); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.bind(items.get(position)); + } + + @Override + public int getItemCount() { + return items.size(); + } + + public void setItems(@NonNull List items) { + this.items = items; + notifyDataSetChanged(); + } + + public void setSelectedItem(String id) { + selectedItem = id; + notifyDataSetChanged(); + } + + public String getSelectedItem() { + return selectedItem; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java new file mode 100644 index 00000000..645533a7 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java @@ -0,0 +1,260 @@ +package it.niedermann.owncloud.notes.main.items; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Px; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.Branded; +import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridBinding; +import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridOnlyTitleBinding; +import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithExcerptBinding; +import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithoutExcerptBinding; +import it.niedermann.owncloud.notes.databinding.ItemNotesListSectionItemBinding; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.Item; +import it.niedermann.owncloud.notes.shared.model.NoteClickListener; +import it.niedermann.owncloud.notes.main.items.grid.NoteViewGridHolder; +import it.niedermann.owncloud.notes.main.items.grid.NoteViewGridHolderOnlyTitle; +import it.niedermann.owncloud.notes.main.items.list.NoteViewHolderWithExcerpt; +import it.niedermann.owncloud.notes.main.items.list.NoteViewHolderWithoutExcerpt; +import it.niedermann.owncloud.notes.main.items.section.SectionItem; +import it.niedermann.owncloud.notes.main.items.section.SectionViewHolder; + +import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences; + +public class ItemAdapter extends RecyclerView.Adapter implements Branded { + + private static final String TAG = ItemAdapter.class.getSimpleName(); + + public static final int TYPE_SECTION = 0; + public static final int TYPE_NOTE_WITH_EXCERPT = 1; + public static final int TYPE_NOTE_WITHOUT_EXCERPT = 2; + public static final int TYPE_NOTE_ONLY_TITLE = 3; + + private final NoteClickListener noteClickListener; + private final boolean gridView; + private List itemList = new ArrayList<>(); + private boolean showCategory = true; + private CharSequence searchQuery; + private final List selected = new ArrayList<>(); + @Px + private final float fontSize; + private final boolean monospace; + @ColorInt + private int mainColor; + @ColorInt + private int textColor; + + public ItemAdapter(@NonNull T context, boolean gridView) { + this.noteClickListener = context; + this.gridView = gridView; + this.mainColor = context.getResources().getColor(R.color.defaultBrand); + this.textColor = Color.WHITE; + final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + this.fontSize = getFontSizeFromPreferences(context, sp); + this.monospace = sp.getBoolean(context.getString(R.string.pref_key_font), false); + // FIXME see getItemId() + // setHasStableIds(true); + } + + + /* + FIXME this causes {@link it.niedermann.owncloud.notes.noteslist.items.list.NotesListViewItemTouchHelper} to not call clearView anymore → After marking a note as favorite, it stays yellow. + @Override + public long getItemId(int position) { + return getItemViewType(position) == TYPE_SECTION + ? ((SectionItem) getItem(position)).getTitle().hashCode() * -1 + : ((DBNote) getItem(position)).getId(); + } + */ + + /** + * Updates the item list and notifies respective view to update. + * + * @param itemList List of items to be set + */ + public void setItemList(@NonNull List itemList) { + this.itemList = itemList; + notifyDataSetChanged(); + } + + /** + * Adds the given note to the top of the list. + * + * @param note Note that should be added. + */ + public void add(@NonNull DBNote note) { + itemList.add(0, note); + notifyItemInserted(0); + notifyItemChanged(0); + } + + /** + * Removes all items from the adapter. + */ + public void removeAll() { + itemList.clear(); + notifyDataSetChanged(); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (gridView) { + switch (viewType) { + case TYPE_SECTION: { + return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(LayoutInflater.from(parent.getContext()))); + } + case TYPE_NOTE_ONLY_TITLE: { + return new NoteViewGridHolderOnlyTitle(ItemNotesListNoteItemGridOnlyTitleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener, monospace, fontSize); + } + case TYPE_NOTE_WITH_EXCERPT: + case TYPE_NOTE_WITHOUT_EXCERPT: { + return new NoteViewGridHolder(ItemNotesListNoteItemGridBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener, monospace, fontSize); + } + default: { + throw new IllegalArgumentException("Not supported viewType: " + viewType); + } + } + } else { + switch (viewType) { + case TYPE_SECTION: { + return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(LayoutInflater.from(parent.getContext()))); + } + case TYPE_NOTE_WITH_EXCERPT: { + return new NoteViewHolderWithExcerpt(ItemNotesListNoteItemWithExcerptBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener); + } + case TYPE_NOTE_ONLY_TITLE: + case TYPE_NOTE_WITHOUT_EXCERPT: { + return new NoteViewHolderWithoutExcerpt(ItemNotesListNoteItemWithoutExcerptBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener); + } + default: { + throw new IllegalArgumentException("Not supported viewType: " + viewType); + } + } + } + } + + @Override + public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case TYPE_SECTION: { + ((SectionViewHolder) holder).bind((SectionItem) itemList.get(position)); + break; + } + case TYPE_NOTE_WITH_EXCERPT: + case TYPE_NOTE_WITHOUT_EXCERPT: + case TYPE_NOTE_ONLY_TITLE: { + ((NoteViewHolder) holder).bind((DBNote) itemList.get(position), showCategory, mainColor, textColor, searchQuery); + break; + } + } + } + + public boolean select(Integer position) { + return !selected.contains(position) && selected.add(position); + } + + public void clearSelection(@NonNull RecyclerView recyclerView) { + for (Integer i : getSelected()) { + RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(i); + if (viewHolder != null) { + viewHolder.itemView.setSelected(false); + } else { + Log.w(TAG, "Could not found viewholder to remove selection"); + } + } + selected.clear(); + } + + @NonNull + public List getSelected() { + return selected; + } + + public void deselect(Integer position) { + for (int i = 0; i < selected.size(); i++) { + if (selected.get(i).equals(position)) { + //position was selected and removed + selected.remove(i); + return; + } + } + // position was not selected + } + + public Item getItem(int notePosition) { + return itemList.get(notePosition); + } + + public void remove(@NonNull Item item) { + itemList.remove(item); + notifyDataSetChanged(); + } + + public void setShowCategory(boolean showCategory) { + this.showCategory = showCategory; + } + + @Override + public int getItemCount() { + return itemList.size(); + } + + @IntRange(from = 0, to = 3) + @Override + public int getItemViewType(int position) { + Item item = getItem(position); + if (item == null) { + throw new IllegalArgumentException("Item at position " + position + " must not be null"); + } + if (getItem(position).isSection()) return TYPE_SECTION; + DBNote note = (DBNote) getItem(position); + if (TextUtils.isEmpty(note.getExcerpt())) { + if (TextUtils.isEmpty(note.getCategory())) { + return TYPE_NOTE_ONLY_TITLE; + } else { + return TYPE_NOTE_WITHOUT_EXCERPT; + } + } + return TYPE_NOTE_WITH_EXCERPT; + } + + @Override + public void applyBrand(int mainColor, int textColor) { + this.mainColor = mainColor; + this.textColor = textColor; + notifyDataSetChanged(); + } + + public void setHighlightSearchQuery(CharSequence searchQuery) { + this.searchQuery = searchQuery; + } + + /** + * @return the position of the first item which matches the given viewtype, -1 if not available + */ + public int getFirstPositionOfViewType(@IntRange(from = 0, to = 3) int viewType) { + for (int i = 0; i < itemList.size(); i++) { + if (getItemViewType(i) == viewType) { + return i; + } + } + return -1; + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java new file mode 100644 index 00000000..13c4f874 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java @@ -0,0 +1,138 @@ +package it.niedermann.owncloud.notes.main.items; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.CallSuper; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.chip.Chip; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandingUtil; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.DBStatus; +import it.niedermann.owncloud.notes.shared.model.NoteClickListener; +import it.niedermann.owncloud.notes.NotesApplication; + +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static it.niedermann.owncloud.notes.shared.util.ColorUtil.contrastRatioIsSufficient; +import static it.niedermann.owncloud.notes.shared.util.ColorUtil.isColorDark; + +public abstract class NoteViewHolder extends RecyclerView.ViewHolder { + @NonNull + private final NoteClickListener noteClickListener; + + public NoteViewHolder(@NonNull View v, @NonNull NoteClickListener noteClickListener) { + super(v); + this.noteClickListener = noteClickListener; + } + + @CallSuper + public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { + itemView.setOnClickListener((view) -> noteClickListener.onNoteClick(getAdapterPosition(), view)); + itemView.setOnLongClickListener((view) -> noteClickListener.onNoteLongClick(getAdapterPosition(), view)); + } + + protected void bindStatus(AppCompatImageView noteStatus, DBStatus status, int mainColor) { + noteStatus.setVisibility(DBStatus.VOID.equals(status) ? INVISIBLE : VISIBLE); + DrawableCompat.setTint(noteStatus.getDrawable(), BrandingUtil.getSecondaryForegroundColorDependingOnTheme(noteStatus.getContext(), mainColor)); + } + + protected void bindCategory(@NonNull Context context, @NonNull TextView noteCategory, boolean showCategory, @NonNull String category, int mainColor) { + final boolean isDarkThemeActive = NotesApplication.isDarkThemeActive(context); + noteCategory.setVisibility(showCategory && !category.isEmpty() ? View.VISIBLE : View.GONE); + noteCategory.setText(category); + + @ColorInt int categoryForeground; + @ColorInt int categoryBackground; + + if (isDarkThemeActive) { + if (isColorDark(mainColor)) { + if (contrastRatioIsSufficient(mainColor, Color.BLACK)) { + categoryBackground = mainColor; + categoryForeground = Color.WHITE; + } else { + categoryBackground = Color.WHITE; + categoryForeground = mainColor; + } + } else { + categoryBackground = mainColor; + categoryForeground = Color.BLACK; + } + } else { + categoryForeground = Color.BLACK; + if (isColorDark(mainColor) || contrastRatioIsSufficient(mainColor, Color.WHITE)) { + categoryBackground = mainColor; + } else { + categoryBackground = Color.BLACK; + } + } + + noteCategory.setTextColor(categoryForeground); + if (noteCategory instanceof Chip) { + ((Chip) noteCategory).setChipStrokeColor(ColorStateList.valueOf(categoryBackground)); + ((Chip) noteCategory).setChipBackgroundColor(ColorStateList.valueOf(isDarkThemeActive ? categoryBackground : Color.TRANSPARENT)); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + DrawableCompat.setTint(noteCategory.getBackground(), categoryBackground); + } else { + final GradientDrawable drawable = (GradientDrawable) noteCategory.getBackground(); + drawable.setStroke(1, categoryBackground); + drawable.setColor(isDarkThemeActive ? categoryBackground : Color.TRANSPARENT); + } + } + } + + protected void bindFavorite(@NonNull ImageView noteFavorite, boolean isFavorite) { + noteFavorite.setImageResource(isFavorite ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp); + noteFavorite.setOnClickListener(view -> noteClickListener.onNoteFavoriteClick(getAdapterPosition(), view)); + } + + protected void bindSearchableContent(@NonNull Context context, @NonNull TextView textView, @Nullable CharSequence searchQuery, @NonNull String content, int mainColor) { + CharSequence processedContent = content; + if (!TextUtils.isEmpty(searchQuery)) { + @ColorInt final int searchBackground = context.getResources().getColor(R.color.bg_highlighted); + @ColorInt final int searchForeground = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, mainColor); + + // The Pattern.quote method will add \Q to the very beginning of the string and \E to the end of the string + // It implies that the string between \Q and \E is a literal string and thus the reserved keyword in such string will be ignored. + // See https://stackoverflow.com/questions/15409296/what-is-the-use-of-pattern-quote-method + final Pattern pattern = Pattern.compile("(" + Pattern.quote(searchQuery.toString()) + ")", Pattern.CASE_INSENSITIVE); + SpannableString spannableString = new SpannableString(content); + Matcher matcher = pattern.matcher(spannableString); + + while (matcher.find()) { + spannableString.setSpan(new ForegroundColorSpan(searchForeground), matcher.start(), matcher.end(), 0); + spannableString.setSpan(new BackgroundColorSpan(searchBackground), matcher.start(), matcher.end(), 0); + } + + processedContent = spannableString; + } + textView.setText(processedContent); + } + + public abstract void showSwipe(boolean left); + + @Nullable + public abstract View getNoteSwipeable(); +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java new file mode 100644 index 00000000..f411cf16 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java @@ -0,0 +1,56 @@ +package it.niedermann.owncloud.notes.main.items.grid; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Px; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import it.niedermann.owncloud.notes.main.items.ItemAdapter; +import it.niedermann.owncloud.notes.main.items.section.SectionItemDecoration; + +public class GridItemDecoration extends SectionItemDecoration { + + @NonNull + private final ItemAdapter adapter; + private final int spanCount; + private final int gutter; + + public GridItemDecoration(@NonNull ItemAdapter adapter, int spanCount, @Px int sectionLeft, @Px int sectionTop, @Px int sectionRight, @Px int sectionBottom, @Px int gutter) { + super(adapter, sectionLeft, sectionTop, sectionRight, sectionBottom); + this.spanCount = spanCount; + this.adapter = adapter; + this.gutter = gutter; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + final int position = parent.getChildAdapterPosition(view); + final StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams(); + + if (adapter.getItemViewType(position) == ItemAdapter.TYPE_SECTION) { + lp.setFullSpan(true); + } else { + final int spanIndex = lp.getSpanIndex(); + + if (position >= 0) { + // First row gets some spacing at the top + if (position < spanCount && position < adapter.getFirstPositionOfViewType(ItemAdapter.TYPE_SECTION)) { + outRect.top = gutter; + } + + // First column gets some spacing at the left and the right side + if (spanIndex == 0) { + outRect.left = gutter; + } + + // 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/owncloud/notes/main/items/grid/NoteViewGridHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java new file mode 100644 index 00000000..765b970a --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java @@ -0,0 +1,57 @@ +package it.niedermann.owncloud.notes.main.items.grid; + +import android.content.Context; +import android.graphics.Typeface; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; + +import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridBinding; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.NoteClickListener; +import it.niedermann.owncloud.notes.main.items.NoteViewHolder; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static it.niedermann.owncloud.notes.shared.util.NoteUtil.EXCERPT_LINE_SEPARATOR; + +public class NoteViewGridHolder extends NoteViewHolder { + @NonNull + private final ItemNotesListNoteItemGridBinding binding; + + public NoteViewGridHolder(@NonNull ItemNotesListNoteItemGridBinding binding, @NonNull NoteClickListener noteClickListener, boolean monospace, @Px float fontSize) { + super(binding.getRoot(), noteClickListener); + this.binding = binding; + + binding.noteTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.1f); + binding.noteExcerpt.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * .8f); + if (monospace) { + binding.noteTitle.setTypeface(Typeface.MONOSPACE); + binding.noteExcerpt.setTypeface(Typeface.MONOSPACE); + } + } + + public void showSwipe(boolean left) { + throw new UnsupportedOperationException(NoteViewGridHolder.class.getSimpleName() + " does not support swiping"); + } + + public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { + super.bind(note, showCategory, mainColor, textColor, searchQuery); + @NonNull final Context context = itemView.getContext(); + bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor); + bindStatus(binding.noteStatus, note.getStatus(), mainColor); + bindFavorite(binding.noteFavorite, note.isFavorite()); + bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor); + bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), mainColor); + binding.noteExcerpt.setVisibility(TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE); + } + + @Nullable + public View getNoteSwipeable() { + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java new file mode 100644 index 00000000..6468e964 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java @@ -0,0 +1,47 @@ +package it.niedermann.owncloud.notes.main.items.grid; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.TypedValue; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; + +import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridOnlyTitleBinding; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.NoteClickListener; +import it.niedermann.owncloud.notes.main.items.NoteViewHolder; + +public class NoteViewGridHolderOnlyTitle extends NoteViewHolder { + @NonNull + private final ItemNotesListNoteItemGridOnlyTitleBinding binding; + + public NoteViewGridHolderOnlyTitle(@NonNull ItemNotesListNoteItemGridOnlyTitleBinding binding, @NonNull NoteClickListener noteClickListener, boolean monospace, @Px float fontSize) { + super(binding.getRoot(), noteClickListener); + this.binding = binding; + + binding.noteTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.1f); + if (monospace) { + binding.noteTitle.setTypeface(Typeface.MONOSPACE); + } + } + + public void showSwipe(boolean left) { + throw new UnsupportedOperationException(NoteViewGridHolderOnlyTitle.class.getSimpleName() + " does not support swiping"); + } + + public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { + super.bind(note, showCategory, mainColor, textColor, searchQuery); + @NonNull final Context context = itemView.getContext(); + bindStatus(binding.noteStatus, note.getStatus(), mainColor); + bindFavorite(binding.noteFavorite, note.isFavorite()); + bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor); + } + + @Nullable + public View getNoteSwipeable() { + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java new file mode 100644 index 00000000..0f41d745 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java @@ -0,0 +1,47 @@ +package it.niedermann.owncloud.notes.main.items.list; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithExcerptBinding; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.DBStatus; +import it.niedermann.owncloud.notes.shared.model.NoteClickListener; +import it.niedermann.owncloud.notes.main.items.NoteViewHolder; + +public class NoteViewHolderWithExcerpt extends NoteViewHolder { + @NonNull + private final ItemNotesListNoteItemWithExcerptBinding binding; + + public NoteViewHolderWithExcerpt(@NonNull ItemNotesListNoteItemWithExcerptBinding binding, @NonNull NoteClickListener noteClickListener) { + super(binding.getRoot(), noteClickListener); + this.binding = binding; + } + + public void showSwipe(boolean left) { + binding.noteFavoriteLeft.setVisibility(left ? View.VISIBLE : View.INVISIBLE); + binding.noteDeleteRight.setVisibility(left ? View.INVISIBLE : View.VISIBLE); + binding.noteSwipeFrame.setBackgroundResource(left ? R.color.bg_warning : R.color.bg_attention); + } + + public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { + super.bind(note, showCategory, mainColor, textColor, searchQuery); + @NonNull final Context context = itemView.getContext(); + binding.noteSwipeable.setAlpha(DBStatus.LOCAL_DELETED.equals(note.getStatus()) ? 0.5f : 1.0f); + bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor); + bindStatus(binding.noteStatus, note.getStatus(), mainColor); + bindFavorite(binding.noteFavorite, note.isFavorite()); + + bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor); + bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt(), mainColor); + } + + @NonNull + public View getNoteSwipeable() { + return binding.noteSwipeable; + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java new file mode 100644 index 00000000..9b056925 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java @@ -0,0 +1,45 @@ +package it.niedermann.owncloud.notes.main.items.list; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithoutExcerptBinding; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.DBStatus; +import it.niedermann.owncloud.notes.shared.model.NoteClickListener; +import it.niedermann.owncloud.notes.main.items.NoteViewHolder; + +public class NoteViewHolderWithoutExcerpt extends NoteViewHolder { + @NonNull + private final ItemNotesListNoteItemWithoutExcerptBinding binding; + + public NoteViewHolderWithoutExcerpt(@NonNull ItemNotesListNoteItemWithoutExcerptBinding binding, @NonNull NoteClickListener noteClickListener) { + super(binding.getRoot(), noteClickListener); + this.binding = binding; + } + + public void showSwipe(boolean left) { + binding.noteFavoriteLeft.setVisibility(left ? View.VISIBLE : View.INVISIBLE); + binding.noteDeleteRight.setVisibility(left ? View.INVISIBLE : View.VISIBLE); + binding.noteSwipeFrame.setBackgroundResource(left ? R.color.bg_warning : R.color.bg_attention); + } + + public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { + super.bind(note, showCategory, mainColor, textColor, searchQuery); + @NonNull final Context context = itemView.getContext(); + binding.noteSwipeable.setAlpha(DBStatus.LOCAL_DELETED.equals(note.getStatus()) ? 0.5f : 1.0f); + bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor); + bindStatus(binding.noteStatus, note.getStatus(), mainColor); + bindFavorite(binding.noteFavorite, note.isFavorite()); + bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor); + } + + @NonNull + public View getNoteSwipeable() { + return binding.noteSwipeable; + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java new file mode 100644 index 00000000..3251269c --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java @@ -0,0 +1,137 @@ +package it.niedermann.owncloud.notes.main.items.list; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.snackbar.Snackbar; +import com.nextcloud.android.sso.model.SingleSignOnAccount; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandedSnackbar; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.main.items.ItemAdapter; +import it.niedermann.owncloud.notes.main.items.NoteViewHolder; +import it.niedermann.owncloud.notes.main.items.section.SectionViewHolder; +import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper.ViewProvider; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; + +public class NotesListViewItemTouchHelper extends ItemTouchHelper { + + private static final String TAG = NotesListViewItemTouchHelper.class.getSimpleName(); + + public NotesListViewItemTouchHelper( + @NonNull SingleSignOnAccount ssoAccount, + @NonNull Context context, + @NonNull NotesDatabase db, + @NonNull ItemAdapter adapter, + @NonNull ISyncCallback syncCallBack, + @NonNull Runnable refreshLists, + @Nullable SwipeRefreshLayout swipeRefreshLayout, + @Nullable ViewProvider viewProvider, + boolean gridView) { + super(new SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { + private boolean swipeRefreshLayoutEnabled; + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + return false; + } + + /** + * Disable swipe on sections and if grid view is enabled + * + * @param recyclerView RecyclerView + * @param viewHolder RecyclerView.ViewHoler + * @return 0 if viewHolder is section or grid view is enabled, otherwise super() + */ + @Override + public int getSwipeDirs(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + if (gridView || viewHolder instanceof SectionViewHolder) return 0; + return super.getSwipeDirs(recyclerView, viewHolder); + } + + /** + * Delete note if note is swiped to left or right + * + * @param viewHolder RecyclerView.ViewHoler + * @param direction int + */ + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + switch (direction) { + case ItemTouchHelper.LEFT: + final DBNote dbNoteWithoutContent = (DBNote) adapter.getItem(viewHolder.getAdapterPosition()); + final DBNote dbNote = db.getNote(dbNoteWithoutContent.getAccountId(), dbNoteWithoutContent.getId()); + db.deleteNoteAndSync(ssoAccount, dbNote.getId()); + adapter.remove(dbNote); + refreshLists.run(); + Log.v(TAG, "Item deleted through swipe ----------------------------------------------"); + if (viewProvider == null) { + Toast.makeText(context, context.getString(R.string.action_note_deleted, dbNote.getTitle()), Toast.LENGTH_LONG).show(); + } else { + BrandedSnackbar.make(viewProvider.getView(), context.getString(R.string.action_note_deleted, dbNote.getTitle()), Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo, (View v) -> { + db.getNoteServerSyncHelper().addCallbackPush(ssoAccount, refreshLists::run); + db.addNoteAndSync(ssoAccount, dbNote.getAccountId(), dbNote); + refreshLists.run(); + BrandedSnackbar.make(viewProvider.getView(), context.getString(R.string.action_note_restored, dbNote.getTitle()), Snackbar.LENGTH_SHORT) + .show(); + }) + .show(); + } + break; + case ItemTouchHelper.RIGHT: + final DBNote adapterNote = (DBNote) adapter.getItem(viewHolder.getAdapterPosition()); + db.toggleFavorite(ssoAccount, adapterNote, syncCallBack); + refreshLists.run(); + break; + default: + //NoOp + } + } + + @Override + public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + NoteViewHolder noteViewHolder = (NoteViewHolder) viewHolder; + // show swipe icon on the side + noteViewHolder.showSwipe(dX > 0); + // move only swipeable part of item (not leave-behind) + getDefaultUIUtil().onDraw(c, recyclerView, noteViewHolder.getNoteSwipeable(), dX, dY, actionState, isCurrentlyActive); + } + + @Override + public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) { + if (actionState == ACTION_STATE_SWIPE && swipeRefreshLayout != null) { + Log.i(TAG, "Start swiping, disable swipeRefreshLayout"); + swipeRefreshLayoutEnabled = swipeRefreshLayout.isEnabled(); + swipeRefreshLayout.setEnabled(false); + } + super.onSelectedChanged(viewHolder, actionState); + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + Log.i(TAG, "End swiping, resetting swipeRefreshLayout state"); + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setEnabled(swipeRefreshLayoutEnabled); + } + getDefaultUIUtil().clearView(((NoteViewHolder) viewHolder).getNoteSwipeable()); + } + + @Override + public float getSwipeEscapeVelocity(float defaultValue) { + return defaultValue * 3; + } + }); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java new file mode 100644 index 00000000..bd01163d --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java @@ -0,0 +1,25 @@ +package it.niedermann.owncloud.notes.main.items.section; + +import it.niedermann.owncloud.notes.shared.model.Item; + +public class SectionItem implements Item { + + private String title; + + public SectionItem(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public boolean isSection() { + return true; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItemDecoration.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItemDecoration.java new file mode 100644 index 00000000..1ebcbe48 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItemDecoration.java @@ -0,0 +1,40 @@ +package it.niedermann.owncloud.notes.main.items.section; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Px; +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.owncloud.notes.main.items.ItemAdapter; + +public class SectionItemDecoration extends RecyclerView.ItemDecoration { + + @NonNull + private final ItemAdapter adapter; + private final int sectionLeft; + private final int sectionTop; + private final int sectionRight; + private final int sectionBottom; + + public SectionItemDecoration(@NonNull ItemAdapter adapter, @Px int sectionLeft, @Px int sectionTop, @Px int sectionRight, @Px int sectionBottom) { + this.adapter = adapter; + this.sectionLeft = sectionLeft; + this.sectionTop = sectionTop; + this.sectionRight = sectionRight; + this.sectionBottom = sectionBottom; + } + + @CallSuper + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + final int position = parent.getChildAdapterPosition(view); + if (adapter.getItemViewType(position) == ItemAdapter.TYPE_SECTION) { + outRect.left = sectionLeft; + outRect.top = sectionTop; + outRect.right = sectionRight; + outRect.bottom = sectionBottom; + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionViewHolder.java new file mode 100644 index 00000000..b1ce4c45 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionViewHolder.java @@ -0,0 +1,18 @@ +package it.niedermann.owncloud.notes.main.items.section; + +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.owncloud.notes.databinding.ItemNotesListSectionItemBinding; + +public class SectionViewHolder extends RecyclerView.ViewHolder { + private final ItemNotesListSectionItemBinding binding; + + public SectionViewHolder(ItemNotesListSectionItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(SectionItem item) { + binding.sectionTitle.setText(item.getTitle()); + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java index 50ab925b..33156fb6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountAdapter.java @@ -12,7 +12,7 @@ import java.util.ArrayList; import java.util.List; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; public class ManageAccountAdapter extends RecyclerView.Adapter { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java index 39ae6891..0ec7f186 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java @@ -15,7 +15,7 @@ import com.bumptech.glide.request.RequestOptions; import it.niedermann.android.glidesso.SingleSignOnUrl; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding; -import it.niedermann.owncloud.notes.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; import static android.view.View.GONE; import static android.view.View.VISIBLE; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java index eb6747c3..7cf64230 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsActivity.java @@ -12,9 +12,9 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.util.List; -import it.niedermann.owncloud.notes.android.activity.LockedActivity; +import it.niedermann.owncloud.notes.LockedActivity; import it.niedermann.owncloud.notes.databinding.ActivityManageAccountsBinding; -import it.niedermann.owncloud.notes.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; import it.niedermann.owncloud.notes.persistence.NotesDatabase; public class ManageAccountsActivity extends LockedActivity { @@ -23,7 +23,6 @@ public class ManageAccountsActivity extends LockedActivity { private ManageAccountAdapter adapter; private NotesDatabase db = null; - @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/AbstractWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/model/AbstractWidgetData.java deleted file mode 100644 index 672ba170..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/AbstractWidgetData.java +++ /dev/null @@ -1,42 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -public abstract class AbstractWidgetData { - - private int appWidgetId; - private long accountId; - private int themeMode; - - protected AbstractWidgetData() { - - } - - protected AbstractWidgetData(int appWidgetId, long accountId, int themeMode) { - this.appWidgetId = appWidgetId; - this.accountId = accountId; - this.themeMode = themeMode; - } - - public int getAppWidgetId() { - return appWidgetId; - } - - public void setAppWidgetId(int appWidgetId) { - this.appWidgetId = appWidgetId; - } - - public long getAccountId() { - return accountId; - } - - public void setAccountId(long accountId) { - this.accountId = accountId; - } - - public int getThemeMode() { - return themeMode; - } - - public void setThemeMode(int themeMode) { - this.themeMode = themeMode; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/ApiVersion.java b/app/src/main/java/it/niedermann/owncloud/notes/model/ApiVersion.java deleted file mode 100644 index 058dcae8..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/ApiVersion.java +++ /dev/null @@ -1,86 +0,0 @@ -package it.niedermann.owncloud.notes.model; - - -import androidx.annotation.NonNull; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@SuppressWarnings("WeakerAccess") -public class ApiVersion implements Comparable { - private static final Pattern NUMBER_EXTRACTION_PATTERN = Pattern.compile("[0-9]+"); - - private String originalVersion = "?"; - private int major; - private int minor; - - public ApiVersion(String originalVersion, int major, int minor) { - this(major, minor); - this.originalVersion = originalVersion; - } - - public ApiVersion(int major, int minor) { - this.major = major; - this.minor = minor; - } - - public int getMajor() { - return major; - } - - public int getMinor() { - return minor; - } - - public String getOriginalVersion() { - return originalVersion; - } - - public static ApiVersion of(String versionString) { - int major = 0, minor = 0; - if (versionString != null) { - String[] split = versionString.split("\\."); - if (split.length > 0) { - major = extractNumber(split[0]); - if (split.length > 1) { - minor = extractNumber(split[1]); - } - } - } - return new ApiVersion(versionString, major, minor); - } - - private static int extractNumber(String containsNumbers) { - final Matcher matcher = NUMBER_EXTRACTION_PATTERN.matcher(containsNumbers); - if (matcher.find()) { - return Integer.parseInt(matcher.group()); - } - return 0; - } - - /** - * @param compare another version object - * @return -1 if the compared major version is higher than the current major version - * 0 if the compared major version is equal to the current major version - * 1 if the compared major version is lower than the current major version - */ - @Override - public int compareTo(ApiVersion compare) { - if (compare.getMajor() > getMajor()) { - return -1; - } else if (compare.getMajor() < getMajor()) { - return 1; - } - return 0; - } - - @NonNull - @Override - public String toString() { - return "Version{" + - "originalVersion='" + originalVersion + '\'' + - ", major=" + major + - ", minor=" + minor + - '}'; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/Capabilities.java b/app/src/main/java/it/niedermann/owncloud/notes/model/Capabilities.java deleted file mode 100644 index aae6ad1b..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/Capabilities.java +++ /dev/null @@ -1,106 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.bumptech.glide.load.HttpException; -import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; - -import org.json.JSONException; -import org.json.JSONObject; - -import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; - -/** - * This entity class is used to return relevant data of the HTTP reponse. - */ -public class Capabilities { - - private static final String TAG = Capabilities.class.getSimpleName(); - - private static final String JSON_OCS = "ocs"; - private static final String JSON_OCS_META = "meta"; - private static final String JSON_OCS_META_STATUSCODE = "statuscode"; - private static final String JSON_OCS_DATA = "data"; - private static final String JSON_OCS_DATA_CAPABILITIES = "capabilities"; - private static final String JSON_OCS_DATA_CAPABILITIES_NOTES = "notes"; - private static final String JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION = "api_version"; - private static final String JSON_OCS_DATA_CAPABILITIES_THEMING = "theming"; - private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR = "color"; - private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT = "color-text"; - - private String apiVersion = null; - private String color = null; - private String textColor = null; - @Nullable - private String eTag; - - public Capabilities(@NonNull String response, @Nullable String eTag) throws NextcloudHttpRequestFailedException { - this.eTag = eTag; - final JSONObject ocs; - try { - ocs = new JSONObject(response).getJSONObject(JSON_OCS); - if (ocs.has(JSON_OCS_META)) { - final JSONObject meta = ocs.getJSONObject(JSON_OCS_META); - if (meta.has(JSON_OCS_META_STATUSCODE)) { - if (meta.getInt(JSON_OCS_META_STATUSCODE) == HTTP_UNAVAILABLE) { - Log.i(TAG, "Capabilities Endpoint: This instance is currently in maintenance mode."); - throw new NextcloudHttpRequestFailedException(HTTP_UNAVAILABLE, new HttpException(HTTP_UNAVAILABLE)); - } - } - } - if (ocs.has(JSON_OCS_DATA)) { - final JSONObject data = ocs.getJSONObject(JSON_OCS_DATA); - if (data.has(JSON_OCS_DATA_CAPABILITIES)) { - final JSONObject capabilities = data.getJSONObject(JSON_OCS_DATA_CAPABILITIES); - if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_NOTES)) { - final JSONObject notes = capabilities.getJSONObject(JSON_OCS_DATA_CAPABILITIES_NOTES); - if (notes.has(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION)) { - this.apiVersion = notes.getString(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION); - } - } - if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_THEMING)) { - final JSONObject theming = capabilities.getJSONObject(JSON_OCS_DATA_CAPABILITIES_THEMING); - if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR)) { - this.color = theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR); - } - if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)) { - this.textColor = theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT); - } - } - } - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - - public String getApiVersion() { - return apiVersion; - } - - public String getColor() { - return color; - } - - public String getTextColor() { - return textColor; - } - - @Nullable - public String getETag() { - return eTag; - } - - @NonNull - @Override - public String toString() { - return "Capabilities{" + - "apiVersion='" + apiVersion + '\'' + - ", color='" + color + '\'' + - ", textColor='" + textColor + '\'' + - '}'; - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/Category.java b/app/src/main/java/it/niedermann/owncloud/notes/model/Category.java deleted file mode 100644 index 72f176c8..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/Category.java +++ /dev/null @@ -1,18 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import androidx.annotation.Nullable; - -import java.io.Serializable; - -public class Category implements Serializable { - - @Nullable - public final String category; - @Nullable - public final Boolean favorite; - - public Category(@Nullable String category, @Nullable Boolean favorite) { - this.category = category; - this.favorite = favorite; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/CloudNote.java b/app/src/main/java/it/niedermann/owncloud/notes/model/CloudNote.java deleted file mode 100644 index 345c76ca..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/CloudNote.java +++ /dev/null @@ -1,103 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import androidx.annotation.NonNull; - -import java.io.Serializable; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Locale; - -import it.niedermann.owncloud.notes.util.NoteUtil; - -/** - * CloudNote represents a remote note from an OwnCloud server. - * It can be directly generated from the JSON answer from the server. - */ -public class CloudNote implements Serializable { - private long remoteId; - private String title = ""; - private Calendar modified; - private String content = ""; - private boolean favorite = false; - private String category = ""; - private String etag = ""; - private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; - - public CloudNote(long remoteId, Calendar modified, String title, String content, boolean favorite, String category, String etag) { - this.remoteId = remoteId; - setTitle(title); - setContent(content); - setFavorite(favorite); - setCategory(category); - setEtag(etag); - this.modified = modified; - } - - public long getRemoteId() { - return remoteId; - } - - public void setRemoteId(long remoteId) { - this.remoteId = remoteId; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = NoteUtil.removeMarkDown(title); - } - - public Calendar getModified() { - return modified; - } - - public String getModified(String format) { - if (modified == null) - return null; - return new SimpleDateFormat(format, Locale.GERMANY).format(this.getModified().getTimeInMillis()); - } - - public void setModified(Calendar modified) { - this.modified = modified; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public boolean isFavorite() { - return favorite; - } - - public void setFavorite(boolean favorite) { - this.favorite = favorite; - } - - public String getEtag() { - return etag; - } - - public void setEtag(String etag) { - this.etag = etag; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category == null ? "" : category; - } - - @NonNull - @Override - public String toString() { - return "R" + getRemoteId() + " " + (isFavorite() ? " (*) " : " ") + getCategory() + " / " + getTitle() + " (" + getModified(DATE_FORMAT) + " / " + getEtag() + ")"; - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/DBNote.java b/app/src/main/java/it/niedermann/owncloud/notes/model/DBNote.java deleted file mode 100644 index d2be6aa2..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/DBNote.java +++ /dev/null @@ -1,80 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import androidx.annotation.NonNull; - -import java.io.Serializable; -import java.util.Calendar; - -/** - * DBNote represents a single note from the local SQLite database with all attributes. - * It extends CloudNote with attributes required for local data management. - */ -public class DBNote extends CloudNote implements Item, Serializable { - - private final long id; - private final long accountId; - private DBStatus status; - private String excerpt; - private int scrollY; - - public DBNote(long id, long remoteId, Calendar modified, String title, String content, boolean favorite, String category, String etag, DBStatus status, long accountId, String excerpt, int scrollY) { - super(remoteId, modified, title, content, favorite, category, etag); - this.id = id; - this.excerpt = excerpt; - this.status = status; - this.accountId = accountId; - this.scrollY = scrollY; - } - - public long getId() { - return id; - } - - public long getAccountId() { - return accountId; - } - - public DBStatus getStatus() { - return status; - } - - public void setStatus(DBStatus status) { - this.status = status; - } - - public String getExcerpt() { - return excerpt; - } - - public void setExcerpt(String excerpt) { - this.excerpt = excerpt; - } - - public void setContent(String content) { - super.setContent(content); - } - - @Override - public boolean isSection() { - return false; - } - - @NonNull - @Override - public String toString() { - return "DBNote{" + - "id=" + id + - ", accountId=" + accountId + - ", status=" + status + - ", excerpt='" + excerpt + '\'' + - '}'; - } - - public int getScrollY() { - return scrollY; - } - - public void setScrollY(int scrollY) { - this.scrollY = scrollY; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/DBStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/model/DBStatus.java deleted file mode 100644 index f98b887e..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/DBStatus.java +++ /dev/null @@ -1,50 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -/** - * Helps to distinguish between different local change types for Server Synchronization. - * Created by stefan on 19.09.15. - */ -public enum DBStatus { - - /** - * VOID means, that the Note was not modified locally - */ - VOID(""), - - /** - * LOCAL_EDITED means that a Note was created and/or changed since the last successful synchronization. - * If it was newly created, then REMOTE_ID is 0 - */ - LOCAL_EDITED("LOCAL_EDITED"), - - /** - * LOCAL_DELETED means that the Note was deleted locally, but this information was not yet synchronized. - * Therefore, the Note have to be kept locally until the synchronization has succeeded. - * However, Notes with this status should not be displayed in the UI. - */ - LOCAL_DELETED("LOCAL_DELETED"); - - private final String title; - - public String getTitle() { - return title; - } - - DBStatus(String title) { - this.title = title; - } - - /** - * Parse a String an get the appropriate DBStatus enum element. - * - * @param str The String containing the DBStatus identifier. Must not null. - * @return The DBStatus fitting to the String. - */ - public static DBStatus parse(String str) { - if (str.isEmpty()) { - return DBStatus.VOID; - } else { - return DBStatus.valueOf(str); - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/GridItemDecoration.java b/app/src/main/java/it/niedermann/owncloud/notes/model/GridItemDecoration.java deleted file mode 100644 index 68b349e0..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/GridItemDecoration.java +++ /dev/null @@ -1,53 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.graphics.Rect; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Px; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.StaggeredGridLayoutManager; - -public class GridItemDecoration extends SectionItemDecoration { - - @NonNull - private final ItemAdapter adapter; - private final int spanCount; - private final int gutter; - - public GridItemDecoration(@NonNull ItemAdapter adapter, int spanCount, @Px int sectionLeft, @Px int sectionTop, @Px int sectionRight, @Px int sectionBottom, @Px int gutter) { - super(adapter, sectionLeft, sectionTop, sectionRight, sectionBottom); - this.spanCount = spanCount; - this.adapter = adapter; - this.gutter = gutter; - } - - @Override - public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { - super.getItemOffsets(outRect, view, parent, state); - final int position = parent.getChildAdapterPosition(view); - final StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams(); - - if (adapter.getItemViewType(position) == ItemAdapter.TYPE_SECTION) { - lp.setFullSpan(true); - } else { - final int spanIndex = lp.getSpanIndex(); - - if (position >= 0) { - // First row gets some spacing at the top - if (position < spanCount && position < adapter.getFirstPositionOfViewType(ItemAdapter.TYPE_SECTION)) { - outRect.top = gutter; - } - - // First column gets some spacing at the left and the right side - if (spanIndex == 0) { - outRect.left = gutter; - } - - // 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/owncloud/notes/model/ISyncCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/model/ISyncCallback.java deleted file mode 100644 index 37026756..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/ISyncCallback.java +++ /dev/null @@ -1,13 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -/** - * Callback - * Created by stefan on 01.10.15. - */ -public interface ISyncCallback { - void onFinish(); - - default void onScheduled() { - - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/Item.java b/app/src/main/java/it/niedermann/owncloud/notes/model/Item.java deleted file mode 100644 index d3191e4d..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/Item.java +++ /dev/null @@ -1,5 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -public interface Item { - boolean isSection(); -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/ItemAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/model/ItemAdapter.java deleted file mode 100644 index f6efada4..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/ItemAdapter.java +++ /dev/null @@ -1,251 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Color; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.ColorInt; -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Px; -import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.Branded; -import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridBinding; -import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridOnlyTitleBinding; -import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithExcerptBinding; -import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithoutExcerptBinding; -import it.niedermann.owncloud.notes.databinding.ItemNotesListSectionItemBinding; - -import static it.niedermann.owncloud.notes.util.NoteUtil.getFontSizeFromPreferences; - -public class ItemAdapter extends RecyclerView.Adapter implements Branded { - - private static final String TAG = ItemAdapter.class.getSimpleName(); - - public static final int TYPE_SECTION = 0; - public static final int TYPE_NOTE_WITH_EXCERPT = 1; - public static final int TYPE_NOTE_WITHOUT_EXCERPT = 2; - public static final int TYPE_NOTE_ONLY_TITLE = 3; - - private final NoteClickListener noteClickListener; - private final boolean gridView; - private List itemList = new ArrayList<>(); - private boolean showCategory = true; - private CharSequence searchQuery; - private final List selected = new ArrayList<>(); - @Px - private final float fontSize; - private final boolean monospace; - @ColorInt - private int mainColor; - @ColorInt - private int textColor; - - public ItemAdapter(@NonNull T context, boolean gridView) { - this.noteClickListener = context; - this.gridView = gridView; - this.mainColor = context.getResources().getColor(R.color.defaultBrand); - this.textColor = Color.WHITE; - final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - this.fontSize = getFontSizeFromPreferences(context, sp); - this.monospace = sp.getBoolean(context.getString(R.string.pref_key_font), false); - // FIXME see getItemId() - // setHasStableIds(true); - } - - - /* - FIXME this causes {@link it.niedermann.owncloud.notes.android.NotesListViewItemTouchHelper} to not call clearView anymore → After marking a note as favorite, it stays yellow. - @Override - public long getItemId(int position) { - return getItemViewType(position) == TYPE_SECTION - ? ((SectionItem) getItem(position)).getTitle().hashCode() * -1 - : ((DBNote) getItem(position)).getId(); - } - */ - - /** - * Updates the item list and notifies respective view to update. - * - * @param itemList List of items to be set - */ - public void setItemList(@NonNull List itemList) { - this.itemList = itemList; - notifyDataSetChanged(); - } - - /** - * Adds the given note to the top of the list. - * - * @param note Note that should be added. - */ - public void add(@NonNull DBNote note) { - itemList.add(0, note); - notifyItemInserted(0); - notifyItemChanged(0); - } - - /** - * Removes all items from the adapter. - */ - public void removeAll() { - itemList.clear(); - notifyDataSetChanged(); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - if (gridView) { - switch (viewType) { - case TYPE_SECTION: { - return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(LayoutInflater.from(parent.getContext()))); - } - case TYPE_NOTE_ONLY_TITLE: { - return new NoteViewGridHolderOnlyTitle(ItemNotesListNoteItemGridOnlyTitleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener, monospace, fontSize); - } - case TYPE_NOTE_WITH_EXCERPT: - case TYPE_NOTE_WITHOUT_EXCERPT: { - return new NoteViewGridHolder(ItemNotesListNoteItemGridBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener, monospace, fontSize); - } - default: { - throw new IllegalArgumentException("Not supported viewType: " + viewType); - } - } - } else { - switch (viewType) { - case TYPE_SECTION: { - return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(LayoutInflater.from(parent.getContext()))); - } - case TYPE_NOTE_WITH_EXCERPT: { - return new NoteViewHolderWithExcerpt(ItemNotesListNoteItemWithExcerptBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener); - } - case TYPE_NOTE_ONLY_TITLE: - case TYPE_NOTE_WITHOUT_EXCERPT: { - return new NoteViewHolderWithoutExcerpt(ItemNotesListNoteItemWithoutExcerptBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), noteClickListener); - } - default: { - throw new IllegalArgumentException("Not supported viewType: " + viewType); - } - } - } - } - - @Override - public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) { - switch (getItemViewType(position)) { - case TYPE_SECTION: { - ((SectionViewHolder) holder).bind((SectionItem) itemList.get(position)); - break; - } - case TYPE_NOTE_WITH_EXCERPT: - case TYPE_NOTE_WITHOUT_EXCERPT: - case TYPE_NOTE_ONLY_TITLE: { - ((NoteViewHolder) holder).bind((DBNote) itemList.get(position), showCategory, mainColor, textColor, searchQuery); - break; - } - } - } - - public boolean select(Integer position) { - return !selected.contains(position) && selected.add(position); - } - - public void clearSelection(@NonNull RecyclerView recyclerView) { - for (Integer i : getSelected()) { - RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(i); - if (viewHolder != null) { - viewHolder.itemView.setSelected(false); - } else { - Log.w(TAG, "Could not found viewholder to remove selection"); - } - } - selected.clear(); - } - - @NonNull - public List getSelected() { - return selected; - } - - public void deselect(Integer position) { - for (int i = 0; i < selected.size(); i++) { - if (selected.get(i).equals(position)) { - //position was selected and removed - selected.remove(i); - return; - } - } - // position was not selected - } - - public Item getItem(int notePosition) { - return itemList.get(notePosition); - } - - public void remove(@NonNull Item item) { - itemList.remove(item); - notifyDataSetChanged(); - } - - public void setShowCategory(boolean showCategory) { - this.showCategory = showCategory; - } - - @Override - public int getItemCount() { - return itemList.size(); - } - - @IntRange(from = 0, to = 3) - @Override - public int getItemViewType(int position) { - Item item = getItem(position); - if (item == null) { - throw new IllegalArgumentException("Item at position " + position + " must not be null"); - } - if (getItem(position).isSection()) return TYPE_SECTION; - DBNote note = (DBNote) getItem(position); - if (TextUtils.isEmpty(note.getExcerpt())) { - if (TextUtils.isEmpty(note.getCategory())) { - return TYPE_NOTE_ONLY_TITLE; - } else { - return TYPE_NOTE_WITHOUT_EXCERPT; - } - } - return TYPE_NOTE_WITH_EXCERPT; - } - - @Override - public void applyBrand(int mainColor, int textColor) { - this.mainColor = mainColor; - this.textColor = textColor; - notifyDataSetChanged(); - } - - public void setHighlightSearchQuery(CharSequence searchQuery) { - this.searchQuery = searchQuery; - } - - /** - * @return the position of the first item which matches the given viewtype, -1 if not available - */ - public int getFirstPositionOfViewType(@IntRange(from = 0, to = 3) int viewType) { - for (int i = 0; i < itemList.size(); i++) { - if (getItemViewType(i) == viewType) { - return i; - } - } - return -1; - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/LocalAccount.java b/app/src/main/java/it/niedermann/owncloud/notes/model/LocalAccount.java deleted file mode 100644 index bfcb4d74..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/LocalAccount.java +++ /dev/null @@ -1,155 +0,0 @@ -package it.niedermann.owncloud.notes.model; - - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.json.JSONArray; -import org.json.JSONException; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.NoSuchElementException; - -import it.niedermann.owncloud.notes.persistence.NotesClient; - -public class LocalAccount { - - private long id; - private String userName; - private String accountName; - private String url; - private String etag; - private String capabilitiesETag; - private long modified; - @Nullable - private ApiVersion preferredApiVersion; - @ColorInt - private int color; - @ColorInt - private int textColor; - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getUserName() { - return userName; - } - - public void setUserName(String userName) { - this.userName = userName; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getEtag() { - return etag; - } - - public void setETag(String etag) { - this.etag = etag; - } - - public String getAccountName() { - return accountName; - } - - public void setAccountName(String accountName) { - this.accountName = accountName; - } - - public long getModified() { - return modified; - } - - public void setModified(long modified) { - this.modified = modified; - } - - @Nullable - public ApiVersion getPreferredApiVersion() { - return preferredApiVersion; - } - - public String getCapabilitiesETag() { - return capabilitiesETag; - } - - public void setCapabilitiesETag(String capabilitiesETag) { - this.capabilitiesETag = capabilitiesETag; - } - - /** - * @param availableApiVersions ["0.2", "1.0", ...] - */ - public void setPreferredApiVersion(@Nullable String availableApiVersions) { - // TODO move this logic to NotesClient? - try { - if (availableApiVersions == null) { - this.preferredApiVersion = null; - return; - } - JSONArray versionsArray = new JSONArray(availableApiVersions); - Collection supportedApiVersions = new HashSet<>(versionsArray.length()); - for (int i = 0; i < versionsArray.length(); i++) { - ApiVersion parsedApiVersion = ApiVersion.of(versionsArray.getString(i)); - for (ApiVersion temp : NotesClient.SUPPORTED_API_VERSIONS) { - if (temp.compareTo(parsedApiVersion) == 0) { - supportedApiVersions.add(parsedApiVersion); - break; - } - } - } - this.preferredApiVersion = Collections.max(supportedApiVersions); - } catch (JSONException | NoSuchElementException e) { - e.printStackTrace(); - this.preferredApiVersion = null; - } - } - - public int getColor() { - return color; - } - - public void setColor(@ColorInt int color) { - this.color = color; - } - - public int getTextColor() { - return textColor; - } - - public void setTextColor(@ColorInt int textColor) { - this.textColor = textColor; - } - - @NonNull - @Override - public String toString() { - return "LocalAccount{" + - "id=" + id + - ", userName='" + userName + '\'' + - ", accountName='" + accountName + '\'' + - ", url='" + url + '\'' + - ", etag='" + etag + '\'' + - ", modified=" + modified + - ", preferredApiVersion='" + preferredApiVersion + '\'' + - ", color=" + color + - ", textColor=" + textColor + - ", capabilitiesETag=" + capabilitiesETag + - '}'; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/NavigationAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/model/NavigationAdapter.java deleted file mode 100644 index 938f2f7e..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/NavigationAdapter.java +++ /dev/null @@ -1,172 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.content.Context; -import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.ColorInt; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.databinding.ItemNavigationBinding; -import it.niedermann.owncloud.notes.util.NoteUtil; - -public class NavigationAdapter extends RecyclerView.Adapter { - - @NonNull - private final Context context; - @ColorInt - private int mainColor; - @DrawableRes - public static final int ICON_FOLDER = R.drawable.ic_folder_grey600_24dp; - @DrawableRes - public static final int ICON_NOFOLDER = R.drawable.ic_folder_open_grey600_24dp; - @DrawableRes - public static final int ICON_SUB_FOLDER = R.drawable.ic_folder_grey600_18dp; - @DrawableRes - public static final int ICON_MULTIPLE = R.drawable.ic_create_new_folder_grey600_24dp; - @DrawableRes - public static final int ICON_MULTIPLE_OPEN = R.drawable.ic_folder_grey600_24dp; - @DrawableRes - public static final int ICON_SUB_MULTIPLE = R.drawable.ic_create_new_folder_grey600_18dp; - - public void applyBrand(int mainColor, int textColor) { - this.mainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, mainColor); - notifyDataSetChanged(); - } - - public static class NavigationItem { - @NonNull - public String id; - @NonNull - public String label; - @DrawableRes - public int icon; - @Nullable - public Integer count; - - public NavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon) { - this.id = id; - this.label = label; - this.count = count; - this.icon = icon; - } - } - - public static class CategoryNavigationItem extends NavigationItem { - @NonNull - public Long categoryId; - - public CategoryNavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon, @NonNull Long categoryId) { - super(id, label, count, icon); - this.categoryId = categoryId; - } - } - - class ViewHolder extends RecyclerView.ViewHolder { - @NonNull - private final View view; - - @NonNull - private final TextView name; - @NonNull - private final TextView count; - @NonNull - private final ImageView icon; - - private NavigationItem currentItem; - - ViewHolder(@NonNull View itemView, @NonNull final ClickListener clickListener) { - super(itemView); - view = itemView; - ItemNavigationBinding binding = ItemNavigationBinding.bind(view); - this.name = binding.navigationItemLabel; - this.count = binding.navigationItemCount; - this.icon = binding.navigationItemIcon; - icon.setOnClickListener(view -> clickListener.onIconClick(currentItem)); - itemView.setOnClickListener(view -> clickListener.onItemClick(currentItem)); - } - - private void bind(@NonNull NavigationItem item) { - currentItem = item; - boolean isSelected = item.id.equals(selectedItem); - name.setText(NoteUtil.extendCategory(item.label)); - count.setVisibility(item.count == null ? View.GONE : View.VISIBLE); - count.setText(String.valueOf(item.count)); - if (item.icon > 0) { - icon.setImageDrawable(DrawableCompat.wrap(icon.getResources().getDrawable(item.icon))); - icon.setVisibility(View.VISIBLE); - } else { - icon.setVisibility(View.GONE); - } - int textColor = isSelected ? mainColor : view.getResources().getColor(R.color.fg_default); - - name.setTextColor(textColor); - count.setTextColor(textColor); - icon.setColorFilter(isSelected ? textColor : 0); - - view.setSelected(isSelected); - } - } - - public interface ClickListener { - void onItemClick(NavigationItem item); - - void onIconClick(NavigationItem item); - } - - @NonNull - private List items = new ArrayList<>(); - private String selectedItem = null; - @NonNull - private final ClickListener clickListener; - - public NavigationAdapter(@NonNull Context context, @NonNull ClickListener clickListener) { - this.context = context; - this.mainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, BrandingUtil.readBrandMainColor(context)); - this.clickListener = clickListener; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_navigation, parent, false); - return new ViewHolder(v, clickListener); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - holder.bind(items.get(position)); - } - - @Override - public int getItemCount() { - return items.size(); - } - - public void setItems(@NonNull List items) { - this.items = items; - notifyDataSetChanged(); - } - - public void setSelectedItem(String id) { - selectedItem = id; - notifyDataSetChanged(); - } - - public String getSelectedItem() { - return selectedItem; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteClickListener.java b/app/src/main/java/it/niedermann/owncloud/notes/model/NoteClickListener.java deleted file mode 100644 index 8dd1748d..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteClickListener.java +++ /dev/null @@ -1,11 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.view.View; - -public interface NoteClickListener { - void onNoteClick(int position, View v); - - void onNoteFavoriteClick(int position, View v); - - boolean onNoteLongClick(int position, View v); -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteListsWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/model/NoteListsWidgetData.java deleted file mode 100644 index ded1a31f..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteListsWidgetData.java +++ /dev/null @@ -1,42 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class NoteListsWidgetData extends AbstractWidgetData { - public static final int MODE_DISPLAY_ALL = 0; - public static final int MODE_DISPLAY_STARRED = 1; - public static final int MODE_DISPLAY_CATEGORY = 2; - - @IntRange(from = 0, to = 2) - private int mode; - @Nullable - private Long categoryId; - - public int getMode() { - return mode; - } - - public void setMode(@IntRange(from = 0, to = 2) int mode) { - this.mode = mode; - } - - @Nullable - public Long getCategoryId() { - return categoryId; - } - - public void setCategoryId(@Nullable Long categoryId) { - this.categoryId = categoryId; - } - - @NonNull - @Override - public String toString() { - return "NoteListsWidgetData{" + - "mode=" + mode + - ", categoryId=" + categoryId + - '}'; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewGridHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewGridHolder.java deleted file mode 100644 index fa1f91c4..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewGridHolder.java +++ /dev/null @@ -1,55 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.content.Context; -import android.graphics.Typeface; -import android.text.TextUtils; -import android.util.TypedValue; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.Px; - -import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridBinding; - -import static android.view.View.GONE; -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; -import static it.niedermann.owncloud.notes.util.NoteUtil.EXCERPT_LINE_SEPARATOR; - -public class NoteViewGridHolder extends NoteViewHolder { - @NonNull - private final ItemNotesListNoteItemGridBinding binding; - - public NoteViewGridHolder(@NonNull ItemNotesListNoteItemGridBinding binding, @NonNull NoteClickListener noteClickListener, boolean monospace, @Px float fontSize) { - super(binding.getRoot(), noteClickListener); - this.binding = binding; - - binding.noteTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.1f); - binding.noteExcerpt.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * .8f); - if (monospace) { - binding.noteTitle.setTypeface(Typeface.MONOSPACE); - binding.noteExcerpt.setTypeface(Typeface.MONOSPACE); - } - } - - public void showSwipe(boolean left) { - throw new UnsupportedOperationException(NoteViewGridHolder.class.getSimpleName() + " does not support swiping"); - } - - public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { - super.bind(note, showCategory, mainColor, textColor, searchQuery); - @NonNull final Context context = itemView.getContext(); - bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor); - bindStatus(binding.noteStatus, note.getStatus(), mainColor); - bindFavorite(binding.noteFavorite, note.isFavorite()); - bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor); - bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), mainColor); - binding.noteExcerpt.setVisibility(TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE); - } - - @Nullable - public View getNoteSwipeable() { - return null; - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewGridHolderOnlyTitle.java b/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewGridHolderOnlyTitle.java deleted file mode 100644 index 3cee19e9..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewGridHolderOnlyTitle.java +++ /dev/null @@ -1,47 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.content.Context; -import android.graphics.Typeface; -import android.util.TypedValue; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.Px; - -import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridOnlyTitleBinding; - -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; - -public class NoteViewGridHolderOnlyTitle extends NoteViewHolder { - @NonNull - private final ItemNotesListNoteItemGridOnlyTitleBinding binding; - - public NoteViewGridHolderOnlyTitle(@NonNull ItemNotesListNoteItemGridOnlyTitleBinding binding, @NonNull NoteClickListener noteClickListener, boolean monospace, @Px float fontSize) { - super(binding.getRoot(), noteClickListener); - this.binding = binding; - - binding.noteTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.1f); - if (monospace) { - binding.noteTitle.setTypeface(Typeface.MONOSPACE); - } - } - - public void showSwipe(boolean left) { - throw new UnsupportedOperationException(NoteViewGridHolderOnlyTitle.class.getSimpleName() + " does not support swiping"); - } - - public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { - super.bind(note, showCategory, mainColor, textColor, searchQuery); - @NonNull final Context context = itemView.getContext(); - bindStatus(binding.noteStatus, note.getStatus(), mainColor); - bindFavorite(binding.noteFavorite, note.isFavorite()); - bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor); - } - - @Nullable - public View getNoteSwipeable() { - return null; - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolder.java deleted file mode 100644 index de83784c..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolder.java +++ /dev/null @@ -1,135 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Color; -import android.graphics.drawable.GradientDrawable; -import android.os.Build; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.BackgroundColorSpan; -import android.text.style.ForegroundColorSpan; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.CallSuper; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.chip.Chip; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.util.Notes; - -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; -import static it.niedermann.owncloud.notes.util.ColorUtil.contrastRatioIsSufficient; -import static it.niedermann.owncloud.notes.util.ColorUtil.isColorDark; - -public abstract class NoteViewHolder extends RecyclerView.ViewHolder { - @NonNull - private final NoteClickListener noteClickListener; - - public NoteViewHolder(@NonNull View v, @NonNull NoteClickListener noteClickListener) { - super(v); - this.noteClickListener = noteClickListener; - } - - @CallSuper - public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { - itemView.setOnClickListener((view) -> noteClickListener.onNoteClick(getAdapterPosition(), view)); - itemView.setOnLongClickListener((view) -> noteClickListener.onNoteLongClick(getAdapterPosition(), view)); - } - - protected void bindStatus(AppCompatImageView noteStatus, DBStatus status, int mainColor) { - noteStatus.setVisibility(DBStatus.VOID.equals(status) ? INVISIBLE : VISIBLE); - DrawableCompat.setTint(noteStatus.getDrawable(), BrandingUtil.getSecondaryForegroundColorDependingOnTheme(noteStatus.getContext(), mainColor)); - } - - protected void bindCategory(@NonNull Context context, @NonNull TextView noteCategory, boolean showCategory, @NonNull String category, int mainColor) { - final boolean isDarkThemeActive = Notes.isDarkThemeActive(context); - noteCategory.setVisibility(showCategory && !category.isEmpty() ? View.VISIBLE : View.GONE); - noteCategory.setText(category); - - @ColorInt int categoryForeground; - @ColorInt int categoryBackground; - - if (isDarkThemeActive) { - if (isColorDark(mainColor)) { - if (contrastRatioIsSufficient(mainColor, Color.BLACK)) { - categoryBackground = mainColor; - categoryForeground = Color.WHITE; - } else { - categoryBackground = Color.WHITE; - categoryForeground = mainColor; - } - } else { - categoryBackground = mainColor; - categoryForeground = Color.BLACK; - } - } else { - categoryForeground = Color.BLACK; - if (isColorDark(mainColor) || contrastRatioIsSufficient(mainColor, Color.WHITE)) { - categoryBackground = mainColor; - } else { - categoryBackground = Color.BLACK; - } - } - - noteCategory.setTextColor(categoryForeground); - if (noteCategory instanceof Chip) { - ((Chip) noteCategory).setChipStrokeColor(ColorStateList.valueOf(categoryBackground)); - ((Chip) noteCategory).setChipBackgroundColor(ColorStateList.valueOf(isDarkThemeActive ? categoryBackground : Color.TRANSPARENT)); - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - DrawableCompat.setTint(noteCategory.getBackground(), categoryBackground); - } else { - final GradientDrawable drawable = (GradientDrawable) noteCategory.getBackground(); - drawable.setStroke(1, categoryBackground); - drawable.setColor(isDarkThemeActive ? categoryBackground : Color.TRANSPARENT); - } - } - } - - protected void bindFavorite(@NonNull ImageView noteFavorite, boolean isFavorite) { - noteFavorite.setImageResource(isFavorite ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp); - noteFavorite.setOnClickListener(view -> noteClickListener.onNoteFavoriteClick(getAdapterPosition(), view)); - } - - protected void bindSearchableContent(@NonNull Context context, @NonNull TextView textView, @Nullable CharSequence searchQuery, @NonNull String content, int mainColor) { - CharSequence processedContent = content; - if (!TextUtils.isEmpty(searchQuery)) { - @ColorInt final int searchBackground = context.getResources().getColor(R.color.bg_highlighted); - @ColorInt final int searchForeground = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, mainColor); - - // The Pattern.quote method will add \Q to the very beginning of the string and \E to the end of the string - // It implies that the string between \Q and \E is a literal string and thus the reserved keyword in such string will be ignored. - // See https://stackoverflow.com/questions/15409296/what-is-the-use-of-pattern-quote-method - final Pattern pattern = Pattern.compile("(" + Pattern.quote(searchQuery.toString()) + ")", Pattern.CASE_INSENSITIVE); - SpannableString spannableString = new SpannableString(content); - Matcher matcher = pattern.matcher(spannableString); - - while (matcher.find()) { - spannableString.setSpan(new ForegroundColorSpan(searchForeground), matcher.start(), matcher.end(), 0); - spannableString.setSpan(new BackgroundColorSpan(searchBackground), matcher.start(), matcher.end(), 0); - } - - processedContent = spannableString; - } - textView.setText(processedContent); - } - - public abstract void showSwipe(boolean left); - - @Nullable - public abstract View getNoteSwipeable(); -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolderWithExcerpt.java b/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolderWithExcerpt.java deleted file mode 100644 index 05dc07c4..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolderWithExcerpt.java +++ /dev/null @@ -1,43 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.content.Context; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithExcerptBinding; - -public class NoteViewHolderWithExcerpt extends NoteViewHolder { - @NonNull - private final ItemNotesListNoteItemWithExcerptBinding binding; - - public NoteViewHolderWithExcerpt(@NonNull ItemNotesListNoteItemWithExcerptBinding binding, @NonNull NoteClickListener noteClickListener) { - super(binding.getRoot(), noteClickListener); - this.binding = binding; - } - - public void showSwipe(boolean left) { - binding.noteFavoriteLeft.setVisibility(left ? View.VISIBLE : View.INVISIBLE); - binding.noteDeleteRight.setVisibility(left ? View.INVISIBLE : View.VISIBLE); - binding.noteSwipeFrame.setBackgroundResource(left ? R.color.bg_warning : R.color.bg_attention); - } - - public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { - super.bind(note, showCategory, mainColor, textColor, searchQuery); - @NonNull final Context context = itemView.getContext(); - binding.noteSwipeable.setAlpha(DBStatus.LOCAL_DELETED.equals(note.getStatus()) ? 0.5f : 1.0f); - bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor); - bindStatus(binding.noteStatus, note.getStatus(), mainColor); - bindFavorite(binding.noteFavorite, note.isFavorite()); - - bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor); - bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt(), mainColor); - } - - @NonNull - public View getNoteSwipeable() { - return binding.noteSwipeable; - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolderWithoutExcerpt.java b/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolderWithoutExcerpt.java deleted file mode 100644 index 2af456a8..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/NoteViewHolderWithoutExcerpt.java +++ /dev/null @@ -1,41 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.content.Context; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithoutExcerptBinding; - -public class NoteViewHolderWithoutExcerpt extends NoteViewHolder { - @NonNull - private final ItemNotesListNoteItemWithoutExcerptBinding binding; - - public NoteViewHolderWithoutExcerpt(@NonNull ItemNotesListNoteItemWithoutExcerptBinding binding, @NonNull NoteClickListener noteClickListener) { - super(binding.getRoot(), noteClickListener); - this.binding = binding; - } - - public void showSwipe(boolean left) { - binding.noteFavoriteLeft.setVisibility(left ? View.VISIBLE : View.INVISIBLE); - binding.noteDeleteRight.setVisibility(left ? View.INVISIBLE : View.VISIBLE); - binding.noteSwipeFrame.setBackgroundResource(left ? R.color.bg_warning : R.color.bg_attention); - } - - public void bind(@NonNull DBNote note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) { - super.bind(note, showCategory, mainColor, textColor, searchQuery); - @NonNull final Context context = itemView.getContext(); - binding.noteSwipeable.setAlpha(DBStatus.LOCAL_DELETED.equals(note.getStatus()) ? 0.5f : 1.0f); - bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor); - bindStatus(binding.noteStatus, note.getStatus(), mainColor); - bindFavorite(binding.noteFavorite, note.isFavorite()); - bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor); - } - - @NonNull - public View getNoteSwipeable() { - return binding.noteSwipeable; - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/SectionItem.java b/app/src/main/java/it/niedermann/owncloud/notes/model/SectionItem.java deleted file mode 100644 index 27e4758a..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/SectionItem.java +++ /dev/null @@ -1,23 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -public class SectionItem implements Item { - - private String title; - - public SectionItem(String title) { - this.title = title; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - @Override - public boolean isSection() { - return true; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/SectionItemDecoration.java b/app/src/main/java/it/niedermann/owncloud/notes/model/SectionItemDecoration.java deleted file mode 100644 index 8868f4e8..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/SectionItemDecoration.java +++ /dev/null @@ -1,38 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import android.graphics.Rect; -import android.view.View; - -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; -import androidx.annotation.Px; -import androidx.recyclerview.widget.RecyclerView; - -public class SectionItemDecoration extends RecyclerView.ItemDecoration { - - @NonNull - private final ItemAdapter adapter; - private final int sectionLeft; - private final int sectionTop; - private final int sectionRight; - private final int sectionBottom; - - public SectionItemDecoration(@NonNull ItemAdapter adapter, @Px int sectionLeft, @Px int sectionTop, @Px int sectionRight, @Px int sectionBottom) { - this.adapter = adapter; - this.sectionLeft = sectionLeft; - this.sectionTop = sectionTop; - this.sectionRight = sectionRight; - this.sectionBottom = sectionBottom; - } - - @CallSuper - public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { - final int position = parent.getChildAdapterPosition(view); - if (adapter.getItemViewType(position) == ItemAdapter.TYPE_SECTION) { - outRect.left = sectionLeft; - outRect.top = sectionTop; - outRect.right = sectionRight; - outRect.bottom = sectionBottom; - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/SectionViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/model/SectionViewHolder.java deleted file mode 100644 index 7f8447fd..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/SectionViewHolder.java +++ /dev/null @@ -1,18 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -import androidx.recyclerview.widget.RecyclerView; - -import it.niedermann.owncloud.notes.databinding.ItemNotesListSectionItemBinding; - -public class SectionViewHolder extends RecyclerView.ViewHolder { - private final ItemNotesListSectionItemBinding binding; - - public SectionViewHolder(ItemNotesListSectionItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - - public void bind(SectionItem item) { - binding.sectionTitle.setText(item.getTitle()); - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/SingleNoteWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/model/SingleNoteWidgetData.java deleted file mode 100644 index 556c1041..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/SingleNoteWidgetData.java +++ /dev/null @@ -1,23 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -public class SingleNoteWidgetData extends AbstractWidgetData { - private long noteId; - - public SingleNoteWidgetData() { - - } - - public SingleNoteWidgetData(int appWidgetId, long accountId, long noteId, int themeMode) { - super(appWidgetId, accountId, themeMode); - this.noteId = noteId; - } - - public long getNoteId() { - return noteId; - } - - public void setNoteId(long noteId) { - this.noteId = noteId; - } - -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/model/SyncResultStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/model/SyncResultStatus.java deleted file mode 100644 index bea217db..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/model/SyncResultStatus.java +++ /dev/null @@ -1,6 +0,0 @@ -package it.niedermann.owncloud.notes.model; - -public class SyncResultStatus { - public boolean pullSuccessful = true; - public boolean pushSuccessful = true; -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/AbstractNotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/AbstractNotesDatabase.java index d80aa094..46d36e1c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/AbstractNotesDatabase.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/AbstractNotesDatabase.java @@ -22,7 +22,7 @@ import it.niedermann.owncloud.notes.persistence.migration.Migration_6_7; import it.niedermann.owncloud.notes.persistence.migration.Migration_7_8; import it.niedermann.owncloud.notes.persistence.migration.Migration_8_9; import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10; -import it.niedermann.owncloud.notes.util.DatabaseIndexUtil; +import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil; abstract class AbstractNotesDatabase extends SQLiteOpenHelper { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java index b9865e3a..9a08bc1e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java @@ -21,7 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import it.niedermann.owncloud.notes.model.Capabilities; +import it.niedermann.owncloud.notes.shared.model.Capabilities; @WorkerThread public class CapabilitiesClient { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java index 31b30b33..26e18311 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java @@ -20,8 +20,8 @@ import java.net.HttpURLConnection; import java.util.Objects; import java.util.concurrent.TimeUnit; -import it.niedermann.owncloud.notes.model.Capabilities; -import it.niedermann.owncloud.notes.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.model.Capabilities; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; public class CapabilitiesWorker extends Worker { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/LoadNotesListTask.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/LoadNotesListTask.java index e7d88691..0380b0be 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/LoadNotesListTask.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/LoadNotesListTask.java @@ -13,12 +13,12 @@ import java.util.Calendar; import java.util.List; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.model.Category; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.Item; -import it.niedermann.owncloud.notes.model.SectionItem; -import it.niedermann.owncloud.notes.util.CategorySortingMethod; -import it.niedermann.owncloud.notes.util.NoteUtil; +import it.niedermann.owncloud.notes.shared.model.Category; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.Item; +import it.niedermann.owncloud.notes.main.items.section.SectionItem; +import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; +import it.niedermann.owncloud.notes.shared.util.NoteUtil; public class LoadNotesListTask extends AsyncTask> { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java index 7158c9d2..4b2f4b7d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java @@ -33,16 +33,16 @@ import java.util.Objects; import java.util.Set; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.fragment.ExceptionDialogFragment; +import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment; import it.niedermann.owncloud.notes.branding.BrandedSnackbar; -import it.niedermann.owncloud.notes.model.CloudNote; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.DBStatus; -import it.niedermann.owncloud.notes.model.ISyncCallback; -import it.niedermann.owncloud.notes.model.LocalAccount; -import it.niedermann.owncloud.notes.model.SyncResultStatus; -import it.niedermann.owncloud.notes.util.SSOUtil; -import it.niedermann.owncloud.notes.util.ServerResponse; +import it.niedermann.owncloud.notes.shared.model.CloudNote; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.DBStatus; +import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.model.SyncResultStatus; +import it.niedermann.owncloud.notes.shared.util.SSOUtil; +import it.niedermann.owncloud.notes.shared.model.ServerResponse; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java index 207b4d49..c9ab9b39 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClient.java @@ -25,10 +25,10 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import it.niedermann.owncloud.notes.model.ApiVersion; -import it.niedermann.owncloud.notes.model.CloudNote; -import it.niedermann.owncloud.notes.util.ServerResponse.NoteResponse; -import it.niedermann.owncloud.notes.util.ServerResponse.NotesResponse; +import it.niedermann.owncloud.notes.shared.model.ApiVersion; +import it.niedermann.owncloud.notes.shared.model.CloudNote; +import it.niedermann.owncloud.notes.shared.model.ServerResponse.NoteResponse; +import it.niedermann.owncloud.notes.shared.model.ServerResponse.NotesResponse; @SuppressWarnings("WeakerAccess") @WorkerThread diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java index 965e3a90..e529dd8c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV02.java @@ -12,9 +12,9 @@ import org.json.JSONObject; import java.util.HashMap; import java.util.Map; -import it.niedermann.owncloud.notes.model.CloudNote; -import it.niedermann.owncloud.notes.util.ServerResponse.NoteResponse; -import it.niedermann.owncloud.notes.util.ServerResponse.NotesResponse; +import it.niedermann.owncloud.notes.shared.model.CloudNote; +import it.niedermann.owncloud.notes.shared.model.ServerResponse.NoteResponse; +import it.niedermann.owncloud.notes.shared.model.ServerResponse.NotesResponse; @WorkerThread public class NotesClientV02 extends NotesClient { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java index 75da6a56..220d4f8f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesClientV1.java @@ -12,9 +12,9 @@ import org.json.JSONObject; import java.util.HashMap; import java.util.Map; -import it.niedermann.owncloud.notes.model.CloudNote; -import it.niedermann.owncloud.notes.util.ServerResponse.NoteResponse; -import it.niedermann.owncloud.notes.util.ServerResponse.NotesResponse; +import it.niedermann.owncloud.notes.shared.model.CloudNote; +import it.niedermann.owncloud.notes.shared.model.ServerResponse.NoteResponse; +import it.niedermann.owncloud.notes.shared.model.ServerResponse.NotesResponse; @WorkerThread public class NotesClientV1 extends NotesClient { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java index 236d566d..1663b289 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java @@ -42,27 +42,27 @@ import java.util.NoSuchElementException; import java.util.Set; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.activity.EditNoteActivity; -import it.niedermann.owncloud.notes.model.ApiVersion; -import it.niedermann.owncloud.notes.model.Capabilities; -import it.niedermann.owncloud.notes.model.Category; -import it.niedermann.owncloud.notes.model.CloudNote; -import it.niedermann.owncloud.notes.model.DBNote; -import it.niedermann.owncloud.notes.model.DBStatus; -import it.niedermann.owncloud.notes.model.ISyncCallback; -import it.niedermann.owncloud.notes.model.LocalAccount; -import it.niedermann.owncloud.notes.model.NavigationAdapter; -import it.niedermann.owncloud.notes.model.NoteListsWidgetData; -import it.niedermann.owncloud.notes.model.SingleNoteWidgetData; -import it.niedermann.owncloud.notes.util.CategorySortingMethod; -import it.niedermann.owncloud.notes.util.ColorUtil; -import it.niedermann.owncloud.notes.util.NoteUtil; - -import static it.niedermann.owncloud.notes.android.activity.EditNoteActivity.ACTION_SHORTCUT; -import static it.niedermann.owncloud.notes.android.appwidget.NoteListWidget.updateNoteListWidgets; -import static it.niedermann.owncloud.notes.android.appwidget.SingleNoteWidget.updateSingleNoteWidgets; -import static it.niedermann.owncloud.notes.model.NoteListsWidgetData.MODE_DISPLAY_CATEGORY; -import static it.niedermann.owncloud.notes.util.NoteUtil.generateNoteExcerpt; +import it.niedermann.owncloud.notes.edit.EditNoteActivity; +import it.niedermann.owncloud.notes.shared.model.ApiVersion; +import it.niedermann.owncloud.notes.shared.model.Capabilities; +import it.niedermann.owncloud.notes.shared.model.Category; +import it.niedermann.owncloud.notes.shared.model.CloudNote; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.shared.model.DBStatus; +import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; +import it.niedermann.owncloud.notes.main.NavigationAdapter; +import it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData; +import it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidgetData; +import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; +import it.niedermann.owncloud.notes.shared.util.ColorUtil; +import it.niedermann.owncloud.notes.shared.util.NoteUtil; + +import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT; +import static it.niedermann.owncloud.notes.widget.notelist.NoteListWidget.updateNoteListWidgets; +import static it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidget.updateSingleNoteWidgets; +import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_CATEGORY; +import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt; /** * Helps to add, get, update and delete Notes with the option to trigger a Resync with the Server. diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java index f1277666..4a29bd6d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java @@ -20,7 +20,7 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.model.LocalAccount; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; public class SyncWorker extends Worker { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_10_11.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_10_11.java index 82a627aa..4d45a3ce 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_10_11.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_10_11.java @@ -8,7 +8,7 @@ import androidx.preference.PreferenceManager; import java.util.Map; -import it.niedermann.owncloud.notes.android.DarkModeSetting; +import it.niedermann.owncloud.notes.preferences.DarkModeSetting; public class Migration_10_11 { /** diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_11_12.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_11_12.java index 82a9984e..6fd531c9 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_11_12.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_11_12.java @@ -5,7 +5,7 @@ import android.database.sqlite.SQLiteDatabase; import androidx.annotation.NonNull; -import it.niedermann.owncloud.notes.model.ApiVersion; +import it.niedermann.owncloud.notes.shared.model.ApiVersion; import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker; public class Migration_11_12 { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_12_13.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_12_13.java index 895a59a1..77954c3f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_12_13.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_12_13.java @@ -6,7 +6,7 @@ import android.database.sqlite.SQLiteDatabase; import androidx.annotation.NonNull; import androidx.work.WorkManager; -import it.niedermann.owncloud.notes.model.Capabilities; +import it.niedermann.owncloud.notes.shared.model.Capabilities; public class Migration_12_13 { /** diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java index d69396d3..66a2f643 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_13_14.java @@ -11,7 +11,7 @@ import androidx.preference.PreferenceManager; import java.util.Map; -import it.niedermann.owncloud.notes.android.DarkModeSetting; +import it.niedermann.owncloud.notes.preferences.DarkModeSetting; public class Migration_13_14 { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java index b025bbaa..e69c8ff1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java @@ -7,7 +7,7 @@ import android.util.Log; import java.util.Hashtable; -import it.niedermann.owncloud.notes.util.DatabaseIndexUtil; +import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil; public class Migration_14_15 { /** diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java index 396f29e1..49056239 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_15_16.java @@ -12,7 +12,7 @@ import androidx.preference.PreferenceManager; import java.util.Map; -import it.niedermann.owncloud.notes.android.DarkModeSetting; +import it.niedermann.owncloud.notes.preferences.DarkModeSetting; public class Migration_15_16 { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_4_5.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_4_5.java index 612ed1cb..aa807944 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_4_5.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_4_5.java @@ -4,8 +4,6 @@ import android.database.sqlite.SQLiteDatabase; import androidx.annotation.NonNull; -import it.niedermann.owncloud.notes.model.DBStatus; - public class Migration_4_5 { /** * Differentiate between local id and remote id @@ -13,6 +11,6 @@ public class Migration_4_5 { public Migration_4_5(@NonNull SQLiteDatabase db) { db.execSQL("ALTER TABLE NOTES ADD COLUMN REMOTEID INTEGER"); db.execSQL("UPDATE NOTES SET REMOTEID=ID WHERE (REMOTEID IS NULL OR REMOTEID=0) AND STATUS!=?", new String[]{"LOCAL_CREATED"}); - db.execSQL("UPDATE NOTES SET REMOTEID=0, STATUS=? WHERE STATUS=?", new String[]{DBStatus.LOCAL_EDITED.getTitle(), "LOCAL_CREATED"}); + db.execSQL("UPDATE NOTES SET REMOTEID=0, STATUS=? WHERE STATUS=?", new String[]{"LOCAL_EDITED", "LOCAL_CREATED"}); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_6_7.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_6_7.java index 399d6f40..e7d0eadd 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_6_7.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_6_7.java @@ -4,7 +4,7 @@ import android.database.sqlite.SQLiteDatabase; import androidx.annotation.NonNull; -import it.niedermann.owncloud.notes.util.DatabaseIndexUtil; +import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil; public class Migration_6_7 { /** diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_7_8.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_7_8.java index 9a968fe1..d1fd40c0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_7_8.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_7_8.java @@ -4,7 +4,7 @@ import android.database.sqlite.SQLiteDatabase; import androidx.annotation.NonNull; -import it.niedermann.owncloud.notes.util.DatabaseIndexUtil; +import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil; public class Migration_7_8 { public Migration_7_8(@NonNull SQLiteDatabase db) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_8_9.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_8_9.java index 31957fc0..03e171f3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_8_9.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_8_9.java @@ -15,9 +15,9 @@ import androidx.preference.PreferenceManager; import java.net.MalformedURLException; import java.net.URL; -import it.niedermann.owncloud.notes.android.appwidget.NoteListWidget; -import it.niedermann.owncloud.notes.android.appwidget.SingleNoteWidget; -import it.niedermann.owncloud.notes.util.DatabaseIndexUtil; +import it.niedermann.owncloud.notes.widget.notelist.NoteListWidget; +import it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidget; +import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil; public class Migration_8_9 { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_9_10.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_9_10.java index c5bf2c02..b17b8675 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_9_10.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_9_10.java @@ -6,7 +6,7 @@ import android.database.sqlite.SQLiteDatabase; import androidx.annotation.NonNull; -import it.niedermann.owncloud.notes.util.NoteUtil; +import it.niedermann.owncloud.notes.shared.util.NoteUtil; public class Migration_9_10 { /** diff --git a/app/src/main/java/it/niedermann/owncloud/notes/preferences/DarkModeSetting.java b/app/src/main/java/it/niedermann/owncloud/notes/preferences/DarkModeSetting.java new file mode 100644 index 00000000..d34ffe74 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/DarkModeSetting.java @@ -0,0 +1,66 @@ +package it.niedermann.owncloud.notes.preferences; + +import androidx.appcompat.app.AppCompatDelegate; + +import java.util.NoSuchElementException; + +/** + * Possible values of the Dark Mode Setting. + *

+ * The Dark Mode Setting can be stored in {@link android.content.SharedPreferences} as String by using {@link DarkModeSetting#name()} and received via {@link DarkModeSetting#valueOf(String)}. + *

+ * Additionally, the equivalent {@link AppCompatDelegate}-Mode can be received via {@link #getModeId()}. To convert a {@link AppCompatDelegate}-Mode to a {@link DarkModeSetting}, use {@link #fromModeID(int)} + * + * @see AppCompatDelegate#MODE_NIGHT_YES + * @see AppCompatDelegate#MODE_NIGHT_NO + * @see AppCompatDelegate#MODE_NIGHT_FOLLOW_SYSTEM + */ +public enum DarkModeSetting { + // WARNING - The names of the constants must *NOT* be changed since they are used as keys in SharedPreferences + + /** + * Always use light mode. + */ + LIGHT(AppCompatDelegate.MODE_NIGHT_NO), + /** + * Always use dark mode. + */ + DARK(AppCompatDelegate.MODE_NIGHT_YES), + /** + * Follow the global system setting for dark mode. + */ + SYSTEM_DEFAULT(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + + private final int modeId; + + DarkModeSetting(int modeId) { + this.modeId = modeId; + } + + public int getModeId() { + return modeId; + } + + /** + * Returns the instance of {@link DarkModeSetting} that corresponds to the ModeID of {@link AppCompatDelegate} + *

+ * Possible ModeIDs are: + *

    + *
  • {@link AppCompatDelegate#MODE_NIGHT_YES}
  • + *
  • {@link AppCompatDelegate#MODE_NIGHT_NO}
  • + *
  • {@link AppCompatDelegate#MODE_NIGHT_FOLLOW_SYSTEM}
  • + *
+ * + * @param id One of the {@link AppCompatDelegate}-Night-Modes + * @return An instance of {@link DarkModeSetting} + */ + public static DarkModeSetting fromModeID(int id) { + for (DarkModeSetting value : DarkModeSetting.values()) { + if (value.modeId == id) { + return value; + } + } + + throw new NoSuchElementException("No NightMode with ID " + id + " found"); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java new file mode 100644 index 00000000..e808a3a0 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java @@ -0,0 +1,37 @@ +package it.niedermann.owncloud.notes.preferences; + +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.LockedActivity; +import it.niedermann.owncloud.notes.databinding.ActivityPreferencesBinding; + +/** + * Allows to change application settings. + */ + +public class PreferencesActivity extends LockedActivity { + + private ActivityPreferencesBinding binding; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivityPreferencesBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setSupportActionBar(binding.toolbar); + setResult(RESULT_CANCELED); + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragment_container_view, new PreferencesFragment()) + .commit(); + } + + @Override + public void applyBrand(int mainColor, int textColor) { + applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java new file mode 100644 index 00000000..d324b9f6 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java @@ -0,0 +1,137 @@ +package it.niedermann.owncloud.notes.preferences; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.Branded; +import it.niedermann.owncloud.notes.branding.BrandedSwitchPreference; +import it.niedermann.owncloud.notes.branding.BrandingUtil; +import it.niedermann.owncloud.notes.persistence.SyncWorker; +import it.niedermann.owncloud.notes.shared.util.DeviceCredentialUtil; +import it.niedermann.owncloud.notes.NotesApplication; + +import static it.niedermann.owncloud.notes.widget.notelist.NoteListWidget.updateNoteListWidgets; + +public class PreferencesFragment extends PreferenceFragmentCompat implements Branded { + + private static final String TAG = PreferencesFragment.class.getSimpleName(); + + private BrandedSwitchPreference fontPref; + private BrandedSwitchPreference lockPref; + private BrandedSwitchPreference wifiOnlyPref; + private BrandedSwitchPreference brandingPref; + private BrandedSwitchPreference gridViewPref; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences); + + fontPref = findPreference(getString(R.string.pref_key_font)); + + brandingPref = findPreference(getString(R.string.pref_key_branding)); + if (brandingPref != null) { + brandingPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { + updateNoteListWidgets(requireContext()); + final Boolean branding = (Boolean) newValue; + Log.v(TAG, "branding: " + branding); + requireActivity().setResult(Activity.RESULT_OK); + requireActivity().recreate(); + return true; + }); + } else { + Log.e(TAG, "Could not find preference with key: \"" + getString(R.string.pref_key_branding) + "\""); + } + + gridViewPref = findPreference(getString(R.string.pref_key_gridview)); + if (gridViewPref != null) { + gridViewPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { + final Boolean gridView = (Boolean) newValue; + Log.v(TAG, "gridView: " + gridView); + requireActivity().setResult(Activity.RESULT_OK); + NotesApplication.updateGridViewEnabled(gridView); + return true; + }); + } else { + Log.e(TAG, "Could not find preference with key: \"" + getString(R.string.pref_key_branding) + "\""); + } + + lockPref = findPreference(getString(R.string.pref_key_lock)); + if (lockPref != null) { + if (!DeviceCredentialUtil.areCredentialsAvailable(requireContext())) { + lockPref.setVisible(false); + Preference securityCategory = findPreference(getString(R.string.pref_category_security)); + if (securityCategory != null) { + securityCategory.setVisible(false); + } else { + Log.e(TAG, "Could not find preference " + getString(R.string.pref_category_security)); + } + } else { + lockPref.setOnPreferenceChangeListener((preference, newValue) -> { + NotesApplication.setLockedPreference((Boolean) newValue); + return true; + }); + } + } else { + Log.e(TAG, "Could not find \"" + getString(R.string.pref_key_lock) + "\"-preference."); + } + + final ListPreference themePref = findPreference(getString(R.string.pref_key_theme)); + assert themePref != null; + themePref.setOnPreferenceChangeListener((preference, newValue) -> { + NotesApplication.setAppTheme(DarkModeSetting.valueOf((String) newValue)); + requireActivity().setResult(Activity.RESULT_OK); + requireActivity().recreate(); + return true; + }); + + wifiOnlyPref = findPreference(getString(R.string.pref_key_wifi_only)); + assert wifiOnlyPref != null; + wifiOnlyPref.setOnPreferenceChangeListener((preference, newValue) -> { + Log.i(TAG, "syncOnWifiOnly: " + newValue); + return true; + }); + + final ListPreference syncPref = findPreference(getString(R.string.pref_key_background_sync)); + assert syncPref != null; + syncPref.setOnPreferenceChangeListener((preference, newValue) -> { + Log.i(TAG, "syncPref: " + preference + " - newValue: " + newValue); + SyncWorker.update(requireContext(), newValue.toString()); + return true; + }); + } + + + @Override + public void onStart() { + super.onStart(); + @Nullable Context context = getContext(); + if (context != null) { + @ColorInt final int mainColor = BrandingUtil.readBrandMainColor(context); + @ColorInt final int textColor = BrandingUtil.readBrandTextColor(context); + applyBrand(mainColor, textColor); + } + } + + @Override + public void applyBrand(int mainColor, int textColor) { + fontPref.applyBrand(mainColor, textColor); + lockPref.applyBrand(mainColor, textColor); + wifiOnlyPref.applyBrand(mainColor, textColor); + brandingPref.applyBrand(mainColor, textColor); + gridViewPref.applyBrand(mainColor, textColor); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/quicksettings/NewNoteTileService.java b/app/src/main/java/it/niedermann/owncloud/notes/quicksettings/NewNoteTileService.java new file mode 100644 index 00000000..01308785 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/quicksettings/NewNoteTileService.java @@ -0,0 +1,34 @@ +package it.niedermann.owncloud.notes.quicksettings; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.os.Build; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; + +import it.niedermann.owncloud.notes.edit.EditNoteActivity; + +/** + * This {@link TileService} adds a quick settings tile that leads to the new note view. + */ +@TargetApi(Build.VERSION_CODES.N) +public class NewNoteTileService extends TileService { + + @Override + public void onStartListening() { + Tile tile = getQsTile(); + tile.setState(Tile.STATE_ACTIVE); + + tile.updateTile(); + } + + @Override + public void onClick() { + // create new note intent + final Intent newNoteIntent = new Intent(getApplicationContext(), EditNoteActivity.class); + // ensure it won't open twice if already running + newNoteIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + // ask to unlock the screen if locked, then start new note intent + unlockAndRun(() -> startActivityAndCollapse(newNoteIntent)); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserAdapter.java new file mode 100644 index 00000000..dd28f431 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserAdapter.java @@ -0,0 +1,44 @@ +package it.niedermann.owncloud.notes.shared.account; + +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.List; + +import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; + +public class AccountChooserAdapter extends RecyclerView.Adapter { + + @NonNull + private final List localAccounts; + @NonNull + private final Consumer targetAccountConsumer; + + public AccountChooserAdapter(@NonNull List localAccounts, @NonNull Consumer targetAccountConsumer) { + super(); + this.localAccounts = localAccounts; + this.targetAccountConsumer = targetAccountConsumer; + } + + @NonNull + @Override + public AccountChooserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new AccountChooserViewHolder(ItemAccountChooseBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull AccountChooserViewHolder holder, int position) { + holder.bind(localAccounts.get(position), targetAccountConsumer); + } + + @Override + public int getItemCount() { + return localAccounts.size(); + } + +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java new file mode 100644 index 00000000..9fc2f382 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java @@ -0,0 +1,37 @@ +package it.niedermann.owncloud.notes.shared.account; + +import android.net.Uri; + +import androidx.core.util.Consumer; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import it.niedermann.android.glidesso.SingleSignOnUrl; +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; + +public class AccountChooserViewHolder extends RecyclerView.ViewHolder { + private final ItemAccountChooseBinding binding; + + protected AccountChooserViewHolder(ItemAccountChooseBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(LocalAccount localAccount, Consumer targetAccountConsumer) { + Glide + .with(binding.accountItemAvatar.getContext()) + .load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64")) + .placeholder(R.drawable.ic_account_circle_grey_24dp) + .error(R.drawable.ic_account_circle_grey_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(binding.accountItemAvatar); + + binding.accountLayout.setOnClickListener((v) -> targetAccountConsumer.accept(localAccount)); + binding.accountName.setText(localAccount.getUserName()); + binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost()); + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java new file mode 100644 index 00000000..b27afd7a --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java @@ -0,0 +1,86 @@ +package it.niedermann.owncloud.notes.shared.model; + + +import androidx.annotation.NonNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@SuppressWarnings("WeakerAccess") +public class ApiVersion implements Comparable { + private static final Pattern NUMBER_EXTRACTION_PATTERN = Pattern.compile("[0-9]+"); + + private String originalVersion = "?"; + private int major; + private int minor; + + public ApiVersion(String originalVersion, int major, int minor) { + this(major, minor); + this.originalVersion = originalVersion; + } + + public ApiVersion(int major, int minor) { + this.major = major; + this.minor = minor; + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public String getOriginalVersion() { + return originalVersion; + } + + public static ApiVersion of(String versionString) { + int major = 0, minor = 0; + if (versionString != null) { + String[] split = versionString.split("\\."); + if (split.length > 0) { + major = extractNumber(split[0]); + if (split.length > 1) { + minor = extractNumber(split[1]); + } + } + } + return new ApiVersion(versionString, major, minor); + } + + private static int extractNumber(String containsNumbers) { + final Matcher matcher = NUMBER_EXTRACTION_PATTERN.matcher(containsNumbers); + if (matcher.find()) { + return Integer.parseInt(matcher.group()); + } + return 0; + } + + /** + * @param compare another version object + * @return -1 if the compared major version is higher than the current major version + * 0 if the compared major version is equal to the current major version + * 1 if the compared major version is lower than the current major version + */ + @Override + public int compareTo(ApiVersion compare) { + if (compare.getMajor() > getMajor()) { + return -1; + } else if (compare.getMajor() < getMajor()) { + return 1; + } + return 0; + } + + @NonNull + @Override + public String toString() { + return "Version{" + + "originalVersion='" + originalVersion + '\'' + + ", major=" + major + + ", minor=" + minor + + '}'; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java new file mode 100644 index 00000000..b182ca63 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java @@ -0,0 +1,106 @@ +package it.niedermann.owncloud.notes.shared.model; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.HttpException; +import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; + +import org.json.JSONException; +import org.json.JSONObject; + +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; + +/** + * This entity class is used to return relevant data of the HTTP reponse. + */ +public class Capabilities { + + private static final String TAG = Capabilities.class.getSimpleName(); + + private static final String JSON_OCS = "ocs"; + private static final String JSON_OCS_META = "meta"; + private static final String JSON_OCS_META_STATUSCODE = "statuscode"; + private static final String JSON_OCS_DATA = "data"; + private static final String JSON_OCS_DATA_CAPABILITIES = "capabilities"; + private static final String JSON_OCS_DATA_CAPABILITIES_NOTES = "notes"; + private static final String JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION = "api_version"; + private static final String JSON_OCS_DATA_CAPABILITIES_THEMING = "theming"; + private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR = "color"; + private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT = "color-text"; + + private String apiVersion = null; + private String color = null; + private String textColor = null; + @Nullable + private String eTag; + + public Capabilities(@NonNull String response, @Nullable String eTag) throws NextcloudHttpRequestFailedException { + this.eTag = eTag; + final JSONObject ocs; + try { + ocs = new JSONObject(response).getJSONObject(JSON_OCS); + if (ocs.has(JSON_OCS_META)) { + final JSONObject meta = ocs.getJSONObject(JSON_OCS_META); + if (meta.has(JSON_OCS_META_STATUSCODE)) { + if (meta.getInt(JSON_OCS_META_STATUSCODE) == HTTP_UNAVAILABLE) { + Log.i(TAG, "Capabilities Endpoint: This instance is currently in maintenance mode."); + throw new NextcloudHttpRequestFailedException(HTTP_UNAVAILABLE, new HttpException(HTTP_UNAVAILABLE)); + } + } + } + if (ocs.has(JSON_OCS_DATA)) { + final JSONObject data = ocs.getJSONObject(JSON_OCS_DATA); + if (data.has(JSON_OCS_DATA_CAPABILITIES)) { + final JSONObject capabilities = data.getJSONObject(JSON_OCS_DATA_CAPABILITIES); + if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_NOTES)) { + final JSONObject notes = capabilities.getJSONObject(JSON_OCS_DATA_CAPABILITIES_NOTES); + if (notes.has(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION)) { + this.apiVersion = notes.getString(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION); + } + } + if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_THEMING)) { + final JSONObject theming = capabilities.getJSONObject(JSON_OCS_DATA_CAPABILITIES_THEMING); + if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR)) { + this.color = theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR); + } + if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)) { + this.textColor = theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT); + } + } + } + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + public String getApiVersion() { + return apiVersion; + } + + public String getColor() { + return color; + } + + public String getTextColor() { + return textColor; + } + + @Nullable + public String getETag() { + return eTag; + } + + @NonNull + @Override + public String toString() { + return "Capabilities{" + + "apiVersion='" + apiVersion + '\'' + + ", color='" + color + '\'' + + ", textColor='" + textColor + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Category.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Category.java new file mode 100644 index 00000000..ab1d10a1 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Category.java @@ -0,0 +1,18 @@ +package it.niedermann.owncloud.notes.shared.model; + +import androidx.annotation.Nullable; + +import java.io.Serializable; + +public class Category implements Serializable { + + @Nullable + public final String category; + @Nullable + public final Boolean favorite; + + public Category(@Nullable String category, @Nullable Boolean favorite) { + this.category = category; + this.favorite = favorite; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java new file mode 100644 index 00000000..35ab8b66 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java @@ -0,0 +1,41 @@ +package it.niedermann.owncloud.notes.shared.model; + +public enum CategorySortingMethod { + SORT_MODIFIED_DESC("MODIFIED DESC"), + SORT_LEXICOGRAPHICAL_ASC("TITLE COLLATE NOCASE ASC"); + + private String sorder; // sorting method OrderBy for SQL + + /*** + * Constructor + * @param orderby given sorting method OrderBy + */ + CategorySortingMethod(String orderby) { + this.sorder = orderby; + } + + /*** + * Retrieve the sorting method id represented in database + * @return the sorting method id for the enum item + */ + public int getCSMID() { + return this.ordinal(); + } + + /*** + * Retrieve the sorting method order for SQL + * @return the sorting method order for the enum item + */ + public String getSorder() { + return this.sorder; + } + + /*** + * Retrieve the corresponding enum value with given the index (ordinal) + * @param index the index / ordinal of the corresponding enum value stored in DB + * @return the corresponding enum item with the index (ordinal) + */ + public static CategorySortingMethod getCSM(int index) { + return CategorySortingMethod.values()[index]; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CloudNote.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CloudNote.java new file mode 100644 index 00000000..cf6c51e8 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CloudNote.java @@ -0,0 +1,103 @@ +package it.niedermann.owncloud.notes.shared.model; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + +import it.niedermann.owncloud.notes.shared.util.NoteUtil; + +/** + * CloudNote represents a remote note from an OwnCloud server. + * It can be directly generated from the JSON answer from the server. + */ +public class CloudNote implements Serializable { + private long remoteId; + private String title = ""; + private Calendar modified; + private String content = ""; + private boolean favorite = false; + private String category = ""; + private String etag = ""; + private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + public CloudNote(long remoteId, Calendar modified, String title, String content, boolean favorite, String category, String etag) { + this.remoteId = remoteId; + setTitle(title); + setContent(content); + setFavorite(favorite); + setCategory(category); + setEtag(etag); + this.modified = modified; + } + + public long getRemoteId() { + return remoteId; + } + + public void setRemoteId(long remoteId) { + this.remoteId = remoteId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = NoteUtil.removeMarkDown(title); + } + + public Calendar getModified() { + return modified; + } + + public String getModified(String format) { + if (modified == null) + return null; + return new SimpleDateFormat(format, Locale.GERMANY).format(this.getModified().getTimeInMillis()); + } + + public void setModified(Calendar modified) { + this.modified = modified; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public boolean isFavorite() { + return favorite; + } + + public void setFavorite(boolean favorite) { + this.favorite = favorite; + } + + public String getEtag() { + return etag; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category == null ? "" : category; + } + + @NonNull + @Override + public String toString() { + return "R" + getRemoteId() + " " + (isFavorite() ? " (*) " : " ") + getCategory() + " / " + getTitle() + " (" + getModified(DATE_FORMAT) + " / " + getEtag() + ")"; + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBNote.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBNote.java new file mode 100644 index 00000000..059a1ce3 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBNote.java @@ -0,0 +1,80 @@ +package it.niedermann.owncloud.notes.shared.model; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.util.Calendar; + +/** + * DBNote represents a single note from the local SQLite database with all attributes. + * It extends CloudNote with attributes required for local data management. + */ +public class DBNote extends CloudNote implements Item, Serializable { + + private final long id; + private final long accountId; + private DBStatus status; + private String excerpt; + private int scrollY; + + public DBNote(long id, long remoteId, Calendar modified, String title, String content, boolean favorite, String category, String etag, DBStatus status, long accountId, String excerpt, int scrollY) { + super(remoteId, modified, title, content, favorite, category, etag); + this.id = id; + this.excerpt = excerpt; + this.status = status; + this.accountId = accountId; + this.scrollY = scrollY; + } + + public long getId() { + return id; + } + + public long getAccountId() { + return accountId; + } + + public DBStatus getStatus() { + return status; + } + + public void setStatus(DBStatus status) { + this.status = status; + } + + public String getExcerpt() { + return excerpt; + } + + public void setExcerpt(String excerpt) { + this.excerpt = excerpt; + } + + public void setContent(String content) { + super.setContent(content); + } + + @Override + public boolean isSection() { + return false; + } + + @NonNull + @Override + public String toString() { + return "DBNote{" + + "id=" + id + + ", accountId=" + accountId + + ", status=" + status + + ", excerpt='" + excerpt + '\'' + + '}'; + } + + public int getScrollY() { + return scrollY; + } + + public void setScrollY(int scrollY) { + this.scrollY = scrollY; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java new file mode 100644 index 00000000..4b7e61e4 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java @@ -0,0 +1,50 @@ +package it.niedermann.owncloud.notes.shared.model; + +/** + * Helps to distinguish between different local change types for Server Synchronization. + * Created by stefan on 19.09.15. + */ +public enum DBStatus { + + /** + * VOID means, that the Note was not modified locally + */ + VOID(""), + + /** + * LOCAL_EDITED means that a Note was created and/or changed since the last successful synchronization. + * If it was newly created, then REMOTE_ID is 0 + */ + LOCAL_EDITED("LOCAL_EDITED"), + + /** + * LOCAL_DELETED means that the Note was deleted locally, but this information was not yet synchronized. + * Therefore, the Note have to be kept locally until the synchronization has succeeded. + * However, Notes with this status should not be displayed in the UI. + */ + LOCAL_DELETED("LOCAL_DELETED"); + + private final String title; + + public String getTitle() { + return title; + } + + DBStatus(String title) { + this.title = title; + } + + /** + * Parse a String an get the appropriate DBStatus enum element. + * + * @param str The String containing the DBStatus identifier. Must not null. + * @return The DBStatus fitting to the String. + */ + public static DBStatus parse(String str) { + if (str.isEmpty()) { + return DBStatus.VOID; + } else { + return DBStatus.valueOf(str); + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ISyncCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ISyncCallback.java new file mode 100644 index 00000000..b5b9093c --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ISyncCallback.java @@ -0,0 +1,13 @@ +package it.niedermann.owncloud.notes.shared.model; + +/** + * Callback + * Created by stefan on 01.10.15. + */ +public interface ISyncCallback { + void onFinish(); + + default void onScheduled() { + + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Item.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Item.java new file mode 100644 index 00000000..362bc390 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Item.java @@ -0,0 +1,5 @@ +package it.niedermann.owncloud.notes.shared.model; + +public interface Item { + boolean isSection(); +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/LocalAccount.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/LocalAccount.java new file mode 100644 index 00000000..aaafd750 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/LocalAccount.java @@ -0,0 +1,155 @@ +package it.niedermann.owncloud.notes.shared.model; + + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.NoSuchElementException; + +import it.niedermann.owncloud.notes.persistence.NotesClient; + +public class LocalAccount { + + private long id; + private String userName; + private String accountName; + private String url; + private String etag; + private String capabilitiesETag; + private long modified; + @Nullable + private ApiVersion preferredApiVersion; + @ColorInt + private int color; + @ColorInt + private int textColor; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getEtag() { + return etag; + } + + public void setETag(String etag) { + this.etag = etag; + } + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public long getModified() { + return modified; + } + + public void setModified(long modified) { + this.modified = modified; + } + + @Nullable + public ApiVersion getPreferredApiVersion() { + return preferredApiVersion; + } + + public String getCapabilitiesETag() { + return capabilitiesETag; + } + + public void setCapabilitiesETag(String capabilitiesETag) { + this.capabilitiesETag = capabilitiesETag; + } + + /** + * @param availableApiVersions ["0.2", "1.0", ...] + */ + public void setPreferredApiVersion(@Nullable String availableApiVersions) { + // TODO move this logic to NotesClient? + try { + if (availableApiVersions == null) { + this.preferredApiVersion = null; + return; + } + JSONArray versionsArray = new JSONArray(availableApiVersions); + Collection supportedApiVersions = new HashSet<>(versionsArray.length()); + for (int i = 0; i < versionsArray.length(); i++) { + ApiVersion parsedApiVersion = ApiVersion.of(versionsArray.getString(i)); + for (ApiVersion temp : NotesClient.SUPPORTED_API_VERSIONS) { + if (temp.compareTo(parsedApiVersion) == 0) { + supportedApiVersions.add(parsedApiVersion); + break; + } + } + } + this.preferredApiVersion = Collections.max(supportedApiVersions); + } catch (JSONException | NoSuchElementException e) { + e.printStackTrace(); + this.preferredApiVersion = null; + } + } + + public int getColor() { + return color; + } + + public void setColor(@ColorInt int color) { + this.color = color; + } + + public int getTextColor() { + return textColor; + } + + public void setTextColor(@ColorInt int textColor) { + this.textColor = textColor; + } + + @NonNull + @Override + public String toString() { + return "LocalAccount{" + + "id=" + id + + ", userName='" + userName + '\'' + + ", accountName='" + accountName + '\'' + + ", url='" + url + '\'' + + ", etag='" + etag + '\'' + + ", modified=" + modified + + ", preferredApiVersion='" + preferredApiVersion + '\'' + + ", color=" + color + + ", textColor=" + textColor + + ", capabilitiesETag=" + capabilitiesETag + + '}'; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java new file mode 100644 index 00000000..7194752c --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java @@ -0,0 +1,11 @@ +package it.niedermann.owncloud.notes.shared.model; + +import android.view.View; + +public interface NoteClickListener { + void onNoteClick(int position, View v); + + void onNoteFavoriteClick(int position, View v); + + boolean onNoteLongClick(int position, View v); +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java new file mode 100644 index 00000000..9f34b907 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ServerResponse.java @@ -0,0 +1,103 @@ +package it.niedermann.owncloud.notes.shared.model; + +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; + +import it.niedermann.owncloud.notes.persistence.NotesClient; + +/** + * Provides entity classes for handling server responses with a single note ({@link NoteResponse}) or a list of notes ({@link NotesResponse}). + */ +public class ServerResponse { + + public static class NoteResponse extends ServerResponse { + public NoteResponse(NotesClient.ResponseData response) { + super(response); + } + + public CloudNote getNote() throws JSONException { + return getNoteFromJSON(new JSONObject(getContent())); + } + } + + public static class NotesResponse extends ServerResponse { + public NotesResponse(NotesClient.ResponseData response) { + super(response); + } + + public List getNotes() throws JSONException { + List notesList = new ArrayList<>(); + JSONArray notes = new JSONArray(getContent()); + for (int i = 0; i < notes.length(); i++) { + JSONObject json = notes.getJSONObject(i); + notesList.add(getNoteFromJSON(json)); + } + return notesList; + } + } + + + private final NotesClient.ResponseData response; + + ServerResponse(NotesClient.ResponseData response) { + this.response = response; + } + + protected String getContent() { + return response == null ? null : response.getContent(); + } + + public String getETag() { + return response.getETag(); + } + + public long getLastModified() { + return response.getLastModified(); + } + + @Nullable + public String getSupportedApiVersions() { + return response.getSupportedApiVersions(); + } + + CloudNote getNoteFromJSON(JSONObject json) throws JSONException { + long id = 0; + String title = ""; + String content = ""; + Calendar modified = null; + boolean favorite = false; + String category = null; + String etag = null; + if (!json.isNull(NotesClient.JSON_ID)) { + id = json.getLong(NotesClient.JSON_ID); + } + if (!json.isNull(NotesClient.JSON_TITLE)) { + title = json.getString(NotesClient.JSON_TITLE); + } + if (!json.isNull(NotesClient.JSON_CONTENT)) { + content = json.getString(NotesClient.JSON_CONTENT); + } + if (!json.isNull(NotesClient.JSON_MODIFIED)) { + modified = GregorianCalendar.getInstance(); + modified.setTimeInMillis(json.getLong(NotesClient.JSON_MODIFIED) * 1000); + } + if (!json.isNull(NotesClient.JSON_FAVORITE)) { + favorite = json.getBoolean(NotesClient.JSON_FAVORITE); + } + if (!json.isNull(NotesClient.JSON_CATEGORY)) { + category = json.getString(NotesClient.JSON_CATEGORY); + } + if (!json.isNull(NotesClient.JSON_ETAG)) { + etag = json.getString(NotesClient.JSON_ETAG); + } + return new CloudNote(id, modified, title, content, favorite, category, etag); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java new file mode 100644 index 00000000..2031568b --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java @@ -0,0 +1,6 @@ +package it.niedermann.owncloud.notes.shared.model; + +public class SyncResultStatus { + public boolean pullSuccessful = true; + public boolean pushSuccessful = true; +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ClipboardUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ClipboardUtil.java new file mode 100644 index 00000000..fa0edeec --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ClipboardUtil.java @@ -0,0 +1,74 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.net.MalformedURLException; +import java.net.URL; + +import it.niedermann.owncloud.notes.R; + +import static android.content.Context.CLIPBOARD_SERVICE; + +public class ClipboardUtil { + + private static final String TAG = ClipboardUtil.class.getSimpleName(); + + private ClipboardUtil() { + // Util class + } + + 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) { + Log.e(TAG, "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); + Log.i(TAG, "Copied to clipboard: [" + label + "] \"" + text + "\""); + Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); + return true; + } + + @Nullable + public static String getClipboardURLorNull(@NonNull Context context) { + final ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); + if (clipboardManager == null) { + return null; + } + final ClipData clipboardData = clipboardManager.getPrimaryClip(); + if (clipboardData == null) { + return null; + } + if (clipboardData.getItemCount() < 1) { + return null; + } + final ClipData.Item clipItem = clipboardData.getItemAt(0); + if (clipItem == null) { + return null; + } + final CharSequence clipText = clipItem.getText(); + if (TextUtils.isEmpty(clipText)) { + return null; + } + try { + return new URL(clipText.toString()).toString(); + } catch (MalformedURLException e) { + Log.d(TAG, "Clipboard does not contain a valid URL: " + clipText); + } + return null; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ColorUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ColorUtil.java new file mode 100644 index 00000000..a7f1b566 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ColorUtil.java @@ -0,0 +1,154 @@ +package it.niedermann.owncloud.notes.shared.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 CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>(); + private static final Map FOREGROUND_CACHE = new HashMap<>(); + private static final Map 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; + } + + 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; + } + + private 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 { + + 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; + } + } + + /** + * @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; + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java new file mode 100644 index 00000000..03eb1097 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java @@ -0,0 +1,18 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.AppGlideModule; + +@GlideModule +public class CustomAppGlideModule extends AppGlideModule { + @Override + public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { + super.registerComponents(context, glide, registry); + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java new file mode 100644 index 00000000..235711ef --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java @@ -0,0 +1,40 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import androidx.annotation.NonNull; + +public class DatabaseIndexUtil { + + private static final String TAG = DatabaseIndexUtil.class.getSimpleName(); + + private DatabaseIndexUtil() { + + } + + public static void createIndex(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String ...columns) { + for (String column: columns) { + createIndex(db, table, column); + } + } + + public static void createIndex(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String column) { + String indexName = table + "_" + column + "_idx"; + Log.v(TAG, "Creating database index: CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")"); + db.execSQL("CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")"); + } + + public static void dropIndexes(@NonNull SQLiteDatabase db) { + try (Cursor c = db.query("sqlite_master", new String[]{"name", "sql"}, "type=?", new String[]{"index"}, null, null, null)) { + while (c.moveToNext()) { + // Skip automatic indexes which we can't drop manually + if (c.getString(1) != null) { + Log.v(TAG, "Deleting database index: DROP INDEX " + c.getString(0)); + db.execSQL("DROP INDEX " + c.getString(0)); + } + } + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DeviceCredentialUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DeviceCredentialUtil.java new file mode 100644 index 00000000..034eea5a --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DeviceCredentialUtil.java @@ -0,0 +1,28 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.app.KeyguardManager; +import android.content.Context; +import android.util.Log; + +/** + * Utility class with methods for handling device credentials. + */ +public class DeviceCredentialUtil { + + private static final String TAG = DeviceCredentialUtil.class.getSimpleName(); + + private DeviceCredentialUtil() { + // utility class -> private constructor + } + + public static boolean areCredentialsAvailable(Context context) { + KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + + if (keyguardManager != null) { + return keyguardManager.isKeyguardSecure(); + } else { + Log.e(TAG, "Keyguard manager is null"); + return false; + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java new file mode 100644 index 00000000..91c2b13a --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java @@ -0,0 +1,132 @@ +/* + * Nextcloud Notes application + * + * @author Mario Danic + * Copyright (C) 2018 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package it.niedermann.owncloud.notes.shared.util; + +import android.content.Context; +import android.graphics.Color; +import android.text.Spannable; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.style.MetricAffectingSpan; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import it.niedermann.owncloud.notes.NotesApplication; +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandingUtil; + +import static it.niedermann.owncloud.notes.shared.util.ColorUtil.isColorDark; + +public class DisplayUtils { + + private DisplayUtils() { + + } + + public static Spannable searchAndColor(Spannable spannable, CharSequence searchText, @NonNull Context context, @Nullable Integer current, @ColorInt int mainColor, @ColorInt int textColor) { + CharSequence text = spannable.toString(); + + Object[] spansToRemove = spannable.getSpans(0, text.length(), Object.class); + for (Object span : spansToRemove) { + if (span instanceof SearchSpan) + spannable.removeSpan(span); + } + + if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) { + return spannable; + } + + Matcher m = Pattern.compile(searchText.toString(), Pattern.CASE_INSENSITIVE | Pattern.LITERAL) + .matcher(text); + + int i = 1; + while (m.find()) { + int start = m.start(); + int end = m.end(); + spannable.setSpan(new SearchSpan(context, mainColor, textColor, (current != null && i == current)), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + i++; + } + + return spannable; + } + + + static class SearchSpan extends MetricAffectingSpan { + + private final boolean current; + @NonNull + Context context; + @ColorInt + private final int mainColor; + @ColorInt + private final int textColor; + @ColorInt + private final int highlightColor; + + SearchSpan(@NonNull Context context, @ColorInt int mainColor, @ColorInt int textColor, boolean current) { + this.context = context; + this.mainColor = mainColor; + this.textColor = textColor; + this.current = current; + this.highlightColor = context.getResources().getColor(R.color.bg_highlighted); + } + + @Override + public void updateDrawState(TextPaint tp) { + if (current) { + if (NotesApplication.isDarkThemeActive(context)) { + if (isColorDark(mainColor)) { + tp.bgColor = Color.WHITE; + tp.setColor(mainColor); + } else { + tp.bgColor = mainColor; + tp.setColor(Color.BLACK); + } + } else { + if (isColorDark(mainColor)) { + tp.bgColor = mainColor; + tp.setColor(Color.WHITE); + } else { + if (ColorUtil.contrastRatioIsSufficient(mainColor, highlightColor)) { + tp.bgColor = highlightColor; + } else { + tp.bgColor = Color.BLACK; + } + tp.setColor(mainColor); + } + } + } else { + tp.bgColor = highlightColor; + tp.setColor(BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, mainColor)); + } + tp.setFakeBoldText(true); + } + + @Override + public void updateMeasureState(@NonNull TextPaint tp) { + tp.setFakeBoldText(true); + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/MarkDownUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/MarkDownUtil.java new file mode 100644 index 00000000..43a8937e --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/MarkDownUtil.java @@ -0,0 +1,124 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.content.Context; +import android.graphics.Color; +import android.text.Spanned; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.core.content.res.ResourcesCompat; + +import com.yydcdut.markdown.MarkdownConfiguration; +import com.yydcdut.markdown.MarkdownConfiguration.Builder; +import com.yydcdut.markdown.MarkdownProcessor; +import com.yydcdut.markdown.span.MDImageSpan; +import com.yydcdut.markdown.theme.ThemeDefault; +import com.yydcdut.markdown.theme.ThemeSonsOfObsidian; + +import it.niedermann.owncloud.notes.NotesApplication; +import it.niedermann.owncloud.notes.R; + +/** + * Created by stefan on 07.12.16. + */ + +@SuppressWarnings("WeakerAccess") +public class MarkDownUtil { + + private static final String TAG = MarkDownUtil.class.getSimpleName(); + + public static final String CHECKBOX_UNCHECKED_MINUS = "- [ ]"; + public static final String CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE = CHECKBOX_UNCHECKED_MINUS + " "; + public static final String CHECKBOX_UNCHECKED_STAR = "* [ ]"; + public static final String CHECKBOX_UNCHECKED_STAR_TRAILING_SPACE = CHECKBOX_UNCHECKED_STAR + " "; + public static final String CHECKBOX_CHECKED_MINUS = "- [x]"; + public static final String CHECKBOX_CHECKED_STAR = "* [x]"; + + private static final String MD_IMAGE_WITH_EMPTY_DESCRIPTION = "![]("; + private static final String MD_IMAGE_WITH_SPACE_DESCRIPTION = "![ ]("; + private static final String[] MD_IMAGE_WITH_EMPTY_DESCRIPTION_ARRAY = new String[]{MD_IMAGE_WITH_EMPTY_DESCRIPTION}; + private static final String[] MD_IMAGE_WITH_SPACE_DESCRIPTION_ARRAY = new String[]{MD_IMAGE_WITH_SPACE_DESCRIPTION}; + + /** + * Ensures every instance of RxMD uses the same configuration + * + * @param context Context + * @return RxMDConfiguration + */ + public static Builder getMarkDownConfiguration(Context context) { + return getMarkDownConfiguration(context, NotesApplication.isDarkThemeActive(context)); + } + + public static Builder getMarkDownConfiguration(Context context, Boolean darkTheme) { + return new MarkdownConfiguration.Builder(context) + .setUnOrderListColor(ResourcesCompat.getColor(context.getResources(), + darkTheme ? R.color.widget_fg_dark_theme : R.color.widget_fg_default, null)) + .setHeader2RelativeSize(1.35f) + .setHeader3RelativeSize(1.25f) + .setHeader4RelativeSize(1.15f) + .setHeader5RelativeSize(1.1f) + .setHeader6RelativeSize(1.05f) + .setHorizontalRulesHeight(2) + .setCodeBgColor(darkTheme ? ResourcesCompat.getColor(context.getResources(), R.color.fg_default_high, null) : Color.LTGRAY) + .setTheme(darkTheme ? new ThemeSonsOfObsidian() : new ThemeDefault()) + .setTodoColor(ResourcesCompat.getColor(context.getResources(), + darkTheme ? R.color.widget_fg_dark_theme : R.color.widget_fg_default, null)) + .setTodoDoneColor(ResourcesCompat.getColor(context.getResources(), + darkTheme ? R.color.widget_fg_dark_theme : R.color.widget_fg_default, null)) + .setLinkFontColor(ResourcesCompat.getColor(context.getResources(), R.color.defaultBrand, null)) + .setDefaultImageSize(400, 300); + } + + /** + * This is a compatibility-method that provides workarounds for several bugs in RxMarkdown + *

+ * https://github.com/stefan-niedermann/nextcloud-notes/issues/772 + * + * @param markdownProcessor RxMarkdown MarkdownProcessor instance + * @param text CharSequence that should be parsed + * @return the processed text but with several workarounds for Bugs in RxMarkdown + */ + @NonNull + public static CharSequence parseCompat(@NonNull final MarkdownProcessor markdownProcessor, CharSequence text) { + if (TextUtils.isEmpty(text)) { + return ""; + } + + while (TextUtils.indexOf(text, MD_IMAGE_WITH_EMPTY_DESCRIPTION) >= 0) { + text = TextUtils.replace(text, MD_IMAGE_WITH_EMPTY_DESCRIPTION_ARRAY, MD_IMAGE_WITH_SPACE_DESCRIPTION_ARRAY); + } + + return markdownProcessor.parse(text); + } + + public static boolean containsImageSpan(@NonNull CharSequence text) { + return ((Spanned) text).getSpans(0, text.length(), MDImageSpan.class).length > 0; + } + + public static boolean lineStartsWithCheckbox(@NonNull String line) { + return lineStartsWithCheckbox(line, true) || lineStartsWithCheckbox(line, false); + } + + public static boolean lineStartsWithCheckbox(@NonNull String line, boolean starAsLeadingCharacter) { + return starAsLeadingCharacter + ? line.startsWith(CHECKBOX_UNCHECKED_STAR) || line.startsWith(CHECKBOX_CHECKED_STAR) + : line.startsWith(CHECKBOX_UNCHECKED_MINUS) || line.startsWith(CHECKBOX_CHECKED_MINUS); + } + + public static int getStartOfLine(@NonNull CharSequence s, int cursorPosition) { + int startOfLine = cursorPosition; + while (startOfLine > 0 && s.charAt(startOfLine - 1) != '\n') { + startOfLine--; + } + return startOfLine; + } + + public static int getEndOfLine(@NonNull CharSequence s, int cursorPosition) { + int nextLinebreak = s.toString().indexOf('\n', cursorPosition); + if (nextLinebreak > -1) { + return nextLinebreak; + } + return cursorPosition; + } +} + diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtils.java new file mode 100644 index 00000000..389e2bef --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteLinksUtils.java @@ -0,0 +1,66 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NoteLinksUtils { + + @VisibleForTesting + static final String RELATIVE_LINK_WORKAROUND_PREFIX = "https://nextcloudnotes/notes/"; + + private static final String linksThatLookLikeNoteLinksRegEx = "\\[[^]]*]\\((\\d+)\\)"; + private static final String replaceNoteRemoteIdsRegEx = "\\[([^\\]]*)\\]\\((%s)\\)"; + + /** + * Replaces all links to other notes of the form `[]()` + * in the markdown string with links to a dummy url. + * + * Why is this needed? + * See discussion in issue #623 + * + * @return Markdown with all note-links replaced with dummy-url-links + */ + public static String replaceNoteLinksWithDummyUrls(String markdown, Set existingNoteRemoteIds) { + Pattern noteLinkCandidates = Pattern.compile(linksThatLookLikeNoteLinksRegEx); + Matcher matcher = noteLinkCandidates.matcher(markdown); + + Set noteRemoteIdsToReplace = new HashSet<>(); + while (matcher.find()) { + String presumedNoteId = matcher.group(1); + if (existingNoteRemoteIds.contains(presumedNoteId)) { + noteRemoteIdsToReplace.add(presumedNoteId); + } + } + + String noteRemoteIdsCondition = TextUtils.join("|", noteRemoteIdsToReplace); + Pattern replacePattern = Pattern.compile(String.format(replaceNoteRemoteIdsRegEx, noteRemoteIdsCondition)); + Matcher replaceMatcher = replacePattern.matcher(markdown); + return replaceMatcher.replaceAll(String.format("[$1](%s$2)", RELATIVE_LINK_WORKAROUND_PREFIX)); + } + + /** + * Tests if the given link is a note-link (which was transformed in {@link #replaceNoteLinksWithDummyUrls}) or not + * + * @param link Link under test + * @return true if the link is a note-link + */ + public static boolean isNoteLink(String link) { + return link.startsWith(RELATIVE_LINK_WORKAROUND_PREFIX); + } + + /** + * Extracts the remoteId back from links that were transformed in {@link #replaceNoteLinksWithDummyUrls}. + * + * @param link Link that was transformed in {@link #replaceNoteLinksWithDummyUrls} + * @return the remoteId of the linked note + */ + public static long extractNoteRemoteId(String link) { + return Long.parseLong(link.replace(RELATIVE_LINK_WORKAROUND_PREFIX, "")); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java new file mode 100644 index 00000000..014d3377 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java @@ -0,0 +1,171 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.regex.Pattern; + +import it.niedermann.owncloud.notes.R; + +/** + * Provides basic functionality for Note operations. + * Created by stefan on 06.10.15. + */ +@SuppressWarnings("WeakerAccess") +public class NoteUtil { + + private static final Pattern pLists = Pattern.compile("^\\s*[*+-]\\s+", Pattern.MULTILINE); + private static final Pattern pHeadings = Pattern.compile("^#+\\s+(.*?)\\s*#*$", Pattern.MULTILINE); + private static final Pattern pHeadingLine = Pattern.compile("^(?:=*|-*)$", Pattern.MULTILINE); + private static final Pattern pEmphasis = Pattern.compile("(\\*+|_+)(.*?)\\1", Pattern.MULTILINE); + private static final Pattern pSpace1 = Pattern.compile("^\\s+", Pattern.MULTILINE); + private static final Pattern pSpace2 = Pattern.compile("\\s+$", Pattern.MULTILINE); + + public static final String EXCERPT_LINE_SEPARATOR = " "; + + private NoteUtil() { + + } + + /** + * Strips all MarkDown from the given String + * + * @param s String - MarkDown + * @return Plain Text-String + */ + @NonNull + public static String removeMarkDown(@Nullable String s) { + if (s == null) + return ""; + String result = s; + result = pLists.matcher(result).replaceAll(""); + result = pHeadings.matcher(result).replaceAll("$1"); + result = pHeadingLine.matcher(result).replaceAll(""); + result = pEmphasis.matcher(result).replaceAll("$2"); + result = pSpace1.matcher(result).replaceAll(""); + result = pSpace2.matcher(result).replaceAll(""); + return result; + } + + /** + * Checks if a line is empty. + *

+     * " "    -> empty
+     * "\n"   -> empty
+     * "\n "  -> empty
+     * " \n"  -> empty
+     * " \n " -> empty
+     * 
+ * + * @param line String - a single Line which ends with \n + * @return boolean isEmpty + */ + public static boolean isEmptyLine(@Nullable String line) { + return removeMarkDown(line).trim().length() == 0; + } + + /** + * Truncates a string to a desired maximum length. + * Like String.substring(int,int), but throw no exception if desired length is longer than the string. + * + * @param str String to truncate + * @param len Maximum length of the resulting string + * @return truncated string + */ + @NonNull + private static String truncateString(@NonNull String str, @SuppressWarnings("SameParameterValue") int len) { + return str.substring(0, Math.min(len, str.length())); + } + + /** + * Generates an excerpt of a content that does not match the given title + * + * @param content {@link String} + * @param title {@link String} In case the content starts with the title, the excerpt should be generated starting from this point + * @return excerpt String + */ + @NonNull + public static String generateNoteExcerpt(@NonNull String content, @Nullable String title) { + content = removeMarkDown(content.trim()); + if(TextUtils.isEmpty(content)) { + return ""; + } + if (!TextUtils.isEmpty(title)) { + final String trimmedTitle = removeMarkDown(title.trim()); + if (content.startsWith(trimmedTitle)) { + content = content.substring(trimmedTitle.length()); + } + } + return truncateString(content.trim(), 200).replace("\n", EXCERPT_LINE_SEPARATOR); + } + + @NonNull + public static String generateNonEmptyNoteTitle(@NonNull String content, Context context) { + String title = generateNoteTitle(content); + if (title.isEmpty()) { + title = context.getString(R.string.action_create); + } + return title; + } + + /** + * Generates a title of a content String (reads fist linew which is not empty) + * + * @param content String + * @return excerpt String + */ + @NonNull + public static String generateNoteTitle(@NonNull String content) { + return getLineWithoutMarkDown(content, 0); + } + + /** + * Reads the requested line and strips all MarkDown. If line is empty, it will go ahead to find the next not-empty line. + * + * @param content String + * @param lineNumber int + * @return lineContent String + */ + @NonNull + public static String getLineWithoutMarkDown(@NonNull String content, int lineNumber) { + String line = ""; + if (content.contains("\n")) { + String[] lines = content.split("\n"); + int currentLine = lineNumber; + while (currentLine < lines.length && NoteUtil.isEmptyLine(lines[currentLine])) { + currentLine++; + } + if (currentLine < lines.length) { + line = NoteUtil.removeMarkDown(lines[currentLine]); + } + } else { + line = content; + } + return line; + } + + @NonNull + public static String extendCategory(@NonNull String category) { + return category.replace("/", " / "); + } + + @SuppressWarnings("WeakerAccess") //PMD... + public static float getFontSizeFromPreferences(@NonNull Context context, @NonNull SharedPreferences sp) { + final String prefValueSmall = context.getString(R.string.pref_value_font_size_small); + final String prefValueMedium = context.getString(R.string.pref_value_font_size_medium); + // final String prefValueLarge = getString(R.string.pref_value_font_size_large); + String fontSize = sp.getString(context.getString(R.string.pref_key_font_size), prefValueMedium); + + if (fontSize.equals(prefValueSmall)) { + return context.getResources().getDimension(R.dimen.note_font_size_small); + } else if (fontSize.equals(prefValueMedium)) { + return context.getResources().getDimension(R.dimen.note_font_size_medium); + } else { + return context.getResources().getDimension(R.dimen.note_font_size_large); + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SSOUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SSOUtil.java new file mode 100644 index 00000000..da529136 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SSOUtil.java @@ -0,0 +1,52 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.app.Activity; +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.nextcloud.android.sso.AccountImporter; +import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; +import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; +import com.nextcloud.android.sso.helper.SingleAccountHelper; +import com.nextcloud.android.sso.ui.UiExceptionManager; + +public class SSOUtil { + + private static final String TAG = SSOUtil.class.getSimpleName(); + + private SSOUtil() { + + } + + /** + * Opens a dialog which allows the user to pick a Nextcloud account (which previously has to be configured in the files app). + * Also allows to configure a new Nextcloud account in the files app and directly import it. + * + * @param activity should implement AccountImporter.onActivityResult + */ + public static void askForNewAccount(@NonNull Activity activity) { + try { + AccountImporter.pickNewAccount(activity); + } catch (NextcloudFilesAppNotInstalledException e1) { + UiExceptionManager.showDialogForException(activity, e1); + Log.w(TAG, "============================================================="); + Log.w(TAG, "Nextcloud app is not installed. Cannot choose account"); + e1.printStackTrace(); + } catch (AndroidGetAccountsPermissionNotGranted e2) { + AccountImporter.requestAndroidAccountPermissionsAndPickAccount(activity); + } + } + + public static boolean isConfigured(Context context) { + try { + SingleAccountHelper.getCurrentSingleSignOnAccount(context); + return true; + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + return false; + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ShareUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ShareUtil.java new file mode 100644 index 00000000..09ad7124 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ShareUtil.java @@ -0,0 +1,20 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN; + +public class ShareUtil { + public static void openShareDialog(@NonNull Context context, @Nullable String subject, @Nullable String text) { + context.startActivity(Intent.createChooser(new Intent() + .setAction(Intent.ACTION_SEND) + .setType(MIMETYPE_TEXT_PLAIN) + .putExtra(Intent.EXTRA_SUBJECT, subject) + .putExtra(Intent.EXTRA_TITLE, subject) + .putExtra(Intent.EXTRA_TEXT, text), subject)); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SupportUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SupportUtil.java new file mode 100644 index 00000000..e480b30b --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SupportUtil.java @@ -0,0 +1,49 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.os.Build; +import android.text.Html; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +/** + * Some helper functionality in alike the Android support library. + * Currently, it offers methods for working with HTML string resources. + */ +public class SupportUtil { + + private SupportUtil() { + + } + + /** + * Fills a {@link TextView} with HTML content and activates links in that {@link TextView}. + * + * @param view The {@link TextView} which should be filled. + * @param stringId The string resource containing HTML tags (escaped by <) + * @param formatArgs Arguments for the string resource. + */ + public static void setHtml(@NonNull TextView view, int stringId, Object... formatArgs) { + view.setText(SupportUtil.fromHtml(view.getResources().getString(stringId, formatArgs))); + view.setMovementMethod(LinkMovementMethod.getInstance()); + } + + /** + * Creates a {@link Spanned} from a HTML string on all SDK versions. + * + * @param source Source string with HTML markup + * @return Spannable for using in a {@link TextView} + * @see Html#fromHtml(String) + * @see Html#fromHtml(String, int) + */ + private static Spanned fromHtml(String source) { + if (Build.VERSION.SDK_INT >= 24) { + return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY); + } else { + //noinspection deprecation + return Html.fromHtml(source); + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/CategorySortingMethod.java b/app/src/main/java/it/niedermann/owncloud/notes/util/CategorySortingMethod.java deleted file mode 100644 index f5199cd6..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/CategorySortingMethod.java +++ /dev/null @@ -1,41 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -public enum CategorySortingMethod { - SORT_MODIFIED_DESC("MODIFIED DESC"), - SORT_LEXICOGRAPHICAL_ASC("TITLE COLLATE NOCASE ASC"); - - private String sorder; // sorting method OrderBy for SQL - - /*** - * Constructor - * @param orderby given sorting method OrderBy - */ - CategorySortingMethod(String orderby) { - this.sorder = orderby; - } - - /*** - * Retrieve the sorting method id represented in database - * @return the sorting method id for the enum item - */ - public int getCSMID() { - return this.ordinal(); - } - - /*** - * Retrieve the sorting method order for SQL - * @return the sorting method order for the enum item - */ - public String getSorder() { - return this.sorder; - } - - /*** - * Retrieve the corresponding enum value with given the index (ordinal) - * @param index the index / ordinal of the corresponding enum value stored in DB - * @return the corresponding enum item with the index (ordinal) - */ - public static CategorySortingMethod getCSM(int index) { - return CategorySortingMethod.values()[index]; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/ClipboardUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/ClipboardUtil.java deleted file mode 100644 index 16738c8f..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/ClipboardUtil.java +++ /dev/null @@ -1,74 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.net.MalformedURLException; -import java.net.URL; - -import it.niedermann.owncloud.notes.R; - -import static android.content.Context.CLIPBOARD_SERVICE; - -public class ClipboardUtil { - - private static final String TAG = ClipboardUtil.class.getSimpleName(); - - private ClipboardUtil() { - // Util class - } - - 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) { - Log.e(TAG, "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); - Log.i(TAG, "Copied to clipboard: [" + label + "] \"" + text + "\""); - Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - return true; - } - - @Nullable - public static String getClipboardURLorNull(Context context) { - final ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); - if (clipboardManager == null) { - return null; - } - final ClipData clipboardData = clipboardManager.getPrimaryClip(); - if (clipboardData == null) { - return null; - } - if (clipboardData.getItemCount() < 1) { - return null; - } - final ClipData.Item clipItem = clipboardData.getItemAt(0); - if (clipItem == null) { - return null; - } - final CharSequence clipText = clipItem.getText(); - if (TextUtils.isEmpty(clipText)) { - return null; - } - try { - return new URL(clipText.toString()).toString(); - } catch (MalformedURLException e) { - Log.d(TAG, "Clipboard does not contain a valid URL: " + clipText); - } - return null; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/ColorUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/ColorUtil.java deleted file mode 100644 index 3b44c0f2..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/ColorUtil.java +++ /dev/null @@ -1,154 +0,0 @@ -package it.niedermann.owncloud.notes.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 CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>(); - private static final Map FOREGROUND_CACHE = new HashMap<>(); - private static final Map 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; - } - - 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; - } - - private 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 { - - 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; - } - } - - /** - * @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; - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/CustomAppGlideModule.java b/app/src/main/java/it/niedermann/owncloud/notes/util/CustomAppGlideModule.java deleted file mode 100644 index c48a1666..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/CustomAppGlideModule.java +++ /dev/null @@ -1,18 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.Registry; -import com.bumptech.glide.annotation.GlideModule; -import com.bumptech.glide.module.AppGlideModule; - -@GlideModule -public class CustomAppGlideModule extends AppGlideModule { - @Override - public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - super.registerComponents(context, glide, registry); - } -} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/DatabaseIndexUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/DatabaseIndexUtil.java deleted file mode 100644 index c6b0c844..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/DatabaseIndexUtil.java +++ /dev/null @@ -1,40 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.util.Log; - -import androidx.annotation.NonNull; - -public class DatabaseIndexUtil { - - private static final String TAG = DatabaseIndexUtil.class.getSimpleName(); - - private DatabaseIndexUtil() { - - } - - public static void createIndex(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String ...columns) { - for (String column: columns) { - createIndex(db, table, column); - } - } - - public static void createIndex(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String column) { - String indexName = table + "_" + column + "_idx"; - Log.v(TAG, "Creating database index: CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")"); - db.execSQL("CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")"); - } - - public static void dropIndexes(@NonNull SQLiteDatabase db) { - try (Cursor c = db.query("sqlite_master", new String[]{"name", "sql"}, "type=?", new String[]{"index"}, null, null, null)) { - while (c.moveToNext()) { - // Skip automatic indexes which we can't drop manually - if (c.getString(1) != null) { - Log.v(TAG, "Deleting database index: DROP INDEX " + c.getString(0)); - db.execSQL("DROP INDEX " + c.getString(0)); - } - } - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/DeviceCredentialUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/DeviceCredentialUtil.java deleted file mode 100644 index d79dc9d6..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/DeviceCredentialUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.app.KeyguardManager; -import android.content.Context; -import android.os.Build; -import android.util.Log; - -/** - * Utility class with methods for handling device credentials. - */ -public class DeviceCredentialUtil { - - private static final String TAG = DeviceCredentialUtil.class.getSimpleName(); - - private DeviceCredentialUtil() { - // utility class -> private constructor - } - - public static boolean areCredentialsAvailable(Context context) { - KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); - - if (keyguardManager != null) { - return keyguardManager.isKeyguardSecure(); - } else { - Log.e(TAG, "Keyguard manager is null"); - return false; - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/util/DisplayUtils.java deleted file mode 100644 index 50931618..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/DisplayUtils.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Nextcloud Notes application - * - * @author Mario Danic - * Copyright (C) 2018 Mario Danic - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package it.niedermann.owncloud.notes.util; - -import android.content.Context; -import android.graphics.Color; -import android.text.Spannable; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.style.MetricAffectingSpan; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandingUtil; - -import static it.niedermann.owncloud.notes.util.ColorUtil.isColorDark; - -public class DisplayUtils { - - private DisplayUtils() { - - } - - public static Spannable searchAndColor(Spannable spannable, CharSequence searchText, @NonNull Context context, @Nullable Integer current, @ColorInt int mainColor, @ColorInt int textColor) { - CharSequence text = spannable.toString(); - - Object[] spansToRemove = spannable.getSpans(0, text.length(), Object.class); - for (Object span : spansToRemove) { - if (span instanceof SearchSpan) - spannable.removeSpan(span); - } - - if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) { - return spannable; - } - - Matcher m = Pattern.compile(searchText.toString(), Pattern.CASE_INSENSITIVE | Pattern.LITERAL) - .matcher(text); - - int i = 1; - while (m.find()) { - int start = m.start(); - int end = m.end(); - spannable.setSpan(new SearchSpan(context, mainColor, textColor, (current != null && i == current)), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - i++; - } - - return spannable; - } - - - static class SearchSpan extends MetricAffectingSpan { - - private final boolean current; - @NonNull - Context context; - @ColorInt - private final int mainColor; - @ColorInt - private final int textColor; - @ColorInt - private final int highlightColor; - - SearchSpan(@NonNull Context context, @ColorInt int mainColor, @ColorInt int textColor, boolean current) { - this.context = context; - this.mainColor = mainColor; - this.textColor = textColor; - this.current = current; - this.highlightColor = context.getResources().getColor(R.color.bg_highlighted); - } - - @Override - public void updateDrawState(TextPaint tp) { - if (current) { - if (Notes.isDarkThemeActive(context)) { - if (isColorDark(mainColor)) { - tp.bgColor = Color.WHITE; - tp.setColor(mainColor); - } else { - tp.bgColor = mainColor; - tp.setColor(Color.BLACK); - } - } else { - if (isColorDark(mainColor)) { - tp.bgColor = mainColor; - tp.setColor(Color.WHITE); - } else { - if (ColorUtil.contrastRatioIsSufficient(mainColor, highlightColor)) { - tp.bgColor = highlightColor; - } else { - tp.bgColor = Color.BLACK; - } - tp.setColor(mainColor); - } - } - } else { - tp.bgColor = highlightColor; - tp.setColor(BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, mainColor)); - } - tp.setFakeBoldText(true); - } - - @Override - public void updateMeasureState(@NonNull TextPaint tp) { - tp.setFakeBoldText(true); - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/MarkDownUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/MarkDownUtil.java deleted file mode 100644 index 5609aaea..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/MarkDownUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.content.Context; -import android.graphics.Color; -import android.text.Spanned; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.core.content.res.ResourcesCompat; - -import com.yydcdut.markdown.MarkdownConfiguration; -import com.yydcdut.markdown.MarkdownConfiguration.Builder; -import com.yydcdut.markdown.MarkdownProcessor; -import com.yydcdut.markdown.span.MDImageSpan; -import com.yydcdut.markdown.theme.ThemeDefault; -import com.yydcdut.markdown.theme.ThemeSonsOfObsidian; - -import it.niedermann.owncloud.notes.R; - -/** - * Created by stefan on 07.12.16. - */ - -@SuppressWarnings("WeakerAccess") -public class MarkDownUtil { - - private static final String TAG = MarkDownUtil.class.getSimpleName(); - - public static final String CHECKBOX_UNCHECKED_MINUS = "- [ ]"; - public static final String CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE = CHECKBOX_UNCHECKED_MINUS + " "; - public static final String CHECKBOX_UNCHECKED_STAR = "* [ ]"; - public static final String CHECKBOX_UNCHECKED_STAR_TRAILING_SPACE = CHECKBOX_UNCHECKED_STAR + " "; - public static final String CHECKBOX_CHECKED_MINUS = "- [x]"; - public static final String CHECKBOX_CHECKED_STAR = "* [x]"; - - private static final String MD_IMAGE_WITH_EMPTY_DESCRIPTION = "![]("; - private static final String MD_IMAGE_WITH_SPACE_DESCRIPTION = "![ ]("; - private static final String[] MD_IMAGE_WITH_EMPTY_DESCRIPTION_ARRAY = new String[]{MD_IMAGE_WITH_EMPTY_DESCRIPTION}; - private static final String[] MD_IMAGE_WITH_SPACE_DESCRIPTION_ARRAY = new String[]{MD_IMAGE_WITH_SPACE_DESCRIPTION}; - - /** - * Ensures every instance of RxMD uses the same configuration - * - * @param context Context - * @return RxMDConfiguration - */ - public static Builder getMarkDownConfiguration(Context context) { - return getMarkDownConfiguration(context, Notes.isDarkThemeActive(context)); - } - - public static Builder getMarkDownConfiguration(Context context, Boolean darkTheme) { - return new MarkdownConfiguration.Builder(context) - .setUnOrderListColor(ResourcesCompat.getColor(context.getResources(), - darkTheme ? R.color.widget_fg_dark_theme : R.color.widget_fg_default, null)) - .setHeader2RelativeSize(1.35f) - .setHeader3RelativeSize(1.25f) - .setHeader4RelativeSize(1.15f) - .setHeader5RelativeSize(1.1f) - .setHeader6RelativeSize(1.05f) - .setHorizontalRulesHeight(2) - .setCodeBgColor(darkTheme ? ResourcesCompat.getColor(context.getResources(), R.color.fg_default_high, null) : Color.LTGRAY) - .setTheme(darkTheme ? new ThemeSonsOfObsidian() : new ThemeDefault()) - .setTodoColor(ResourcesCompat.getColor(context.getResources(), - darkTheme ? R.color.widget_fg_dark_theme : R.color.widget_fg_default, null)) - .setTodoDoneColor(ResourcesCompat.getColor(context.getResources(), - darkTheme ? R.color.widget_fg_dark_theme : R.color.widget_fg_default, null)) - .setLinkFontColor(ResourcesCompat.getColor(context.getResources(), R.color.defaultBrand, null)) - .setRxMDImageLoader(new NotesImageLoader(context)) - .setDefaultImageSize(400, 300); - } - - /** - * This is a compatibility-method that provides workarounds for several bugs in RxMarkdown - *

- * https://github.com/stefan-niedermann/nextcloud-notes/issues/772 - * - * @param markdownProcessor RxMarkdown MarkdownProcessor instance - * @param text CharSequence that should be parsed - * @return the processed text but with several workarounds for Bugs in RxMarkdown - */ - @NonNull - public static CharSequence parseCompat(@NonNull final MarkdownProcessor markdownProcessor, CharSequence text) { - if (TextUtils.isEmpty(text)) { - return ""; - } - - while (TextUtils.indexOf(text, MD_IMAGE_WITH_EMPTY_DESCRIPTION) >= 0) { - text = TextUtils.replace(text, MD_IMAGE_WITH_EMPTY_DESCRIPTION_ARRAY, MD_IMAGE_WITH_SPACE_DESCRIPTION_ARRAY); - } - - return markdownProcessor.parse(text); - } - - public static boolean containsImageSpan(@NonNull CharSequence text) { - return ((Spanned) text).getSpans(0, text.length(), MDImageSpan.class).length > 0; - } - - public static boolean lineStartsWithCheckbox(@NonNull String line) { - return lineStartsWithCheckbox(line, true) || lineStartsWithCheckbox(line, false); - } - - public static boolean lineStartsWithCheckbox(@NonNull String line, boolean starAsLeadingCharacter) { - return starAsLeadingCharacter - ? line.startsWith(CHECKBOX_UNCHECKED_STAR) || line.startsWith(CHECKBOX_CHECKED_STAR) - : line.startsWith(CHECKBOX_UNCHECKED_MINUS) || line.startsWith(CHECKBOX_CHECKED_MINUS); - } - - public static int getStartOfLine(@NonNull CharSequence s, int cursorPosition) { - int startOfLine = cursorPosition; - while (startOfLine > 0 && s.charAt(startOfLine - 1) != '\n') { - startOfLine--; - } - return startOfLine; - } - - public static int getEndOfLine(@NonNull CharSequence s, int cursorPosition) { - int nextLinebreak = s.toString().indexOf('\n', cursorPosition); - if (nextLinebreak > -1) { - return nextLinebreak; - } - return cursorPosition; - } -} - diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/NoteLinksUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/util/NoteLinksUtils.java deleted file mode 100644 index b8d5c027..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/NoteLinksUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.text.TextUtils; - -import androidx.annotation.VisibleForTesting; - -import java.util.HashSet; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class NoteLinksUtils { - - @VisibleForTesting - static final String RELATIVE_LINK_WORKAROUND_PREFIX = "https://nextcloudnotes/notes/"; - - private static final String linksThatLookLikeNoteLinksRegEx = "\\[[^]]*]\\((\\d+)\\)"; - private static final String replaceNoteRemoteIdsRegEx = "\\[([^\\]]*)\\]\\((%s)\\)"; - - /** - * Replaces all links to other notes of the form `[]()` - * in the markdown string with links to a dummy url. - * - * Why is this needed? - * See discussion in issue #623 - * - * @return Markdown with all note-links replaced with dummy-url-links - */ - public static String replaceNoteLinksWithDummyUrls(String markdown, Set existingNoteRemoteIds) { - Pattern noteLinkCandidates = Pattern.compile(linksThatLookLikeNoteLinksRegEx); - Matcher matcher = noteLinkCandidates.matcher(markdown); - - Set noteRemoteIdsToReplace = new HashSet<>(); - while (matcher.find()) { - String presumedNoteId = matcher.group(1); - if (existingNoteRemoteIds.contains(presumedNoteId)) { - noteRemoteIdsToReplace.add(presumedNoteId); - } - } - - String noteRemoteIdsCondition = TextUtils.join("|", noteRemoteIdsToReplace); - Pattern replacePattern = Pattern.compile(String.format(replaceNoteRemoteIdsRegEx, noteRemoteIdsCondition)); - Matcher replaceMatcher = replacePattern.matcher(markdown); - return replaceMatcher.replaceAll(String.format("[$1](%s$2)", RELATIVE_LINK_WORKAROUND_PREFIX)); - } - - /** - * Tests if the given link is a note-link (which was transformed in {@link #replaceNoteLinksWithDummyUrls}) or not - * - * @param link Link under test - * @return true if the link is a note-link - */ - public static boolean isNoteLink(String link) { - return link.startsWith(RELATIVE_LINK_WORKAROUND_PREFIX); - } - - /** - * Extracts the remoteId back from links that were transformed in {@link #replaceNoteLinksWithDummyUrls}. - * - * @param link Link that was transformed in {@link #replaceNoteLinksWithDummyUrls} - * @return the remoteId of the linked note - */ - public static long extractNoteRemoteId(String link) { - return Long.parseLong(link.replace(RELATIVE_LINK_WORKAROUND_PREFIX, "")); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/NoteUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/NoteUtil.java deleted file mode 100644 index 6e6f6244..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/NoteUtil.java +++ /dev/null @@ -1,171 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.regex.Pattern; - -import it.niedermann.owncloud.notes.R; - -/** - * Provides basic functionality for Note operations. - * Created by stefan on 06.10.15. - */ -@SuppressWarnings("WeakerAccess") -public class NoteUtil { - - private static final Pattern pLists = Pattern.compile("^\\s*[*+-]\\s+", Pattern.MULTILINE); - private static final Pattern pHeadings = Pattern.compile("^#+\\s+(.*?)\\s*#*$", Pattern.MULTILINE); - private static final Pattern pHeadingLine = Pattern.compile("^(?:=*|-*)$", Pattern.MULTILINE); - private static final Pattern pEmphasis = Pattern.compile("(\\*+|_+)(.*?)\\1", Pattern.MULTILINE); - private static final Pattern pSpace1 = Pattern.compile("^\\s+", Pattern.MULTILINE); - private static final Pattern pSpace2 = Pattern.compile("\\s+$", Pattern.MULTILINE); - - public static final String EXCERPT_LINE_SEPARATOR = " "; - - private NoteUtil() { - - } - - /** - * Strips all MarkDown from the given String - * - * @param s String - MarkDown - * @return Plain Text-String - */ - @NonNull - public static String removeMarkDown(@Nullable String s) { - if (s == null) - return ""; - String result = s; - result = pLists.matcher(result).replaceAll(""); - result = pHeadings.matcher(result).replaceAll("$1"); - result = pHeadingLine.matcher(result).replaceAll(""); - result = pEmphasis.matcher(result).replaceAll("$2"); - result = pSpace1.matcher(result).replaceAll(""); - result = pSpace2.matcher(result).replaceAll(""); - return result; - } - - /** - * Checks if a line is empty. - *

-     * " "    -> empty
-     * "\n"   -> empty
-     * "\n "  -> empty
-     * " \n"  -> empty
-     * " \n " -> empty
-     * 
- * - * @param line String - a single Line which ends with \n - * @return boolean isEmpty - */ - public static boolean isEmptyLine(@Nullable String line) { - return removeMarkDown(line).trim().length() == 0; - } - - /** - * Truncates a string to a desired maximum length. - * Like String.substring(int,int), but throw no exception if desired length is longer than the string. - * - * @param str String to truncate - * @param len Maximum length of the resulting string - * @return truncated string - */ - @NonNull - private static String truncateString(@NonNull String str, @SuppressWarnings("SameParameterValue") int len) { - return str.substring(0, Math.min(len, str.length())); - } - - /** - * Generates an excerpt of a content that does not match the given title - * - * @param content {@link String} - * @param title {@link String} In case the content starts with the title, the excerpt should be generated starting from this point - * @return excerpt String - */ - @NonNull - public static String generateNoteExcerpt(@NonNull String content, @Nullable String title) { - content = removeMarkDown(content.trim()); - if(TextUtils.isEmpty(content)) { - return ""; - } - if (!TextUtils.isEmpty(title)) { - final String trimmedTitle = removeMarkDown(title.trim()); - if (content.startsWith(trimmedTitle)) { - content = content.substring(trimmedTitle.length()); - } - } - return truncateString(content.trim(), 200).replace("\n", EXCERPT_LINE_SEPARATOR); - } - - @NonNull - public static String generateNonEmptyNoteTitle(@NonNull String content, Context context) { - String title = generateNoteTitle(content); - if (title.isEmpty()) { - title = context.getString(R.string.action_create); - } - return title; - } - - /** - * Generates a title of a content String (reads fist linew which is not empty) - * - * @param content String - * @return excerpt String - */ - @NonNull - public static String generateNoteTitle(@NonNull String content) { - return getLineWithoutMarkDown(content, 0); - } - - /** - * Reads the requested line and strips all MarkDown. If line is empty, it will go ahead to find the next not-empty line. - * - * @param content String - * @param lineNumber int - * @return lineContent String - */ - @NonNull - public static String getLineWithoutMarkDown(@NonNull String content, int lineNumber) { - String line = ""; - if (content.contains("\n")) { - String[] lines = content.split("\n"); - int currentLine = lineNumber; - while (currentLine < lines.length && NoteUtil.isEmptyLine(lines[currentLine])) { - currentLine++; - } - if (currentLine < lines.length) { - line = NoteUtil.removeMarkDown(lines[currentLine]); - } - } else { - line = content; - } - return line; - } - - @NonNull - public static String extendCategory(@NonNull String category) { - return category.replace("/", " / "); - } - - @SuppressWarnings("WeakerAccess") //PMD... - public static float getFontSizeFromPreferences(@NonNull Context context, @NonNull SharedPreferences sp) { - final String prefValueSmall = context.getString(R.string.pref_value_font_size_small); - final String prefValueMedium = context.getString(R.string.pref_value_font_size_medium); - // final String prefValueLarge = getString(R.string.pref_value_font_size_large); - String fontSize = sp.getString(context.getString(R.string.pref_key_font_size), prefValueMedium); - - if (fontSize.equals(prefValueSmall)) { - return context.getResources().getDimension(R.dimen.note_font_size_small); - } else if (fontSize.equals(prefValueMedium)) { - return context.getResources().getDimension(R.dimen.note_font_size_medium); - } else { - return context.getResources().getDimension(R.dimen.note_font_size_large); - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/Notes.java b/app/src/main/java/it/niedermann/owncloud/notes/util/Notes.java deleted file mode 100644 index 1b660313..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/Notes.java +++ /dev/null @@ -1,93 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.util.Log; - -import androidx.appcompat.app.AppCompatDelegate; -import androidx.preference.PreferenceManager; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.android.DarkModeSetting; - -import static androidx.preference.PreferenceManager.getDefaultSharedPreferences; - -public class Notes extends Application { - private static final String TAG = Notes.class.getSimpleName(); - - private static final long LOCK_TIME = 30 * 1000; - private static boolean lockedPreference = false; - private static boolean isLocked = true; - private static long lastInteraction = 0; - private static String PREF_KEY_THEME; - private static boolean isGridViewEnabled = false; - - @Override - public void onCreate() { - PREF_KEY_THEME = getString(R.string.pref_key_theme); - setAppTheme(getAppTheme(getApplicationContext())); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - lockedPreference = prefs.getBoolean(getString(R.string.pref_key_lock), false); - isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), false); - super.onCreate(); - } - - public static void setAppTheme(DarkModeSetting setting) { - AppCompatDelegate.setDefaultNightMode(setting.getModeId()); - } - - public static boolean isGridViewEnabled() { - return isGridViewEnabled; - } - - public static void updateGridViewEnabled(boolean gridView) { - isGridViewEnabled = gridView; - } - - public static DarkModeSetting getAppTheme(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - String mode; - try { - mode = prefs.getString(PREF_KEY_THEME, DarkModeSetting.SYSTEM_DEFAULT.name()); - } catch (ClassCastException e) { - boolean darkModeEnabled = prefs.getBoolean(PREF_KEY_THEME, false); - mode = darkModeEnabled ? DarkModeSetting.DARK.name() : DarkModeSetting.LIGHT.name(); - } - return DarkModeSetting.valueOf(mode); - } - - public static boolean isDarkThemeActive(Context context, DarkModeSetting setting) { - if (setting == DarkModeSetting.SYSTEM_DEFAULT) { - return isDarkThemeActive(context); - } else { - return setting == DarkModeSetting.DARK; - } - } - - public static boolean isDarkThemeActive(Context context) { - int uiMode = context.getResources().getConfiguration().uiMode; - return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; - } - - public static void setLockedPreference(boolean lockedPreference) { - Log.i(TAG, "New locked preference: " + lockedPreference); - Notes.lockedPreference = lockedPreference; - } - - public static boolean isLocked() { - if (!isLocked && System.currentTimeMillis() > (LOCK_TIME + lastInteraction)) { - isLocked = true; - } - return lockedPreference && isLocked; - } - - public static void unlock() { - isLocked = false; - } - - public static void updateLastInteraction() { - lastInteraction = System.currentTimeMillis(); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/NotesImageLoader.java b/app/src/main/java/it/niedermann/owncloud/notes/util/NotesImageLoader.java deleted file mode 100644 index e0b008ac..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/NotesImageLoader.java +++ /dev/null @@ -1,42 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.content.Context; -import android.util.Base64; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.yydcdut.markdown.loader.DefaultLoader; - -import java.io.IOException; - -/** - * Extends Default Loader with Base64 capabilities - */ -public class NotesImageLoader extends DefaultLoader { - - /** - * Constructor - * - * @param context Context - */ - NotesImageLoader(Context context) { - super(context); - } - - @Nullable - @Override - public byte[] loadSync(@NonNull String url) throws IOException { - byte[] bytes = super.loadSync(url); - if (bytes == null && url.trim().startsWith("data:image/")) { - return base64(url); - } - return bytes; - } - - @Nullable - private static byte[] base64(@NonNull String url) { - String content = url.substring(url.indexOf(",")); - return Base64.decode(content, Base64.DEFAULT); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/NotesTextWatcher.java b/app/src/main/java/it/niedermann/owncloud/notes/util/NotesTextWatcher.java deleted file mode 100644 index 76f1bfce..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/NotesTextWatcher.java +++ /dev/null @@ -1,98 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Log; -import android.widget.EditText; - -import androidx.annotation.NonNull; - -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_UNCHECKED_STAR_TRAILING_SPACE; - -/** - * Implements auto-continuation for checked-lists - */ -public abstract class NotesTextWatcher implements TextWatcher { - - private static final String TAG = NotesTextWatcher.class.getSimpleName(); - - private static final String codeBlock = "```"; - - private static final int lengthCheckbox = 6; - - private boolean resetSelection = false; - private boolean afterTextChangedHandeled = false; - private int resetSelectionTo = -1; - - private final EditText editText; - - protected NotesTextWatcher(EditText editText) { - this.editText = editText; - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Nothing to do here... - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // https://github.com/stefan-niedermann/nextcloud-notes/issues/608 - if (count == 1 && s.charAt(start) == '\n') { // 'Enter' was pressed - autoContinueCheckboxListsOnEnter(s, start, count); - } - // https://github.com/stefan-niedermann/nextcloud-notes/issues/558 - if (s.toString().contains(codeBlock)) { - preventCursorJumpToTopWithinCodeBlock(s, start, count); - } - } - - @Override - public void afterTextChanged(Editable s) { - if (resetSelection && !afterTextChangedHandeled) { - Log.v(TAG, "Resetting selection to " + resetSelectionTo); - afterTextChangedHandeled = true; - setNewText(new StringBuilder(s), resetSelectionTo); - afterTextChangedHandeled = false; - resetSelection = false; - resetSelectionTo = -1; - } - } - - private void autoContinueCheckboxListsOnEnter(@NonNull CharSequence s, int start, int count) { - // Find start of line - int startOfLine = MarkDownUtil.getStartOfLine(s, start); - String line = s.subSequence(startOfLine, start).toString(); - - if (line.equals(CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE) || line.equals(CHECKBOX_UNCHECKED_STAR_TRAILING_SPACE)) { - editText.setSelection(startOfLine + 1); - setNewText(new StringBuilder(s).replace(startOfLine, startOfLine + lengthCheckbox + 1, "\n"), startOfLine + 1); - } else if (MarkDownUtil.lineStartsWithCheckbox(line, false)) { - setNewText(new StringBuilder(s).insert(start + count, CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE), start + lengthCheckbox + 1); - } else if (MarkDownUtil.lineStartsWithCheckbox(line, true)) { - setNewText(new StringBuilder(s).insert(start + count, CHECKBOX_UNCHECKED_STAR_TRAILING_SPACE), start + lengthCheckbox + 1); - } - } - - private void preventCursorJumpToTopWithinCodeBlock(@NonNull CharSequence s, int start, int count) { - // Find start of line - int startOfLine = MarkDownUtil.getStartOfLine(s, start); - String line = s.subSequence(startOfLine, start).toString(); - // "start" is the direct sibling of the codeBlock - if (line.startsWith(codeBlock) && start - startOfLine == codeBlock.length() && !resetSelection) { - resetSelectionTo = editText.getSelectionEnd(); - resetSelection = true; - Log.v(TAG, "Entered a character directly behind a codeBlock - prepare selection reset to " + resetSelectionTo); - } else if (s.subSequence(startOfLine, start + count).toString().startsWith(codeBlock) && !resetSelection) { - resetSelectionTo = editText.getSelectionEnd(); - resetSelection = true; - Log.v(TAG, "One completed a ``-codeBlock with the third `-character - prepare selection reset to " + resetSelectionTo); - } - } - - private void setNewText(@NonNull StringBuilder newText, int selection) { - editText.setText(newText); - editText.setSelection(selection); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/SSOUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/SSOUtil.java deleted file mode 100644 index 77fa0d2c..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/SSOUtil.java +++ /dev/null @@ -1,52 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.app.Activity; -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.nextcloud.android.sso.AccountImporter; -import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted; -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; -import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; -import com.nextcloud.android.sso.helper.SingleAccountHelper; -import com.nextcloud.android.sso.ui.UiExceptionManager; - -public class SSOUtil { - - private static final String TAG = SSOUtil.class.getSimpleName(); - - private SSOUtil() { - - } - - /** - * Opens a dialog which allows the user to pick a Nextcloud account (which previously has to be configured in the files app). - * Also allows to configure a new Nextcloud account in the files app and directly import it. - * - * @param activity should implement AccountImporter.onActivityResult - */ - public static void askForNewAccount(@NonNull Activity activity) { - try { - AccountImporter.pickNewAccount(activity); - } catch (NextcloudFilesAppNotInstalledException e1) { - UiExceptionManager.showDialogForException(activity, e1); - Log.w(TAG, "============================================================="); - Log.w(TAG, "Nextcloud app is not installed. Cannot choose account"); - e1.printStackTrace(); - } catch (AndroidGetAccountsPermissionNotGranted e2) { - AccountImporter.requestAndroidAccountPermissionsAndPickAccount(activity); - } - } - - public static boolean isConfigured(Context context) { - try { - SingleAccountHelper.getCurrentSingleSignOnAccount(context); - return true; - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { - return false; - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/ServerResponse.java b/app/src/main/java/it/niedermann/owncloud/notes/util/ServerResponse.java deleted file mode 100644 index ca915b98..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/ServerResponse.java +++ /dev/null @@ -1,104 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import androidx.annotation.Nullable; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.List; - -import it.niedermann.owncloud.notes.model.CloudNote; -import it.niedermann.owncloud.notes.persistence.NotesClient; - -/** - * Provides entity classes for handling server responses with a single note ({@link NoteResponse}) or a list of notes ({@link NotesResponse}). - */ -public class ServerResponse { - - public static class NoteResponse extends ServerResponse { - public NoteResponse(NotesClient.ResponseData response) { - super(response); - } - - public CloudNote getNote() throws JSONException { - return getNoteFromJSON(new JSONObject(getContent())); - } - } - - public static class NotesResponse extends ServerResponse { - public NotesResponse(NotesClient.ResponseData response) { - super(response); - } - - public List getNotes() throws JSONException { - List notesList = new ArrayList<>(); - JSONArray notes = new JSONArray(getContent()); - for (int i = 0; i < notes.length(); i++) { - JSONObject json = notes.getJSONObject(i); - notesList.add(getNoteFromJSON(json)); - } - return notesList; - } - } - - - private final NotesClient.ResponseData response; - - ServerResponse(NotesClient.ResponseData response) { - this.response = response; - } - - protected String getContent() { - return response == null ? null : response.getContent(); - } - - public String getETag() { - return response.getETag(); - } - - public long getLastModified() { - return response.getLastModified(); - } - - @Nullable - public String getSupportedApiVersions() { - return response.getSupportedApiVersions(); - } - - CloudNote getNoteFromJSON(JSONObject json) throws JSONException { - long id = 0; - String title = ""; - String content = ""; - Calendar modified = null; - boolean favorite = false; - String category = null; - String etag = null; - if (!json.isNull(NotesClient.JSON_ID)) { - id = json.getLong(NotesClient.JSON_ID); - } - if (!json.isNull(NotesClient.JSON_TITLE)) { - title = json.getString(NotesClient.JSON_TITLE); - } - if (!json.isNull(NotesClient.JSON_CONTENT)) { - content = json.getString(NotesClient.JSON_CONTENT); - } - if (!json.isNull(NotesClient.JSON_MODIFIED)) { - modified = GregorianCalendar.getInstance(); - modified.setTimeInMillis(json.getLong(NotesClient.JSON_MODIFIED) * 1000); - } - if (!json.isNull(NotesClient.JSON_FAVORITE)) { - favorite = json.getBoolean(NotesClient.JSON_FAVORITE); - } - if (!json.isNull(NotesClient.JSON_CATEGORY)) { - category = json.getString(NotesClient.JSON_CATEGORY); - } - if (!json.isNull(NotesClient.JSON_ETAG)) { - etag = json.getString(NotesClient.JSON_ETAG); - } - return new CloudNote(id, modified, title, content, favorite, category, etag); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/ShareUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/ShareUtil.java deleted file mode 100644 index 1a1f6f37..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/ShareUtil.java +++ /dev/null @@ -1,20 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.content.Context; -import android.content.Intent; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN; - -public class ShareUtil { - public static void openShareDialog(@NonNull Context context, @Nullable String subject, @Nullable String text) { - context.startActivity(Intent.createChooser(new Intent() - .setAction(Intent.ACTION_SEND) - .setType(MIMETYPE_TEXT_PLAIN) - .putExtra(Intent.EXTRA_SUBJECT, subject) - .putExtra(Intent.EXTRA_TITLE, subject) - .putExtra(Intent.EXTRA_TEXT, text), subject)); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/SupportUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/util/SupportUtil.java deleted file mode 100644 index 39abc105..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/SupportUtil.java +++ /dev/null @@ -1,49 +0,0 @@ -package it.niedermann.owncloud.notes.util; - -import android.os.Build; -import android.text.Html; -import android.text.Spanned; -import android.text.method.LinkMovementMethod; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -/** - * Some helper functionality in alike the Android support library. - * Currently, it offers methods for working with HTML string resources. - */ -public class SupportUtil { - - private SupportUtil() { - - } - - /** - * Fills a {@link TextView} with HTML content and activates links in that {@link TextView}. - * - * @param view The {@link TextView} which should be filled. - * @param stringId The string resource containing HTML tags (escaped by <) - * @param formatArgs Arguments for the string resource. - */ - public static void setHtml(@NonNull TextView view, int stringId, Object... formatArgs) { - view.setText(SupportUtil.fromHtml(view.getResources().getString(stringId, formatArgs))); - view.setMovementMethod(LinkMovementMethod.getInstance()); - } - - /** - * Creates a {@link Spanned} from a HTML string on all SDK versions. - * - * @param source Source string with HTML markup - * @return Spannable for using in a {@link TextView} - * @see Html#fromHtml(String) - * @see Html#fromHtml(String, int) - */ - private static Spanned fromHtml(String source) { - if (Build.VERSION.SDK_INT >= 24) { - return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY); - } else { - //noinspection deprecation - return Html.fromHtml(source); - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/format/ContextBasedFormattingCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/util/format/ContextBasedFormattingCallback.java deleted file mode 100644 index 68010dab..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/format/ContextBasedFormattingCallback.java +++ /dev/null @@ -1,115 +0,0 @@ -package it.niedermann.owncloud.notes.util.format; - -import android.graphics.Typeface; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.StyleSpan; -import android.util.Log; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.EditText; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.util.MarkDownUtil; - -import static it.niedermann.owncloud.notes.util.ClipboardUtil.getClipboardURLorNull; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.getEndOfLine; -import static it.niedermann.owncloud.notes.util.MarkDownUtil.getStartOfLine; - -public class ContextBasedFormattingCallback implements ActionMode.Callback { - - private static final String TAG = ContextBasedFormattingCallback.class.getSimpleName(); - - private final EditText editText; - - public ContextBasedFormattingCallback(EditText editText) { - this.editText = editText; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mode.getMenuInflater().inflate(R.menu.context_based_formatting, menu); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - CharSequence text = editText.getText(); - int originalCursorPosition = editText.getSelectionStart(); - if (originalCursorPosition >= 0 && originalCursorPosition <= text.length()) { - int startOfLine = getStartOfLine(text, originalCursorPosition); - int endOfLine = getEndOfLine(text, startOfLine); - String line = text.subSequence(startOfLine, endOfLine).toString(); - if (MarkDownUtil.lineStartsWithCheckbox(line)) { - menu.findItem(R.id.checkbox).setVisible(false); - Log.i(TAG, "Hide checkbox menu item because line starts already with checkbox"); - } - } else { - Log.e(TAG, "SelectionStart is " + originalCursorPosition + ". Expected to be between 0 and " + text.length()); - } - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch (item.getItemId()) { - case R.id.checkbox: - insertCheckbox(); - return true; - case R.id.link: - insertLink(); - return true; - default: - return false; - } - } - - private void insertCheckbox() { - CharSequence text = editText.getText(); - int originalCursorPosition = editText.getSelectionStart(); - int startOfLine = getStartOfLine(text, originalCursorPosition); - Log.i(TAG, "Inserting checkbox at position " + startOfLine); - CharSequence part1 = text.subSequence(0, startOfLine); - CharSequence part2 = text.subSequence(startOfLine, text.length()); - editText.setText(TextUtils.concat(part1, CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE, part2)); - editText.setSelection(originalCursorPosition + CHECKBOX_UNCHECKED_MINUS_TRAILING_SPACE.length()); - } - - private void insertLink() { - SpannableStringBuilder ssb = new SpannableStringBuilder(editText.getText()); - int start = editText.getText().length(); - int end = start; - boolean textToFormatIsLink = TextUtils.indexOf(editText.getText().subSequence(start, end), "http") == 0; - if (textToFormatIsLink) { - Log.i(TAG, "Inserting link description for position " + start + " to " + end); - ssb.insert(end, ")"); - ssb.insert(start, "[]("); - } else { - String clipboardURL = getClipboardURLorNull(editText.getContext()); - if (clipboardURL != null) { - Log.i(TAG, "Inserting link from clipboard at position " + start + " to " + end + ": " + clipboardURL); - ssb.insert(end, "](" + clipboardURL + ")"); - end += clipboardURL.length(); - } else { - Log.i(TAG, "Inserting empty link for position " + start + " to " + end); - ssb.insert(end, "]()"); - } - ssb.insert(start, "["); - } - end++; - ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1); - editText.setText(ssb); - if (textToFormatIsLink) { - editText.setSelection(start + 1); - } else { - editText.setSelection(end + 2); // after ]( - } - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - // Nothing to do here... - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/format/ContextBasedRangeFormattingCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/util/format/ContextBasedRangeFormattingCallback.java deleted file mode 100644 index cac2da3a..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/format/ContextBasedRangeFormattingCallback.java +++ /dev/null @@ -1,150 +0,0 @@ -package it.niedermann.owncloud.notes.util.format; - -import android.graphics.Typeface; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.StyleSpan; -import android.util.SparseIntArray; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.EditText; - -import it.niedermann.owncloud.notes.R; - -import static it.niedermann.owncloud.notes.util.ClipboardUtil.getClipboardURLorNull; - -public class ContextBasedRangeFormattingCallback implements ActionMode.Callback { - - private static final String TAG = ContextBasedRangeFormattingCallback.class.getSimpleName(); - - private final EditText editText; - - public ContextBasedRangeFormattingCallback(EditText editText) { - this.editText = editText; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mode.getMenuInflater().inflate(R.menu.context_based_range_formatting, menu); - - SparseIntArray styleFormatMap = new SparseIntArray(); - styleFormatMap.append(R.id.bold, Typeface.BOLD); - styleFormatMap.append(R.id.italic, Typeface.ITALIC); - - MenuItem item; - CharSequence title; - SpannableStringBuilder ssb; - - for (int i = 0; i < styleFormatMap.size(); i++) { - item = menu.findItem(styleFormatMap.keyAt(i)); - title = item.getTitle(); - ssb = new SpannableStringBuilder(title); - ssb.setSpan(new StyleSpan(styleFormatMap.valueAt(i)), 0, title.length(), 0); - item.setTitle(ssb); - } - - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - // TODO hide actions if not available? - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - int start = editText.getSelectionStart(); - int end = editText.getSelectionEnd(); - SpannableStringBuilder ssb = new SpannableStringBuilder(editText.getText()); - final String markdown; - - - switch (item.getItemId()) { - case R.id.bold: - markdown = "**"; - if (hasAlreadyMarkdown(start, end, markdown)) { - this.removeMarkdown(ssb, start, end, markdown); - } else { - this.addMarkdown(ssb, start, end, markdown, Typeface.BOLD); - } - editText.setText(ssb); - editText.setSelection(end + markdown.length() * 2); - return true; - case R.id.italic: - markdown = "*"; - if (hasAlreadyMarkdown(start, end, markdown)) { - this.removeMarkdown(ssb, start, end, markdown); - } else { - this.addMarkdown(ssb, start, end, markdown, Typeface.ITALIC); - } - editText.setText(ssb); - editText.setSelection(end + markdown.length() * 2); - return true; - case R.id.link: - boolean textToFormatIsLink = TextUtils.indexOf(editText.getText().subSequence(start, end), "http") == 0; - if (textToFormatIsLink) { - ssb.insert(end, ")"); - ssb.insert(start, "[]("); - } else { - String clipboardURL = getClipboardURLorNull(editText.getContext()); - if (clipboardURL != null) { - ssb.insert(end, "](" + clipboardURL + ")"); - end += clipboardURL.length(); - } else { - ssb.insert(end, "]()"); - } - ssb.insert(start, "["); - } - end++; - ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1); - editText.setText(ssb); - if (textToFormatIsLink) { - editText.setSelection(start + 1); - } else { - editText.setSelection(end + 2); // after ]( - } - return true; - case android.R.id.cut: - // https://github.com/stefan-niedermann/nextcloud-notes/issues/604 - // https://github.com/stefan-niedermann/nextcloud-notes/issues/477 - try { - editText.onTextContextMenuItem(item.getItemId()); - return true; - } catch (IndexOutOfBoundsException e) { - e.printStackTrace(); - editText.setSelection(0, 0); - editText.clearFocus(); - return true; - } - default: - return false; - } - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - // Nothing to do here... - } - - private boolean hasAlreadyMarkdown(int start, int end, String markdown) { - return start > markdown.length() && markdown.contentEquals(editText.getText().subSequence(start - markdown.length(), start)) && - editText.getText().length() > end + markdown.length() && markdown.contentEquals(editText.getText().subSequence(end, end + markdown.length())); - } - - private void removeMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown) { - // FIXME disabled, because it does not work properly and might cause data loss - // ssb.delete(start - markdown.length(), start); - // ssb.delete(end - markdown.length(), end); - // ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1); - } - - private void addMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown, int typeface) { - ssb.insert(end, markdown); - ssb.insert(start, markdown); - editText.getText().charAt(start); - editText.getText().charAt(start + 1); - ssb.setSpan(new StyleSpan(typeface), start, end + markdown.length() * 2, 1); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/AbstractWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/AbstractWidgetData.java new file mode 100644 index 00000000..019a2153 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/AbstractWidgetData.java @@ -0,0 +1,42 @@ +package it.niedermann.owncloud.notes.widget; + +public abstract class AbstractWidgetData { + + private int appWidgetId; + private long accountId; + private int themeMode; + + protected AbstractWidgetData() { + + } + + protected AbstractWidgetData(int appWidgetId, long accountId, int themeMode) { + this.appWidgetId = appWidgetId; + this.accountId = accountId; + this.themeMode = themeMode; + } + + public int getAppWidgetId() { + return appWidgetId; + } + + public void setAppWidgetId(int appWidgetId) { + this.appWidgetId = appWidgetId; + } + + public long getAccountId() { + return accountId; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + public int getThemeMode() { + return themeMode; + } + + public void setThemeMode(int themeMode) { + this.themeMode = themeMode; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/createnote/CreateNoteWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/createnote/CreateNoteWidget.java new file mode 100644 index 00000000..d4a6b1d2 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/createnote/CreateNoteWidget.java @@ -0,0 +1,51 @@ +package it.niedermann.owncloud.notes.widget.createnote; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.widget.RemoteViews; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.edit.EditNoteActivity; + +/** + * Implementation of App Widget functionality. + */ +public class CreateNoteWidget extends AppWidgetProvider { + + private static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, + int appWidgetId) { + + // Construct the RemoteViews object + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_create_note); + Intent intent = new Intent(context, EditNoteActivity.class); + + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + views.setOnClickPendingIntent(R.id.widget_create_note, pendingIntent); + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + + // There may be multiple widgets active, so update all of them + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + + @Override + public void onEnabled(Context context) { + // Enter relevant functionality for when the first widget is created + } + + @Override + public void onDisabled(Context context) { + // Enter relevant functionality for when the last widget is disabled + } +} + diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java new file mode 100644 index 00000000..7584b966 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java @@ -0,0 +1,193 @@ +package it.niedermann.owncloud.notes.widget.notelist; + +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.graphics.Color; +import android.net.Uri; +import android.util.Log; +import android.widget.RemoteViews; + +import java.util.NoSuchElementException; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.preferences.DarkModeSetting; +import it.niedermann.owncloud.notes.edit.EditNoteActivity; +import it.niedermann.owncloud.notes.main.MainActivity; +import it.niedermann.owncloud.notes.branding.BrandingUtil; +import it.niedermann.owncloud.notes.shared.model.Category; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.NotesApplication; + +import static it.niedermann.owncloud.notes.edit.EditNoteActivity.PARAM_CATEGORY; +import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_ALL; +import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_CATEGORY; +import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_STARRED; + +public class NoteListWidget extends AppWidgetProvider { + private static final String TAG = NoteListWidget.class.getSimpleName(); + + public static final int PENDING_INTENT_NEW_NOTE_RQ = 0; + public static final int PENDING_INTENT_EDIT_NOTE_RQ = 1; + public static final int PENDING_INTENT_OPEN_APP_RQ = 2; + + static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { + final NotesDatabase db = NotesDatabase.getInstance(context); + + RemoteViews views; + DarkModeSetting darkTheme; + + for (int appWidgetId : appWidgetIds) { + try { + final NoteListsWidgetData data = db.getNoteListWidgetData(appWidgetId); + final LocalAccount localAccount = db.getAccount(data.getAccountId()); + + String category = null; + if (data.getCategoryId() != null) { + category = db.getCategoryTitleById(data.getAccountId(), data.getCategoryId()); + } + + darkTheme = DarkModeSetting.fromModeID(data.getThemeMode()); + + Intent serviceIntent = new Intent(context, NoteListWidgetService.class); + serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))); + + // Launch application when user taps the header icon or app title + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(new ComponentName(context.getPackageName(), + MainActivity.class.getName())); + + // Open the main app if the user taps the widget header + PendingIntent openAppI = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_APP_RQ, + intent, + PendingIntent.FLAG_UPDATE_CURRENT); + + // Launch create note activity if user taps "+" icon on header + PendingIntent newNoteI = PendingIntent.getActivity(context, (PENDING_INTENT_NEW_NOTE_RQ + appWidgetId), + new Intent(context, EditNoteActivity.class).putExtra(PARAM_CATEGORY, new Category(category, data.getMode() == MODE_DISPLAY_STARRED)), + PendingIntent.FLAG_UPDATE_CURRENT); + + PendingIntent templatePI = PendingIntent.getActivity(context, PENDING_INTENT_EDIT_NOTE_RQ, + new Intent(context, EditNoteActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT); + + Log.v(TAG, "-- data - " + data); + + if (NotesApplication.isDarkThemeActive(context, darkTheme)) { + views = new RemoteViews(context.getPackageName(), R.layout.widget_note_list_dark); + views.setTextViewText(R.id.widget_note_list_title_tv_dark, getWidgetTitle(context, data.getMode(), category)); + views.setOnClickPendingIntent(R.id.widget_note_header_icon_dark, openAppI); + views.setOnClickPendingIntent(R.id.widget_note_list_title_tv_dark, openAppI); + views.setOnClickPendingIntent(R.id.widget_note_list_create_icon_dark, newNoteI); + views.setPendingIntentTemplate(R.id.note_list_widget_lv_dark, templatePI); + views.setRemoteAdapter(appWidgetId, R.id.note_list_widget_lv_dark, serviceIntent); + views.setEmptyView(R.id.note_list_widget_lv_dark, R.id.widget_note_list_placeholder_tv_dark); + awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.note_list_widget_lv_dark); + if (BrandingUtil.isBrandingEnabled(context)) { + views.setInt(R.id.widget_note_header_dark, "setBackgroundColor", localAccount.getColor()); + views.setInt(R.id.widget_note_header_icon_dark, "setColorFilter", localAccount.getTextColor()); + views.setInt(R.id.widget_note_list_create_icon_dark, "setColorFilter", localAccount.getTextColor()); + views.setTextColor(R.id.widget_note_list_title_tv_dark, localAccount.getTextColor()); + } else { + views.setInt(R.id.widget_note_header_dark, "setBackgroundColor", context.getResources().getColor(R.color.defaultBrand)); + views.setInt(R.id.widget_note_header_icon_dark, "setColorFilter", Color.WHITE); + views.setInt(R.id.widget_note_list_create_icon_dark, "setColorFilter", Color.WHITE); + views.setTextColor(R.id.widget_note_list_title_tv_dark, Color.WHITE); + } + } else { + views = new RemoteViews(context.getPackageName(), R.layout.widget_note_list); + views.setTextViewText(R.id.widget_note_list_title_tv, getWidgetTitle(context, data.getMode(), category)); + views.setOnClickPendingIntent(R.id.widget_note_header_icon, openAppI); + views.setOnClickPendingIntent(R.id.widget_note_list_title_tv, openAppI); + views.setOnClickPendingIntent(R.id.widget_note_list_create_icon, newNoteI); + views.setPendingIntentTemplate(R.id.note_list_widget_lv, templatePI); + views.setRemoteAdapter(appWidgetId, R.id.note_list_widget_lv, serviceIntent); + views.setEmptyView(R.id.note_list_widget_lv, R.id.widget_note_list_placeholder_tv); + awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.note_list_widget_lv); + if (BrandingUtil.isBrandingEnabled(context)) { + views.setInt(R.id.widget_note_header, "setBackgroundColor", localAccount.getColor()); + views.setInt(R.id.widget_note_header_icon, "setColorFilter", localAccount.getTextColor()); + views.setInt(R.id.widget_note_list_create_icon, "setColorFilter", localAccount.getTextColor()); + views.setTextColor(R.id.widget_note_list_title_tv, localAccount.getTextColor()); + } else { + views.setInt(R.id.widget_note_header, "setBackgroundColor", context.getResources().getColor(R.color.defaultBrand)); + views.setInt(R.id.widget_note_header_icon, "setColorFilter", Color.WHITE); + views.setInt(R.id.widget_note_list_create_icon, "setColorFilter", Color.WHITE); + views.setTextColor(R.id.widget_note_list_title_tv, Color.WHITE); + } + } + + awm.updateAppWidget(appWidgetId, views); + } catch (NoSuchElementException e) { + Log.i(TAG, "onUpdate has been triggered before the user finished configuring the widget"); + } + } + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.onUpdate(context, appWidgetManager, appWidgetIds); + updateAppWidget(context, appWidgetManager, appWidgetIds); + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + AppWidgetManager awm = AppWidgetManager.getInstance(context); + + if (intent.getAction() != null) { + if (intent.getAction().equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)) { + if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) { + if (intent.getExtras() != null) { + updateAppWidget(context, awm, new int[]{intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)}); + } else { + Log.w(TAG, "intent.getExtras() is null"); + } + } else { + updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, NoteListWidget.class))); + } + } + } else { + Log.w(TAG, "intent.getAction() is null"); + } + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + super.onDeleted(context, appWidgetIds); + final NotesDatabase db = NotesDatabase.getInstance(context); + + for (int appWidgetId : appWidgetIds) { + db.removeNoteListWidget(appWidgetId); + } + } + + private static String getWidgetTitle(Context context, int displayMode, String category) { + switch (displayMode) { + case MODE_DISPLAY_ALL: + return context.getString(R.string.app_name); + case MODE_DISPLAY_STARRED: + return context.getString(R.string.label_favorites); + case MODE_DISPLAY_CATEGORY: + if ("".equals(category)) { + return context.getString(R.string.action_uncategorized); + } else { + return category; + } + default: + return null; + } + } + + /** + * Update note list widgets, if the note data was changed. + */ + public static void updateNoteListWidgets(Context context) { + context.sendBroadcast(new Intent(context, NoteListWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java new file mode 100644 index 00000000..e927c3b1 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java @@ -0,0 +1,181 @@ +package it.niedermann.owncloud.notes.widget.notelist; + +import android.app.Activity; +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; +import com.nextcloud.android.sso.helper.SingleAccountHelper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.LockedActivity; +import it.niedermann.owncloud.notes.main.MainActivity; +import it.niedermann.owncloud.notes.shared.model.LocalAccount; +import it.niedermann.owncloud.notes.main.NavigationAdapter; +import it.niedermann.owncloud.notes.main.NavigationAdapter.CategoryNavigationItem; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.NotesApplication; + +public class NoteListWidgetConfigurationActivity extends LockedActivity { + private static final String TAG = Activity.class.getSimpleName(); + + private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + + + private LocalAccount localAccount = null; + + private NavigationAdapter adapterCategories; + private NavigationAdapter.NavigationItem itemRecent; + private NavigationAdapter.NavigationItem itemFavorites; + private NotesDatabase db = null; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setResult(RESULT_CANCELED); + setContentView(R.layout.activity_note_list_configuration); + + db = NotesDatabase.getInstance(this); + try { + this.localAccount = db.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + e.printStackTrace(); + Toast.makeText(this, R.string.widget_not_logged_in, Toast.LENGTH_LONG).show(); + // TODO Present user with app login screen + Log.w(TAG, "onCreate: user not logged in"); + finish(); + return; + } + 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) { + Log.d(TAG, "INVALID_APPWIDGET_ID"); + finish(); + } + + itemRecent = new NavigationAdapter.NavigationItem(MainActivity.ADAPTER_KEY_RECENT, + getString(R.string.label_all_notes), + null, + R.drawable.ic_access_time_grey600_24dp); + itemFavorites = new NavigationAdapter.NavigationItem(MainActivity.ADAPTER_KEY_STARRED, + getString(R.string.label_favorites), + null, + R.drawable.ic_star_yellow_24dp); + RecyclerView recyclerView; + RecyclerView.LayoutManager layoutManager; + + adapterCategories = new NavigationAdapter(this, new NavigationAdapter.ClickListener() { + @Override + public void onItemClick(NavigationAdapter.NavigationItem item) { + NoteListsWidgetData data = new NoteListsWidgetData(); + + data.setAppWidgetId(appWidgetId); + if (itemRecent.equals(item)) { + data.setMode(NoteListsWidgetData.MODE_DISPLAY_ALL); + } else if (itemFavorites.equals(item)) { + data.setMode(NoteListsWidgetData.MODE_DISPLAY_STARRED); + } else { + data.setMode(NoteListsWidgetData.MODE_DISPLAY_CATEGORY); + if (item instanceof CategoryNavigationItem) { + data.setCategoryId(((CategoryNavigationItem) item).categoryId); + } else { + throw new IllegalStateException("Tried to choose a category, but "); + } + } + + data.setAccountId(localAccount.getId()); + data.setThemeMode(NotesApplication.getAppTheme(getApplicationContext()).getModeId()); + + db.createOrUpdateNoteListWidgetData(data); + + Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, + getApplicationContext(), NoteListWidget.class); + updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + setResult(RESULT_OK, updateIntent); + getApplicationContext().sendBroadcast(updateIntent); + finish(); + } + + public void onIconClick(NavigationAdapter.NavigationItem item) { + onItemClick(item); + } + }); + + recyclerView = findViewById(R.id.recycler_view); + recyclerView.setHasFixedSize(true); + layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAdapter(adapterCategories); + } + + @Override + protected void onResume() { + super.onResume(); + new LoadCategoryListTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @Override + public void applyBrand(int mainColor, int textColor) { + } + + private class LoadCategoryListTask extends AsyncTask> { + @Override + protected List doInBackground(Void... voids) { + if (localAccount == null) { + return new ArrayList<>(); + } + NavigationAdapter.NavigationItem itemUncategorized; + List categories = db.getCategories(localAccount.getId()); + + if (!categories.isEmpty() && categories.get(0).label.isEmpty()) { + itemUncategorized = categories.get(0); + itemUncategorized.label = getString(R.string.action_uncategorized); + itemUncategorized.icon = NavigationAdapter.ICON_NOFOLDER; + } + + Map favorites = db.getFavoritesCount(localAccount.getId()); + //noinspection ConstantConditions + int numFavorites = favorites.containsKey("1") ? favorites.get("1") : 0; + //noinspection ConstantConditions + int numNonFavorites = favorites.containsKey("0") ? favorites.get("0") : 0; + itemFavorites.count = numFavorites; + itemRecent.count = numFavorites + numNonFavorites; + + ArrayList items = new ArrayList<>(); + items.add(itemRecent); + items.add(itemFavorites); + + for (NavigationAdapter.NavigationItem item : categories) { + int slashIndex = item.label.indexOf('/'); + + item.label = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex); + item.id = "category:" + item.label; + items.add(item); + } + return items; + } + + @Override + protected void onPostExecute(List items) { + adapterCategories.setItems(items); + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java new file mode 100644 index 00000000..5fde5010 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.java @@ -0,0 +1,147 @@ +package it.niedermann.owncloud.notes.widget.notelist; + +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import java.util.List; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.preferences.DarkModeSetting; +import it.niedermann.owncloud.notes.edit.EditNoteActivity; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.NotesApplication; + +import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_ALL; +import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_CATEGORY; +import static it.niedermann.owncloud.notes.widget.notelist.NoteListsWidgetData.MODE_DISPLAY_STARRED; + +public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFactory { + private static final String TAG = NoteListWidgetFactory.class.getSimpleName(); + + private final Context context; + private final NoteListsWidgetData data; + private final boolean darkTheme; + private NotesDatabase db; + private List dbNotes; + + NoteListWidgetFactory(Context context, Intent intent) { + this.context = context; + final int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + + db = NotesDatabase.getInstance(context); + data = db.getNoteListWidgetData(appWidgetId); + + darkTheme = NotesApplication.isDarkThemeActive(context, DarkModeSetting.fromModeID(data.getThemeMode())); + } + + @Override + public void onCreate() { + } + + @Override + public void onDataSetChanged() { + try { + Log.v(TAG, "--- data - " + data); + switch (data.getMode()) { + case MODE_DISPLAY_ALL: + dbNotes = db.getNotes(data.getAccountId()); + break; + case MODE_DISPLAY_STARRED: + dbNotes = db.searchNotes(data.getAccountId(), null, null, true); + break; + case MODE_DISPLAY_CATEGORY: + if (data.getCategoryId() != null) { + dbNotes = db.searchNotes(data.getAccountId(), null, db.getCategoryTitleById(data.getAccountId(), data.getCategoryId()), null); + } + break; + } + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + } + + @Override + public void onDestroy() { + //NoOp + } + + /** + * getCount() + * + * @return Total number of entries + */ + @Override + public int getCount() { + if (dbNotes == null) { + return 0; + } + + return dbNotes.size(); + } + + @Override + public RemoteViews getViewAt(int position) { + RemoteViews note_content; + + if (dbNotes == null || position > dbNotes.size() - 1 || dbNotes.get(position) == null) { + Log.e(TAG, "Could not find position \"" + position + "\" in dbNotes list."); + return null; + } + + DBNote note = dbNotes.get(position); + final Intent fillInIntent = new Intent(); + final Bundle extras = new Bundle(); + + extras.putLong(EditNoteActivity.PARAM_NOTE_ID, note.getId()); + extras.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, note.getAccountId()); + fillInIntent.putExtras(extras); + fillInIntent.setData(Uri.parse(fillInIntent.toUri(Intent.URI_INTENT_SCHEME))); + + if (darkTheme) { + note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry_dark); + note_content.setOnClickFillInIntent(R.id.widget_note_list_entry_dark, fillInIntent); + note_content.setTextViewText(R.id.widget_entry_content_tv_dark, note.getTitle()); + note_content.setImageViewResource(R.id.widget_entry_fav_icon_dark, note.isFavorite() + ? R.drawable.ic_star_yellow_24dp + : R.drawable.ic_star_grey_ccc_24dp); + } else { + note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry); + note_content.setOnClickFillInIntent(R.id.widget_note_list_entry, fillInIntent); + note_content.setTextViewText(R.id.widget_entry_content_tv, note.getTitle()); + note_content.setImageViewResource(R.id.widget_entry_fav_icon, note.isFavorite() + ? R.drawable.ic_star_yellow_24dp + : R.drawable.ic_star_grey_ccc_24dp); + } + + return note_content; + + } + + @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; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetService.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetService.java new file mode 100644 index 00000000..873cd241 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetService.java @@ -0,0 +1,11 @@ +package it.niedermann.owncloud.notes.widget.notelist; + +import android.content.Intent; +import android.widget.RemoteViewsService; + +public class NoteListWidgetService extends RemoteViewsService { + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new NoteListWidgetFactory(this.getApplicationContext(), intent); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListsWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListsWidgetData.java new file mode 100644 index 00000000..a21714ce --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListsWidgetData.java @@ -0,0 +1,44 @@ +package it.niedermann.owncloud.notes.widget.notelist; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import it.niedermann.owncloud.notes.widget.AbstractWidgetData; + +public class NoteListsWidgetData extends AbstractWidgetData { + public static final int MODE_DISPLAY_ALL = 0; + public static final int MODE_DISPLAY_STARRED = 1; + public static final int MODE_DISPLAY_CATEGORY = 2; + + @IntRange(from = 0, to = 2) + private int mode; + @Nullable + private Long categoryId; + + public int getMode() { + return mode; + } + + public void setMode(@IntRange(from = 0, to = 2) int mode) { + this.mode = mode; + } + + @Nullable + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(@Nullable Long categoryId) { + this.categoryId = categoryId; + } + + @NonNull + @Override + public String toString() { + return "NoteListsWidgetData{" + + "mode=" + mode + + ", categoryId=" + categoryId + + '}'; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java new file mode 100644 index 00000000..777cd25e --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java @@ -0,0 +1,96 @@ +package it.niedermann.owncloud.notes.widget.singlenote; + +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.util.Log; +import android.widget.RemoteViews; + +import java.util.NoSuchElementException; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.preferences.DarkModeSetting; +import it.niedermann.owncloud.notes.edit.EditNoteActivity; +import it.niedermann.owncloud.notes.edit.BaseNoteFragment; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.NotesApplication; + +public class SingleNoteWidget extends AppWidgetProvider { + + private static final String TAG = SingleNoteWidget.class.getSimpleName(); + + static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { + final Intent templateIntent = new Intent(context, EditNoteActivity.class); + final NotesDatabase db = NotesDatabase.getInstance(context); + + for (int appWidgetId : appWidgetIds) { + try { + final SingleNoteWidgetData data = db.getSingleNoteWidgetData(appWidgetId); + + templateIntent.putExtra(BaseNoteFragment.PARAM_ACCOUNT_ID, data.getAccountId()); + + final PendingIntent templatePendingIntent = PendingIntent.getActivity(context, appWidgetId, templateIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + Intent serviceIntent = new Intent(context, SingleNoteWidgetService.class); + serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))); + + RemoteViews views; + + if (NotesApplication.isDarkThemeActive(context, DarkModeSetting.fromModeID(data.getThemeMode()))) { + views = new RemoteViews(context.getPackageName(), R.layout.widget_single_note_dark); + views.setPendingIntentTemplate(R.id.single_note_widget_lv_dark, templatePendingIntent); + views.setRemoteAdapter(R.id.single_note_widget_lv_dark, serviceIntent); + views.setEmptyView(R.id.single_note_widget_lv_dark, R.id.widget_single_note_placeholder_tv_dark); + awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.single_note_widget_lv_dark); + } else { + views = new RemoteViews(context.getPackageName(), R.layout.widget_single_note); + views.setPendingIntentTemplate(R.id.single_note_widget_lv, templatePendingIntent); + views.setRemoteAdapter(R.id.single_note_widget_lv, serviceIntent); + views.setEmptyView(R.id.single_note_widget_lv, R.id.widget_single_note_placeholder_tv); + awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.single_note_widget_lv); + } + awm.updateAppWidget(appWidgetId, views); + } catch (NoSuchElementException e) { + Log.i(TAG, "onUpdate has been triggered before the user finished configuring the widget"); + } + } + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.onUpdate(context, appWidgetManager, appWidgetIds); + updateAppWidget(context, appWidgetManager, appWidgetIds); + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + AppWidgetManager awm = AppWidgetManager.getInstance(context); + + updateAppWidget(context, AppWidgetManager.getInstance(context), + (awm.getAppWidgetIds(new ComponentName(context, SingleNoteWidget.class)))); + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + final NotesDatabase db = NotesDatabase.getInstance(context); + + for (int appWidgetId : appWidgetIds) { + db.removeSingleNoteWidget(appWidgetId); + } + super.onDeleted(context, appWidgetIds); + } + + /** + * Update single note widget, if the note data was changed. + */ + public static void updateSingleNoteWidgets(Context context) { + context.sendBroadcast(new Intent(context, SingleNoteWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java new file mode 100644 index 00000000..ef3bbbe6 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java @@ -0,0 +1,74 @@ +package it.niedermann.owncloud.notes.widget.singlenote; + +import android.app.Activity; +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.database.SQLException; +import android.os.Bundle; +import android.view.Menu; +import android.view.View; +import android.widget.Toast; + +import androidx.appcompat.widget.Toolbar; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.exception.ExceptionHandler; +import it.niedermann.owncloud.notes.main.MainActivity; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.NotesApplication; + +public class SingleNoteWidgetConfigurationActivity extends MainActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + setResult(Activity.RESULT_CANCELED); + + fabCreate.setVisibility(View.GONE); + Toolbar toolbar = binding.activityNotesListView.toolbar; + SwipeRefreshLayout swipeRefreshLayout = binding.activityNotesListView.swiperefreshlayout; + toolbar.setTitle(R.string.activity_select_single_note); + swipeRefreshLayout.setEnabled(false); + swipeRefreshLayout.setRefreshing(false); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return true; + } + + @Override + public void onNoteClick(int position, View v) { + final DBNote note = (DBNote) adapter.getItem(position); + final Bundle extras = getIntent().getExtras(); + + if (extras == null) { + finish(); + return; + } + + int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + + try { + db.createOrUpdateSingleNoteWidgetData( + new SingleNoteWidgetData( + appWidgetId, + note.getAccountId(), + note.getId(), + NotesApplication.getAppTheme(this).getModeId() + ) + ); + final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, + getApplicationContext(), SingleNoteWidget.class) + .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + setResult(RESULT_OK, updateIntent); + getApplicationContext().sendBroadcast(updateIntent); + } catch (SQLException e) { + Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + } + + finish(); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetData.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetData.java new file mode 100644 index 00000000..79b83006 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetData.java @@ -0,0 +1,25 @@ +package it.niedermann.owncloud.notes.widget.singlenote; + +import it.niedermann.owncloud.notes.widget.AbstractWidgetData; + +public class SingleNoteWidgetData extends AbstractWidgetData { + private long noteId; + + public SingleNoteWidgetData() { + + } + + public SingleNoteWidgetData(int appWidgetId, long accountId, long noteId, int themeMode) { + super(appWidgetId, accountId, themeMode); + this.noteId = noteId; + } + + public long getNoteId() { + return noteId; + } + + public void setNoteId(long noteId) { + this.noteId = noteId; + } + +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java new file mode 100644 index 00000000..939c41ef --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetFactory.java @@ -0,0 +1,146 @@ +package it.niedermann.owncloud.notes.widget.singlenote; + +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import com.yydcdut.markdown.MarkdownProcessor; +import com.yydcdut.markdown.syntax.text.TextFactory; + +import java.util.NoSuchElementException; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.preferences.DarkModeSetting; +import it.niedermann.owncloud.notes.edit.EditNoteActivity; +import it.niedermann.owncloud.notes.shared.model.DBNote; +import it.niedermann.owncloud.notes.persistence.NotesDatabase; +import it.niedermann.owncloud.notes.shared.util.MarkDownUtil; +import it.niedermann.owncloud.notes.NotesApplication; + +import static it.niedermann.owncloud.notes.shared.util.MarkDownUtil.parseCompat; + +public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFactory { + + private final MarkdownProcessor markdownProcessor; + private final Context context; + private final int appWidgetId; + + private NotesDatabase db; + private DBNote note; + private boolean darkModeActive = false; + + private static final String TAG = SingleNoteWidget.class.getSimpleName(); + + SingleNoteWidgetFactory(Context context, Intent intent) { + this.context = context; + appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + db = NotesDatabase.getInstance(context); + markdownProcessor = new MarkdownProcessor(this.context); + markdownProcessor.factory(TextFactory.create()); + try { + SingleNoteWidgetData data = db.getSingleNoteWidgetData(appWidgetId); + darkModeActive = NotesApplication.isDarkThemeActive(context, DarkModeSetting.fromModeID(data.getThemeMode())); + } catch (NoSuchElementException e) { + Log.w(TAG, "Widget with ID " + appWidgetId + " seems to be not configured yet."); + } finally { + markdownProcessor.config(MarkDownUtil.getMarkDownConfiguration(this.context, darkModeActive).build()); + } + } + + @Override + public void onCreate() { + + } + + @Override + public void onDataSetChanged() { + try { + final SingleNoteWidgetData data = db.getSingleNoteWidgetData(appWidgetId); + final long noteId = data.getNoteId(); + Log.v(TAG, "Fetch note with id " + noteId); + note = db.getNote(data.getAccountId(), noteId); + + if (note == null) { + Log.e(TAG, "Error: note not found"); + } + } catch (NoSuchElementException e) { + Log.w(TAG, "Widget with ID " + appWidgetId + " seems to be not configured yet."); + } + } + + @Override + public void onDestroy() { + //NoOp + } + + /** + * Returns the number of items in the data set. In this case, always 1 as a single note is + * being displayed. Will return 0 when the note can't be displayed. + */ + @Override + public int getCount() { + return (note != null) ? 1 : 0; + } + + /** + * Returns a RemoteView containing the note content in a TextView and + * a fillInIntent to handle the user tapping on the item in the list view. + * + * @param position The position of the item in the list + * @return The RemoteView at the specified position in the list + */ + @Override + public RemoteViews getViewAt(int position) { + if (note == null) { + return null; + } + + RemoteViews note_content; + + final Intent fillInIntent = new Intent(); + final Bundle extras = new Bundle(); + + extras.putLong(EditNoteActivity.PARAM_NOTE_ID, note.getId()); + extras.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, note.getAccountId()); + fillInIntent.putExtras(extras); + if (darkModeActive) { + note_content = new RemoteViews(context.getPackageName(), R.layout.widget_single_note_content_dark); + note_content.setOnClickFillInIntent(R.id.single_note_content_tv_dark, fillInIntent); + note_content.setTextViewText(R.id.single_note_content_tv_dark, parseCompat(markdownProcessor, note.getContent())); + + } else { + note_content = new RemoteViews(context.getPackageName(), R.layout.widget_single_note_content); + note_content.setOnClickFillInIntent(R.id.single_note_content_tv, fillInIntent); + note_content.setTextViewText(R.id.single_note_content_tv, parseCompat(markdownProcessor, note.getContent())); + } + + return note_content; + } + + + // TODO Set loading view + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetService.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetService.java new file mode 100644 index 00000000..691acae9 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetService.java @@ -0,0 +1,11 @@ +package it.niedermann.owncloud.notes.widget.singlenote; + +import android.content.Intent; +import android.widget.RemoteViewsService; + +public class SingleNoteWidgetService extends RemoteViewsService { + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new SingleNoteWidgetFactory(this.getApplicationContext(), intent); + } +} -- cgit v1.2.3