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

github.com/stefan-niedermann/nextcloud-notes.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Niedermann <info@niedermann.it>2021-06-28 11:06:50 +0300
committerStefan Niedermann <info@niedermann.it>2021-06-28 11:06:50 +0300
commit9fe1b079efa58a822c3432859abbc9565739543b (patch)
tree7dd87a5a9b838832374084d20770d20b2d4ff3d5
parent0d1136aad3cae384e7a9c636f79e4d121b28dd89 (diff)
parent7c5cb1b2552ee711eeaec98e3e8928e8e033cb0a (diff)
Merge branch 'master' into 916-settings
# Conflicts: # app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java # app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java # app/src/main/res/values/strings.xml
-rw-r--r--.github/workflows/android.yml9
-rw-r--r--FAQ.md40
-rw-r--r--README.md3
-rw-r--r--app/build.gradle48
-rw-r--r--app/src/main/AndroidManifest.xml6
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java35
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java41
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java29
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java5
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java159
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java70
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java15
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java3
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java55
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java6
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java11
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java28
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java36
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java5
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java112
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java70
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java25
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java12
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java13
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java40
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java1
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java20
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_21_22.java43
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_22_23.java101
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java84
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java9
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java10
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java36
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesViewModel.java9
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java7
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java86
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java8
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsResponse.java30
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUser.java16
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtil.java105
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java19
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java41
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/DeviceCredentialUtil.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java47
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java4
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/NotesColorUtil.java1
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/SSOUtil.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/ShareUtil.java5
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/shared/util/SupportUtil.java2
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java5
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java13
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java6
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java4
-rw-r--r--app/src/main/res/drawable-v21/bg_navdrawer_item.xml37
-rw-r--r--app/src/main/res/drawable-v21/grid_item_background_selector.xml20
-rw-r--r--app/src/main/res/drawable-v21/list_item_background_selector.xml20
-rw-r--r--app/src/main/res/drawable/bg_navdrawer_item.xml33
-rw-r--r--app/src/main/res/drawable/grid_item_background_selector.xml22
-rw-r--r--app/src/main/res/drawable/ic_color_lens_grey600_24dp.xml11
-rw-r--r--app/src/main/res/drawable/ic_launcher_background.xml1
-rw-r--r--app/src/main/res/drawable/ic_settings_grey600_24dp.xml12
-rw-r--r--app/src/main/res/drawable/list_item_background_selector.xml22
-rw-r--r--app/src/main/res/layout/empty_content_view.xml3
-rw-r--r--app/src/main/res/layout/fragment_about_credits_tab.xml7
-rw-r--r--app/src/main/res/layout/fragment_note_edit.xml2
-rw-r--r--app/src/main/res/layout/fragment_note_preview.xml2
-rw-r--r--app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml5
-rw-r--r--app/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin4323 -> 0 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin2716 -> 0 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin6070 -> 0 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin9147 -> 0 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin13007 -> 0 bytes
-rw-r--r--app/src/main/res/values-ar/strings.xml11
-rw-r--r--app/src/main/res/values-b+en+001/strings.xml1
-rw-r--r--app/src/main/res/values-ca/strings.xml12
-rw-r--r--app/src/main/res/values-cs-rCZ/strings.xml19
-rw-r--r--app/src/main/res/values-de/strings.xml17
-rw-r--r--app/src/main/res/values-el/strings.xml10
-rw-r--r--app/src/main/res/values-es/strings.xml15
-rw-r--r--app/src/main/res/values-eu/strings.xml16
-rw-r--r--app/src/main/res/values-fa/strings.xml10
-rw-r--r--app/src/main/res/values-fi-rFI/strings.xml39
-rw-r--r--app/src/main/res/values-fr/strings.xml20
-rw-r--r--app/src/main/res/values-gl/strings.xml10
-rw-r--r--app/src/main/res/values-he/strings.xml10
-rw-r--r--app/src/main/res/values-hr/strings.xml10
-rw-r--r--app/src/main/res/values-hu-rHU/strings.xml25
-rw-r--r--app/src/main/res/values-is/strings.xml1
-rw-r--r--app/src/main/res/values-it/strings.xml25
-rw-r--r--app/src/main/res/values-ja-rJP/strings.xml30
-rw-r--r--app/src/main/res/values-ko/strings.xml10
-rw-r--r--app/src/main/res/values-lt-rLT/strings.xml10
-rw-r--r--app/src/main/res/values-nb-rNO/strings.xml10
-rw-r--r--app/src/main/res/values-night/colors.xml1
-rw-r--r--app/src/main/res/values-nl/strings.xml25
-rw-r--r--app/src/main/res/values-pl/strings.xml17
-rw-r--r--app/src/main/res/values-pt-rBR/strings.xml17
-rw-r--r--app/src/main/res/values-ru/strings.xml10
-rw-r--r--app/src/main/res/values-sc/strings.xml10
-rw-r--r--app/src/main/res/values-sk-rSK/strings.xml40
-rw-r--r--app/src/main/res/values-sl/strings.xml15
-rw-r--r--app/src/main/res/values-sr/strings.xml10
-rw-r--r--app/src/main/res/values-sv/strings.xml10
-rw-r--r--app/src/main/res/values-tr/strings.xml35
-rw-r--r--app/src/main/res/values-uk/strings.xml10
-rw-r--r--app/src/main/res/values-vi/strings.xml229
-rw-r--r--app/src/main/res/values-zh-rCN/strings.xml17
-rw-r--r--app/src/main/res/values-zh-rHK/strings.xml17
-rw-r--r--app/src/main/res/values-zh-rTW/strings.xml9
-rw-r--r--app/src/main/res/values/arrays.xml6
-rw-r--r--app/src/main/res/values/colors.xml2
-rw-r--r--app/src/main/res/values/dimens.xml10
-rw-r--r--app/src/main/res/values/strings.xml20
-rw-r--r--app/src/main/res/xml/preferences.xml7
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragmentTest.java74
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecorationTest.java100
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java35
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/ApiProviderTest.java92
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClientTest.java99
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java71
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java142
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTaskTest.java98
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesTestingUtil.java (renamed from app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDatabaseTestUtil.java)36
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializerTest.java309
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/persistence/util/NotesColorUtilTest.java3
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/shared/model/CapabilitiesTest.java113
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtilTest.java229
-rw-r--r--app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java7
-rw-r--r--build.gradle6
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004002.txt5
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004003.txt4
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004004.txt3
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004005.txt1
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004006.txt8
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004007.txt8
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004008.txt8
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004009.txt13
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004010.txt3
-rw-r--r--fastlane/metadata/android/en-US/changelogs/3004011.txt3
-rw-r--r--markdown/build.gradle33
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/ListTagHandler.java80
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java317
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java6
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/glide/DownsampleWithMaxWidth.java33
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/CustomGlideStore.java48
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java28
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java128
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/span/InterceptedURLSpan.java10
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/span/ToggleTaskListSpan.java38
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/model/EListType.java2
-rw-r--r--markdown/src/main/res/drawable/ic_baseline_broken_image_24.xml5
-rw-r--r--markdown/src/main/res/drawable/ic_baseline_image_24.xml5
-rw-r--r--markdown/src/main/res/values-night/colors.xml1
-rw-r--r--markdown/src/main/res/values/colors.xml1
-rw-r--r--markdown/src/test/java/it/niedermann/android/markdown/ExampleUnitTest.java17
-rw-r--r--markdown/src/test/java/it/niedermann/android/markdown/ListTagHandlerTest.java142
-rw-r--r--markdown/src/test/java/it/niedermann/android/markdown/MarkdownUtilTest.java (renamed from markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java)326
-rw-r--r--markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java204
166 files changed, 3787 insertions, 1764 deletions
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 9db26418..3ef6b52e 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -12,6 +12,15 @@ jobs:
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Lint
+ run: bash ./gradlew lintDebug --stacktrace
+
test:
name: Unit tests
runs-on: ubuntu-latest
diff --git a/FAQ.md b/FAQ.md
index 60797b52..17391397 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -27,17 +27,45 @@ Sorry. There are so many different environments, that it is impossible for us to
First of all make sure you have updated to and tried with the latest available versions of both, this app and the [Notes server app](https://apps.nextcloud.com/apps/notes).
-In case you receive a `NextcloudApiNotRespondingException`, try to disable the battery optimization for both apps.
-In all other cases please try to clear the storage of **both** apps, Nextcloud Android **and** Nextcloud Notes Android.
+### `NextcloudApiNotRespondingException`
+
+Try to disable the battery "optimization" for both apps. Some manufacturers prevent the app from communicating with the Nextcloud Android properly.
+This is a [known issue of the SingleSignOn mechanism](https://github.com/nextcloud/Android-SingleSignOn#troubleshooting) which we only can work around but not solve on our side.
+
+### `UnknownErrorException: Read timed out`
+
+This issue is caused by a connection time out. This can be the case if there are infrastructural or environmental problems (like a misconfigured server or a bad network connection).
+Probably you will experience it when importing an account, because at this moment, all your Notes will getting downloaded at once. Given you have a lots of notes, this might take longer than the connection is available.
+Further synchronizations are usually not causing this issue, because the Notes app tries to synchronize only *changed* notes after the first import.
+If your notes are not ten thousands of characters long, it is very unlikely that this causes a connection timeout.
+
+We plan to improve the import of an account and make it more reliable by [fetching notes step by step](https://github.com/stefan-niedermann/nextcloud-notes/issues/761#issuecomment-836989421) in a future release.
+Until then you can as a workaround for the first import try to
+1. move all your notes to a different folder on your Nextcloud instance
+2. import your account on your smartphone
+3. put your notes back to the original folder step by step and sync everytime you put some notes back
+
+### `NextcloudFilesAppAccountNotFoundException`
+
+We are not yet sure what exactly causes this issue, but investigate it by [adding more debug logs to recent versions](https://github.com/stefan-niedermann/nextcloud-notes/issues/1256#issuecomment-859505153). In theory this might happen if an already imported account has been deleted in the Nextcloud app.
+As a workaround you can remove the account (or clear the storage of the app as described below if you can't access the account manager anymore) and import it again.
+
+### `TokenMismatchException` and all others
+
+In all other cases please try to clear the storage of **both** apps, Nextcloud Android **and** Nextcloud Notes Android. Not yet synchronized changes will be lost by performing this step.
You can achieve this by navigating to
```
Android settings
- ↳ Apps
- ↳ Nextcloud / Notes
- ↳ Storage
- ↳ Clear storage
+ ↓
+ Apps
+ ↓
+Nextcloud / Notes
+ ↓
+ Storage
+ ↓
+ Clear storage
```
Then set up your account in the Nextcloud Android app again and import the configured account in the Nextcloud Notes Android app.
diff --git a/README.md b/README.md
index 98bf81b0..804304c2 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ An android client for [Nextcloud Notes App](https://github.com/nextcloud/notes/)
## :eyes: Screenshots
-| List View | Edit Mode | Preview |
+| _ | _ | _ |
| :--: | :--: | :--: |
| ![Screenshot of list view](/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png) | ![Screenshot of edit mode](/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png) | ![Screenshot of preview](/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png) |
@@ -47,7 +47,6 @@ An android client for [Nextcloud Notes App](https://github.com/nextcloud/notes/)
## :checkered_flag: Planned features
* Local accounts ([#615](https://github.com/stefan-niedermann/nextcloud-notes/issues/615))
- * Trashbin ([#238](https://github.com/stefan-niedermann/nextcloud-notes/issues/238))
## :family: Join the team
* Test the app with different devices
diff --git a/app/build.gradle b/app/build.gradle
index 3dcaf300..d52b9ff6 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,17 +5,15 @@ android {
buildToolsVersion '30.0.3'
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
defaultConfig {
applicationId "it.niedermann.owncloud.notes"
minSdkVersion 21
- targetSdkVersion 29
- versionCode 3004001
- versionName "3.4.1"
+ targetSdkVersion 30
+ versionCode 3004010
+ versionName "3.4.10"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
@@ -71,51 +69,51 @@ android {
}
dependencies {
+ // Markdown
+ implementation project(path: ':markdown')
+
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
+
// Nextcloud SSO
- implementation "com.github.nextcloud:Android-SingleSignOn:0.5.6"
- implementation 'com.github.stefan-niedermann.nextcloud-commons:sso-glide:1.2.5'
- implementation 'com.github.stefan-niedermann.nextcloud-commons:exception:1.2.5'
+ implementation 'com.github.nextcloud:Android-SingleSignOn:0.5.6'
+ implementation 'com.github.stefan-niedermann.nextcloud-commons:sso-glide:1.4.0'
+ implementation 'com.github.stefan-niedermann.nextcloud-commons:exception:1.4.0'
implementation 'com.github.stefan-niedermann:android-commons:0.2.0'
// Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
- // Markdown
- implementation project(path: ':markdown')
-
// Android X
- implementation "androidx.appcompat:appcompat:1.2.0"
- implementation "androidx.fragment:fragment:1.3.3"
- implementation "androidx.preference:preference:1.1.1"
- implementation "androidx.recyclerview:recyclerview:1.2.0"
+ implementation 'androidx.appcompat:appcompat:1.3.0'
+ implementation 'androidx.fragment:fragment:1.3.5'
+ implementation 'androidx.preference:preference:1.1.1'
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.work:work-runtime:2.5.0'
- implementation "com.google.android.material:material:1.3.0"
+ implementation 'com.google.android.material:material:1.3.0'
// Database
- implementation "androidx.room:room-runtime:2.3.0"
- annotationProcessor "androidx.room:room-compiler:2.3.0"
+ implementation 'androidx.room:room-runtime:2.3.0'
+ annotationProcessor 'androidx.room:room-compiler:2.3.0'
// Retrofit
+ //noinspection GradleDependency
implementation 'com.squareup.retrofit2:retrofit:2.6.4'
// Gson
- implementation 'com.google.code.gson:gson:2.8.6'
+ implementation 'com.google.code.gson:gson:2.8.7'
// ReactiveX
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
-
// Testing
- testImplementation 'junit:junit:4.13.2'
- testImplementation 'org.mockito:mockito-core:3.9.0'
- testImplementation 'org.robolectric:robolectric:4.5.1'
testImplementation 'androidx.test:core:1.3.0'
- testImplementation 'androidx.test.ext:junit:1.1.2'
testImplementation 'androidx.arch.core:core-testing:2.1.0'
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.mockito:mockito-core:3.11.2'
+ testImplementation 'org.robolectric:robolectric:4.5.1'
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5b144279..eb6b4b3d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,11 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <queries>
+ <package android:name="com.nextcloud.client" />
+ <package android:name="com.nextcloud.android.beta" />
+ </queries>
+
<application
android:name=".NotesApplication"
android:allowBackup="true"
@@ -13,7 +18,6 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name_long"
android:networkSecurityConfig="@xml/network_security_config"
- android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="n">
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java
index fc4b8fc6..29d2d0ad 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java
@@ -42,7 +42,7 @@ public class AppendToNoteActivity extends MainActivity {
fullNote$.removeObservers(this);
final String oldContent = fullNote.getContent();
String newContent;
- if (oldContent != null && oldContent.length() > 0) {
+ if (!TextUtils.isEmpty(oldContent)) {
newContent = oldContent + "\n\n" + receivedText;
} else {
newContent = receivedText;
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 f5643d86..e1ba3e8e 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
@@ -67,7 +67,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
account$.observe(requireActivity(), (currentLocalAccount) -> {
account$.removeObservers(requireActivity());
- binding.accountName.setText(currentLocalAccount.getUserName());
+ binding.accountName.setText(currentLocalAccount.getDisplayName());
binding.accountHost.setText(Uri.parse(currentLocalAccount.getUrl()).getHost());
Glide.with(requireContext())
.load(currentLocalAccount.getUrl() + "/index.php/avatar/" + Uri.encode(currentLocalAccount.getUserName()) + "/64")
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 db04e0db..1f096c96 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
@@ -25,7 +25,7 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
}
public void bind(@NonNull Account localAccount, @NonNull Consumer<Account> onAccountClick) {
- binding.accountName.setText(localAccount.getUserName());
+ binding.accountName.setText(localAccount.getDisplayName());
binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost());
Glide.with(itemView.getContext())
.load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64"))
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
index b1cf1d54..4fc883ac 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
@@ -24,6 +24,7 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.LiveData;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
@@ -32,6 +33,8 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.util.ArrayList;
import java.util.Calendar;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import it.niedermann.android.util.ColorUtil;
import it.niedermann.owncloud.notes.R;
@@ -47,6 +50,7 @@ import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
+import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
import it.niedermann.owncloud.notes.shared.util.NotesColorUtil;
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
@@ -60,6 +64,7 @@ import static java.lang.Boolean.TRUE;
public abstract class BaseNoteFragment extends BrandedFragment implements CategoryDialogListener, EditTitleListener {
private static final String TAG = BaseNoteFragment.class.getSimpleName();
+ protected final ExecutorService executor = Executors.newCachedThreadPool();
protected static final int MENU_ID_PIN = -1;
public static final String PARAM_NOTE_ID = "noteId";
@@ -94,9 +99,9 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
}
@Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- new Thread(() -> {
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ executor.submit(() -> {
try {
SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext().getApplicationContext());
this.localAccount = repo.getAccountByName(ssoAccount.name);
@@ -141,7 +146,7 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
e.printStackTrace();
}
- }).start();
+ });
setHasOptionsMenu(true);
}
@@ -193,7 +198,8 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
if (note != null) {
prepareFavoriteOption(menu.findItem(R.id.menu_favorite));
- menu.findItem(R.id.menu_title).setVisible(localAccount.getPreferredApiVersion() != null && localAccount.getPreferredApiVersion().compareTo(ApiVersion.API_VERSION_1_0) >= 0);
+ final ApiVersion preferredApiVersion = ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion());
+ menu.findItem(R.id.menu_title).setVisible(preferredApiVersion != null && preferredApiVersion.compareTo(ApiVersion.API_VERSION_1_0) >= 0);
menu.findItem(R.id.menu_delete).setVisible(!isNew);
}
}
@@ -211,13 +217,13 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.menu_cancel) {
- new Thread(() -> {
+ executor.submit(() -> {
if (originalNote == null) {
repo.deleteNoteAndSync(localAccount, note.getId());
} else {
repo.updateNoteAndSync(localAccount, originalNote, null, null, null);
}
- }).start();
+ });
listener.close();
return true;
} else if (itemId == R.id.menu_delete) {
@@ -236,11 +242,9 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
showEditTitleDialog();
return true;
} else if (itemId == R.id.menu_move) {
- new Thread(() -> {
- AccountPickerDialogFragment
- .newInstance(new ArrayList<>(), note.getAccountId())
- .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName());
- }).start();
+ executor.submit(() -> AccountPickerDialogFragment
+ .newInstance(new ArrayList<>(repo.getAccounts()), note.getAccountId())
+ .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName()));
return true;
} else if (itemId == R.id.menu_share) {
ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent());
@@ -363,14 +367,15 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
public void onTitleEdited(String newTitle) {
titleModified = true;
note.setTitle(newTitle);
- new Thread(() -> {
+ executor.submit(() -> {
note = repo.updateNoteAndSync(localAccount, note, note.getContent(), newTitle, null);
requireActivity().runOnUiThread(() -> listener.onNoteUpdated(note));
- }).start();
+ });
}
public void moveNote(Account account) {
- repo.moveNoteToAnotherAccount(account, note);
+ final LiveData<Note> moveLiveData = repo.moveNoteToAnotherAccount(account, note);
+ moveLiveData.observe(this, (v) -> moveLiveData.removeObservers(this));
listener.close();
}
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
index 298a6c3f..83c8eb1a 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
@@ -29,6 +29,7 @@ import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.FragmentNoteEditBinding;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
+import it.niedermann.owncloud.notes.shared.util.DisplayUtils;
import static androidx.core.view.ViewCompat.isAttachedToWindow;
import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences;
@@ -59,6 +60,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
}
};
private TextWatcher textWatcher;
+ private boolean keyboardShown = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -138,22 +140,17 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
public void onResume() {
super.onResume();
binding.editContent.addTextChangedListener(textWatcher);
+
+ if (keyboardShown) {
+ openSoftKeyboard();
+ }
}
@Override
protected void onNoteLoaded(Note note) {
super.onNoteLoaded(note);
if (TextUtils.isEmpty(note.getContent())) {
- binding.editContent.post(() -> {
- binding.editContent.requestFocus();
-
- final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- if (imm != null) {
- imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT);
- } else {
- Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null.");
- }
- });
+ openSoftKeyboard();
}
binding.editContent.setMarkdownString(note.getContent());
@@ -166,11 +163,32 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
}
}
+ private void openSoftKeyboard() {
+ binding.editContent.postDelayed(() -> {
+ binding.editContent.requestFocus();
+
+ final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT);
+ } else {
+ Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null.");
+ }
+ //Without a small delay the keyboard does not show reliably
+ }, 100);
+ }
+
@Override
public void onPause() {
super.onPause();
binding.editContent.removeTextChangedListener(textWatcher);
cancelTimers();
+
+ final ViewGroup parentView = requireActivity().findViewById(android.R.id.content);
+ if (parentView != null && parentView.getChildCount() > 0) {
+ keyboardShown = DisplayUtils.isSoftKeyboardVisible(parentView.getChildAt(0));
+ } else {
+ keyboardShown = false;
+ }
}
private void cancelTimers() {
@@ -184,7 +202,8 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
*/
@Override
protected String getContent() {
- return binding.editContent.getText().toString();
+ final Editable editable = binding.editContent.getText();
+ return editable == null ? "" : editable.toString();
}
@Override
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
index f1626149..f15d0e59 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
@@ -155,22 +155,22 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O
public void onRefresh() {
if (noteLoaded && repo.isSyncPossible() && SSOUtil.isConfigured(getContext())) {
binding.swiperefreshlayout.setRefreshing(true);
- new Thread(() -> {
+ executor.submit(() -> {
try {
final Account account = repo.getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()).name);
- repo.addCallbackPull(account, () -> new Thread(() -> {
+ repo.addCallbackPull(account, () -> executor.submit(() -> {
note = repo.getNoteById(note.getId());
changedText = note.getContent();
requireActivity().runOnUiThread(() -> {
binding.singleNoteContent.setMarkdownString(note.getContent());
binding.swiperefreshlayout.setRefreshing(false);
});
- }).start());
+ }));
repo.scheduleSync(account, false);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
e.printStackTrace();
}
- }).start();
+ });
} else {
binding.swiperefreshlayout.setRefreshing(false);
Toast.makeText(requireContext(), getString(R.string.error_sync, getString(R.string.error_no_network)), Toast.LENGTH_LONG).show();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java
index c942b345..2e78af2c 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java
@@ -9,6 +9,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
+import androidx.preference.PreferenceManager;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.exceptions.AccountImportCancelledException;
@@ -20,14 +21,17 @@ import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.ui.UiExceptionManager;
import java.net.HttpURLConnection;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.databinding.ActivityImportAccountBinding;
import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment;
import it.niedermann.owncloud.notes.exception.ExceptionHandler;
-import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
import it.niedermann.owncloud.notes.persistence.ApiProvider;
+import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
+import it.niedermann.owncloud.notes.persistence.SyncWorker;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
@@ -37,6 +41,8 @@ public class ImportAccountActivity extends AppCompatActivity {
private static final String TAG = ImportAccountActivity.class.getSimpleName();
public static final int REQUEST_CODE_IMPORT_ACCOUNT = 1;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
private ImportAccountViewModel importAccountViewModel;
private ActivityImportAccountBinding binding;
@@ -84,12 +90,19 @@ public class ImportAccountActivity extends AppCompatActivity {
runOnUiThread(() -> binding.progressCircular.setVisibility(View.VISIBLE));
SingleAccountHelper.setCurrentAccount(getApplicationContext(), ssoAccount.name);
- new Thread(() -> {
+ executor.submit(() -> {
Log.i(TAG, "Added account: " + "name:" + ssoAccount.name + ", " + ssoAccount.url + ", userId" + ssoAccount.userId);
try {
Log.i(TAG, "Loading capabilities for " + ssoAccount.name);
- final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null);
- importAccountViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, new IResponseCallback<Account>() {
+ final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null, ApiProvider.getInstance());
+ final String displayName = CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance());
+ importAccountViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback<Account>() {
+
+ /**
+ * Update syncing when adding account
+ * https://github.com/stefan-niedermann/nextcloud-deck/issues/531
+ * @param account the account to add
+ */
@Override
public void onSuccess(Account account) {
runOnUiThread(() -> {
@@ -98,6 +111,8 @@ public class ImportAccountActivity extends AppCompatActivity {
setResult(RESULT_OK);
finish();
});
+ SyncWorker.update(ImportAccountActivity.this, PreferenceManager.getDefaultSharedPreferences(ImportAccountActivity.this)
+ .getBoolean(getString(R.string.pref_key_background_sync), true));
}
@Override
@@ -110,7 +125,7 @@ public class ImportAccountActivity extends AppCompatActivity {
});
} catch (Throwable t) {
t.printStackTrace();
- ApiProvider.invalidateAPICache(ssoAccount);
+ ApiProvider.getInstance().invalidateAPICache(ssoAccount);
SingleAccountHelper.setCurrentAccount(this, null);
runOnUiThread(() -> {
restoreCleanState();
@@ -120,7 +135,7 @@ public class ImportAccountActivity extends AppCompatActivity {
} else if (t instanceof NetworkErrorException) {
binding.status.setText(getString(R.string.error_sync, getString(R.string.error_no_network)));
binding.status.setVisibility(View.VISIBLE);
- } else if (t instanceof UnknownErrorException && t.getMessage().contains("No address associated with hostname")) {
+ } else if (t instanceof UnknownErrorException && t.getMessage() != null && t.getMessage().contains("No address associated with hostname")) {
// https://github.com/stefan-niedermann/nextcloud-notes/issues/1014
binding.status.setText(R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account);
binding.status.setVisibility(View.VISIBLE);
@@ -129,7 +144,7 @@ public class ImportAccountActivity extends AppCompatActivity {
}
});
}
- }).start();
+ });
});
} catch (AccountImportCancelledException e) {
restoreCleanState();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java
index 905a59b1..70b3b565 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java
@@ -3,6 +3,7 @@ package it.niedermann.owncloud.notes.importaccount;
import android.app.Application;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
@@ -23,7 +24,7 @@ public class ImportAccountViewModel extends AndroidViewModel {
this.repo = NotesRepository.getInstance(application);
}
- public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @NonNull IResponseCallback<Account> callback) {
- repo.addAccount(url, username, accountName, capabilities, callback);
+ public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) {
+ repo.addAccount(url, username, accountName, capabilities, displayName, callback);
}
}
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
index d889689d..83bf8e0b 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
@@ -4,19 +4,18 @@ import android.accounts.NetworkErrorException;
import android.animation.AnimatorInflater;
import android.app.SearchManager;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.net.Uri;
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.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
@@ -49,6 +48,11 @@ import com.nextcloud.android.sso.helper.SingleAccountHelper;
import java.net.HttpURLConnection;
import java.util.Collection;
import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
import it.niedermann.owncloud.notes.LockedActivity;
import it.niedermann.owncloud.notes.R;
@@ -73,9 +77,9 @@ import it.niedermann.owncloud.notes.main.menu.MenuAdapter;
import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
import it.niedermann.owncloud.notes.main.navigation.NavigationClickListener;
import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+import it.niedermann.owncloud.notes.persistence.ApiProvider;
import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker;
-import it.niedermann.owncloud.notes.persistence.ApiProvider;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
@@ -83,7 +87,9 @@ import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+import it.niedermann.owncloud.notes.shared.util.CustomAppGlideModule;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
+import it.niedermann.owncloud.notes.shared.util.ShareUtil;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.O;
@@ -92,7 +98,6 @@ import static android.view.View.VISIBLE;
import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive;
import static it.niedermann.owncloud.notes.NotesApplication.isGridViewEnabled;
import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
-import static it.niedermann.owncloud.notes.main.menu.MenuAdapter.SERVER_SETTINGS;
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.DEFAULT_CATEGORY;
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.FAVORITES;
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.RECENT;
@@ -104,18 +109,19 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
private static final String TAG = MainActivity.class.getSimpleName();
+ protected final ExecutorService executor = Executors.newCachedThreadPool();
+
protected MainViewModel mainViewModel;
private CategoryViewModel categoryViewModel;
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 ADAPTER_KEY_UNCATEGORIZED = "uncategorized";
- private final static int create_note_cmd = 0;
- private final static int show_single_note_cmd = 1;
+ private static final int REQUEST_CODE_CREATE_NOTE = 0;
+ private static final int REQUEST_CODE_SERVER_SETTINGS = 1;
protected ItemAdapter adapter;
private NavigationAdapter adapterCategories;
@@ -165,14 +171,47 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
if (count == 0) {
startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
} else {
- new Thread(() -> {
+ executor.submit(() -> {
try {
final Account account = mainViewModel.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()).name);
runOnUiThread(() -> mainViewModel.postCurrentAccount(account));
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ // Verbose log output for https://github.com/stefan-niedermann/nextcloud-notes/issues/1256
+ runOnUiThread(() -> new AlertDialog.Builder(this)
+ .setTitle(NextcloudFilesAppAccountNotFoundException.class.getSimpleName())
+ .setMessage(R.string.backup)
+ .setPositiveButton(R.string.simple_backup, (a, b) -> executor.submit(() -> {
+ final List<Note> modifiedNotes = new LinkedList<>();
+ for (Account account : mainViewModel.getAccounts()) {
+ modifiedNotes.addAll(mainViewModel.getLocalModifiedNotes(account.getId()));
+ }
+ if (modifiedNotes.size() == 1) {
+ final Note note = modifiedNotes.get(0);
+ ShareUtil.openShareDialog(this, note.getTitle(), note.getContent());
+ } else {
+ ShareUtil.openShareDialog(this,
+ getResources().getQuantityString(R.plurals.share_multiple, modifiedNotes.size(), modifiedNotes.size()),
+ mainViewModel.collectNoteContents(modifiedNotes.stream().map(Note::getId).collect(Collectors.toList())));
+ }
+ }))
+ .setNegativeButton(R.string.simple_error, (a, b) -> {
+ final SharedPreferences ssoPreferences = AccountImporter.getSharedPreferences(getApplicationContext());
+ final StringBuilder ssoPreferencesString = new StringBuilder()
+ .append("Current SSO account: ").append(ssoPreferences.getString("PREF_CURRENT_ACCOUNT_STRING", null)).append("\n")
+ .append("\n")
+ .append("SSO SharedPreferences: ").append("\n");
+ for (Map.Entry<String, ?> entry : ssoPreferences.getAll().entrySet()) {
+ ssoPreferencesString.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
+ }
+ ssoPreferencesString.append("\n")
+ .append("Available accounts in DB: ").append(TextUtils.join(", ", mainViewModel.getAccounts().stream().map(Account::getAccountName).collect(Collectors.toList())));
+ runOnUiThread(() -> ExceptionDialogFragment.newInstance(new RuntimeException(e.getMessage(), new RuntimeException(ssoPreferencesString.toString(), e))).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
+ })
+ .show());
+ } catch (NoCurrentAccountSelectedException e) {
runOnUiThread(() -> ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
}
- }).start();
+ });
}
});
@@ -212,13 +251,13 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
}
fabCreate.setOnClickListener((View view) -> {
- Intent createIntent = new Intent(getApplicationContext(), EditNoteActivity.class);
+ final Intent createIntent = new Intent(getApplicationContext(), EditNoteActivity.class);
createIntent.putExtra(EditNoteActivity.PARAM_CATEGORY, selectedCategory);
if (activityBinding.searchView.getQuery().length() > 0) {
createIntent.putExtra(EditNoteActivity.PARAM_CONTENT, activityBinding.searchView.getQuery().toString());
invalidateOptionsMenu();
}
- startActivityForResult(createIntent, create_note_cmd);
+ startActivityForResult(createIntent, REQUEST_CODE_CREATE_NOTE);
});
});
mainViewModel.getNotesListLiveData().observe(this, notes -> {
@@ -298,18 +337,18 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
activityBinding.launchAccountSwitcher.setOnClickListener((v) -> AccountSwitcherDialog.newInstance(nextAccount.getId()).show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName()));
if (menuAdapter == null) {
- menuAdapter = new MenuAdapter(getApplicationContext(), nextAccount, (menuItem) -> {
+ menuAdapter = new MenuAdapter(getApplicationContext(), nextAccount, REQUEST_CODE_SERVER_SETTINGS, (menuItem) -> {
@Nullable Integer resultCode = menuItem.getResultCode();
if (resultCode == null) {
startActivity(menuItem.getIntent());
} else {
- startActivityForResult(menuItem.getIntent(), menuItem.getResultCode());
+ startActivityForResult(menuItem.getIntent(), resultCode);
}
});
binding.navigationMenu.setAdapter(menuAdapter);
} else {
- menuAdapter.updateAccount(nextAccount);
+ menuAdapter.updateAccount(this, nextAccount);
}
});
}
@@ -353,36 +392,14 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
setSupportActionBar(binding.activityNotesListView.toolbar);
activityBinding.homeToolbar.setOnClickListener((v) -> {
if (activityBinding.toolbar.getVisibility() == GONE) {
- updateToolbars(false);
+ updateToolbars(true);
}
});
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);
+ updateToolbars(false);
return true;
}
return false;
@@ -438,12 +455,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
});
swipeRefreshLayout.setOnRefreshListener(() -> {
- Log.i(TAG, "Clearing Glide memory cache");
- Glide.get(this).clearMemory();
- new Thread(() -> {
- Log.i(TAG, "Clearing Glide disk cache");
- Glide.get(getApplicationContext()).clearDiskCache();
- }, "CLEAR_GLIDE_CACHE").start();
+ CustomAppGlideModule.clearCache(this);
final LiveData<Account> syncLiveData = mainViewModel.getCurrentAccount();
final Observer<Account> syncObserver = currentAccount -> {
syncLiveData.removeObservers(this);
@@ -586,7 +598,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
@Override
public boolean onSupportNavigateUp() {
if (activityBinding.toolbar.getVisibility() == VISIBLE) {
- updateToolbars(true);
+ updateToolbars(false);
return true;
} else {
return super.onSupportNavigateUp();
@@ -638,33 +650,37 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
- case create_note_cmd: {
+ case REQUEST_CODE_CREATE_NOTE: {
listView.scrollToPosition(0);
break;
}
- case SERVER_SETTINGS: {
+ case REQUEST_CODE_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
- ActivityCompat.recreate(this);
+ if (RESULT_OK == resultCode) {
+ ActivityCompat.recreate(this);
+ return;
+ }
break;
}
default: {
try {
AccountImporter.onActivityResult(requestCode, resultCode, data, this, (ssoAccount) -> {
CapabilitiesWorker.update(this);
- new Thread(() -> {
+ executor.submit(() -> {
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);
- mainViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, new IResponseCallback<Account>() {
+ final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null, ApiProvider.getInstance());
+ final String displayName = CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance());
+ mainViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback<Account>() {
@Override
public void onSuccess(Account result) {
- new Thread(() -> {
+ executor.submit(() -> {
Log.i(TAG, capabilities.toString());
final Account a = mainViewModel.getLocalAccountByAccountName(ssoAccount.name);
runOnUiThread(() -> mainViewModel.postCurrentAccount(a));
- }).start();
+ });
}
@Override
@@ -673,7 +689,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
}
});
} catch (Throwable e) {
- ApiProvider.invalidateAPICache(ssoAccount);
+ ApiProvider.getInstance().invalidateAPICache(ssoAccount);
// Happens when importing an already existing account the second time
if (e instanceof TokenMismatchException && mainViewModel.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.");
@@ -682,7 +698,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
// 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 if (e instanceof UnknownErrorException && e.getMessage().contains("No address associated with hostname")) {
+ } else if (e instanceof UnknownErrorException && e.getMessage() != null && e.getMessage().contains("No address associated with hostname")) {
// https://github.com/stefan-niedermann/nextcloud-notes/issues/1014
runOnUiThread(() -> Snackbar.make(coordinatorLayout, R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account, Snackbar.LENGTH_LONG).show());
} else {
@@ -693,7 +709,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
});
}
}
- }).start();
+ });
});
} catch (AccountImportCancelledException e) {
Log.i(TAG, "AccountImport has been cancelled.");
@@ -706,10 +722,9 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
public void onNoteClick(int position, View v) {
boolean hasCheckedItems = tracker.getSelection().size() > 0;
if (!hasCheckedItems) {
- Note note = (Note) adapter.getItem(position);
- Intent intent = new Intent(getApplicationContext(), EditNoteActivity.class);
- intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId());
- startActivityForResult(intent, show_single_note_cmd);
+ final Note note = (Note) adapter.getItem(position);
+ startActivity(new Intent(getApplicationContext(), EditNoteActivity.class)
+ .putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()));
}
}
@@ -722,18 +737,24 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
@Override
public void onBackPressed() {
if (activityBinding.toolbar.getVisibility() == VISIBLE) {
- updateToolbars(true);
+ updateToolbars(false);
+ } else if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START);
} else {
super.onBackPressed();
}
}
- private void updateToolbars(boolean disableSearch) {
- activityBinding.homeToolbar.setVisibility(disableSearch ? VISIBLE : GONE);
- activityBinding.toolbar.setVisibility(disableSearch ? GONE : VISIBLE);
- activityBinding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator(activityBinding.appBar.getContext(),
- disableSearch ? R.animator.appbar_elevation_off : R.animator.appbar_elevation_on));
- if (disableSearch) {
+ private void updateToolbars(boolean enableSearch) {
+ activityBinding.homeToolbar.setVisibility(enableSearch ? GONE : VISIBLE);
+ activityBinding.toolbar.setVisibility(enableSearch ? VISIBLE : GONE);
+ activityBinding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator(activityBinding.appBar.getContext(), enableSearch
+ ? R.animator.appbar_elevation_on
+ : R.animator.appbar_elevation_off));
+ if (enableSearch) {
+ activityBinding.searchView.setIconified(false);
+ fabCreate.show();
+ } else {
activityBinding.searchView.setQuery(null, true);
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
index ec0e71c7..92790bc4 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
@@ -20,18 +20,23 @@ import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.stream.Collectors;
+import it.niedermann.owncloud.notes.BuildConfig;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.exception.IntendedOfflineException;
import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+import it.niedermann.owncloud.notes.persistence.ApiProvider;
import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
import it.niedermann.owncloud.notes.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
@@ -64,6 +69,8 @@ public class MainViewModel extends AndroidViewModel {
private static final String TAG = MainViewModel.class.getSimpleName();
+ private final ExecutorService executor = Executors.newCachedThreadPool();
+
private final SavedStateHandle state;
private static final String KEY_CURRENT_ACCOUNT = "currentAccount";
@@ -214,7 +221,7 @@ public class MainViewModel extends AndroidViewModel {
} else {
Log.v(TAG, "[getNotesListLiveData] - selectedCategory: " + selectedCategory);
return switchMap(getSearchTerm(), searchTerm -> {
- Log.v(TAG, "[getNotesListLiveData] - searchTerm: " + searchTerm);
+ Log.v(TAG, "[getNotesListLiveData] - searchTerm: " + (BuildConfig.DEBUG ? "******" : searchTerm));
return switchMap(getCategorySortingMethodOfSelectedCategory(), sortingMethod -> {
final long accountId = currentAccount.getId();
final String searchQueryOrWildcard = searchTerm == null ? "%" : "%" + searchTerm.trim() + "%";
@@ -388,32 +395,35 @@ public class MainViewModel extends AndroidViewModel {
* Updates the network status if necessary and pulls the latest {@link Capabilities} of the given {@param localAccount}
*/
public void synchronizeCapabilities(@NonNull Account localAccount, @NonNull IResponseCallback<Void> callback) {
- new Thread(() -> {
+ executor.submit(() -> {
if (!repo.isSyncPossible()) {
repo.updateNetworkStatus();
}
if (repo.isSyncPossible()) {
try {
- final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplication(), AccountImporter.getSingleSignOnAccount(getApplication(), localAccount.getAccountName()), localAccount.getCapabilitiesETag());
- repo.updateCapabilitiesETag(localAccount.getId(), capabilities.getETag());
- repo.updateBrand(localAccount.getId(), capabilities.getColor(), capabilities.getTextColor());
- localAccount.setColor(capabilities.getColor());
- localAccount.setTextColor(capabilities.getTextColor());
- BrandingUtil.saveBrandColors(getApplication(), localAccount.getColor(), localAccount.getTextColor());
- repo.updateApiVersion(localAccount.getId(), capabilities.getApiVersion());
- callback.onSuccess(null);
+ final SingleSignOnAccount ssoAccount = AccountImporter.getSingleSignOnAccount(getApplication(), localAccount.getAccountName());
+ try {
+ final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplication(), ssoAccount, localAccount.getCapabilitiesETag(), ApiProvider.getInstance());
+ repo.updateCapabilitiesETag(localAccount.getId(), capabilities.getETag());
+ repo.updateBrand(localAccount.getId(), capabilities.getColor(), capabilities.getTextColor());
+ localAccount.setColor(capabilities.getColor());
+ localAccount.setTextColor(capabilities.getTextColor());
+ BrandingUtil.saveBrandColors(getApplication(), localAccount.getColor(), localAccount.getTextColor());
+ repo.updateApiVersion(localAccount.getId(), capabilities.getApiVersion());
+ callback.onSuccess(null);
+ } catch (Throwable t) {
+ if (t.getClass() == NextcloudHttpRequestFailedException.class || t instanceof NextcloudHttpRequestFailedException) {
+ if (((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_NOT_MODIFIED) {
+ Log.d(TAG, "Server returned HTTP Status Code " + ((NextcloudHttpRequestFailedException) t).getStatusCode() + " - Capabilities not modified.");
+ callback.onSuccess(null);
+ return;
+ }
+ }
+ callback.onError(t);
+ }
} catch (NextcloudFilesAppAccountNotFoundException e) {
repo.deleteAccount(localAccount);
callback.onError(e);
- } catch (Throwable t) {
- if (t.getClass() == NextcloudHttpRequestFailedException.class || t instanceof NextcloudHttpRequestFailedException) {
- if (((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_NOT_MODIFIED) {
- Log.d(TAG, "Server returned HTTP Status Code " + ((NextcloudHttpRequestFailedException) t).getStatusCode() + " - Capabilities not modified.");
- callback.onSuccess(null);
- return;
- }
- }
- callback.onError(t);
}
} else {
if (repo.isNetworkConnected() && repo.isSyncOnlyOnWifi()) {
@@ -422,14 +432,14 @@ public class MainViewModel extends AndroidViewModel {
callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected."));
}
}
- }, "SYNC_CAPABILITIES").start();
+ }, "SYNC_CAPABILITIES");
}
/**
* Updates the network status if necessary and pulls the latest notes of the given {@param localAccount}
*/
public void synchronizeNotes(@NonNull Account currentAccount, @NonNull IResponseCallback<Void> callback) {
- new Thread(() -> {
+ executor.submit(() -> {
Log.v(TAG, "[synchronize] - currentAccount: " + currentAccount.getAccountName());
if (!repo.isSyncPossible()) {
repo.updateNetworkStatus();
@@ -444,7 +454,7 @@ public class MainViewModel extends AndroidViewModel {
callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected."));
}
}
- }, "SYNC_NOTES").start();
+ }, "SYNC_NOTES");
}
public LiveData<Boolean> getSyncStatus() {
@@ -483,9 +493,9 @@ public class MainViewModel extends AndroidViewModel {
});
}
- public LiveData<Note> moveNoteToAnotherAccount(Account account, Long noteId) {
+ public LiveData<Note> moveNoteToAnotherAccount(Account account, long noteId) {
return switchMap(repo.getNoteById$(noteId), (note) -> {
- Log.v(TAG, "[moveNoteToAnotherAccount] - note: " + note);
+ Log.v(TAG, "[moveNoteToAnotherAccount] - note: " + (BuildConfig.DEBUG ? note : note.getTitle()));
return repo.moveNoteToAnotherAccount(account, note);
});
}
@@ -528,8 +538,8 @@ public class MainViewModel extends AndroidViewModel {
});
}
- public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @NonNull IResponseCallback<Account> callback) {
- repo.addAccount(url, username, accountName, capabilities, callback);
+ public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) {
+ repo.addAccount(url, username, accountName, capabilities, displayName, callback);
}
public LiveData<Note> getFullNote$(long id) {
@@ -548,12 +558,12 @@ public class MainViewModel extends AndroidViewModel {
} else {
Log.v(TAG, "[getNote] - currentAccount: " + currentAccount.getAccountName());
final MutableLiveData<List<Note>> notes = new MutableLiveData<>();
- new Thread(() -> notes.postValue(
+ executor.submit(() -> notes.postValue(
ids
.stream()
.map(repo::getNoteById)
.collect(Collectors.toList())
- )).start();
+ ));
return notes;
}
});
@@ -584,6 +594,10 @@ public class MainViewModel extends AndroidViewModel {
repo.createOrUpdateSingleNoteWidgetData(data);
}
+ public List<Note> getLocalModifiedNotes(long accountId) {
+ return repo.getLocalModifiedNotes(accountId);
+ }
+
public LiveData<Integer> getAccountsCount() {
return repo.countAccounts$();
}
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
index fa9bb879..cb51cdfc 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java
@@ -21,6 +21,8 @@ import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment;
@@ -32,6 +34,7 @@ import it.niedermann.owncloud.notes.shared.util.ShareUtil;
public class MultiSelectedActionModeCallback implements Callback {
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
@ColorInt
private final int colorAccent;
@NonNull
@@ -125,11 +128,9 @@ public class MultiSelectedActionModeCallback implements Callback {
final LiveData<Account> currentAccount$ = mainViewModel.getCurrentAccount();
currentAccount$.observe(lifecycleOwner, account -> {
currentAccount$.removeObservers(lifecycleOwner);
- new Thread(() -> {
- AccountPickerDialogFragment
- .newInstance(new ArrayList<>(mainViewModel.getAccounts()), account.getId())
- .show(fragmentManager, AccountPickerDialogFragment.class.getSimpleName());
- }).start();
+ executor.submit(() -> AccountPickerDialogFragment
+ .newInstance(new ArrayList<>(mainViewModel.getAccounts()), account.getId())
+ .show(fragmentManager, AccountPickerDialogFragment.class.getSimpleName()));
});
return true;
} else if (itemId == R.id.menu_share) {
@@ -139,7 +140,7 @@ public class MultiSelectedActionModeCallback implements Callback {
}
tracker.clearSelection();
- new Thread(() -> {
+ executor.submit(() -> {
if (selection.size() == 1) {
final Note note = mainViewModel.getFullNote(selection.get(0));
ShareUtil.openShareDialog(context, note.getTitle(), note.getContent());
@@ -148,7 +149,7 @@ public class MultiSelectedActionModeCallback implements Callback {
context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()),
mainViewModel.collectNoteContents(selection));
}
- }).start();
+ });
return true;
} else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it
final LiveData<Account> accountLiveData = mainViewModel.getCurrentAccount();
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
index 2cfcee88..01743638 100644
--- 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
@@ -20,6 +20,9 @@ public class GridItemDecoration extends SectionItemDecoration {
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);
+ if(spanCount < 1) {
+ throw new IllegalArgumentException("Requires at least one span");
+ }
this.spanCount = spanCount;
this.adapter = adapter;
this.gutter = gutter;
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java
index 82d88d11..24c1a44e 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java
@@ -2,15 +2,18 @@ package it.niedermann.owncloud.notes.main.menu;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
+import com.nextcloud.android.sso.Constants;
+import com.nextcloud.android.sso.helper.VersionCheckHelper;
+
import it.niedermann.owncloud.notes.FormattingHelpActivity;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.about.AboutActivity;
@@ -20,21 +23,16 @@ import it.niedermann.owncloud.notes.preferences.PreferencesActivity;
public class MenuAdapter extends RecyclerView.Adapter<MenuViewHolder> {
- public static final int SERVER_SETTINGS = 2;
-
@NonNull
private final MenuItem[] menuItems;
@NonNull
private final Consumer<MenuItem> onClick;
- @NonNull
- private final Context context;
- public MenuAdapter(@NonNull Context context, @NonNull Account account, @NonNull Consumer<MenuItem> onClick) {
- this.context = context;
+ public MenuAdapter(@NonNull Context context, @NonNull Account account, int settingsRequestCode, @NonNull Consumer<MenuItem> onClick) {
this.menuItems = new MenuItem[]{
new MenuItem(new Intent(context, FormattingHelpActivity.class), R.string.action_formatting_help, R.drawable.ic_baseline_help_outline_24),
- new MenuItem(generateTrashbinIntent(account), R.string.action_trashbin, R.drawable.ic_delete_grey600_24dp),
- new MenuItem(new Intent(context, PreferencesActivity.class), SERVER_SETTINGS, R.string.action_settings, R.drawable.ic_settings_grey600_24dp),
+ new MenuItem(generateTrashbinIntent(context, account), R.string.action_trashbin, R.drawable.ic_delete_grey600_24dp),
+ new MenuItem(new Intent(context, PreferencesActivity.class), settingsRequestCode, R.string.action_settings, R.drawable.ic_settings_grey600_24dp),
new MenuItem(new Intent(context, AboutActivity.class), R.string.simple_about, R.drawable.ic_info_outline_grey600_24dp)
};
this.onClick = onClick;
@@ -54,11 +52,11 @@ public class MenuAdapter extends RecyclerView.Adapter<MenuViewHolder> {
@Override
public void onBindViewHolder(@NonNull MenuViewHolder holder, int position) {
- holder.bind(menuItems[position], onClick, ContextCompat.getColor(context, R.color.fg_default));
+ holder.bind(menuItems[position], onClick);
}
- public void updateAccount(@NonNull Account account) {
- menuItems[1].setIntent(new Intent(generateTrashbinIntent(account)));
+ public void updateAccount(@NonNull Context context, @NonNull Account account) {
+ menuItems[1].setIntent(new Intent(generateTrashbinIntent(context, account)));
}
@Override
@@ -67,7 +65,38 @@ public class MenuAdapter extends RecyclerView.Adapter<MenuViewHolder> {
}
@NonNull
- private static Intent generateTrashbinIntent(@NonNull Account account) {
+ private static Intent generateTrashbinIntent(@NonNull Context context, @NonNull Account account) {
+ // https://github.com/nextcloud/android/pull/8405#issuecomment-852966877
+ final int minVersionCode = 30170090;
+ try {
+ if (VersionCheckHelper.getNextcloudFilesVersionCode(context, true) > minVersionCode) {
+ return generateTrashbinAppIntent(context, account, true);
+ } else if (VersionCheckHelper.getNextcloudFilesVersionCode(context, false) > minVersionCode) {
+ return generateTrashbinAppIntent(context, account, false);
+ } else {
+ // Files app is too old to be able to switch the account when launching the TrashbinActivity
+ return generateTrashbinWebIntent(account);
+ }
+ } catch (PackageManager.NameNotFoundException | SecurityException e) {
+ e.printStackTrace();
+ return generateTrashbinWebIntent(account);
+ }
+ }
+
+ private static Intent generateTrashbinAppIntent(@NonNull Context context, @NonNull Account account, boolean prod) throws PackageManager.NameNotFoundException {
+ final PackageManager packageManager = context.getPackageManager();
+ final String packageName = prod ? Constants.PACKAGE_NAME_PROD : Constants.PACKAGE_NAME_DEV;
+ final Intent intent = new Intent();
+ intent.setClassName(packageName, "com.owncloud.android.ui.trashbin.TrashbinActivity");
+ if (packageManager.resolveActivity(intent, 0) != null) {
+ return intent
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(Intent.EXTRA_USER, account.getAccountName());
+ }
+ throw new PackageManager.NameNotFoundException("Could not resolve target activity.");
+ }
+
+ private static Intent generateTrashbinWebIntent(@NonNull Account account) {
return new Intent(Intent.ACTION_VIEW, Uri.parse(account.getUrl() + "/index.php/apps/files/?dir=/&view=trashbin"));
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java
index 0e08e279..d5a2f608 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java
@@ -2,12 +2,12 @@ package it.niedermann.owncloud.notes.main.menu;
import android.content.Context;
-import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
+import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ItemNavigationBinding;
import static android.view.View.GONE;
@@ -21,10 +21,10 @@ public class MenuViewHolder extends RecyclerView.ViewHolder {
this.binding = binding;
}
- public void bind(@NonNull MenuItem menuItem, @NonNull Consumer<MenuItem> onClick, @ColorInt int textColor) {
+ public void bind(@NonNull MenuItem menuItem, @NonNull Consumer<MenuItem> onClick) {
@NonNull Context context = itemView.getContext();
binding.navigationItemLabel.setText(context.getString(menuItem.getLabelResource()));
- binding.navigationItemLabel.setTextColor(textColor);
+ binding.navigationItemLabel.setTextColor(binding.getRoot().getResources().getColor(R.color.fg_default));
binding.navigationItemIcon.setImageDrawable(ContextCompat.getDrawable(context, menuItem.getDrawableResource()));
binding.navigationItemCount.setVisibility(GONE);
binding.getRoot().setOnClickListener((v) -> onClick.accept(menuItem));
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java
index 2ee45cf8..ce88f2c2 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java
@@ -13,8 +13,9 @@ import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
-import it.niedermann.owncloud.notes.persistence.NotesDatabase;
import it.niedermann.owncloud.notes.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
@@ -23,7 +24,7 @@ import static androidx.lifecycle.Transformations.distinctUntilChanged;
public class ManageAccountsViewModel extends AndroidViewModel {
- private static final String TAG = ManageAccountsViewModel.class.getSimpleName();
+ private final ExecutorService executor = Executors.newCachedThreadPool();
@NonNull
private final NotesRepository repo;
@@ -46,7 +47,7 @@ public class ManageAccountsViewModel extends AndroidViewModel {
}
public void deleteAccount(@NonNull Account account, @NonNull Context context) {
- new Thread(() -> {
+ executor.submit(() -> {
final List<Account> accounts = repo.getAccounts();
for (int i = 0; i < accounts.size(); i++) {
if (accounts.get(i).getId() == account.getId()) {
@@ -61,7 +62,7 @@ public class ManageAccountsViewModel extends AndroidViewModel {
break;
}
}
- }).start();
+ });
}
public void selectAccount(@Nullable Account account, @NonNull Context context) {
@@ -69,6 +70,6 @@ public class ManageAccountsViewModel extends AndroidViewModel {
}
public void countUnsynchronizedNotes(long accountId, @NonNull IResponseCallback<Long> callback) {
- new Thread(() -> callback.onSuccess(repo.countUnsynchronizedNotes(accountId))).start();
+ executor.submit(() -> callback.onSuccess(repo.countUnsynchronizedNotes(accountId)));
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
index b1bca215..6b851ddb 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
@@ -16,8 +16,8 @@ import com.nextcloud.android.sso.api.NextcloudAPI;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.util.Calendar;
-import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import it.niedermann.owncloud.notes.persistence.sync.CapabilitiesDeserializer;
import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
@@ -36,17 +36,27 @@ public class ApiProvider {
private static final String TAG = ApiProvider.class.getSimpleName();
+ private static final ApiProvider INSTANCE = new ApiProvider();
+
private static final String API_ENDPOINT_OCS = "/ocs/v2.php/cloud/";
- private static final Map<String, NextcloudAPI> API_CACHE = new HashMap<>();
+ private static final Map<String, NextcloudAPI> API_CACHE = new ConcurrentHashMap<>();
+
+ private static final Map<String, OcsAPI> API_CACHE_OCS = new ConcurrentHashMap<>();
+ private static final Map<String, NotesAPI> API_CACHE_NOTES = new ConcurrentHashMap<>();
- private static final Map<String, OcsAPI> API_CACHE_OCS = new HashMap<>();
- private static final Map<String, NotesAPI> API_CACHE_NOTES = new HashMap<>();
+ public static ApiProvider getInstance() {
+ return INSTANCE;
+ }
+
+ private ApiProvider() {
+ // Singleton
+ }
/**
* An {@link OcsAPI} currently shares the {@link Gson} configuration with the {@link NotesAPI} and therefore divides all {@link Calendar} milliseconds by 1000 while serializing and multiplies values by 1000 during deserialization.
*/
- public static synchronized OcsAPI getOcsAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
+ public synchronized OcsAPI getOcsAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
if (API_CACHE_OCS.containsKey(ssoAccount.name)) {
return API_CACHE_OCS.get(ssoAccount.name);
}
@@ -58,7 +68,7 @@ public class ApiProvider {
/**
* In case the {@param preferredApiVersion} changes, call {@link #invalidateAPICache(SingleSignOnAccount)} or {@link #invalidateAPICache()} to make sure that this call returns a {@link NotesAPI} that uses the correct compatibility layer.
*/
- public static synchronized NotesAPI getNotesAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable ApiVersion preferredApiVersion) {
+ public synchronized NotesAPI getNotesAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable ApiVersion preferredApiVersion) {
if (API_CACHE_NOTES.containsKey(ssoAccount.name)) {
return API_CACHE_NOTES.get(ssoAccount.name);
}
@@ -67,7 +77,7 @@ public class ApiProvider {
return notesAPI;
}
- private static synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
+ private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
if (API_CACHE.containsKey(ssoAccount.name)) {
return API_CACHE.get(ssoAccount.name);
} else {
@@ -104,7 +114,7 @@ public class ApiProvider {
*
* @param ssoAccount the ssoAccount for which the API cache should be cleared.
*/
- public static synchronized void invalidateAPICache(@NonNull SingleSignOnAccount ssoAccount) {
+ public synchronized void invalidateAPICache(@NonNull SingleSignOnAccount ssoAccount) {
Log.v(TAG, "Invalidating API cache for " + ssoAccount.name);
if (API_CACHE.containsKey(ssoAccount.name)) {
final NextcloudAPI nextcloudAPI = API_CACHE.get(ssoAccount.name);
@@ -120,7 +130,7 @@ public class ApiProvider {
/**
* Invalidates the whole API cache for all accounts
*/
- public static synchronized void invalidateAPICache() {
+ public synchronized void invalidateAPICache() {
for (String key : API_CACHE.keySet()) {
Log.v(TAG, "Invalidating API cache for " + key);
if (API_CACHE.containsKey(key)) {
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 8afc64b8..33e42382 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
@@ -14,6 +14,9 @@ import java.util.Map;
import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import it.niedermann.owncloud.notes.shared.model.OcsResponse;
+import it.niedermann.owncloud.notes.shared.model.OcsUser;
+import retrofit2.Response;
@WorkerThread
public class CapabilitiesClient {
@@ -22,11 +25,12 @@ public class CapabilitiesClient {
private static final String HEADER_KEY_ETAG = "ETag";
- public static Capabilities getCapabilities(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable String lastETag) throws Throwable {
- final OcsAPI ocsAPI = ApiProvider.getOcsAPI(context, ssoAccount);
+ @WorkerThread
+ public static Capabilities getCapabilities(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable String lastETag, @NonNull ApiProvider apiProvider) throws Throwable {
+ final OcsAPI ocsAPI = apiProvider.getOcsAPI(context, ssoAccount);
try {
- final ParsedResponse<Capabilities> response = ocsAPI.getCapabilities(lastETag).blockingSingle();
- final Capabilities capabilities = response.getResponse();
+ final ParsedResponse<OcsResponse<Capabilities>> response = ocsAPI.getCapabilities(lastETag).blockingSingle();
+ final Capabilities capabilities = response.getResponse().ocs.data;
final Map<String, String> headers = response.getHeaders();
if (headers != null) {
capabilities.setETag(headers.get(HEADER_KEY_ETAG));
@@ -36,11 +40,33 @@ public class CapabilitiesClient {
return capabilities;
} catch (RuntimeException e) {
final Throwable cause = e.getCause();
- if(cause != null) {
+ if (cause != null) {
throw cause;
} else {
throw e;
}
}
}
+
+ @WorkerThread
+ @Nullable
+ public static String getDisplayName(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @NonNull ApiProvider apiProvider) {
+ final OcsAPI ocsAPI = apiProvider.getOcsAPI(context, ssoAccount);
+ try {
+ final Response<OcsResponse<OcsUser>> userResponse = ocsAPI.getUser(ssoAccount.userId).execute();
+ if (userResponse.isSuccessful()) {
+ final OcsResponse<OcsUser> ocsResponse = userResponse.body();
+ if (ocsResponse != null) {
+ return ocsResponse.ocs.data.displayName;
+ } else {
+ Log.w(TAG, "ocsResponse is null");
+ }
+ } else {
+ Log.w(TAG, "Fetching user was not successful.");
+ }
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ return null;
+ }
}
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 1dff46cf..4f8852e7 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
@@ -47,17 +47,18 @@ public class CapabilitiesWorker extends Worker {
try {
final SingleSignOnAccount ssoAccount = AccountImporter.getSingleSignOnAccount(getApplicationContext(), account.getAccountName());
Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name);
- final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, account.getCapabilitiesETag());
+ final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, account.getCapabilitiesETag(), ApiProvider.getInstance());
repo.updateCapabilitiesETag(account.getId(), capabilities.getETag());
repo.updateBrand(account.getId(), capabilities.getColor(), capabilities.getTextColor());
repo.updateApiVersion(account.getId(), capabilities.getApiVersion());
Log.i(TAG, capabilities.toString());
+ repo.updateDisplayName(account.getId(), CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance()));
} catch (Throwable e) {
if (e instanceof NextcloudHttpRequestFailedException) {
if (((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
Log.i(TAG, "Capabilities not modified.");
return Result.success();
- } else if(((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) {
+ } else if (((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) {
Log.i(TAG, "Server is in maintenance mode.");
return Result.success();
}
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 4f4aaa78..5cc50641 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
@@ -32,6 +32,8 @@ import it.niedermann.owncloud.notes.persistence.migration.Migration_17_18;
import it.niedermann.owncloud.notes.persistence.migration.Migration_18_19;
import it.niedermann.owncloud.notes.persistence.migration.Migration_19_20;
import it.niedermann.owncloud.notes.persistence.migration.Migration_20_21;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_21_22;
+import it.niedermann.owncloud.notes.persistence.migration.Migration_22_23;
import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10;
@Database(
@@ -41,7 +43,7 @@ import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10;
CategoryOptions.class,
SingleNoteWidgetData.class,
NotesListWidgetData.class
- }, version = 21
+ }, version = 23
)
@TypeConverters({Converters.class})
public abstract class NotesDatabase extends RoomDatabase {
@@ -74,7 +76,9 @@ public abstract class NotesDatabase extends RoomDatabase {
new Migration_17_18(),
new Migration_18_19(context),
new Migration_19_20(context),
- new Migration_20_21()
+ new Migration_20_21(),
+ new Migration_21_22(context),
+ new Migration_22_23()
)
.fallbackToDestructiveMigrationOnDowngrade()
.fallbackToDestructiveMigration()
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
index afd6145d..f36fd72a 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
@@ -30,11 +30,9 @@ import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
-import org.json.JSONArray;
-import org.json.JSONException;
-
import java.util.ArrayList;
import java.util.Calendar;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -44,6 +42,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData;
+import it.niedermann.owncloud.notes.BuildConfig;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.edit.EditNoteActivity;
import it.niedermann.owncloud.notes.persistence.entity.Account;
@@ -62,6 +61,7 @@ import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
import it.niedermann.owncloud.notes.shared.model.NotesSettings;
import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
+import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
import it.niedermann.owncloud.notes.shared.util.SSOUtil;
import retrofit2.Call;
@@ -70,7 +70,6 @@ import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.O;
import static androidx.lifecycle.Transformations.distinctUntilChanged;
import static androidx.lifecycle.Transformations.map;
-import static androidx.lifecycle.Transformations.switchMap;
import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT;
import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt;
import static it.niedermann.owncloud.notes.widget.notelist.NoteListWidget.updateNoteListWidgets;
@@ -84,6 +83,7 @@ public class NotesRepository {
private static NotesRepository instance;
+ private final ApiProvider apiProvider;
private final ExecutorService executor;
private final Context context;
private final NotesDatabase db;
@@ -137,22 +137,23 @@ public class NotesRepository {
public static synchronized NotesRepository getInstance(@NonNull Context context) {
if (instance == null) {
- instance = new NotesRepository(context, NotesDatabase.getInstance(context.getApplicationContext()), Executors.newCachedThreadPool());
+ instance = new NotesRepository(context, NotesDatabase.getInstance(context.getApplicationContext()), Executors.newCachedThreadPool(), ApiProvider.getInstance());
}
return instance;
}
- private NotesRepository(@NonNull final Context context, @NonNull final NotesDatabase db, @NonNull final ExecutorService executor) {
+ private NotesRepository(@NonNull final Context context, @NonNull final NotesDatabase db, @NonNull final ExecutorService executor, @NonNull ApiProvider apiProvider) {
this.context = context.getApplicationContext();
this.db = db;
this.executor = executor;
+ this.apiProvider = apiProvider;
this.defaultNonEmptyTitle = NoteUtil.generateNonEmptyNoteTitle("", this.context);
this.syncOnlyOnWifiKey = context.getApplicationContext().getResources().getString(R.string.pref_key_wifi_only);
// Registers BroadcastReceiver to track network connection changes.
- context.getApplicationContext().registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+ this.context.registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context.getApplicationContext());
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context);
prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
@@ -163,8 +164,8 @@ public class NotesRepository {
// Accounts
@AnyThread
- public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @NonNull IResponseCallback<Account> callback) {
- final Account createdAccount = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account(url, username, accountName, capabilities)));
+ public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) {
+ final Account createdAccount = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account(url, username, accountName, displayName, capabilities)));
if (createdAccount == null) {
callback.onError(new Exception("Could not read created account."));
} else {
@@ -180,10 +181,10 @@ public class NotesRepository {
@WorkerThread
public void deleteAccount(@NonNull Account account) {
try {
- ApiProvider.invalidateAPICache(AccountImporter.getSingleSignOnAccount(context, account.getAccountName()));
+ apiProvider.invalidateAPICache(AccountImporter.getSingleSignOnAccount(context, account.getAccountName()));
} catch (NextcloudFilesAppAccountNotFoundException e) {
e.printStackTrace();
- ApiProvider.invalidateAPICache();
+ apiProvider.invalidateAPICache();
}
db.getAccountDao().deleteAccount(account);
@@ -403,10 +404,12 @@ public class NotesRepository {
@MainThread
public LiveData<Note> moveNoteToAnotherAccount(Account account, @NonNull Note note) {
- return switchMap(db.getNoteDao().getContent$(note.getId()), (content) -> {
- final Note fullNote = new Note(null, note.getModified(), note.getTitle(), content, note.getCategory(), note.getFavorite(), null);
- deleteNoteAndSync(account, note.getId());
- return addNoteAndSync(account, fullNote);
+ final Note fullNote = new Note(null, note.getModified(), note.getTitle(), note.getContent(), note.getCategory(), note.getFavorite(), null);
+ deleteNoteAndSync(account, note.getId());
+ return map(addNoteAndSync(account, fullNote), (createdNote) -> {
+ db.getNoteDao().updateStatus(createdNote.getId(), DBStatus.LOCAL_EDITED);
+ createdNote.setStatus(DBStatus.LOCAL_EDITED);
+ return createdNote;
});
}
@@ -460,23 +463,27 @@ public class NotesRepository {
* @return changed {@link Note} if differs from database, otherwise the old {@link Note}.
*/
@WorkerThread
- public Note updateNoteAndSync(Account localAccount, @NonNull Note oldNote, @Nullable String newContent, @Nullable String newTitle, @Nullable ISyncCallback callback) {
+ public Note updateNoteAndSync(@NonNull Account localAccount, @NonNull Note oldNote, @Nullable String newContent, @Nullable String newTitle, @Nullable ISyncCallback callback) {
final Note newNote;
+ // Re-read the up to date remoteId from the database because the UI might not have the state after synchronization yet
+ // https://github.com/stefan-niedermann/nextcloud-notes/issues/1198
+ @Nullable final Long remoteId = db.getNoteDao().getRemoteId(oldNote.getId());
if (newContent == null) {
- newNote = new Note(oldNote.getId(), oldNote.getRemoteId(), oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), oldNote.getExcerpt(), oldNote.getScrollY());
+ newNote = new Note(oldNote.getId(), remoteId, oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), oldNote.getExcerpt(), oldNote.getScrollY());
} else {
final String title;
if (newTitle != null) {
title = newTitle;
} else {
- if ((oldNote.getRemoteId() == null || localAccount.getPreferredApiVersion() == null || localAccount.getPreferredApiVersion().compareTo(ApiVersion.API_VERSION_1_0) < 0) &&
+ final ApiVersion preferredApiVersion = ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion());
+ if ((remoteId == null || preferredApiVersion == null || preferredApiVersion.compareTo(ApiVersion.API_VERSION_1_0) < 0) &&
(defaultNonEmptyTitle.equals(oldNote.getTitle()))) {
title = NoteUtil.generateNonEmptyNoteTitle(newContent, context);
} else {
title = oldNote.getTitle();
}
}
- newNote = new Note(oldNote.getId(), oldNote.getRemoteId(), Calendar.getInstance(), title, newContent, oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), generateNoteExcerpt(newContent, title), oldNote.getScrollY());
+ newNote = new Note(oldNote.getId(), remoteId, Calendar.getInstance(), title, newContent, oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), generateNoteExcerpt(newContent, title), oldNote.getScrollY());
}
int rows = db.getNoteDao().updateNote(newNote);
// if data was changed, set new status and schedule sync (with callback); otherwise invoke callback directly.
@@ -540,25 +547,25 @@ public class NotesRepository {
private void updateDynamicShortcuts(long accountId) {
executor.submit(() -> {
if (SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
- ShortcutManager shortcutManager = context.getApplicationContext().getSystemService(ShortcutManager.class);
+ ShortcutManager shortcutManager = this.context.getSystemService(ShortcutManager.class);
if (shortcutManager != null) {
if (!shortcutManager.isRateLimitingActive()) {
List<ShortcutInfo> newShortcuts = new ArrayList<>();
for (Note note : db.getNoteDao().getRecentNotes(accountId)) {
if (!TextUtils.isEmpty(note.getTitle())) {
- Intent intent = new Intent(context.getApplicationContext(), EditNoteActivity.class);
+ Intent intent = new Intent(this.context, EditNoteActivity.class);
intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId());
intent.setAction(ACTION_SHORTCUT);
- newShortcuts.add(new ShortcutInfo.Builder(context.getApplicationContext(), note.getId() + "")
+ newShortcuts.add(new ShortcutInfo.Builder(this.context, note.getId() + "")
.setShortLabel(note.getTitle() + "")
- .setIcon(Icon.createWithResource(context.getApplicationContext(), note.getFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp))
+ .setIcon(Icon.createWithResource(this.context, note.getFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp))
.setIntent(intent)
.build());
} else {
// Prevent crash https://github.com/stefan-niedermann/nextcloud-notes/issues/613
- Log.e(TAG, "shortLabel cannot be empty " + note);
+ Log.e(TAG, "shortLabel cannot be empty " + (BuildConfig.DEBUG ? note : note.getTitle()));
}
}
Log.d(TAG, "Update dynamic shortcuts");
@@ -571,40 +578,23 @@ public class NotesRepository {
}
/**
- * @param apiVersion has to be a JSON array as a string <code>["0.2", "1.0", ...]</code>
- * @return whether or not the given {@link ApiVersion} has been written to the database
- * @throws IllegalArgumentException if the apiVersion does not match the expected format
+ * @param raw has to be a JSON array as a string <code>["0.2", "1.0", ...]</code>
*/
- public boolean updateApiVersion(long accountId, @Nullable String apiVersion) throws IllegalArgumentException {
- if (apiVersion != null) {
- try {
- JSONArray apiVersions = new JSONArray(apiVersion);
- for (int i = 0; i < apiVersions.length(); i++) {
- ApiVersion.of(apiVersions.getString(i));
- }
- if (apiVersions.length() > 0) {
- final int updatedRows = db.getAccountDao().updateApiVersion(accountId, apiVersion);
- if (updatedRows == 0) {
- Log.d(TAG, "ApiVersion not updated, because it did not change");
- } else if (updatedRows == 1) {
- Log.i(TAG, "Updated apiVersion to \"" + apiVersion + "\" for accountId = " + accountId);
- ApiProvider.invalidateAPICache();
- } else {
- Log.w(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + apiVersion + "\"");
- }
- return true;
- } else {
- Log.i(TAG, "Given API version is a valid JSON array but does not contain any valid API versions. Do not update database.");
- }
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("API version does contain a non-valid version: " + apiVersion);
- } catch (JSONException e) {
- throw new IllegalArgumentException("API version must contain be a JSON array: " + apiVersion);
+ public void updateApiVersion(long accountId, @Nullable String raw) {
+ final Collection<ApiVersion> apiVersions = ApiVersionUtil.parse(raw);
+ if (apiVersions.size() > 0) {
+ final int updatedRows = db.getAccountDao().updateApiVersion(accountId, ApiVersionUtil.serialize(apiVersions));
+ if (updatedRows == 0) {
+ Log.d(TAG, "ApiVersion not updated, because it did not change");
+ } else if (updatedRows == 1) {
+ Log.i(TAG, "Updated apiVersion to \"" + raw + "\" for accountId = " + accountId);
+ apiProvider.invalidateAPICache();
+ } else {
+ Log.w(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + raw + "\"");
}
} else {
- Log.v(TAG, "Given API version is null. Do not update database");
+ Log.v(TAG, "Could not extract any version from the given String: " + raw);
}
- return false;
}
/**
@@ -709,7 +699,7 @@ public class NotesRepository {
@Override
protected void finalize() throws Throwable {
- context.getApplicationContext().unregisterReceiver(networkReceiver);
+ this.context.unregisterReceiver(networkReceiver);
super.finalize();
}
@@ -783,7 +773,7 @@ public class NotesRepository {
*
* @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server.
*/
- public synchronized void scheduleSync(Account account, boolean onlyLocalChanges) {
+ public synchronized void scheduleSync(@Nullable Account account, boolean onlyLocalChanges) {
if (account == null) {
Log.i(TAG, SingleSignOnAccount.class.getSimpleName() + " is null. Is this a local account?");
} else {
@@ -795,7 +785,7 @@ public class NotesRepository {
syncActive.put(account.getId(), true);
try {
Log.d(TAG, "... starting now");
- final NotesServerSyncTask syncTask = new NotesServerSyncTask(context, this, account, onlyLocalChanges) {
+ final NotesServerSyncTask syncTask = new NotesServerSyncTask(context, this, account, onlyLocalChanges, apiProvider) {
@Override
void onPreExecute() {
syncStatus.postValue(true);
@@ -873,7 +863,7 @@ public class NotesRepository {
public void updateNetworkStatus() {
try {
- final ConnectivityManager connMgr = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ final ConnectivityManager connMgr = (ConnectivityManager) this.context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connMgr == null) {
throw new NetworkErrorException("ConnectivityManager is null");
}
@@ -927,4 +917,8 @@ public class NotesRepository {
public Call<NotesSettings> putServerSettings(@NonNull SingleSignOnAccount ssoAccount, @NonNull NotesSettings settings, @Nullable ApiVersion preferredApiVersion) {
return ApiProvider.getNotesAPI(context, ssoAccount, preferredApiVersion).putSettings(settings);
}
+
+ public void updateDisplayName(long id, @Nullable String displayName) {
+ db.getAccountDao().updateDisplayName(id, displayName);
+ }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java
index 88fb44f3..a7df68bd 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java
@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.api.ParsedResponse;
+import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import com.nextcloud.android.sso.exceptions.TokenMismatchException;
@@ -19,15 +20,16 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
+import it.niedermann.owncloud.notes.BuildConfig;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
+import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil;
import retrofit2.Response;
import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED;
@@ -51,6 +53,8 @@ abstract class NotesServerSyncTask extends Thread {
private NotesAPI notesAPI;
@NonNull
+ private final ApiProvider apiProvider;
+ @NonNull
private final Context context;
@NonNull
private final NotesRepository repo;
@@ -64,13 +68,14 @@ abstract class NotesServerSyncTask extends Thread {
@NonNull
protected final ArrayList<Throwable> exceptions = new ArrayList<>();
- NotesServerSyncTask(@NonNull Context context, @NonNull NotesRepository repo, @NonNull Account localAccount, boolean onlyLocalChanges) throws NextcloudFilesAppAccountNotFoundException {
+ NotesServerSyncTask(@NonNull Context context, @NonNull NotesRepository repo, @NonNull Account localAccount, boolean onlyLocalChanges, @NonNull ApiProvider apiProvider) throws NextcloudFilesAppAccountNotFoundException {
super(TAG);
this.context = context;
this.repo = repo;
this.localAccount = localAccount;
this.ssoAccount = AccountImporter.getSingleSignOnAccount(context, localAccount.getAccountName());
this.onlyLocalChanges = onlyLocalChanges;
+ this.apiProvider = apiProvider;
}
void addCallbacks(Account account, List<ISyncCallback> callbacks) {
@@ -81,7 +86,7 @@ abstract class NotesServerSyncTask extends Thread {
public void run() {
onPreExecute();
- notesAPI = ApiProvider.getNotesAPI(context, ssoAccount, localAccount.getPreferredApiVersion());
+ notesAPI = apiProvider.getNotesAPI(context, ssoAccount, ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion()));
Log.i(TAG, "STARTING SYNCHRONIZATION");
@@ -109,7 +114,7 @@ abstract class NotesServerSyncTask extends Thread {
boolean success = true;
final List<Note> notes = repo.getLocalModifiedNotes(localAccount.getId());
for (Note note : notes) {
- Log.d(TAG, " Process Local Note: " + note);
+ Log.d(TAG, " Process Local Note: " + (BuildConfig.DEBUG ? note : note.getTitle()));
try {
Note remoteNote;
switch (note.getStatus()) {
@@ -120,27 +125,37 @@ abstract class NotesServerSyncTask extends Thread {
final Response<Note> editResponse = notesAPI.editNote(note).execute();
if (editResponse.isSuccessful()) {
remoteNote = editResponse.body();
- } else {
- if (editResponse.code() == HTTP_NOT_FOUND) {
- Log.v(TAG, " ...Note does no longer exist on server → recreate");
- final Response<Note> createResponse = notesAPI.createNote(note).execute();
- if (createResponse.isSuccessful()) {
- remoteNote = createResponse.body();
- } else {
- throw new Exception(createResponse.errorBody().string());
+ if (remoteNote == null) {
+ Log.e(TAG, " ...Tried to edit \"" + note.getTitle() + "\" (#" + note.getId() + ") but the server response was null.");
+ throw new Exception("Server returned null after editing \"" + note.getTitle() + "\" (#" + note.getId() + ")");
+ }
+ } else if (editResponse.code() == HTTP_NOT_FOUND) {
+ Log.v(TAG, " ...Note does no longer exist on server → recreate");
+ final Response<Note> createResponse = notesAPI.createNote(note).execute();
+ if (createResponse.isSuccessful()) {
+ remoteNote = createResponse.body();
+ if (remoteNote == null) {
+ Log.e(TAG, " ...Tried to recreate \"" + note.getTitle() + "\" (#" + note.getId() + ") but the server response was null.");
+ throw new Exception("Server returned null after recreating \"" + note.getTitle() + "\" (#" + note.getId() + ")");
}
} else {
- throw new Exception(editResponse.errorBody().string());
+ throw new Exception(createResponse.message());
}
+ } else {
+ throw new Exception(editResponse.message());
}
} else {
Log.v(TAG, " ...Note does not have a remoteId yet → create");
final Response<Note> createResponse = notesAPI.createNote(note).execute();
if (createResponse.isSuccessful()) {
remoteNote = createResponse.body();
+ if (remoteNote == null) {
+ Log.e(TAG, " ...Tried to create \"" + note.getTitle() + "\" (#" + note.getId() + ") but the server response was null.");
+ throw new Exception("Server returned null after creating \"" + note.getTitle() + "\" (#" + note.getId() + ")");
+ }
repo.updateRemoteId(note.getId(), remoteNote.getRemoteId());
} else {
- throw new Exception(createResponse.errorBody().string());
+ throw new Exception(createResponse.message());
}
}
// Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
@@ -156,7 +171,7 @@ abstract class NotesServerSyncTask extends Thread {
if (deleteResponse.code() == HTTP_NOT_FOUND) {
Log.v(TAG, " ...delete (note has already been deleted remotely)");
} else {
- throw new Exception(deleteResponse.errorBody().string());
+ throw new Exception(deleteResponse.message());
}
}
}
@@ -175,7 +190,7 @@ abstract class NotesServerSyncTask extends Thread {
}
} catch (Exception e) {
if (e instanceof TokenMismatchException) {
- ApiProvider.invalidateAPICache(ssoAccount);
+ apiProvider.invalidateAPICache(ssoAccount);
}
exceptions.add(e);
success = false;
@@ -206,7 +221,7 @@ abstract class NotesServerSyncTask extends Thread {
final Set<Long> remoteIDs = new HashSet<>();
// pull remote changes: update or create each remote note
for (Note remoteNote : remoteNotes) {
- Log.v(TAG, " Process Remote Note: " + remoteNote);
+ Log.v(TAG, " Process Remote Note: " + (BuildConfig.DEBUG ? remoteNote : remoteNote.getTitle()));
remoteIDs.add(remoteNote.getRemoteId());
if (remoteNote.getModified() == null) {
Log.v(TAG, " ... unchanged");
@@ -217,7 +232,7 @@ abstract class NotesServerSyncTask extends Thread {
repo.updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(
localId, remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getCategory(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()));
} else {
- Log.e(TAG, "Tried to update note from server, but local id of note is null. " + remoteNote);
+ Log.e(TAG, "Tried to update note from server, but local id of note is null. " + (BuildConfig.DEBUG ? remoteNote : remoteNote.getTitle()));
}
} else {
Log.v(TAG, " ... create");
@@ -248,19 +263,10 @@ abstract class NotesServerSyncTask extends Thread {
repo.updateETag(localAccount.getId(), localAccount.getETag());
repo.updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis());
-
- String supportedApiVersions = null;
- final String supportedApiVersionsHeader = fetchResponse.getHeaders().get(HEADER_KEY_X_NOTES_API_VERSIONS);
- if (supportedApiVersionsHeader != null) {
- supportedApiVersions = "[" + Objects.requireNonNull(supportedApiVersionsHeader) + "]";
- }
- try {
- if (repo.updateApiVersion(localAccount.getId(), supportedApiVersions)) {
- localAccount.setApiVersion(supportedApiVersions);
- }
- } catch (Exception e) {
- exceptions.add(e);
- }
+ final String newApiVersion = ApiVersionUtil.sanitize(fetchResponse.getHeaders().get(HEADER_KEY_X_NOTES_API_VERSIONS));
+ localAccount.setApiVersion(newApiVersion);
+ repo.updateApiVersion(localAccount.getId(), newApiVersion);
+ Log.d(TAG, "ApiVersion: " + newApiVersion);
return true;
} catch (Throwable t) {
final Throwable cause = t.getCause();
@@ -274,6 +280,8 @@ abstract class NotesServerSyncTask extends Thread {
Log.d(TAG, "Server returned HTTP Status Code " + httpException.getStatusCode() + " - Server is in maintenance mode.");
return true;
}
+ } else if (cause.getClass() == NextcloudApiNotRespondingException.class || cause instanceof NextcloudApiNotRespondingException) {
+ apiProvider.invalidateAPICache(ssoAccount);
}
}
exceptions.add(t);
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 1d4a8bc7..adb7eff0 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
@@ -15,7 +15,6 @@ import androidx.work.WorkerParameters;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
-import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.persistence.entity.Account;
public class SyncWorker extends Worker {
@@ -44,22 +43,20 @@ public class SyncWorker extends Worker {
return Result.success();
}
- public static void update(@NonNull Context context, @NonNull String preferenceValue) {
+ /**
+ * Set up sync work to enabled every 15 minutes or just disabled
+ * https://github.com/stefan-niedermann/nextcloud-notes/issues/1168
+ * @param context the application
+ * @param backgroundSync the toggle result backgroundSync
+ */
+
+ public static void update(@NonNull Context context, boolean backgroundSync) {
deregister(context);
- if (!context.getString(R.string.pref_value_sync_off).equals(preferenceValue)) {
- int repeatInterval = 15;
- TimeUnit unit = TimeUnit.MINUTES;
- if (context.getString(R.string.pref_value_sync_1_hour).equals(preferenceValue)) {
- repeatInterval = 1;
- unit = TimeUnit.HOURS;
- } else if (context.getString(R.string.pref_value_sync_6_hours).equals(preferenceValue)) {
- repeatInterval = 6;
- unit = TimeUnit.HOURS;
- }
- PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(SyncWorker.class, repeatInterval, unit)
+ if (backgroundSync) {
+ PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(SyncWorker.class, 15, TimeUnit.MINUTES)
.setConstraints(constraints).build();
WorkManager.getInstance(context.getApplicationContext()).enqueueUniquePeriodicWork(WORKER_TAG, ExistingPeriodicWorkPolicy.REPLACE, work);
- Log.i(TAG, "Registering worker running each " + repeatInterval + " " + unit);
+ Log.i(TAG, "Registering worker running each " + 15 + " " + TimeUnit.MINUTES);
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java
index 085c0a16..7723e1f0 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java
@@ -1,6 +1,7 @@
package it.niedermann.owncloud.notes.persistence.dao;
import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
@@ -18,10 +19,10 @@ public interface AccountDao {
long insert(Account localAccount);
@Delete
- int deleteAccount(Account localAccount);
+ void deleteAccount(Account localAccount);
- String getAccounts = "SELECT * FROM Account";
- String getAccountById = "SELECT * FROM Account WHERE ID = :accountId";
+ String getAccounts = "SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName FROM Account";
+ String getAccountById = "SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName FROM Account WHERE ID = :accountId";
@Query(getAccounts)
LiveData<List<Account>> getAccounts$();
@@ -35,7 +36,7 @@ public interface AccountDao {
@Query(getAccountById)
Account getAccountById(long accountId);
- @Query("SELECT * FROM Account WHERE ACCOUNTNAME = :accountName")
+ @Query("SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName FROM Account WHERE ACCOUNTNAME = :accountName")
Account getAccountByName(String accountName);
@Query("SELECT COUNT(*) FROM Account")
@@ -55,4 +56,7 @@ public interface AccountDao {
@Query("UPDATE Account SET APIVERSION = :apiVersion WHERE id = :id AND ((APIVERSION IS NULL AND :apiVersion IS NOT NULL) OR (APIVERSION IS NOT NULL AND :apiVersion IS NULL) OR APIVERSION <> :apiVersion)")
int updateApiVersion(Long id, String apiVersion);
+
+ @Query("UPDATE Account SET DISPLAYNAME = :displayName WHERE id = :id")
+ void updateDisplayName(long id, @Nullable String displayName);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
index 618dff37..ca111727 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
@@ -29,11 +29,7 @@ public interface NoteDao {
@Update(onConflict = OnConflictStrategy.REPLACE)
int updateNote(Note newNote);
- @Query("DELETE FROM NOTE WHERE accountId = :accountId")
- int deleteByAccountId(Long accountId);
-
String getNoteById = "SELECT * FROM NOTE WHERE id = :id";
- String getContent = "SELECT content FROM NOTE WHERE id = :id";
String count = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId";
String countFavorites = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId AND favorite = 1";
String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC";
@@ -51,6 +47,9 @@ public interface NoteDao {
@Query(getNoteById)
Note getNoteById(long id);
+ @Query("SELECT remoteId FROM NOTE WHERE id = :id")
+ Long getRemoteId(long id);
+
@Query(count)
LiveData<Integer> count$(long accountId);
@@ -63,12 +62,6 @@ public interface NoteDao {
@Query(countFavorites)
Integer countFavorites(long accountId);
- @Query(getContent)
- LiveData<String> getContent$(Long id);
-
- @Query(getContent)
- String getContent(Long id);
-
@Query(searchRecentByModified)
LiveData<List<Note>> searchRecentByModified$(long accountId, String query);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
index 09f3fc26..016b2fd0 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
@@ -58,43 +58,21 @@ public class Account implements Serializable {
private int textColor = Color.WHITE;
@Nullable
private String capabilitiesETag;
+ @Nullable
+ private String displayName;
public Account() {
// Default constructor
}
- public Account(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) {
+ public Account(@NonNull String url, @NonNull String username, @NonNull String accountName, @Nullable String displayName, @NonNull Capabilities capabilities) {
setUrl(url);
setUserName(username);
setAccountName(accountName);
+ setDisplayName(displayName);
setCapabilities(capabilities);
}
- @Nullable
- public ApiVersion getPreferredApiVersion() {
- // TODO move this logic to NotesClient?
- try {
- if (apiVersion == null) {
- return null;
- }
- final JSONArray versionsArray = new JSONArray(apiVersion);
- final Collection<ApiVersion> supportedApiVersions = new HashSet<>(versionsArray.length());
- for (int i = 0; i < versionsArray.length(); i++) {
- final ApiVersion parsedApiVersion = ApiVersion.of(versionsArray.getString(i));
- for (ApiVersion temp : ApiVersion.SUPPORTED_API_VERSIONS) {
- if (temp.equals(parsedApiVersion)) {
- supportedApiVersions.add(parsedApiVersion);
- break;
- }
- }
- }
- return Collections.max(supportedApiVersions);
- } catch (JSONException | NoSuchElementException e) {
- e.printStackTrace();
- return null;
- }
- }
-
public void setCapabilities(@NonNull Capabilities capabilities) {
capabilitiesETag = capabilities.getETag();
apiVersion = capabilities.getApiVersion();
@@ -189,6 +167,15 @@ public class Account implements Serializable {
this.capabilitiesETag = capabilitiesETag;
}
+ @Nullable
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(@Nullable String displayName) {
+ this.displayName = displayName;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -227,6 +214,7 @@ public class Account implements Serializable {
return result;
}
+ @NonNull
@Override
public String toString() {
return "Account{" +
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java
index 376c099d..e0d0325c 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java
@@ -255,6 +255,7 @@ public class Note implements Serializable, Item {
return result;
}
+ @NonNull
@Override
public String toString() {
return "Note{" +
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 6938e41c..a66fc0e9 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
@@ -12,10 +12,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.Hashtable;
-import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil;
-
public class Migration_14_15 extends Migration {
+ private static final String TAG = Migration_14_15.class.getSimpleName();
+
public Migration_14_15() {
super(14, 15);
}
@@ -43,14 +43,14 @@ public class Migration_14_15 extends Migration {
"EXCERPT TEXT NOT NULL DEFAULT '', " +
"FOREIGN KEY(CATEGORY) REFERENCES CATEGORIES(CATEGORY_ID), " +
"FOREIGN KEY(ACCOUNT_ID) REFERENCES ACCOUNTS(ID))");
- DatabaseIndexUtil.createIndex(db, "NOTES", "REMOTEID", "ACCOUNT_ID", "STATUS", "FAVORITE", "CATEGORY", "MODIFIED");
+ createIndex(db, "NOTES", "REMOTEID", "ACCOUNT_ID", "STATUS", "FAVORITE", "CATEGORY", "MODIFIED");
db.execSQL("CREATE TABLE CATEGORIES(" +
"CATEGORY_ID INTEGER PRIMARY KEY AUTOINCREMENT, " +
"CATEGORY_ACCOUNT_ID INTEGER NOT NULL, " +
"CATEGORY_TITLE TEXT NOT NULL, " +
"UNIQUE( CATEGORY_ACCOUNT_ID , CATEGORY_TITLE), " +
"FOREIGN KEY(CATEGORY_ACCOUNT_ID) REFERENCES ACCOUNTS(ID))");
- DatabaseIndexUtil.createIndex(db, "CATEGORIES", "CATEGORY_ID", "CATEGORY_ACCOUNT_ID", "CATEGORY_TITLE");
+ createIndex(db, "CATEGORIES", "CATEGORY_ID", "CATEGORY_ACCOUNT_ID", "CATEGORY_TITLE");
// A hashtable storing categoryTitle - categoryId Mapping
// This is used to prevent too many searches in database
Hashtable<String, Integer> categoryTitleIdMap = new Hashtable<>();
@@ -91,4 +91,16 @@ public class Migration_14_15 extends Migration {
tmpNotesCursor.close();
db.execSQL("DROP TABLE IF EXISTS " + tmpTableNotes);
}
+
+ private static void createIndex(@NonNull SupportSQLiteDatabase db, @NonNull String table, @NonNull String... columns) {
+ for (String column : columns) {
+ createIndex(db, table, column);
+ }
+ }
+
+ private static void createIndex(@NonNull SupportSQLiteDatabase 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 " + indexName + " ON " + table + "(" + column + ")");
+ }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java
index 343f9e81..576e2204 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java
@@ -9,9 +9,13 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
import com.bumptech.glide.Glide;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
public class Migration_18_19 extends Migration {
private static final String TAG = Migration_18_19.class.getSimpleName();
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
@NonNull
private final Context context;
@@ -27,9 +31,9 @@ public class Migration_18_19 extends Migration {
*/
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {
- new Thread(() -> {
+ executor.submit(() -> {
Log.i(TAG, "Clearing Glide disk cache");
Glide.get(context.getApplicationContext()).clearDiskCache();
- }).start();
+ });
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_21_22.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_21_22.java
new file mode 100644
index 00000000..f4413bba
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_21_22.java
@@ -0,0 +1,43 @@
+package it.niedermann.owncloud.notes.persistence.migration;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.NonNull;
+import androidx.preference.PreferenceManager;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import it.niedermann.owncloud.notes.persistence.SyncWorker;
+
+/**
+ * Enabling backgroundSync, set from {@link String} values to {@link Boolean} values
+ * https://github.com/stefan-niedermann/nextcloud-notes/issues/1168
+ */
+public class Migration_21_22 extends Migration {
+ @NonNull
+ private final Context context;
+
+ public Migration_21_22(@NonNull Context context) {
+ super(21, 22);
+ this.context = context;
+ }
+
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ if (sharedPreferences.contains("backgroundSync")) {
+ editor.remove("backgroundSync");
+ if (sharedPreferences.getString("backgroundSync", "").equals("off")) {
+ editor.putBoolean("backgroundSync", false);
+ } else {
+ editor.putBoolean("backgroundSync", true);
+ SyncWorker.update(context, true);
+ }
+ } else {
+ SyncWorker.update(context, true);
+ }
+ editor.apply();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_22_23.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_22_23.java
new file mode 100644
index 00000000..b6a7494b
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_22_23.java
@@ -0,0 +1,101 @@
+package it.niedermann.owncloud.notes.persistence.migration;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.OnConflictStrategy;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import it.niedermann.owncloud.notes.persistence.ApiProvider;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
+
+/**
+ * Add <code>displayName</code> property to {@link Account}.
+ * <p>
+ * See: <a href="https://github.com/stefan-niedermann/nextcloud-notes/issues/1079">#1079 Show DisplayName instead of uid attribute for LDAP users</a>
+ * <p>
+ * Sanitizes the stored API versions in the database.
+ */
+public class Migration_22_23 extends Migration {
+
+ public Migration_22_23() {
+ super(22, 23);
+ }
+
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
+ addDisplayNameToAccounts(db);
+ sanitizeAccounts(db);
+ }
+
+ private static void addDisplayNameToAccounts(@NonNull SupportSQLiteDatabase db) {
+ db.execSQL("ALTER TABLE Account ADD COLUMN displayName TEXT");
+ }
+
+ private static void sanitizeAccounts(@NonNull SupportSQLiteDatabase db) {
+ final Cursor cursor = db.query("SELECT id, apiVersion FROM ACCOUNT", null);
+ final ContentValues values = new ContentValues(1);
+
+ final int COLUMN_POSITION_ID = cursor.getColumnIndex("id");
+ final int COLUMN_POSITION_API_VERSION = cursor.getColumnIndex("apiVersion");
+
+ while (cursor.moveToNext()) {
+ values.put("APIVERSION", sanitizeApiVersion(cursor.getString(COLUMN_POSITION_API_VERSION)));
+ db.update("ACCOUNT", OnConflictStrategy.REPLACE, values, "ID = ?", new String[]{String.valueOf(cursor.getLong(COLUMN_POSITION_ID))});
+ }
+ cursor.close();
+ ApiProvider.getInstance().invalidateAPICache();
+ }
+
+ @Nullable
+ public static String sanitizeApiVersion(@Nullable String raw) {
+ if (TextUtils.isEmpty(raw)) {
+ return null;
+ }
+
+ JSONArray a;
+ try {
+ a = new JSONArray(raw);
+ } catch (JSONException e) {
+ try {
+ a = new JSONArray("[" + raw + "]");
+ } catch (JSONException e1) {
+ return null;
+ }
+ }
+
+ final Collection<ApiVersion> result = new ArrayList<>();
+ for (int i = 0; i < a.length(); i++) {
+ try {
+ final ApiVersion version = ApiVersion.of(a.getString(i));
+ if (version.getMajor() != 0 || version.getMinor() != 0) {
+ result.add(version);
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ if (result.isEmpty()) {
+ return null;
+ }
+ return "[" +
+ result
+ .stream()
+ .filter(Objects::nonNull)
+ .map(v -> v.getMajor() + "." + v.getMinor())
+ .collect(Collectors.joining(","))
+ + "]";
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
index c9bf78da..141443e3 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
@@ -1,77 +1,63 @@
package it.niedermann.owncloud.notes.persistence.sync;
import android.graphics.Color;
-import android.util.Log;
-import com.bumptech.glide.load.HttpException;
+import androidx.annotation.ColorInt;
+
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import java.lang.reflect.Type;
import it.niedermann.android.util.ColorUtil;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
-import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
-
+/**
+ * Deserialization of <code><a href="https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html?highlight=ocs#theming-capabilities">OcsCapabilities</a></code> to {@link Capabilities} is more complex than just mapping the JSON values to the Pojo properties.
+ *
+ * <ul>
+ * <li>The supported API versions of the Notes app are checked and <code>null</code>ed in case they are not present to maintain backward compatibility</li>
+ * <li>The color hex codes of the theming app are sanitized and mapped to {@link ColorInt}s</li>
+ * </ul>
+ */
public class CapabilitiesDeserializer implements JsonDeserializer<Capabilities> {
- private static final String TAG = CapabilitiesDeserializer.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 static final String CAPABILITIES = "capabilities";
+ private static final String CAPABILITIES_NOTES = "notes";
+ private static final String CAPABILITIES_NOTES_API_VERSION = "api_version";
+ private static final String CAPABILITIES_THEMING = "theming";
+ private static final String CAPABILITIES_THEMING_COLOR = "color";
+ private static final String CAPABILITIES_THEMING_COLOR_TEXT = "color-text";
@Override
public Capabilities deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
final Capabilities response = new Capabilities();
- final JsonObject ocs = json.getAsJsonObject().getAsJsonObject(JSON_OCS);
- if (ocs.has(JSON_OCS_META)) {
- final JsonObject meta = ocs.getAsJsonObject(JSON_OCS_META);
- if (meta.has(JSON_OCS_META_STATUSCODE)) {
- if (meta.get(JSON_OCS_META_STATUSCODE).getAsInt() == HTTP_UNAVAILABLE) {
- Log.i(TAG, "Capabilities Endpoint: This instance is currently in maintenance mode.");
- throw new JsonParseException(new NextcloudHttpRequestFailedException(HTTP_UNAVAILABLE, new HttpException(HTTP_UNAVAILABLE)));
+ final JsonObject data = json.getAsJsonObject();
+ if (data.has(CAPABILITIES)) {
+ final JsonObject capabilities = data.getAsJsonObject(CAPABILITIES);
+ if (capabilities.has(CAPABILITIES_NOTES)) {
+ final JsonObject notes = capabilities.getAsJsonObject(CAPABILITIES_NOTES);
+ if (notes.has(CAPABILITIES_NOTES_API_VERSION)) {
+ response.setApiVersion(notes.get(CAPABILITIES_NOTES_API_VERSION).toString());
}
}
- }
- if (ocs.has(JSON_OCS_DATA)) {
- final JsonObject data = ocs.getAsJsonObject(JSON_OCS_DATA);
- if (data.has(JSON_OCS_DATA_CAPABILITIES)) {
- final JsonObject capabilities = data.getAsJsonObject(JSON_OCS_DATA_CAPABILITIES);
- if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_NOTES)) {
- final JsonObject notes = capabilities.getAsJsonObject(JSON_OCS_DATA_CAPABILITIES_NOTES);
- if (notes.has(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION)) {
- final JsonElement apiVersion = notes.get(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION);
- response.setApiVersion(apiVersion.isJsonArray() ? apiVersion.toString() : null);
+ if (capabilities.has(CAPABILITIES_THEMING)) {
+ final JsonObject theming = capabilities.getAsJsonObject(CAPABILITIES_THEMING);
+ if (theming.has(CAPABILITIES_THEMING_COLOR)) {
+ try {
+ response.setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(CAPABILITIES_THEMING_COLOR).getAsString())));
+ } catch (Exception e) {
+ e.printStackTrace();
}
}
- if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_THEMING)) {
- final JsonObject theming = capabilities.getAsJsonObject(JSON_OCS_DATA_CAPABILITIES_THEMING);
- if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR)) {
- try {
- response.setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR).getAsString())));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)) {
- try {
- response.setTextColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT).getAsString())));
- } catch (Exception e) {
- e.printStackTrace();
- }
+ if (theming.has(CAPABILITIES_THEMING_COLOR_TEXT)) {
+ try {
+ response.setTextColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(CAPABILITIES_THEMING_COLOR_TEXT).getAsString())));
+ } catch (Exception e) {
+ e.printStackTrace();
}
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java
index 4ab8371e..3e552ae6 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java
@@ -80,10 +80,14 @@ public class NotesAPI {
}
public Call<Note> editNote(@NonNull Note note) {
+ final Long remoteId = note.getRemoteId();
+ if (remoteId == null) {
+ throw new IllegalArgumentException("remoteId of a " + Note.class.getSimpleName() + " must not be null if this object is used for editing a remote note.");
+ }
if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) {
- return notesAPI_1_0.editNote(note, note.getRemoteId());
+ return notesAPI_1_0.editNote(note, remoteId);
} else if (ApiVersion.API_VERSION_0_2.equals(usedApiVersion)) {
- return notesAPI_0_2.editNote(new Note_0_2(note), note.getRemoteId());
+ return notesAPI_0_2.editNote(new Note_0_2(note), remoteId);
} else {
throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support editNote().");
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java
index 27ef57c4..e24ef7ef 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java
@@ -5,8 +5,12 @@ import com.nextcloud.android.sso.api.ParsedResponse;
import io.reactivex.Observable;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import it.niedermann.owncloud.notes.shared.model.OcsResponse;
+import it.niedermann.owncloud.notes.shared.model.OcsUser;
+import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header;
+import retrofit2.http.Path;
/**
* @link <a href="https://deck.readthedocs.io/en/latest/API/">Deck REST API</a>
@@ -14,5 +18,8 @@ import retrofit2.http.Header;
public interface OcsAPI {
@GET("capabilities?format=json")
- Observable<ParsedResponse<Capabilities>> getCapabilities(@Header("If-None-Match") String eTag);
+ Observable<ParsedResponse<OcsResponse<Capabilities>>> getCapabilities(@Header("If-None-Match") String eTag);
+
+ @GET("users/{userId}?format=json")
+ Call<OcsResponse<OcsUser>> getUser(@Path("userId") String userId);
}
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
index f872aac3..2dcd99ec 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java
@@ -3,28 +3,28 @@ package it.niedermann.owncloud.notes.preferences;
import android.os.Bundle;
import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
import it.niedermann.owncloud.notes.LockedActivity;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ActivityPreferencesBinding;
-/**
- * Allows to change application settings.
- */
-
public class PreferencesActivity extends LockedActivity {
+ private PreferencesViewModel viewModel;
private ActivityPreferencesBinding binding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ viewModel = new ViewModelProvider(this).get(PreferencesViewModel.class);
+ viewModel.resultCode$.observe(this, this::setResult);
+
binding = ActivityPreferencesBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
- setResult(RESULT_CANCELED);
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container_view, new PreferencesFragment())
.commit();
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
index 8af467fb..a38fd56c 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java
@@ -8,6 +8,7 @@ import android.util.Log;
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
+import androidx.lifecycle.ViewModelProvider;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
@@ -20,27 +21,25 @@ import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.persistence.SyncWorker;
import it.niedermann.owncloud.notes.shared.util.DeviceCredentialUtil;
-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 PreferencesViewModel viewModel;
+
private BrandedSwitchPreference fontPref;
private BrandedSwitchPreference lockPref;
private BrandedSwitchPreference wifiOnlyPref;
private BrandedSwitchPreference gridViewPref;
private BrandedSwitchPreference preventScreenCapturePref;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
+ private BrandedSwitchPreference backgroundSyncPref;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
+ viewModel = new ViewModelProvider(requireActivity()).get(PreferencesViewModel.class);
+
fontPref = findPreference(getString(R.string.pref_key_font));
gridViewPref = findPreference(getString(R.string.pref_key_gridview));
@@ -48,7 +47,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
gridViewPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> {
final Boolean gridView = (Boolean) newValue;
Log.v(TAG, "gridView: " + gridView);
- requireActivity().setResult(Activity.RESULT_OK);
+ viewModel.resultCode$.setValue(Activity.RESULT_OK);
NotesApplication.updateGridViewEnabled(gridView);
return true;
});
@@ -79,7 +78,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
assert themePref != null;
themePref.setOnPreferenceChangeListener((preference, newValue) -> {
NotesApplication.setAppTheme(DarkModeSetting.valueOf((String) newValue));
- requireActivity().setResult(Activity.RESULT_OK);
+ viewModel.resultCode$.setValue(Activity.RESULT_OK);
ActivityCompat.recreate(requireActivity());
return true;
});
@@ -91,11 +90,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
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());
+ backgroundSyncPref = findPreference(getString(R.string.pref_key_background_sync));
+ assert backgroundSyncPref != null;
+ backgroundSyncPref.setOnPreferenceChangeListener((preference, newValue) -> {
+ Log.i(TAG, "backgroundSync: " + newValue);
+ SyncWorker.update(requireContext(), (Boolean) newValue);
return true;
});
}
@@ -112,6 +111,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
}
}
+ /**
+ * Change color for backgroundSyncPref as well
+ * https://github.com/stefan-niedermann/nextcloud-deck/issues/531
+ *
+ * @param mainColor color of main brand
+ * @param textColor color of text
+ */
+
@Override
public void applyBrand(int mainColor, int textColor) {
fontPref.applyBrand(mainColor, textColor);
@@ -119,5 +126,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra
wifiOnlyPref.applyBrand(mainColor, textColor);
gridViewPref.applyBrand(mainColor, textColor);
preventScreenCapturePref.applyBrand(mainColor, textColor);
+ backgroundSyncPref.applyBrand(mainColor, textColor);
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesViewModel.java
new file mode 100644
index 00000000..dfde6c92
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesViewModel.java
@@ -0,0 +1,9 @@
+package it.niedermann.owncloud.notes.preferences;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+public class PreferencesViewModel extends ViewModel {
+
+ public final MutableLiveData<Integer> resultCode$ = new MutableLiveData<>();
+}
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
index 7688b1b1..5d3d2963 100644
--- 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
@@ -31,7 +31,7 @@ public class AccountChooserViewHolder extends RecyclerView.ViewHolder {
.into(binding.accountItemAvatar);
binding.accountLayout.setOnClickListener((v) -> targetAccountConsumer.accept(localAccount));
- binding.accountName.setText(localAccount.getUserName());
+ binding.accountName.setText(localAccount.getDisplayName());
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
index ee2fdc3a..589f2571 100644
--- 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
@@ -42,10 +42,6 @@ public class ApiVersion implements Comparable<ApiVersion> {
return minor;
}
- public String getOriginalVersion() {
- return originalVersion;
- }
-
public static ApiVersion of(String versionString) {
int major = 0, minor = 0;
if (versionString != null) {
@@ -90,6 +86,9 @@ public class ApiVersion implements Comparable<ApiVersion> {
// return getMajor() >= 1 && getMinor() >= 2;
}
+ /**
+ * Checks only the <strong>{@link #major}</strong> version.
+ */
@Override
public boolean equals(Object o) {
if (this == o) return true;
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
index 5514a91b..06bd867d 100644
--- 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
@@ -1,103 +1,21 @@
package it.niedermann.owncloud.notes.shared.model;
import android.graphics.Color;
-import android.util.Log;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.bumptech.glide.load.HttpException;
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import it.niedermann.android.util.ColorUtil;
-
-import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
-
-/**
- * This entity class is used to return relevant data of the HTTP response.
- */
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;
-
@ColorInt
- private int color = -16743735;
+ private int color = -16743735; // #0082C9
@ColorInt
- private int textColor = -16777216;
+ private int textColor = Color.WHITE;
@Nullable
private String eTag;
- public Capabilities() {
-
- }
-
- @VisibleForTesting
- 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)) {
- try {
- this.color = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR)));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)) {
- try {
- this.textColor = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
-
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
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
index 94bcda38..6a36ade1 100644
--- 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
@@ -7,7 +7,7 @@ public enum CategorySortingMethod {
private final int id;
private final String title; // sorting method OrderBy for SQL
- /***
+ /**
* Constructor
* @param title given sorting method OrderBy
*/
@@ -16,7 +16,7 @@ public enum CategorySortingMethod {
this.title = title;
}
- /***
+ /**
* Retrieve the sorting method id represented in database
* @return the sorting method id for the enum item
*/
@@ -24,7 +24,7 @@ public enum CategorySortingMethod {
return this.id;
}
- /***
+ /**
* Retrieve the sorting method order for SQL
* @return the sorting method order for the enum item
*/
@@ -32,7 +32,7 @@ public enum CategorySortingMethod {
return this.title;
}
- /***
+ /**
* Retrieve the corresponding enum value with given the index (ordinal)
* @param id the id of the corresponding enum value stored in DB
* @return the corresponding enum item with the index (ordinal)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsResponse.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsResponse.java
new file mode 100644
index 00000000..0fea9a92
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsResponse.java
@@ -0,0 +1,30 @@
+package it.niedermann.owncloud.notes.shared.model;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * <a href="https://www.open-collaboration-services.org/">OpenCollaborationServices</a>
+ *
+ * @param <T> defines the payload of this {@link OcsResponse}.
+ */
+public class OcsResponse<T> {
+
+ @Expose
+ public OcsWrapper<T> ocs;
+
+ public static class OcsWrapper<T> {
+ @Expose
+ public OcsMeta meta;
+ @Expose
+ public T data;
+ }
+
+ public static class OcsMeta {
+ @Expose
+ public String status;
+ @Expose
+ public int statuscode;
+ @Expose
+ public String message;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUser.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUser.java
new file mode 100644
index 00000000..9248abdf
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUser.java
@@ -0,0 +1,16 @@
+package it.niedermann.owncloud.notes.shared.model;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Equivalent of an <code><a href="https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html?highlight=ocs#user-metadata">OcsUser</a></code>
+ */
+public class OcsUser {
+ @Expose
+ @SerializedName("id")
+ public String userId;
+ @Expose
+ @SerializedName("displayname")
+ public String displayName;
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtil.java
new file mode 100644
index 00000000..57788472
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtil.java
@@ -0,0 +1,105 @@
+package it.niedermann.owncloud.notes.shared.util;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
+
+public class ApiVersionUtil {
+
+ private ApiVersionUtil() {
+ throw new UnsupportedOperationException("Do not instantiate this util class.");
+ }
+
+ /**
+ * @return a {@link Collection} of all valid {@link ApiVersion}s which have been found in {@param raw}.
+ */
+ @NonNull
+ public static Collection<ApiVersion> parse(@Nullable String raw) {
+ if (TextUtils.isEmpty(raw)) {
+ return Collections.emptyList();
+ }
+
+ JSONArray a;
+ try {
+ a = new JSONArray(raw);
+ } catch (JSONException e) {
+ try {
+ a = new JSONArray("[" + raw + "]");
+ } catch (JSONException e1) {
+ return Collections.emptyList();
+ }
+ }
+
+ final Collection<ApiVersion> result = new ArrayList<>();
+ for (int i = 0; i < a.length(); i++) {
+ try {
+ final ApiVersion version = ApiVersion.of(a.getString(i));
+ if (version.getMajor() != 0 || version.getMinor() != 0) {
+ result.add(version);
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @return a serialized {@link String} of the given {@param apiVersions} or <code>null</code>.
+ */
+ @Nullable
+ public static String serialize(@Nullable Collection<ApiVersion> apiVersions) {
+ if (apiVersions == null || apiVersions.isEmpty()) {
+ return null;
+ }
+ return "[" +
+ apiVersions
+ .stream()
+ .filter(Objects::nonNull)
+ .map(v -> v.getMajor() + "." + v.getMinor())
+ .collect(Collectors.joining(","))
+ + "]";
+ }
+
+ @Nullable
+ public static String sanitize(@Nullable String raw) {
+ return serialize(parse(raw));
+ }
+
+ /**
+ * @return the highest {@link ApiVersion} that is supported by the server according to {@param raw},
+ * whose major version is also supported by this app (see {@link ApiVersion#SUPPORTED_API_VERSIONS}).
+ * Returns <code>null</code> if no better version could be found.
+ */
+ @Nullable
+ public static ApiVersion getPreferredApiVersion(@Nullable String raw) {
+ return parse(raw)
+ .stream()
+ .filter(version -> Arrays.asList(ApiVersion.SUPPORTED_API_VERSIONS).contains(version))
+ .max((o1, o2) -> {
+ if (o2.getMajor() > o1.getMajor()) {
+ return -1;
+ } else if (o2.getMajor() < o1.getMajor()) {
+ return 1;
+ } else if (o2.getMinor() > o1.getMinor()) {
+ return -1;
+ } else if (o2.getMinor() < o1.getMinor()) {
+ return 1;
+ }
+ return 0;
+ })
+ .orElse(null);
+ }
+}
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
index 03eb1097..35625119 100644
--- 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
@@ -1,18 +1,37 @@
package it.niedermann.owncloud.notes.shared.util;
import android.content.Context;
+import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
@GlideModule
public class CustomAppGlideModule extends AppGlideModule {
+
+ private static final String TAG = CustomAppGlideModule.class.getSimpleName();
+ private static final ExecutorService clearDiskCacheExecutor = Executors.newSingleThreadExecutor();
+
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
super.registerComponents(context, glide, registry);
}
+
+ @UiThread
+ public static void clearCache(@NonNull Context context) {
+ Log.i(TAG, "Clearing Glide memory cache");
+ Glide.get(context).clearMemory();
+ clearDiskCacheExecutor.submit(() -> {
+ Log.i(TAG, "Clearing Glide disk cache");
+ Glide.get(context.getApplicationContext()).clearDiskCache();
+ });
+ }
} \ 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
deleted file mode 100644
index 8506c713..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package it.niedermann.owncloud.notes.shared.util;
-
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-
-public class DatabaseIndexUtil {
-
- private static final String TAG = DatabaseIndexUtil.class.getSimpleName();
-
- private DatabaseIndexUtil() {
-
- }
-
- public static void createIndex(@NonNull SupportSQLiteDatabase db, @NonNull String table, @NonNull String... columns) {
- for (String column : columns) {
- createIndex(db, table, column);
- }
- }
-
- public static void createIndex(@NonNull SupportSQLiteDatabase 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 " + indexName + " ON " + table + "(" + column + ")");
- }
-
- public static void dropIndexes(@NonNull SupportSQLiteDatabase db) {
- try (Cursor c = db.query("SELECT name, sql FROM sqlite_master WHERE type = 'index'")) {
- 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
index 034eea5a..14163e91 100644
--- 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
@@ -12,7 +12,7 @@ public class DeviceCredentialUtil {
private static final String TAG = DeviceCredentialUtil.class.getSimpleName();
private DeviceCredentialUtil() {
- // utility class -> private constructor
+ throw new UnsupportedOperationException("Do not instantiate this util class.");
}
public static boolean areCredentialsAvailable(Context context) {
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
index ad6b6793..06cbf57d 100644
--- 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
@@ -2,27 +2,21 @@ package it.niedermann.owncloud.notes.shared.util;
import android.content.Context;
import android.content.res.Resources;
-import android.graphics.Color;
-import android.text.Spannable;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.text.style.MetricAffectingSpan;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.WindowInsets;
-import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
import java.util.Collection;
import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import java.util.stream.Collectors;
-import it.niedermann.android.util.ColorUtil;
-import it.niedermann.owncloud.notes.NotesApplication;
import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
@@ -30,7 +24,7 @@ import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
public class DisplayUtils {
private DisplayUtils() {
-
+ throw new UnsupportedOperationException("Do not instantiate this util class.");
}
public static List<NavigationItem.CategoryNavigationItem> convertToCategoryNavigationItem(@NonNull Context context, @NonNull Collection<CategoryWithNotesCount> counter) {
@@ -52,4 +46,29 @@ public class DisplayUtils {
}
return new NavigationItem.CategoryNavigationItem("category:" + counter.getCategory(), counter.getCategory(), counter.getTotalNotes(), icon, counter.getAccountId(), counter.getCategory());
}
+
+ /**
+ * Detect if the soft keyboard is open.
+ * On API prior to 30 we fall back to workaround which might be less reliable
+ *
+ * @param parentView View
+ * @return keyboardVisibility Boolean
+ */
+ public static boolean isSoftKeyboardVisible(@NonNull View parentView) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ final WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(parentView);
+ if (insets != null) {
+ return insets.isVisible(WindowInsets.Type.ime());
+ }
+ }
+
+ //Arbitrary keyboard height
+ final int defaultKeyboardHeightDP = 100;
+ final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0);
+ final Rect rect = new Rect();
+ final int estimatedKeyboardHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, parentView.getResources().getDisplayMetrics());
+ parentView.getWindowVisibleDisplayFrame(rect);
+ final int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top);
+ return heightDiff >= estimatedKeyboardHeight;
+ }
}
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
index e5b8afae..3d5a118c 100644
--- 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
@@ -22,7 +22,7 @@ public class NoteUtil {
public static final String EXCERPT_LINE_SEPARATOR = " ";
private NoteUtil() {
-
+ throw new UnsupportedOperationException("Do not instantiate this util class.");
}
/**
@@ -117,7 +117,7 @@ public class NoteUtil {
line = removeMarkdown(lines[currentLine]);
}
} else {
- line = content;
+ line = removeMarkdown(content);
}
return line;
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NotesColorUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NotesColorUtil.java
index a445208b..42aaf79d 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NotesColorUtil.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NotesColorUtil.java
@@ -14,6 +14,7 @@ public final class NotesColorUtil {
private static final Map<ColorPair, Boolean> CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>();
private NotesColorUtil() {
+ throw new UnsupportedOperationException("Do not instantiate this util class.");
}
public static boolean contrastRatioIsSufficient(@ColorInt int colorOne, @ColorInt int colorTwo) {
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
index da529136..1e2542b0 100644
--- 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
@@ -19,7 +19,7 @@ public class SSOUtil {
private static final String TAG = SSOUtil.class.getSimpleName();
private SSOUtil() {
-
+ throw new UnsupportedOperationException("Do not instantiate this util class.");
}
/**
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
index 115d18dd..4b7aebd9 100644
--- 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
@@ -14,6 +14,11 @@ import it.niedermann.android.markdown.MarkdownUtil;
import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN;
public class ShareUtil {
+
+ private ShareUtil() {
+ throw new UnsupportedOperationException("Do not instantiate this util class.");
+ }
+
public static void openShareDialog(@NonNull Context context, @Nullable String subject, @Nullable String text) {
context.startActivity(Intent.createChooser(new Intent()
.setAction(Intent.ACTION_SEND)
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
index 8bf80cb9..27fec716 100644
--- 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
@@ -13,7 +13,7 @@ import androidx.core.text.HtmlCompat;
public class SupportUtil {
private SupportUtil() {
-
+ throw new UnsupportedOperationException("Do not instantiate this util class.");
}
/**
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
index 7cc84de3..8825ad98 100644
--- 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
@@ -11,6 +11,8 @@ import android.util.Log;
import android.widget.RemoteViews;
import java.util.NoSuchElementException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.persistence.NotesRepository;
@@ -18,6 +20,7 @@ import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
public class NoteListWidget extends AppWidgetProvider {
private static final String TAG = NoteListWidget.class.getSimpleName();
+ private final ExecutorService executor = Executors.newCachedThreadPool();
static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) {
final NotesRepository repo = NotesRepository.getInstance(context);
@@ -83,7 +86,7 @@ public class NoteListWidget extends AppWidgetProvider {
final NotesRepository repo = NotesRepository.getInstance(context);
for (int appWidgetId : appWidgetIds) {
- new Thread(() -> repo.removeNoteListWidget(appWidgetId)).start();
+ executor.submit(() -> repo.removeNoteListWidget(appWidgetId));
}
}
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
index a750ee1e..3d7122da 100644
--- 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
@@ -14,6 +14,9 @@ import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundExce
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
import it.niedermann.owncloud.notes.LockedActivity;
import it.niedermann.owncloud.notes.NotesApplication;
import it.niedermann.owncloud.notes.R;
@@ -34,6 +37,8 @@ import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.
public class NoteListWidgetConfigurationActivity extends LockedActivity {
private static final String TAG = Activity.class.getSimpleName();
+ private final ExecutorService executor = Executors.newCachedThreadPool();
+
private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
private Account localAccount = null;
@@ -104,7 +109,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity {
data.setAccountId(localAccount.getId());
data.setThemeMode(NotesApplication.getAppTheme(getApplicationContext()).getModeId());
- new Thread(() -> {
+ executor.submit(() -> {
repo.createOrUpdateNoteListWidgetData(data);
final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, getApplicationContext(), NoteListWidget.class)
@@ -112,7 +117,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity {
setResult(RESULT_OK, updateIntent);
getApplicationContext().sendBroadcast(updateIntent);
finish();
- }).start();
+ });
}
public void onIconClick(NavigationItem item) {
@@ -122,7 +127,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity {
binding.recyclerView.setAdapter(adapterCategories);
- new Thread(() -> {
+ executor.submit(() -> {
try {
this.localAccount = repo.getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
@@ -133,7 +138,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity {
finish();
}
runOnUiThread(() -> viewModel.getAdapterCategories(localAccount.getId()).observe(this, (navigationItems) -> adapterCategories.setItems(navigationItems)));
- }).start();
+ });
}
@Override
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
index e208603a..106b9f93 100644
--- 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
@@ -10,6 +10,9 @@ import android.net.Uri;
import android.util.Log;
import android.widget.RemoteViews;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.edit.BaseNoteFragment;
import it.niedermann.owncloud.notes.edit.EditNoteActivity;
@@ -19,6 +22,7 @@ import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
public class SingleNoteWidget extends AppWidgetProvider {
private static final String TAG = SingleNoteWidget.class.getSimpleName();
+ private final ExecutorService executor = Executors.newCachedThreadPool();
static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) {
final Intent templateIntent = new Intent(context, EditNoteActivity.class);
@@ -69,7 +73,7 @@ public class SingleNoteWidget extends AppWidgetProvider {
final NotesRepository repo = NotesRepository.getInstance(context);
for (int appWidgetId : appWidgetIds) {
- new Thread(() -> repo.removeSingleNoteWidget(appWidgetId)).start();
+ executor.submit(() -> repo.removeSingleNoteWidget(appWidgetId));
}
super.onDeleted(context, appWidgetIds);
}
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
index eb06201a..a487e669 100644
--- 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
@@ -52,7 +52,7 @@ public class SingleNoteWidgetConfigurationActivity extends MainActivity {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
- new Thread(() -> {
+ executor.submit(() -> {
try {
mainViewModel.createOrUpdateSingleNoteWidgetData(
new SingleNoteWidgetData(
@@ -71,6 +71,6 @@ public class SingleNoteWidgetConfigurationActivity extends MainActivity {
} catch (SQLException e) {
Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
- }).start();
+ });
}
}
diff --git a/app/src/main/res/drawable-v21/bg_navdrawer_item.xml b/app/src/main/res/drawable-v21/bg_navdrawer_item.xml
deleted file mode 100644
index a366c0d2..00000000
--- a/app/src/main/res/drawable-v21/bg_navdrawer_item.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="?colorControlHighlight">
-
- <item
- android:id="@android:id/mask"
- android:left="@dimen/spacer_1x"
- android:right="@dimen/spacer_1x">
-
- <shape android:shape="rectangle">
- <!-- value of color is irrelevant, but solid needs to be defined for mask to work -->
- <solid android:color="@color/bg_highlighted" />
- <corners android:radius="@dimen/spacer_1hx" />
- </shape>
- </item>
-
- <item
- android:left="@dimen/spacer_1x"
- android:right="@dimen/spacer_1x">
-
- <selector>
- <item android:state_selected="true">
- <shape android:shape="rectangle">
- <corners android:radius="@dimen/spacer_1hx" />
- <solid android:color="@color/bg_highlighted" />
- </shape>
- </item>
-
- <item>
- <shape android:shape="rectangle">
- <corners android:radius="@dimen/spacer_1hx" />
- </shape>
- </item>
- </selector>
- </item>
-
-</ripple> \ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/grid_item_background_selector.xml b/app/src/main/res/drawable-v21/grid_item_background_selector.xml
deleted file mode 100644
index 0959be91..00000000
--- a/app/src/main/res/drawable-v21/grid_item_background_selector.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Selector is used for Background Colors in List Items -->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/bg_highlighted">
- <!-- :selected -->
- <item>
- <selector>
- <item android:state_selected="true">
- <color android:color="@color/bg_highlighted" />
- </item>
-
- <item android:state_activated="true">
- <color android:color="@color/bg_highlighted" />
- </item>
-
- <item>
- <color android:color="@android:color/transparent" />
- </item>
- </selector>
- </item>
-</ripple> \ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/list_item_background_selector.xml b/app/src/main/res/drawable-v21/list_item_background_selector.xml
deleted file mode 100644
index 573bd924..00000000
--- a/app/src/main/res/drawable-v21/list_item_background_selector.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Selector is used for Background Colors in List Items -->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/bg_highlighted">
- <!-- :selected -->
- <item>
- <selector>
- <item android:state_selected="true">
- <color android:color="@color/bg_highlighted" />
- </item>
-
- <item android:state_activated="true">
- <color android:color="@color/bg_highlighted" />
- </item>
-
- <item>
- <color android:color="@color/bg_normal" />
- </item>
- </selector>
- </item>
-</ripple> \ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_navdrawer_item.xml b/app/src/main/res/drawable/bg_navdrawer_item.xml
index b12a8634..a366c0d2 100644
--- a/app/src/main/res/drawable/bg_navdrawer_item.xml
+++ b/app/src/main/res/drawable/bg_navdrawer_item.xml
@@ -1,14 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
-<selector xmlns:android="http://schemas.android.com/apk/res/android" android:left="@dimen/spacer_1x" android:right="@dimen/spacer_1x">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?colorControlHighlight">
+
+ <item
+ android:id="@android:id/mask"
+ android:left="@dimen/spacer_1x"
+ android:right="@dimen/spacer_1x">
- <item android:state_selected="true">
<shape android:shape="rectangle">
+ <!-- value of color is irrelevant, but solid needs to be defined for mask to work -->
<solid android:color="@color/bg_highlighted" />
+ <corners android:radius="@dimen/spacer_1hx" />
</shape>
</item>
- <item>
- <shape android:shape="rectangle" />
+ <item
+ android:left="@dimen/spacer_1x"
+ android:right="@dimen/spacer_1x">
+
+ <selector>
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/spacer_1hx" />
+ <solid android:color="@color/bg_highlighted" />
+ </shape>
+ </item>
+
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/spacer_1hx" />
+ </shape>
+ </item>
+ </selector>
</item>
-</selector> \ No newline at end of file
+</ripple> \ No newline at end of file
diff --git a/app/src/main/res/drawable/grid_item_background_selector.xml b/app/src/main/res/drawable/grid_item_background_selector.xml
index 24ab9be7..0959be91 100644
--- a/app/src/main/res/drawable/grid_item_background_selector.xml
+++ b/app/src/main/res/drawable/grid_item_background_selector.xml
@@ -1,6 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!-- Selector is used for Background Colors in List Items -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/bg_highlighted">
<!-- :selected -->
- <item android:drawable="@color/bg_highlighted" android:state_selected="true" />
- <item android:drawable="@android:color/transparent" />
-</selector>
+ <item>
+ <selector>
+ <item android:state_selected="true">
+ <color android:color="@color/bg_highlighted" />
+ </item>
+
+ <item android:state_activated="true">
+ <color android:color="@color/bg_highlighted" />
+ </item>
+
+ <item>
+ <color android:color="@android:color/transparent" />
+ </item>
+ </selector>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_color_lens_grey600_24dp.xml b/app/src/main/res/drawable/ic_color_lens_grey600_24dp.xml
deleted file mode 100644
index 6c715757..00000000
--- a/app/src/main/res/drawable/ic_color_lens_grey600_24dp.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<vector android:autoMirrored="true"
- android:height="24dp"
- android:tint="#757575"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0"
- android:width="24dp"
- xmlns:android="http://schemas.android.com/apk/res/android">
- <path
- android:fillColor="#FF000000"
- android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" />
-</vector>
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index d3983a7e..9127e535 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -2,7 +2,6 @@
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
- android:autoMirrored="true"
android:viewportWidth="1344"
android:viewportHeight="1344">
<path
diff --git a/app/src/main/res/drawable/ic_settings_grey600_24dp.xml b/app/src/main/res/drawable/ic_settings_grey600_24dp.xml
index 114597ef..eca3850c 100644
--- a/app/src/main/res/drawable/ic_settings_grey600_24dp.xml
+++ b/app/src/main/res/drawable/ic_settings_grey600_24dp.xml
@@ -1,11 +1,13 @@
-<vector android:autoMirrored="true"
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:width="24dp"
android:height="24dp"
+ android:autoMirrored="true"
android:tint="#757575"
- android:viewportHeight="24.0"
android:viewportWidth="24.0"
- android:width="24dp"
- xmlns:android="http://schemas.android.com/apk/res/android">
+ android:viewportHeight="24.0">
<path
android:fillColor="#FF757575"
- android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z" />
+ android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"
+ tools:ignore="VectorPath" />
</vector>
diff --git a/app/src/main/res/drawable/list_item_background_selector.xml b/app/src/main/res/drawable/list_item_background_selector.xml
index 1e7b4874..573bd924 100644
--- a/app/src/main/res/drawable/list_item_background_selector.xml
+++ b/app/src/main/res/drawable/list_item_background_selector.xml
@@ -1,6 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!-- Selector is used for Background Colors in List Items -->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/bg_highlighted">
<!-- :selected -->
- <item android:drawable="@color/bg_highlighted" android:state_selected="true" />
- <item android:drawable="@color/bg_normal" />
-</selector>
+ <item>
+ <selector>
+ <item android:state_selected="true">
+ <color android:color="@color/bg_highlighted" />
+ </item>
+
+ <item android:state_activated="true">
+ <color android:color="@color/bg_highlighted" />
+ </item>
+
+ <item>
+ <color android:color="@color/bg_normal" />
+ </item>
+ </selector>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/app/src/main/res/layout/empty_content_view.xml b/app/src/main/res/layout/empty_content_view.xml
index 2e73cfb9..654b38b7 100644
--- a/app/src/main/res/layout/empty_content_view.xml
+++ b/app/src/main/res/layout/empty_content_view.xml
@@ -1,4 +1,5 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/empty_content_view"
android:layout_width="match_parent"
@@ -16,7 +17,7 @@
android:scaleX="1.5"
android:scaleY="1.5"
android:src="@drawable/ic_launcher_foreground"
- android:tint="@color/fg_default_high" />
+ app:tint="@color/fg_default_high" />
<TextView
android:id="@+id/empty_content_view_title"
diff --git a/app/src/main/res/layout/fragment_about_credits_tab.xml b/app/src/main/res/layout/fragment_about_credits_tab.xml
index 325055ef..35dcae9f 100644
--- a/app/src/main/res/layout/fragment_about_credits_tab.xml
+++ b/app/src/main/res/layout/fragment_about_credits_tab.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -13,13 +14,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:layout_marginBottom="30dp"
android:layout_marginTop="30dp"
- android:drawableTop="@mipmap/ic_launcher"
+ android:layout_marginBottom="30dp"
android:text="@string/app_name"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="@color/fg_default_low"
- android:textSize="26sp" />
+ android:textSize="26sp"
+ app:drawableTopCompat="@mipmap/ic_launcher" />
<TextView
style="?android:attr/listSeparatorTextViewStyle"
diff --git a/app/src/main/res/layout/fragment_note_edit.xml b/app/src/main/res/layout/fragment_note_edit.xml
index 1e84909f..c377998c 100644
--- a/app/src/main/res/layout/fragment_note_edit.xml
+++ b/app/src/main/res/layout/fragment_note_edit.xml
@@ -46,6 +46,7 @@
android:id="@+id/searchPrev"
style="@style/fab"
android:layout_gravity="bottom|end"
+ android:contentDescription="@string/simple_prev"
android:translationY="-56dp"
android:visibility="gone"
app:backgroundTint="@color/defaultBrand"
@@ -57,6 +58,7 @@
android:id="@+id/searchNext"
style="@style/fab"
android:layout_gravity="bottom|end"
+ android:contentDescription="@string/simple_next"
android:visibility="gone"
app:backgroundTint="@color/defaultBrand"
app:fabSize="mini"
diff --git a/app/src/main/res/layout/fragment_note_preview.xml b/app/src/main/res/layout/fragment_note_preview.xml
index be1a9f62..0b660f8d 100644
--- a/app/src/main/res/layout/fragment_note_preview.xml
+++ b/app/src/main/res/layout/fragment_note_preview.xml
@@ -42,6 +42,7 @@
android:id="@+id/searchPrev"
style="@style/fab"
android:layout_gravity="bottom|end"
+ android:contentDescription="@string/simple_prev"
android:translationY="-56dp"
android:visibility="gone"
app:backgroundTint="@color/defaultBrand"
@@ -53,6 +54,7 @@
android:id="@+id/searchNext"
style="@style/fab"
android:layout_gravity="bottom|end"
+ android:contentDescription="@string/simple_next"
android:visibility="gone"
app:backgroundTint="@color/defaultBrand"
app:fabSize="mini"
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index eca70cfe..00000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
- <background android:drawable="@drawable/ic_launcher_background" />
- <foreground android:drawable="@drawable/ic_launcher_foreground" />
-</adaptive-icon> \ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index ba3e2510..00000000
--- a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index 5e6dd7a8..00000000
--- a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index 9ee96303..00000000
--- a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index b464fdc0..00000000
--- a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index dc1e2866..00000000
--- a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index d2e12cdb..9019ed60 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -81,7 +81,6 @@
<string name="widget_create_note">انشاء ملاحظة</string>
<string name="widget_not_logged_in">الرجاء تسجيل الدخول قبل إستخدامك لهذه الأداة</string>
<string name="widget_entry_fav_contentDescription">تُستخدم أيقونة النجمة للإشارة إلى عنصر كعنصر مفضل</string>
- <string name="widget_app_launcher_contentDescription">يطلق التطبيق</string>
<string name="activity_select_single_note">اختيار الملاحظة</string>
@@ -135,10 +134,8 @@
<string name="append_to_note">إلحاق بالملاحظة</string>
<string name="change_note_title">تغيير عنوان الملاحظة</string>
<string name="menu_edit_title">تحرير العنوان</string>
- <string name="settings_branding">وسم</string>
<string name="simple_security">الأمان</string>
<string name="simple_synchronization">المزامنة</string>
- <string name="simple_behavior">السلوك</string>
<string name="manage_accounts">إدارة الحسابات</string>
<string name="action_formatting_help">التنسيق</string>
@@ -154,13 +151,6 @@
<item>كبير</item>
</string-array>
- <string-array name="sync_entries">
- <item>إيقاف</item>
- <item>15 دقيقة</item>
- <item>ساعة</item>
- <item>6 ساعات</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>فاتح</item>
<item>ليلي</item>
@@ -168,7 +158,6 @@
</string-array>
<plurals name="ab_selected">
- <item quantity="zero">%d محدد</item>
<item quantity="one">%d محدد</item>
<item quantity="two">%d محددان</item>
<item quantity="few">%d محددين</item>
diff --git a/app/src/main/res/values-b+en+001/strings.xml b/app/src/main/res/values-b+en+001/strings.xml
index c82bc2c3..fc96bd11 100644
--- a/app/src/main/res/values-b+en+001/strings.xml
+++ b/app/src/main/res/values-b+en+001/strings.xml
@@ -95,7 +95,6 @@
<string name="simple_move">Move</string>
<string name="category_readonly">Read only</string>
<string name="simple_checkbox">Checkbox</string>
- <string name="simple_beta">Beta</string>
<!-- Array: note modes -->
<string-array name="noteMode_entries">
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index ea8943dd..614ae0a0 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -83,7 +83,6 @@
<string name="widget_create_note">Crea una nota</string>
<string name="widget_not_logged_in">Inicieu la sessió a l\'aplicació Notes abans d\'utilitzar aquest giny</string>
<string name="widget_entry_fav_contentDescription">La icona de l\'estrella serveix per a marcar un element com a preferit</string>
- <string name="widget_app_launcher_contentDescription">Inicia l\'aplicació</string>
<string name="activity_select_single_note">Seleccioneu una nota</string>
@@ -137,12 +136,10 @@
<string name="append_to_note">Afegeix a la nota</string>
<string name="change_note_title">Canvia el títol de la nota</string>
<string name="menu_edit_title">Edita el títol</string>
- <string name="settings_branding">Marca</string>
<string name="settings_gridview">Visualització de quadrícula</string>
<string name="simple_security">Seguretat</string>
<string name="appearance_and_behavior">Aparença i comportament</string>
<string name="simple_synchronization">Sincronització</string>
- <string name="simple_behavior">Comportament</string>
<string name="manage_accounts">Gestiona els comptes</string>
<string name="action_formatting_help">Format</string>
@@ -158,13 +155,6 @@
<item>Gran</item>
</string-array>
- <string-array name="sync_entries">
- <item>Desactivada</item>
- <item>15 minuts</item>
- <item>1 hora</item>
- <item>6 hores</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Clar</item>
<item>Fosc</item>
@@ -256,4 +246,4 @@
<string name="remove_account">Suprimeix %1$s</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Heu de tenir connexió a Internet per a afegir un compte.</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index a5252866..e1504137 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -52,6 +52,7 @@
<string name="error_sync">Synchronizace se nezdařila: %1$s</string>
<string name="error_synchronization">Synchronizace se nezdařila</string>
<string name="error_no_network">Žádné síťové spojení</string>
+ <string name="error_maintenance_mode">Na serveru probíhá údržba</string>
<string name="error_unknown">Došlo k neznámé chybě.</string>
<string name="about_version_title">Verze</string>
@@ -83,7 +84,6 @@
<string name="widget_create_note">Vytvořit poznámku</string>
<string name="widget_not_logged_in">Abyste mohli využívat tuto miniaplikaci plochy, přihlaste se do aplikace Poznámky</string>
<string name="widget_entry_fav_contentDescription">Ikonka hvězdičky slouží pro označení položky jako oblíbené</string>
- <string name="widget_app_launcher_contentDescription">Spustí aplikaci</string>
<string name="activity_select_single_note">Vybrat poznámku</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">Připojit k poznámce</string>
<string name="change_note_title">Změnit nadpis poznámky</string>
<string name="menu_edit_title">Upravit nadpis</string>
- <string name="settings_branding">Opatření vlastním logem</string>
<string name="settings_gridview">Zobrazení v mřížce</string>
<string name="simple_security">Zabezpečení</string>
<string name="appearance_and_behavior">Vzhled a chování</string>
<string name="simple_synchronization">Synchronizace</string>
- <string name="simple_behavior">Chování</string>
<string name="manage_accounts">Spravovat účty</string>
<string name="action_formatting_help">Formátování</string>
@@ -158,13 +156,6 @@
<item>Velké</item>
</string-array>
- <string-array name="sync_entries">
- <item>Vypnuto</item>
- <item>15 minut</item>
- <item>1 hodina</item>
- <item>6 hodin</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Světlý</item>
<item>Tmavý</item>
@@ -220,7 +211,7 @@
<string name="formatting_help_lists_body_8">Dát lajk</string>
<string name="formatting_help_lists_body_9">A toto</string>
- <string name="formatting_help_checkboxes_title">Zaškrtávací pole</string>
+ <string name="formatting_help_checkboxes_title">Zaškrtávací kolonky</string>
<string name="formatting_help_checkboxes_body_1">Pro vytvoření zaškrtávací kolonky použijte seznam následovaný závorkami</string>
<string name="formatting_help_checkboxes_body_2">Položka 1</string>
<string name="formatting_help_checkboxes_body_3">Položka 2</string>
@@ -266,4 +257,8 @@
<string name="remove_account">Odebrat %1$s</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Abyste mohli přidat účet je třeba, abyste byli připojení k Internetu.</string>
-</resources>
+ <string name="simple_next">Další</string>
+ <string name="simple_prev">Předchozí</string>
+ <string name="simple_backup">Zazálohovat</string>
+ <string name="simple_repair">Opravit</string>
+ </resources>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index a90c81e1..b2872dba 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -52,6 +52,7 @@
<string name="error_sync">Synchronisierung fehlgeschlagen: %1$s</string>
<string name="error_synchronization">Synchronisierung fehlgeschlagen</string>
<string name="error_no_network">Keine Netzwerkverbindung</string>
+ <string name="error_maintenance_mode">Server befindet sich im Wartungsmodus</string>
<string name="error_unknown">Unbekannter Fehler aufgetreten.</string>
<string name="about_version_title">Version</string>
@@ -83,7 +84,6 @@
<string name="widget_create_note">Notiz erstellen</string>
<string name="widget_not_logged_in">Bitte melden Sie sich bei Notes vor Nutzung des Widgets an</string>
<string name="widget_entry_fav_contentDescription">Das Stern-Symbol markiert den Eintrag als Favorit</string>
- <string name="widget_app_launcher_contentDescription">Startet die App</string>
<string name="activity_select_single_note">Notiz auswählen</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">An Notiz anhängen</string>
<string name="change_note_title">Notiz-Titel ändern</string>
<string name="menu_edit_title">Titel ändern</string>
- <string name="settings_branding">Branding</string>
<string name="settings_gridview">Kachelansicht</string>
<string name="simple_security">Sicherheit</string>
<string name="appearance_and_behavior">Aussehen und Verhalten</string>
<string name="simple_synchronization">Synchronisierung</string>
- <string name="simple_behavior">Verhalten</string>
<string name="manage_accounts">Konten verwalten</string>
<string name="action_formatting_help">Formatierung</string>
@@ -158,13 +156,6 @@
<item>Groß</item>
</string-array>
- <string-array name="sync_entries">
- <item>Aus</item>
- <item>15 Minuten</item>
- <item>1 Stunde</item>
- <item>6 Stunden</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Hell</item>
<item>Dunkel</item>
@@ -256,4 +247,8 @@
<string name="remove_account">%1$s entfernen</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Sie müssen mit dem Internet verbunden sein, um ein Konto hinzufügen zu können.</string>
-</resources>
+ <string name="simple_next">Weiter</string>
+ <string name="simple_prev">Vorheriges</string>
+ <string name="simple_backup">Sicherung</string>
+ <string name="simple_repair">Reparieren</string>
+ </resources>
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 10afe072..a3849fa5 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">Δημιουργία Σημείωσης</string>
<string name="widget_not_logged_in">Παρακαλούμε συνδεθείτε στις Σημειώσεις πρωτού να χρησιμοποιήσετε αυτό το γραφικό στοιχείο.</string>
<string name="widget_entry_fav_contentDescription">Το εικονίδιο Αστέρι χρησιμέυει για την σήμανση ως αγαπημένο</string>
- <string name="widget_app_launcher_contentDescription">Εκκινεί την εφαρμογή</string>
<string name="activity_select_single_note">Επιλογή σημείωσης</string>
@@ -130,10 +129,8 @@
<string name="append_to_note">Προσάρτηση στη σημείωση</string>
<string name="change_note_title">Αλλαγή τίτλου σημείωσης</string>
<string name="menu_edit_title">Επεξεργασία τίτλου</string>
- <string name="settings_branding">Επωνυμία</string>
<string name="simple_security">Ασφάλεια</string>
<string name="simple_synchronization">Συγχρονισμός</string>
- <string name="simple_behavior">Συμπεριφορά</string>
<string name="manage_accounts">Διαχείριση λογαριασμών</string>
<string name="action_formatting_help">Διαμόρφωση σε εξέλιξη</string>
@@ -149,13 +146,6 @@
<item>Μεγάλο</item>
</string-array>
- <string-array name="sync_entries">
- <item>Απενεργοποίηση </item>
- <item>15 λεπτά</item>
- <item>1 ώρα</item>
- <item>6 ώρες</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Φωτεινό</item>
<item>Σκούρο</item>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 4edaecf3..4e77a61f 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -52,6 +52,7 @@
<string name="error_sync">Fallo en la sincronización: %1$s</string>
<string name="error_synchronization">Fallo en la sincronización</string>
<string name="error_no_network">Sin conexión de red</string>
+ <string name="error_maintenance_mode">El servidor está en modo mantenimiento</string>
<string name="error_unknown">Ha ocurrido un error desconocido.</string>
<string name="about_version_title">Versión</string>
@@ -83,7 +84,6 @@
<string name="widget_create_note">Crear nota</string>
<string name="widget_not_logged_in">Por favor, inicia sesión en Notas antes de usar este widget</string>
<string name="widget_entry_fav_contentDescription">El icono de estrella se usa para indicar que el elemento es un favorito</string>
- <string name="widget_app_launcher_contentDescription">Lanza la app</string>
<string name="activity_select_single_note">Selecciona nota</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">Añadir a la nota</string>
<string name="change_note_title">Cambiar título de la nota</string>
<string name="menu_edit_title">Editar título</string>
- <string name="settings_branding">Marca</string>
<string name="settings_gridview">Vista en cuadrícula</string>
<string name="simple_security">Seguridad</string>
<string name="appearance_and_behavior">Apariencia y comportamiento</string>
<string name="simple_synchronization">Sincronización</string>
- <string name="simple_behavior">Comportamiento</string>
<string name="manage_accounts">Gestionar cuentas</string>
<string name="action_formatting_help">Formato</string>
@@ -158,13 +156,6 @@
<item>Grande</item>
</string-array>
- <string-array name="sync_entries">
- <item>Off</item>
- <item>15 minutos</item>
- <item>1 hora</item>
- <item>6 horas</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Claro</item>
<item>Oscuro</item>
@@ -256,4 +247,6 @@
<string name="remove_account">Eliminar %1$s</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Tienes que estar conectado a internet para poder añadir una cuenta.</string>
-</resources>
+ <string name="simple_next">Siguiente</string>
+ <string name="simple_prev">Anterior</string>
+ </resources>
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index c7d6a119..04d41b12 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -52,6 +52,7 @@
<string name="error_sync">Sinkronizazioak huts egin du: %1$s</string>
<string name="error_synchronization">Sinkronizazioak huts egin du</string>
<string name="error_no_network">Ez dago sare konexiorik</string>
+ <string name="error_maintenance_mode">Zerbitzaria mantentze moduan dago</string>
<string name="error_unknown">Errore ezezagun bat gertatu da.</string>
<string name="about_version_title">Bertsioa</string>
@@ -83,7 +84,6 @@
<string name="widget_create_note">Sortu oharra</string>
<string name="widget_not_logged_in">Hasi saioa oharrak aplikazioan trepeta hau erabili aurretik</string>
<string name="widget_entry_fav_contentDescription">Izarraren ikonoa elementu bat gogoko gisa adierazteko erabiltzen da</string>
- <string name="widget_app_launcher_contentDescription">Aplikazioa abiatzen du</string>
<string name="activity_select_single_note">Hautatu oharra</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">Gehitu notara</string>
<string name="change_note_title">Aldatu notaren titulua</string>
<string name="menu_edit_title">Editatu izenburua</string>
- <string name="settings_branding">Marka</string>
<string name="settings_gridview">Sareta ikuspegia</string>
<string name="simple_security">Segurtasuna</string>
<string name="appearance_and_behavior">Itxura eta portaera</string>
<string name="simple_synchronization">Sinkronizazioa</string>
- <string name="simple_behavior">Portaera</string>
<string name="manage_accounts">Kudeatu kontuak</string>
<string name="action_formatting_help">Formatua</string>
@@ -158,13 +156,6 @@
<item>Handia</item>
</string-array>
- <string-array name="sync_entries">
- <item>Off</item>
- <item>15 minutu</item>
- <item>Ordu 1</item>
- <item>6 ordu</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Argia</item>
<item>Iluna</item>
@@ -256,4 +247,7 @@
<string name="remove_account">%1$s kendu </string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Kontu bat gehitzeko internetera konektatuta egon behar zara.</string>
-</resources>
+ <string name="simple_next">Hurrengoa</string>
+ <string name="simple_prev">Aurrekoa</string>
+ <string name="simple_repair">Konpondu</string>
+ </resources>
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index c964dbf0..ed7ef469 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">ساختن یادداشت</string>
<string name="widget_not_logged_in">لطفاً قبل از استفاده از این ویجت، به یادداشت‌ها وارد شوید</string>
<string name="widget_entry_fav_contentDescription">از آیکون ستاره برای مشخص کردن یک مورد به عنوان مورد علاقه استفاده می‌شود</string>
- <string name="widget_app_launcher_contentDescription">برنامه راه اندازی می‌شود</string>
<string name="activity_select_single_note">انتخاب یادداشت</string>
@@ -130,10 +129,8 @@
<string name="append_to_note">به یادداشت اضافه کنید</string>
<string name="change_note_title">عوض‌کردن عنوان یادداشت</string>
<string name="menu_edit_title">ویرایش عنوان</string>
- <string name="settings_branding">شخصی‌سازی</string>
<string name="simple_security">امنیت</string>
<string name="simple_synchronization">همگام‌سازی</string>
- <string name="simple_behavior">رفتار</string>
<string name="manage_accounts">مدیریت حساب‌ها</string>
<string name="action_formatting_help">قالب‌بندی</string>
@@ -149,13 +146,6 @@
<item>بزرگ</item>
</string-array>
- <string-array name="sync_entries">
- <item>خاموش</item>
- <item>۱۵ دقیقه</item>
- <item>۱ ساعت</item>
- <item>۶ ساعت</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>روشن</item>
<item>تیره</item>
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index c256b480..38bc538b 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -12,6 +12,7 @@
<string name="action_sorting_method">Järjestämistapa</string>
<string name="simple_cancel">Peruuta</string>
<string name="simple_edit">Muokkaa</string>
+ <string name="simple_remove">Poista</string>
<string name="action_edit_save">Tallenna</string>
<string name="simple_about">Tietoja</string>
<string name="simple_link">Linkki</string>
@@ -42,10 +43,15 @@
<string name="settings_font_title">Tasalevyinen fontti</string>
<string name="settings_font_size">Fontin koko</string>
<string name="settings_wifi_only">Synkronoi vain wifi-yhteydellä</string>
+ <string name="settings_lock">Sovelluksen lukitus (Beta)</string>
+ <string name="settings_lock_summary">Laitteen valtuudet</string>
<string name="settings_background_sync">Taustasynkronointi</string>
+ <string name="settings_prevent_screen_capture">Estä ruudunkaappaus</string>
+
<string name="error_sync">Synkronointi epäonnistui: %1$s</string>
<string name="error_synchronization">Synkronointi epäonnistui</string>
<string name="error_no_network">Ei verkkoyhteyttä</string>
+ <string name="error_maintenance_mode">Palvelin on huoltotilassa</string>
<string name="error_unknown">Tapahtui tuntematon virhe.</string>
<string name="about_version_title">Versio</string>
@@ -77,7 +83,6 @@
<string name="widget_create_note">Luo muistiinpano</string>
<string name="widget_not_logged_in">Kirjaudu Notes sovellukseen ennen kuin käytät pienoissovellusta</string>
<string name="widget_entry_fav_contentDescription">Tähtikuvaketta käytetään suosikkikohteen ilmaisemiseen</string>
- <string name="widget_app_launcher_contentDescription">Käynnistää sovelluksen</string>
<string name="activity_select_single_note">Valitse muistiinpano</string>
@@ -104,12 +109,14 @@
<string name="simple_more">Lisää</string>
<string name="simple_move">Siirrä</string>
<string name="category_readonly">Vain luku</string>
+ <string name="no_category">Ei luokkaa</string>
<string name="add_category">Lisää %1$s</string>
<string name="simple_checkbox">Valintaruutu</string>
<string name="unlock_notes">Avaa muistiinpanojen lukitus</string>
<string name="error_dialog_title">Voi ei - Mitä nyt? 🙁</string>
<string name="error_dialog_tip_token_mismatch_retry">Yritä pakottaa sovelluksen sulkeminen ja käynnistä se uudelleen. Nextcloud-sovellukseen on saattanut olla virheellinen yhteys.</string>
<string name="error_dialog_tip_token_mismatch_clear_storage">Jos ongelma jatkuu, yritä tyhjentää sekä Nextcloud- että Nextcloud Notes-sovellusten tallennustilat ratkaistaksesi ongelman.</string>
+ <string name="error_dialog_tip_clear_storage">Voit tyhjentää tallennustilan avaamalla sovelluksen tiedot ja valitsemalla Tallennustila → Tyhjennä tallennustila. Tämä poistaa kaikki synkronoimattomat muistiinpanot!</string>
<string name="error_dialog_tip_files_outdated">Nextcloud-sovelluksesi on vanhentunut. Lataa uusin version Play Storesta tai F-Droidista.</string>
<string name="error_dialog_tip_files_force_stop">Nextcloud-sovelluksessasi vaikuttaa olevan jokin ongelma. Yritä pakottaa sekä Nextcloud- että Nextcloud Notes-sovelluksen lopetus.</string>
<string name="error_dialog_tip_files_delete_storage">Jos pakotettu lopettaminen ei auta, voit yrittää tyhjentää sovelluksien tallennustilan.</string>
@@ -129,10 +136,10 @@
<string name="append_to_note">Lisää muistiinpanoon</string>
<string name="change_note_title">Muuta muistinpanon otsikkoa</string>
<string name="menu_edit_title">Muokkaa otsikkoa</string>
- <string name="settings_branding">Brändäys</string>
+ <string name="settings_gridview">Ruudukkonäkymä</string>
<string name="simple_security">Tietoturva</string>
+ <string name="appearance_and_behavior">Ulkoasu ja toiminta</string>
<string name="simple_synchronization">Synkronointi</string>
- <string name="simple_behavior">Toiminta</string>
<string name="manage_accounts">Tilien hallinta</string>
<string name="action_formatting_help">Muotoilu</string>
@@ -148,13 +155,6 @@
<item>Suuri</item>
</string-array>
- <string-array name="sync_entries">
- <item>Pois</item>
- <item>15 minuuttia</item>
- <item>1 tunti</item>
- <item>6 tuntia</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Vaalea</item>
<item>Tumma</item>
@@ -182,6 +182,7 @@
<string name="formatting_help_codefence_inline" translateable="false">`%1$s`</string>
<string name="formatting_help_codefence_inline_escaped" translateable="false">\\`%1$s\\`</string>
<string name="formatting_help_codefence" translateable="false">```</string>
+ <string name="formatting_help_codefence_outer" translateable="false">````</string>
<string name="formatting_help_codefence_javascript" translateable="false">```javascript</string>
<string name="formatting_help_cbf_title">Kontekstiin perustuva muotoilu</string>
<string name="formatting_help_cbf_body_1">Muistiinpanosovelluksen päätavoitteena on tarjota häiriötön työkalu. Voit silti muotoilla tekstiä Markdown-syntaksilla. Moniin alla mainittuihin esimerkkeihin on pikanäppäimet, jotta voit kirjoittaa muistiinpanoja ilman muotoilumerkkien kirjoittamista.</string>
@@ -219,11 +220,29 @@
<string name="formatting_help_code_body_2">Markdown tukee myös koodin aitaamista, jolla useamman rivin koodi näytetään sisentämättömänä:</string>
<string name="formatting_help_code_body_3">Ja jos haluat käyttää syntaksin korostusta, sisällytä ohjelmointikieli:</string>
+ <string name="formatting_help_tables_title">Taulukot</string>
+ <!-- Column header of a sample table -->
+ <string name="formatting_help_tables_column">Sarake %1d</string>
+ <!-- Table cell value of a sample table -->
+ <string name="formatting_help_tables_value">Arvo %1d</string>
+
+ <string name="formatting_help_images_title">Kuvat</string>
<string name="simple_other">Muu</string>
<string name="sort_last_modified">Järjestä muokkauspäiväyksen mukaan</string>
<string name="sort_alphabetically">Järjestä aakkosjärjestyksen mukaan</string>
<string name="error_action_open_battery_settings">Akkuasetukset</string>
<string name="error_action_open_deck_info">Avaa sovelluksen tiedot</string>
<string name="error_action_open_network">Verkkoasetukset</string>
+ <string name="no_account_configured_yet">Tiliä ei ole vielä asetettu</string>
<string name="no_other_accounts">Et ole vielä määrittänyt muita tilejä.</string>
+ <string name="choose_account">Valitse tili</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">Tilin %1$s poistamalla poistetaan myös yksi synkronoimaton muutos, jota ei voida jälkeenpäin palauttaa.</item>
+ <item quantity="other">Tilin %1$s poistamalla poistetaan myös %2$d synkronoimatonta muutosta, joita ei voida jälkeenpäin palauttaa.</item>
+ </plurals>
+ <string name="remove_account">Poista %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Sinun tulee olla yhteydessä internetiin, jotta voit lisätä tilin.</string>
+ <string name="simple_next">Seuraava</string>
+ <string name="simple_prev">Edellinen</string>
</resources>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 6dec8b43..64485bc5 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -12,6 +12,7 @@
<string name="action_sorting_method">Méthode de tri</string>
<string name="simple_cancel">Annuler</string>
<string name="simple_edit">Modifier</string>
+ <string name="simple_remove">Supprimer</string>
<string name="action_edit_save">Enregistrer</string>
<string name="simple_about">À propos</string>
<string name="simple_link">Lien</string>
@@ -42,10 +43,15 @@
<string name="settings_font_title">Texte monospace</string>
<string name="settings_font_size">Taille des caractères</string>
<string name="settings_wifi_only">Synchroniser uniquement en Wifi</string>
+ <string name="settings_lock">Verrouillage de l\'application (Bêta)</string>
+ <string name="settings_lock_summary">Informations d\'identiication du terminal</string>
<string name="settings_background_sync">Synchronisation en tâche de fond</string>
+ <string name="settings_prevent_screen_capture">Empêcher la capture d\'écran</string>
+
<string name="error_sync">Échec de synchronisation : %1$s</string>
<string name="error_synchronization">Échec de synchronisation</string>
<string name="error_no_network">Aucune connexion réseau</string>
+ <string name="error_maintenance_mode">Le serveur est en mode maintenance</string>
<string name="error_unknown">Une erreur inconnue est survenue</string>
<string name="about_version_title">Version</string>
@@ -77,7 +83,6 @@
<string name="widget_create_note">Créer une note</string>
<string name="widget_not_logged_in">Veuillez vous connecter à Notes avant d\'utiliser ce widget</string>
<string name="widget_entry_fav_contentDescription">L\'icône étoile est utilisée pour désigner un élément comme favori</string>
- <string name="widget_app_launcher_contentDescription">Démarre l\'application</string>
<string name="activity_select_single_note">Choisir une note</string>
@@ -131,10 +136,10 @@
<string name="append_to_note">Ajouter à la note</string>
<string name="change_note_title">Modifier le titre de la note</string>
<string name="menu_edit_title">Modifier le titre</string>
- <string name="settings_branding">marque</string>
+ <string name="settings_gridview">Affichage grille</string>
<string name="simple_security">Sécurité</string>
+ <string name="appearance_and_behavior">Apparence et comportement</string>
<string name="simple_synchronization">Synchronisation</string>
- <string name="simple_behavior">Comportement</string>
<string name="manage_accounts">Gérer les comptes</string>
<string name="action_formatting_help">Formatage</string>
@@ -150,13 +155,6 @@
<item>Grand</item>
</string-array>
- <string-array name="sync_entries">
- <item>Éteint</item>
- <item>15 minutes</item>
- <item>1 heure</item>
- <item>6 heures</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Clair</item>
<item>Sombre</item>
@@ -239,5 +237,7 @@
<string name="error_action_open_network">Paramètres réseau</string>
<string name="no_account_configured_yet">Aucun compte n\'est encore configuré</string>
<string name="no_other_accounts">Vous n\'avez pas encore configuré d\'autres comptes.</string>
+ <string name="choose_account">Sélectionnez un compte</string>
<string name="context_based_formatting">Suggestions de mise en forme contextuelle</string>
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Vous devez être connecté à internet pour ajouter un compte.</string>
</resources>
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 8f1b72df..10d80313 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -81,7 +81,6 @@
<string name="widget_create_note">Crear unha nota</string>
<string name="widget_not_logged_in">Acceda a Notas antes de empregar este trebello</string>
<string name="widget_entry_fav_contentDescription">A icona de estrela empregase para indicar que o elemento é un favorito</string>
- <string name="widget_app_launcher_contentDescription">Inicia a apli</string>
<string name="activity_select_single_note">Seleccionar unha nota</string>
@@ -135,12 +134,10 @@
<string name="append_to_note">Anexo á nota</string>
<string name="change_note_title">Cambiar o título da nota</string>
<string name="menu_edit_title">Editar o título</string>
- <string name="settings_branding">Xestión da marca</string>
<string name="settings_gridview">Ver como grella</string>
<string name="simple_security">Seguridade</string>
<string name="appearance_and_behavior">Aparencia e comportamento</string>
<string name="simple_synchronization">Sincronización</string>
- <string name="simple_behavior">Comportamento</string>
<string name="manage_accounts">Xestionar contas</string>
<string name="action_formatting_help">Formatado</string>
@@ -156,13 +153,6 @@
<item>Grande</item>
</string-array>
- <string-array name="sync_entries">
- <item>Apagado</item>
- <item>15 minutos</item>
- <item>1 hora</item>
- <item>6 horas</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Claro</item>
<item>Escuro</item>
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index 53ac6e35..31f17170 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">יצירת פתק</string>
<string name="widget_not_logged_in">נא להיכנס לפתקים לפני השימוש בחלונית הזאת</string>
<string name="widget_entry_fav_contentDescription">סמל הכוכב משמש לציון פריט כמועדף</string>
- <string name="widget_app_launcher_contentDescription">יישומון שיגור</string>
<string name="activity_select_single_note">נא לבחור פתק</string>
@@ -118,10 +117,8 @@
<string name="shared_text_empty">הטקסט ששותף היה ריק</string>
<string name="append_to_note">הוספה לסוף הפתק</string>
<string name="menu_edit_title">עריכת כותרת</string>
- <string name="settings_branding">מיתוג</string>
<string name="simple_security">אבטחה</string>
<string name="simple_synchronization">סנכרון</string>
- <string name="simple_behavior">התנהגות</string>
<string name="manage_accounts">ניהול חשבונות</string>
<string name="action_formatting_help">עיצוב</string>
@@ -137,13 +134,6 @@
<item>גדול</item>
</string-array>
- <string-array name="sync_entries">
- <item>כבוי</item>
- <item>15 דקות</item>
- <item>שעה</item>
- <item>6 שעות</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>בהירה</item>
<item>כהה</item>
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 51018133..b0ecc6d1 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -79,7 +79,6 @@
<string name="widget_create_note">Stvori bilješku</string>
<string name="widget_not_logged_in">Prijavite se u aplikaciju Bilješke prije korištenja ovog widgeta</string>
<string name="widget_entry_fav_contentDescription">Ikona zvjezdice upotrebljava se za označavanje stavke kao favorita</string>
- <string name="widget_app_launcher_contentDescription">Pokreće aplikaciju</string>
<string name="activity_select_single_note">Odaberi bilješku</string>
@@ -133,11 +132,9 @@
<string name="append_to_note">Dodaj bilješci</string>
<string name="change_note_title">Promijeni naslov bilješke</string>
<string name="menu_edit_title">Uredi naslov</string>
- <string name="settings_branding">Brendiranje</string>
<string name="settings_gridview">Prikaz rešetke</string>
<string name="simple_security">Sigurnost</string>
<string name="simple_synchronization">Sinkronizacija</string>
- <string name="simple_behavior">Ponašanje</string>
<string name="manage_accounts">Upravljaj računima</string>
<string name="action_formatting_help">Oblikovanje</string>
@@ -153,13 +150,6 @@
<item>Veliki</item>
</string-array>
- <string-array name="sync_entries">
- <item>Isključeno</item>
- <item>15 minuta</item>
- <item>1 sat</item>
- <item>6 sati</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Svijetlo</item>
<item>Tamno</item>
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index 41bfe6c1..d1adce14 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Minden jegyzet</string>
<string name="label_favorites">Kedvencek</string>
<string name="action_create">Új jegyzet</string>
+ <string name="welcome_text">Üdvözli a %1$s</string>
<string name="action_settings">Beállítások</string>
<string name="action_trashbin">Törölt jegyzetek</string>
<string name="action_search">Keresés</string>
<string name="action_sorting_method">Rendezési mód</string>
<string name="simple_cancel">Mégse</string>
<string name="simple_edit">Szerkesztés</string>
+ <string name="simple_remove">Eltávolítás</string>
<string name="action_edit_save">Mentés</string>
<string name="simple_about">Névjegy</string>
<string name="simple_link">Hivatkozás</string>
@@ -50,6 +52,7 @@
<string name="error_sync">Szinkronizálás sikertelen: %1$s</string>
<string name="error_synchronization">Szinkronizálás sikertelen</string>
<string name="error_no_network">Nincs hálózati kapcsolat</string>
+ <string name="error_maintenance_mode">A kiszolgáló karbantartási módban van</string>
<string name="error_unknown">Ismeretlen hiba történt.</string>
<string name="about_version_title">Verzió</string>
@@ -81,7 +84,6 @@
<string name="widget_create_note">Jegyzet létrehozása</string>
<string name="widget_not_logged_in">Jelentkezzen be a Jegyzetekbe mielőtt ezt a modult használná</string>
<string name="widget_entry_fav_contentDescription">A csillag ikon az elem kedvencként megjelölésére szolgál</string>
- <string name="widget_app_launcher_contentDescription">Elindítja az alkalmazást</string>
<string name="activity_select_single_note">Válasszon jegyzetet</string>
@@ -135,12 +137,10 @@
<string name="append_to_note">Hozzáfűzés a jegyzethez</string>
<string name="change_note_title">Jegyzet címének módosítása</string>
<string name="menu_edit_title">Cím szerkesztése</string>
- <string name="settings_branding">Márkázas</string>
<string name="settings_gridview">Rács nézet</string>
<string name="simple_security">Biztonság</string>
<string name="appearance_and_behavior">Megjelenés és viselkedés</string>
<string name="simple_synchronization">Szinkronizálás</string>
- <string name="simple_behavior">Viselkedés</string>
<string name="manage_accounts">Fiókok kezelése</string>
<string name="action_formatting_help">Formázás</string>
@@ -156,13 +156,6 @@
<item>Nagy</item>
</string-array>
- <string-array name="sync_entries">
- <item>Ki</item>
- <item>15 perc</item>
- <item>1 óra</item>
- <item>6 óra</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Világos</item>
<item>Sötét</item>
@@ -194,7 +187,7 @@
<string name="formatting_help_codefence_javascript" translateable="false">```javascript</string>
<string name="formatting_help_cbf_title">Környezetfüggő formázás</string>
<string name="formatting_help_cbf_body_1">A Jegyzetek alkalmazás egyik célkitűzése, hogy egyszerű legyen, és ne vonja el a figyelmet. Viszont Markdownnal formázhatja a szövegeket. A lent említett példák némelyikénél rövidítéseket is használhat, így anélkül formázhatja meg a jegyzeteket, hogy beírná a lenti kódokat.</string>
- <string name="formatting_help_cbf_body_2">Csak válasszon ki egy szövegtartományt, vagy érintse meg a kurzort bármely helyen, és megjelenik egy előugró menü, amely az alapértelmezett bejegyzések mellett %1$s, %2$s, %3$s tartalmaz olyan bejegyzéseket, mint %4$s vagy %5$s.</string>
+ <string name="formatting_help_cbf_body_2">Csak válasszon ki egy szövegtartományt, vagy érintse meg bárhol a kurzort, és megjelenik egy előugró menü, amely az alapértelmezett %1$s, %2$s, %3$s bejegyzések mellett ezeket is tartalmazza: %4$s, %5$s.</string>
<string name="formatting_help_text_title">Szöveg</string>
<string name="formatting_help_text_body">Nagyon egyszerűen írhat Markdownnal %1$sfélkövéren%1$s, valamint %2$sdőlten%2$s. Át is %3$shúzhat%3$s szavakat, valamint [hivatkozhat a Nextcloudra](https://nextcloud.com).</string>
@@ -245,5 +238,15 @@
<string name="error_action_open_network">Hálózati beállítások</string>
<string name="no_account_configured_yet">Nincs még fiók beállítva</string>
<string name="no_other_accounts">Még egyetlen más fiókot sem állított be.</string>
+ <string name="choose_account">Válasszon fiókot</string>
<string name="context_based_formatting">Környezetfüggő formázási felbukkanó menü</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">A(z) %1$s fiók eltávolítása véglegesen töröl egy nem szinkronizált változtatást.</item>
+ <item quantity="other">A(z) %1$s fiók eltávolítása véglegesen töröl %2$d nem szinkronizált változtatást.</item>
+ </plurals>
+ <string name="remove_account">%1$s eltávolítása</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Internetkapcsolatra van szükség, hogy fiókot adjon hozzá.</string>
+ <string name="simple_next">Következő</string>
+ <string name="simple_prev">Előző</string>
</resources>
diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml
index ce6e10f1..b411997b 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -102,7 +102,6 @@
<string name="no_category">Enginn flokkur</string>
<string name="add_category">Bæta við %1$s</string>
<string name="simple_checkbox">Hakreitur</string>
- <string name="simple_beta">BETA-prófunarútgáfa</string>
<!-- Array: note modes -->
<string-array name="noteMode_entries">
<item>Opna í breytingaham</item>
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 41c06489..ddb4e6be 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Tutte le note</string>
<string name="label_favorites">Preferite</string>
<string name="action_create">Nuova nota</string>
+ <string name="welcome_text">Benvenuti in %1$s</string>
<string name="action_settings">Impostazioni</string>
<string name="action_trashbin">Note eliminate</string>
<string name="action_search">Cerca</string>
<string name="action_sorting_method">Metodo di ordinamento</string>
<string name="simple_cancel">Annulla</string>
<string name="simple_edit">Modifica</string>
+ <string name="simple_remove">Rimuovi</string>
<string name="action_edit_save">Salva</string>
<string name="simple_about">Informazioni</string>
<string name="simple_link">Collegamento</string>
@@ -50,6 +52,7 @@
<string name="error_sync">Sincronizzazione non riuscita: %1$s</string>
<string name="error_synchronization">Sincronizzazione non riuscita</string>
<string name="error_no_network">Nessuna connessione di rete</string>
+ <string name="error_maintenance_mode">Il server è in modalità di manutenzione</string>
<string name="error_unknown">Si è verificato un errore sconosciuto.</string>
<string name="about_version_title">Versione</string>
@@ -81,7 +84,6 @@
<string name="widget_create_note">Crea nota</string>
<string name="widget_not_logged_in">Accedi a Notes prima di utilizzare questo widget</string>
<string name="widget_entry_fav_contentDescription">L\'icona della stella è utilizzata per marcare un elemento come preferito</string>
- <string name="widget_app_launcher_contentDescription">Avvia l\'applicazione</string>
<string name="activity_select_single_note">Seleziona nota</string>
@@ -135,12 +137,10 @@
<string name="append_to_note">Aggiungi a nota</string>
<string name="change_note_title">Cambia titolo della nota</string>
<string name="menu_edit_title">Modifica titolo</string>
- <string name="settings_branding">Marchio</string>
<string name="settings_gridview">Vista griglia</string>
<string name="simple_security">Sicurezza</string>
<string name="appearance_and_behavior">Aspetto e comportamento</string>
<string name="simple_synchronization">Sincronizzazione</string>
- <string name="simple_behavior">Comportamento</string>
<string name="manage_accounts">Gestisci account</string>
<string name="action_formatting_help">Formattazione</string>
@@ -156,13 +156,6 @@
<item>Grande</item>
</string-array>
- <string-array name="sync_entries">
- <item>Spenta</item>
- <item>15 minuti</item>
- <item>1 ora</item>
- <item>6 ore</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Chiaro</item>
<item>Scuro</item>
@@ -245,5 +238,17 @@
<string name="error_action_open_network">Impostazioni di rete</string>
<string name="no_account_configured_yet">Ancora nessun account configurato</string>
<string name="no_other_accounts">Non hai configurato ancora alcun account.</string>
+ <string name="choose_account">Scegli account</string>
<string name="context_based_formatting">Finestra di formattazione basata sul contesto</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">La rimozione dell\'account %1$s eliminerà irrevocabilmente anche una modifica non sincronizzata.</item>
+ <item quantity="other">La rimozione dell\'account %1$s eliminerà irrevocabilmente anche %2$d modifiche non sincronizzate.</item>
+ </plurals>
+ <string name="remove_account">Rimuovi %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Devi essere connesso a Internet per aggiungere un account.</string>
+ <string name="simple_next">Successivo</string>
+ <string name="simple_prev">Precedente</string>
+ <string name="simple_backup">Backup</string>
+ <string name="simple_repair">Ripara</string>
</resources>
diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml
index e0f60ef9..90a9c681 100644
--- a/app/src/main/res/values-ja-rJP/strings.xml
+++ b/app/src/main/res/values-ja-rJP/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">すべてのノート</string>
<string name="label_favorites">お気に入り</string>
<string name="action_create">新しいノート</string>
+ <string name="welcome_text">%1$sへようこそ</string>
<string name="action_settings">設定</string>
<string name="action_trashbin">ゴミ箱</string>
<string name="action_search">検索</string>
<string name="action_sorting_method">並べ替え方法</string>
<string name="simple_cancel">キャンセル</string>
<string name="simple_edit">編集</string>
+ <string name="simple_remove">削除</string>
<string name="action_edit_save">保存</string>
<string name="simple_about">バージョン情報</string>
<string name="simple_link">リンク</string>
@@ -42,7 +44,10 @@
<string name="settings_font_title">モノスペースフォント</string>
<string name="settings_font_size">フォントサイズ</string>
<string name="settings_wifi_only">Wi-Fi接続時のみ同期する</string>
+ <string name="settings_lock">アプリロック(ベータ)</string>
<string name="settings_background_sync">バックグラウンド同期</string>
+ <string name="settings_prevent_screen_capture">スクリーンショットを防止</string>
+
<string name="error_sync">同期に失敗しました: %1$s</string>
<string name="error_synchronization">同期に失敗</string>
<string name="error_no_network">ネットワークに接続されていません</string>
@@ -77,7 +82,6 @@
<string name="widget_create_note">ノートを作成</string>
<string name="widget_not_logged_in">このウィジェットを使用する前に、ノートにログインしてください</string>
<string name="widget_entry_fav_contentDescription">スターアイコンは、アイテムをお気に入りとして示すために使用されます</string>
- <string name="widget_app_launcher_contentDescription">アプリを起動する</string>
<string name="activity_select_single_note">ノートを選択</string>
@@ -114,26 +118,24 @@
<string name="error_dialog_tip_files_outdated">Nextcloudアプリが古いようです。プレイストアかF-Droidから最新版を入手してください。</string>
<string name="error_dialog_tip_files_force_stop">Nextcloudアプリで何か不具合が発生しました。NextcloudアプリとNextcloud Notesアプリの両方を強制終了してください。</string>
<string name="error_dialog_tip_files_delete_storage">強制終了で改善されない場合は、両方のアプリのストレージをクリアしてみてください。</string>
- <string name="error_dialog_timeout_instance">一定時間内にサーバからの反応がありませんでした。サーバが正常に動いているか確認してください。</string>
+ <string name="error_dialog_timeout_instance">一定時間内にサーバーからの反応がありませんでした。サーバーが正常に動いているか確認してください。</string>
<string name="error_dialog_timeout_toggle">ネットワーク接続を確認してください。モバイルデータやWi-Fiのオフ/オンを繰り返してみるとうまくいく場合があります。</string>
<string name="error_dialog_check_server">サーバーからの反応が不正です。Webインターフェースからnotesにアクセス出来るか確認してください。</string>
<string name="error_dialog_check_server_logs">Nextcloudのセットアップに問題があります。サーバーのログファイルを確認してください。</string>
- <string name="error_dialog_check_maintenance">あなたのNextcloudサーバがメンテナンスモード中かどうか確認してください。</string>
- <string name="error_dialog_insufficient_storage">あなたのNextcloudサーバのストレージに空き容量がありません。ローカルの変更をクラウドに同期するにはいくつかファイルを削除してください。</string>
+ <string name="error_dialog_check_maintenance">Nextcloudサーバーがメンテナンスモード中かどうか確認してください。</string>
+ <string name="error_dialog_insufficient_storage">Nextcloudサーバーのストレージに空き容量がありません。ローカルの変更をクラウドに同期するには一部のファイルを削除してください。</string>
<string name="error_dialog_contact_us">問題が継続する場合はお問い合わせください。我々の連絡先はサイドバーのaboutセクションにかかれています。</string>
<string name="error_dialog_we_need_info">サポートには下記技術情報が必要です:</string>
<string name="error_dialog_server_app_enabled">ご利用のサーバーに ”Notes” プラグインアプリがインストールされ、有効になっていることを確認してください。</string>
- <string name="error_dialog_redirect">サーバがHTTPステータスコード 302を返しました。これはNotesアプリがインストールされていないか、構成が誤っていることを意味します。.htaccessファイルのカスタムオーバーライドやOIDクライアントのようなNextcloudアプリが原因の可能性があります。</string>
+ <string name="error_dialog_redirect">サーバーがHTTPステータスコード 302を返しました。これはNotesアプリがインストールされていないか、構成が誤っていることを意味します。.htaccessファイルのカスタムオーバーライドやOIDクライアントのようなNextcloudアプリが原因の可能性があります。</string>
<string name="error_dialog_tip_disable_battery_optimizations">NextcloudとNotesアプリに対してバッテリー最適化をすべて無効にしてください。</string>
<string name="added_content">\"%1$s\"に追加</string>
<string name="shared_text_empty">共有テキストは空です</string>
<string name="append_to_note">ノートに追記</string>
<string name="change_note_title">ノートタイトルを変更</string>
<string name="menu_edit_title">タイトルを編集</string>
- <string name="settings_branding">ブランディング</string>
<string name="simple_security">セキュリティ</string>
<string name="simple_synchronization">同期</string>
- <string name="simple_behavior">表示形式</string>
<string name="manage_accounts">アカウント管理</string>
<string name="action_formatting_help">フォーマット</string>
@@ -149,17 +151,10 @@
<item>大</item>
</string-array>
- <string-array name="sync_entries">
- <item>オフ</item>
- <item>15 分</item>
- <item>1時間</item>
- <item>6 時間</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>明るい</item>
<item>暗い</item>
- <item>既定</item>
+ <item>システムのデフォルト</item>
</string-array>
<plurals name="ab_selected">
@@ -216,6 +211,8 @@
<string name="formatting_help_code_body_2">Markdownは、コードフェンシングと呼ばれるものもサポートしています。これにより、インデントなしで複数行に適用できます:</string>
<string name="formatting_help_code_body_3">言語毎の構文の強調表示を使用する場合は、言語指定します:</string>
+ <string name="formatting_help_tables_title">テーブル</string>
+ <string name="formatting_help_images_title">画像</string>
<string name="simple_other">その他</string>
<string name="sort_last_modified">更新日付でソート</string>
<string name="sort_alphabetically">アルファベット順にソート</string>
@@ -223,4 +220,7 @@
<string name="error_action_open_deck_info">アプリ情報を開く</string>
<string name="error_action_open_network">ネットワーク設定</string>
<string name="no_other_accounts">他のアカウントはまだ設定されていません。</string>
+ <string name="choose_account">アカウントを選択</string>
+ <string name="remove_account">%1$sを削除</string>
+
</resources>
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 17ec0dfd..e27657f2 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">새 메모</string>
<string name="widget_not_logged_in">이 위젯을 사용하려면 먼저 로그인하십시오</string>
<string name="widget_entry_fav_contentDescription">스타 아이콘은 항목을 즐겨찾기로 표시하는데 사용됩니다.</string>
- <string name="widget_app_launcher_contentDescription">앱 실행</string>
<string name="activity_select_single_note">메모 선택</string>
@@ -130,10 +129,8 @@
<string name="append_to_note">메모에 추가</string>
<string name="change_note_title">메모 제목 변경</string>
<string name="menu_edit_title">제목 수정</string>
- <string name="settings_branding">브랜딩</string>
<string name="simple_security">보안</string>
<string name="simple_synchronization">동기화</string>
- <string name="simple_behavior">행동</string>
<string name="manage_accounts">계정 관리</string>
<string name="action_formatting_help">서식</string>
@@ -149,13 +146,6 @@
<item>크게</item>
</string-array>
- <string-array name="sync_entries">
- <item>꺼짐</item>
- <item>15분</item>
- <item>1시간</item>
- <item>6시간</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>밝게</item>
<item>어둡게</item>
diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml
index ae481196..058fc7ec 100644
--- a/app/src/main/res/values-lt-rLT/strings.xml
+++ b/app/src/main/res/values-lt-rLT/strings.xml
@@ -76,7 +76,6 @@
<string name="widget_create_note">Sukurti užrašus</string>
<string name="widget_not_logged_in">Prieš naudodami šį valdiklį, prisijunkite prie Užrašų</string>
<string name="widget_entry_fav_contentDescription">Žvaigždės piktograma yra naudojama pažymėti elementą kaip mėgstamą</string>
- <string name="widget_app_launcher_contentDescription">Paleidžia programėlę</string>
<string name="activity_select_single_note">Pasirinkti užrašus</string>
@@ -129,10 +128,8 @@
<string name="append_to_note">Pridėti prie užrašų</string>
<string name="change_note_title">Pakeisti užrašo pavadinimą</string>
<string name="menu_edit_title">Redaguoti pavadinimą</string>
- <string name="settings_branding">Prekės ženklas</string>
<string name="simple_security">Saugumas</string>
<string name="simple_synchronization">Sinchronizacija</string>
- <string name="simple_behavior">Funkcionavimas</string>
<string name="manage_accounts">Tvarkyti paskyras</string>
<string name="action_formatting_help">Formatavimas</string>
@@ -148,13 +145,6 @@
<item>Didelis</item>
</string-array>
- <string-array name="sync_entries">
- <item>Išjungta</item>
- <item>15 minučių</item>
- <item>1 valanda</item>
- <item>6 valandos</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Šviesus</item>
<item>Tamsus</item>
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index ee751ac1..2397c5bf 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">Opprett notat</string>
<string name="widget_not_logged_in">Logg inn i Notater før du bruker dette miniprogrammet</string>
<string name="widget_entry_fav_contentDescription">Stjerne ikon er brukt for å angi et element som en favoritt</string>
- <string name="widget_app_launcher_contentDescription">Åpner appen</string>
<string name="activity_select_single_note">Velg notat</string>
@@ -130,10 +129,8 @@
<string name="append_to_note">Legg til i notat</string>
<string name="change_note_title">Endre tittel på notat</string>
<string name="menu_edit_title">Rediger tittel</string>
- <string name="settings_branding">Branding</string>
<string name="simple_security">Sikkerhet</string>
<string name="simple_synchronization">Synkronisering</string>
- <string name="simple_behavior">Atferd</string>
<string name="manage_accounts">Håndter kontoer</string>
<string name="action_formatting_help">Formatering</string>
@@ -149,13 +146,6 @@
<item>Stor</item>
</string-array>
- <string-array name="sync_entries">
- <item>ett kvarter</item>
- <item>15 minuter</item>
- <item>1 time</item>
- <item>6 timer</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Lys</item>
<item>Mørk</item>
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index eda73204..2717e0cb 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -23,5 +23,4 @@
<color name="widget_background">#dd000000</color>
<color name="widget_foreground">#d8d8d8</color>
- <color name="widget_foreground_strong">#f5f5f5</color>
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 802b553b..ee9e74d3 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Alle notities</string>
<string name="label_favorites">Favorieten</string>
<string name="action_create">Nieuwe notitie</string>
+ <string name="welcome_text">Welkom bij %1$s</string>
<string name="action_settings">Instellingen</string>
<string name="action_trashbin">Verwijder notities</string>
<string name="action_search">Zoeken</string>
<string name="action_sorting_method">Sorteer methode</string>
<string name="simple_cancel">Annuleren</string>
<string name="simple_edit">Bewerken</string>
+ <string name="simple_remove">Verwijderen</string>
<string name="action_edit_save">Opslaan</string>
<string name="simple_about">Over</string>
<string name="simple_link">Link</string>
@@ -50,6 +52,7 @@
<string name="error_sync">Synchronisatie mislukt: %1$s</string>
<string name="error_synchronization">Synchronisatie mislukt</string>
<string name="error_no_network">Geen netwerkverbinding</string>
+ <string name="error_maintenance_mode">Server in onderhoudsmodus</string>
<string name="error_unknown">Een onbekende fout trad op.</string>
<string name="about_version_title">Versie</string>
@@ -81,7 +84,6 @@
<string name="widget_create_note">Maak notitie</string>
<string name="widget_not_logged_in">Login bij Notities voordat je deze widget gebruikt</string>
<string name="widget_entry_fav_contentDescription">\'Ster\'pictogram gebruikt om object als favoriet te markeren</string>
- <string name="widget_app_launcher_contentDescription">Start app</string>
<string name="activity_select_single_note">Notitie selecteren</string>
@@ -135,12 +137,10 @@
<string name="append_to_note">Achteraan aan notitie toevoegen</string>
<string name="change_note_title">Notitie titel wijzigen</string>
<string name="menu_edit_title">Titel bewerken</string>
- <string name="settings_branding">Brandmerken</string>
<string name="settings_gridview">Rasterweergave</string>
<string name="simple_security">Beveiliging</string>
<string name="appearance_and_behavior">Uiterlijk en gedrag</string>
<string name="simple_synchronization">Synchronisatie</string>
- <string name="simple_behavior">Gedrag</string>
<string name="manage_accounts">Accounts beheren </string>
<string name="action_formatting_help">Opmaak</string>
@@ -156,13 +156,6 @@
<item>Groot</item>
</string-array>
- <string-array name="sync_entries">
- <item>Uit</item>
- <item>15 minuten</item>
- <item>1 uur</item>
- <item>6 uur</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Licht</item>
<item>Donker</item>
@@ -245,5 +238,17 @@
<string name="error_action_open_network">Netwerk instellingen</string>
<string name="no_account_configured_yet">Nog geen account geconfigureerd</string>
<string name="no_other_accounts">U heeft nog geen andere accounts geconfigureerd.</string>
+ <string name="choose_account">Kies account</string>
<string name="context_based_formatting">Context gebaseerde formatterings-popover</string>
+ <plurals name="remove_account_message">
+ <item quantity="one">Als je het %1$s account verwijdert, wordt ook één onherstelbare niet-gesynchroniseerde wijzigingen verwijderd.</item>
+ <item quantity="other">Als je het %1$s account verwijdert, worden ook %2$d onherstelbare niet-gesynchroniseerde wijzigingen verwijderd.</item>
+ </plurals>
+ <string name="remove_account">Verwijder %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Je moet verbonden zijn met het Internet om een account toe te voegen.</string>
+ <string name="simple_next">Volgende</string>
+ <string name="simple_prev">Vorige</string>
+ <string name="simple_backup">Back-up</string>
+ <string name="simple_repair">Herstel</string>
</resources>
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index cd513c01..9dde150a 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -52,6 +52,7 @@
<string name="error_sync">Synchronizacja nie powiodła się: %1$s</string>
<string name="error_synchronization">Synchronizacja nie powiodła się</string>
<string name="error_no_network">Brak połączenia sieciowego</string>
+ <string name="error_maintenance_mode">Serwer jest w trybie konserwacji</string>
<string name="error_unknown">Wystąpił nieznany błąd.</string>
<string name="about_version_title">Wersja</string>
@@ -83,7 +84,6 @@
<string name="widget_create_note">Utwórz notatkę</string>
<string name="widget_not_logged_in">Zaloguj się do Notatek przed użyciem tego widżetu</string>
<string name="widget_entry_fav_contentDescription">Ikona gwiazdki służy do oznaczenia pozycji jako ulubiona</string>
- <string name="widget_app_launcher_contentDescription">Uruchamia aplikację</string>
<string name="activity_select_single_note">Wybierz notatkę</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">Dołącz do notatki</string>
<string name="change_note_title">Zmień tytuł notatki</string>
<string name="menu_edit_title">Edytuj tytuł</string>
- <string name="settings_branding">Motyw serwera</string>
<string name="settings_gridview">Widok siatki</string>
<string name="simple_security">Bezpieczeństwo</string>
<string name="appearance_and_behavior">Wygląd i zachowanie</string>
<string name="simple_synchronization">Synchronizacja</string>
- <string name="simple_behavior">Zachowanie</string>
<string name="manage_accounts">Zarządzaj kontami</string>
<string name="action_formatting_help">Formatowanie</string>
@@ -158,13 +156,6 @@
<item>Duża</item>
</string-array>
- <string-array name="sync_entries">
- <item>Wyłączona</item>
- <item>15 minut</item>
- <item>1 godzina</item>
- <item>6 godzin</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Jasny</item>
<item>Ciemny</item>
@@ -266,4 +257,8 @@
<string name="remove_account">Usuń %1$s</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Aby dodać konto, musisz mieć połączenie z Internetem.</string>
-</resources>
+ <string name="simple_next">Następna</string>
+ <string name="simple_prev">Poprzednia</string>
+ <string name="simple_backup">Kopia zapasowa</string>
+ <string name="simple_repair">Naprawa</string>
+ </resources>
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 5fa5d5e9..0cea2c95 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -52,6 +52,7 @@
<string name="error_sync">A sincronização falhou: %1$s</string>
<string name="error_synchronization">A sincronização falhou</string>
<string name="error_no_network">Sem conexão de rede</string>
+ <string name="error_maintenance_mode">O servidor está em modo de manutenção </string>
<string name="error_unknown">Ocorreu um erro desconhecido.</string>
<string name="about_version_title">Versão</string>
@@ -83,7 +84,6 @@
<string name="widget_create_note">Criar anotação</string>
<string name="widget_not_logged_in">Faça login no Notes antes de usar este widget</string>
<string name="widget_entry_fav_contentDescription">O ícone da estrela é usado para marcar um item como favorito</string>
- <string name="widget_app_launcher_contentDescription">Lançar o aplicativo</string>
<string name="activity_select_single_note">Selecionar anotação</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">Anexar à nota</string>
<string name="change_note_title">Alterar o título da nota</string>
<string name="menu_edit_title">Editar título</string>
- <string name="settings_branding">Marcação</string>
<string name="settings_gridview">Visualização em grade</string>
<string name="simple_security">Segurança</string>
<string name="appearance_and_behavior">Aparência e comportamento</string>
<string name="simple_synchronization">Sincronização</string>
- <string name="simple_behavior">Comportamento</string>
<string name="manage_accounts">Gerenciar contas</string>
<string name="action_formatting_help">Formatação</string>
@@ -158,13 +156,6 @@
<item>Grande</item>
</string-array>
- <string-array name="sync_entries">
- <item>Desligar</item>
- <item>15 minutos</item>
- <item>1 hora</item>
- <item>6 horas</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Claro</item>
<item>Escuro</item>
@@ -256,4 +247,8 @@
<string name="remove_account">Remover %1$s</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Você deve estar conectado à Internet para adicionar uma conta. </string>
-</resources>
+ <string name="simple_next">Próximo</string>
+ <string name="simple_prev">Anterior</string>
+ <string name="simple_backup">Backup</string>
+ <string name="simple_repair">Reparar</string>
+ </resources>
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 0b8dc0da..1134ec04 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -83,7 +83,6 @@
<string name="widget_create_note">Создать заметку</string>
<string name="widget_not_logged_in">Для использования виджета необходимо выполнить вход в приложении «Заметки»</string>
<string name="widget_entry_fav_contentDescription">Используйте звёздочку для исключения заметки из избранного</string>
- <string name="widget_app_launcher_contentDescription">Запуск приложения</string>
<string name="activity_select_single_note">Выберите заметку</string>
@@ -137,12 +136,10 @@
<string name="append_to_note">Добавить в заметку</string>
<string name="change_note_title">Изменить заголовок заметки</string>
<string name="menu_edit_title">Редактировать заголовок</string>
- <string name="settings_branding">Брендирование</string>
<string name="settings_gridview">Вид сеткой</string>
<string name="simple_security">Безопасность</string>
<string name="appearance_and_behavior">Внешний вид и поведение</string>
<string name="simple_synchronization">Синхронизация</string>
- <string name="simple_behavior">Режим</string>
<string name="manage_accounts">Управление аккаунтами</string>
<string name="action_formatting_help">Форматирование</string>
@@ -158,13 +155,6 @@
<item>Большой</item>
</string-array>
- <string-array name="sync_entries">
- <item>Отключены</item>
- <item>15 минут</item>
- <item>1 час</item>
- <item>6 часов</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Светлая</item>
<item>Тёмная</item>
diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml
index c50c6a67..d8dd207e 100644
--- a/app/src/main/res/values-sc/strings.xml
+++ b/app/src/main/res/values-sc/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">Crea Nota</string>
<string name="widget_not_logged_in">Incarca su butone + pro creare una nota noa</string>
<string name="widget_entry_fav_contentDescription">S\'icona de s\'isteddu s\'impreat pro marcare un\'elementu comente preferidu</string>
- <string name="widget_app_launcher_contentDescription">Avia aplicatzione</string>
<string name="activity_select_single_note">Seletziona nota</string>
@@ -131,10 +130,8 @@
<string name="append_to_note">Agiunghe a sa nota</string>
<string name="change_note_title">Càmbia su tìtulu de sa nota</string>
<string name="menu_edit_title">Modìfica tìtulu</string>
- <string name="settings_branding">Marca</string>
<string name="simple_security">Seguresa</string>
<string name="simple_synchronization">Sincronizatzione</string>
- <string name="simple_behavior">Cumportamentu</string>
<string name="manage_accounts">Gesti contos</string>
<string name="action_formatting_help">Formatatzione</string>
@@ -150,13 +147,6 @@
<item>Mannu</item>
</string-array>
- <string-array name="sync_entries">
- <item>Istudadu</item>
- <item>15 minùtos</item>
- <item>1 ora</item>
- <item>6 oras</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Craru</item>
<item>Iscuru</item>
diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml
index f1d0d0bc..50cb4ba0 100644
--- a/app/src/main/res/values-sk-rSK/strings.xml
+++ b/app/src/main/res/values-sk-rSK/strings.xml
@@ -6,12 +6,14 @@
<string name="label_all_notes">Všetky poznámky</string>
<string name="label_favorites">Obľúbené</string>
<string name="action_create">Nová poznámka</string>
+ <string name="welcome_text">Vitajte v %1$s</string>
<string name="action_settings">Settings</string>
<string name="action_trashbin">Zmazané poznámky</string>
<string name="action_search">Hľadať</string>
<string name="action_sorting_method">Metóda triedenia</string>
<string name="simple_cancel">Zrušiť</string>
<string name="simple_edit">Upraviť</string>
+ <string name="simple_remove">Odobrať</string>
<string name="action_edit_save">Uložiť</string>
<string name="simple_about">O aplikácii</string>
<string name="simple_link">Odkaz</string>
@@ -42,10 +44,15 @@
<string name="settings_font_title">Písmo s pevnou šírkou</string>
<string name="settings_font_size">Veľkosť písma</string>
<string name="settings_wifi_only">Synchronizovať len na Wi-Fi</string>
+ <string name="settings_lock">Zámok aplikácie (Beta)</string>
+ <string name="settings_lock_summary">Prihlasovacie údaje zariadenia</string>
<string name="settings_background_sync">Synchronizácia na pozadí</string>
+ <string name="settings_prevent_screen_capture">Zakázať snímanie obrazovky</string>
+
<string name="error_sync">Synchronizácia zlyhala: %1$s</string>
<string name="error_synchronization">Synchronizácia zlyhala</string>
<string name="error_no_network">Bez sieťového pripojenia</string>
+ <string name="error_maintenance_mode">Server je v móde údržby</string>
<string name="error_unknown">Vyskytla sa neznáma chyba</string>
<string name="about_version_title">Verzia</string>
@@ -77,7 +84,6 @@
<string name="widget_create_note">Vytvoriť poznámku</string>
<string name="widget_not_logged_in">Prihláste sa prosím do Poznámok skôr než začnete používať tento widget</string>
<string name="widget_entry_fav_contentDescription">Ikona hviezdičky označuje položku ako obľúbenú</string>
- <string name="widget_app_launcher_contentDescription">Spustí aplikáciu</string>
<string name="activity_select_single_note">Vybrať poznámku</string>
@@ -111,6 +117,7 @@
<string name="error_dialog_title">Ale nie - Čo teraz? 🙁</string>
<string name="error_dialog_tip_token_mismatch_retry">Skúste vyinútiť zatvorenie aplikácie a znova ju reštartovať. Možno došlo k chybnému pripojeniu k aplikácii Nextcloud.</string>
<string name="error_dialog_tip_token_mismatch_clear_storage">Ak problém pretrváva, skúste vyčistiť úložisko oboch aplikácií: Nextcloud a Nextcloud Poznámky.</string>
+ <string name="error_dialog_tip_clear_storage">Úložisko môžete vyčistiť otvorením informácii o aplikácii a výberom Úložisko → Vyčistiť úložisko. ⚠️ Varovanie: Toto vymaže poznámky ktore ešte neboli synchronizované!</string>
<string name="error_dialog_tip_files_outdated">Váš Nextcloud vyzerá byť zastaralý. Navštívte Play Store alebo F-Droid pre získanie najnovšej verzie.</string>
<string name="error_dialog_tip_files_force_stop">Niečo s vašim Nextcloudom nie je v poriadku. Skúste vynútiť ukončenie aplikácií Nextcloud a Nextcloud Poznámky.</string>
<string name="error_dialog_tip_files_delete_storage">Ak ich ukončenie nepomôže, môžete skúsiť vyčistiť úložiská oboch aplikácií.</string>
@@ -130,10 +137,10 @@
<string name="append_to_note">Pripojiť k poznámke</string>
<string name="change_note_title">Zmeniť názov poznámky</string>
<string name="menu_edit_title">Upraviť názov</string>
- <string name="settings_branding">Použitie vlastného loga</string>
+ <string name="settings_gridview">Zobrazenie v mriežke</string>
<string name="simple_security">Zabezpečenie</string>
+ <string name="appearance_and_behavior">Vzhľad a správanie</string>
<string name="simple_synchronization">Synchronizácia</string>
- <string name="simple_behavior">Chod</string>
<string name="manage_accounts">Spravovať účty</string>
<string name="action_formatting_help">Formátovanie</string>
@@ -149,13 +156,6 @@
<item>Veľká</item>
</string-array>
- <string-array name="sync_entries">
- <item>Vypnúť</item>
- <item>15 minút</item>
- <item>1 hodina</item>
- <item>6 hodín</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Svetlý</item>
<item>Tmavý</item>
@@ -191,6 +191,7 @@
<string name="formatting_help_codefence_inline" translateable="false">`%1$s`</string>
<string name="formatting_help_codefence_inline_escaped" translateable="false">\\`%1$s\\`</string>
<string name="formatting_help_codefence" translateable="false">```</string>
+ <string name="formatting_help_codefence_outer" translateable="false">````</string>
<string name="formatting_help_codefence_javascript" translateable="false">```javascript</string>
<string name="formatting_help_cbf_title">Kontextové formátovanie</string>
<string name="formatting_help_cbf_body_1">Hlavným cieľom aplikácie Notes je poskytnúť nástroj bez rozptyľovania. Svoje texty však budete môcť formátovať pomocou aplikácie Markdown. Pre rôzne príklady uvedené nižšie môžete použiť klávesové skratky, pomocou ktorých môžete svoje poznámky formátovať bez toho, aby ste museli zadávať kódy uvedené nižšie.</string>
@@ -228,11 +229,30 @@
<string name="formatting_help_code_body_2">Markdown tiež podporuje niečo, čo sa nazýva oplotenie kódu, čo umožňuje použiť viac riadkov bez odsadenia:</string>
<string name="formatting_help_code_body_3">Ak chcete použiť zvýraznenie syntaxe, uveďte jazyk:</string>
+ <string name="formatting_help_tables_title">Tabuľky</string>
+ <!-- Column header of a sample table -->
+ <string name="formatting_help_tables_column">Stĺpec %1d</string>
+ <!-- Table cell value of a sample table -->
+ <string name="formatting_help_tables_value">Hodnota %1d</string>
+
+ <string name="formatting_help_images_title">Obrázky</string>
+ <string name="formatting_help_images_alt">Efektný obrázok</string>
+
<string name="simple_other">Iné</string>
<string name="sort_last_modified">Zoradiť podľa upraveného dátumu</string>
<string name="sort_alphabetically">Zoradiť podľa abecedy</string>
<string name="error_action_open_battery_settings">Nastavenia batérie.</string>
<string name="error_action_open_deck_info">Otvoriť informácie o aplikácii</string>
<string name="error_action_open_network">Nastavenia siete</string>
+ <string name="no_account_configured_yet">Nie je ešte nastavený žiadny účet</string>
<string name="no_other_accounts">Zatiaľ nemáte nastavené žiadne ďalšie účty.</string>
+ <string name="choose_account">Zvoliť účet</string>
+ <string name="context_based_formatting">Kontextové okno formátovania</string>
+ <string name="remove_account">Vzdialené: %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Ak chcete pridať účet, musíte byť pripojení k internetu.</string>
+ <string name="simple_next">Ďalšie</string>
+ <string name="simple_prev">Predchádzajúce</string>
+ <string name="simple_backup">Záloha</string>
+ <string name="simple_repair">Obnova</string>
</resources>
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 5b369369..afdc562b 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -52,6 +52,7 @@
<string name="error_sync">Usklajevanje je spodletelo: %1$s</string>
<string name="error_synchronization">Usklajevanje je spodletelo</string>
<string name="error_no_network">Ni omrežne povezave</string>
+ <string name="error_maintenance_mode">Strežnik je v načinu vzdrževanja.</string>
<string name="error_unknown">Prišlo je do neznane napake.</string>
<string name="about_version_title">Različica</string>
@@ -83,7 +84,6 @@
<string name="widget_create_note">Ustvari zabeležko</string>
<string name="widget_not_logged_in">Pred uporabo gradnika se je treba prijaviti v program Beležka Notes</string>
<string name="widget_entry_fav_contentDescription">Zvezdica označuje priljubljene predmete</string>
- <string name="widget_app_launcher_contentDescription">Zažene program</string>
<string name="activity_select_single_note">Izbor zabeležke</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">Pripni k zabeležki</string>
<string name="change_note_title">Spremeni naslov zabeležke</string>
<string name="menu_edit_title">Uredi naslov</string>
- <string name="settings_branding">Prilagajanje oblikovanja</string>
<string name="settings_gridview">Mrežni pogled</string>
<string name="simple_security">Varnost</string>
<string name="appearance_and_behavior">Videz in obnašanje</string>
<string name="simple_synchronization">Usklajevanje</string>
- <string name="simple_behavior">Obnašanje</string>
<string name="manage_accounts">Upravljanje z računi</string>
<string name="action_formatting_help">Oblikovanje</string>
@@ -158,13 +156,6 @@
<item>Velika</item>
</string-array>
- <string-array name="sync_entries">
- <item>Onemogočeno</item>
- <item>15 minut</item>
- <item>1 ura</item>
- <item>6 ur</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Svetla</item>
<item>Temna</item>
@@ -266,4 +257,6 @@
<string name="remove_account">Odstrani %1$s</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Za dodajanje računa mora biti vzpostavljena povezava z omrežjem.</string>
-</resources>
+ <string name="simple_next">Naslednje</string>
+ <string name="simple_prev">Predhodno</string>
+ </resources>
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 5bd5c5be..e7b96c7e 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">Направи белешку</string>
<string name="widget_not_logged_in">Пријавите се на Белешке пре коришћења овог виџета</string>
<string name="widget_entry_fav_contentDescription">Икона звезде се користи да означи ставку као омиљену</string>
- <string name="widget_app_launcher_contentDescription">Покреће апликацију</string>
<string name="activity_select_single_note">Одабери белешку</string>
@@ -130,10 +129,8 @@
<string name="append_to_note">Додај на белешку</string>
<string name="change_note_title">Промени назив белешке</string>
<string name="menu_edit_title">Измени наслов</string>
- <string name="settings_branding">Брендирање</string>
<string name="simple_security">Безбедност</string>
<string name="simple_synchronization">Синхронизација</string>
- <string name="simple_behavior">Понашање</string>
<string name="manage_accounts">Управљање налозима</string>
<string name="action_formatting_help">Форматирање</string>
@@ -149,13 +146,6 @@
<item>Велики</item>
</string-array>
- <string-array name="sync_entries">
- <item>Искључена</item>
- <item>15 минута</item>
- <item>1 сат</item>
- <item>6 сати</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>светла</item>
<item>тамна</item>
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 77ba991c..0d5b6145 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">Skapa anteckning</string>
<string name="widget_not_logged_in">Vänligen logga in i Anteckningar innan denna används</string>
<string name="widget_entry_fav_contentDescription">Stjärnan används för att beteckna ett objekt som favorit</string>
- <string name="widget_app_launcher_contentDescription">Startar app</string>
<string name="activity_select_single_note">Välj anteckning</string>
@@ -130,10 +129,8 @@
<string name="append_to_note">Lägg till i anteckning</string>
<string name="change_note_title">Ändra anteckningstitel</string>
<string name="menu_edit_title">Redigera titel</string>
- <string name="settings_branding">Varumärke</string>
<string name="simple_security">Säkerhet</string>
<string name="simple_synchronization">Synkronisering</string>
- <string name="simple_behavior">Beteende</string>
<string name="manage_accounts">Hantera konton</string>
<string name="action_formatting_help">Formatering</string>
@@ -149,13 +146,6 @@
<item>Stor</item>
</string-array>
- <string-array name="sync_entries">
- <item>Av</item>
- <item>15 minuter</item>
- <item>1 timme</item>
- <item>6 timmar</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Ljust</item>
<item>Mörkt</item>
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 537b7c52..40a9e4bc 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -2,9 +2,9 @@
<resources>
<string name="app_name">Notlar</string>
- <string name="app_name_long">Nextcloud Notlar</string>
+ <string name="app_name_long">Nextcloud Notes</string>
<string name="label_all_notes">Tum notlar</string>
- <string name="label_favorites">Sık Kullanılanlar</string>
+ <string name="label_favorites">Sık kullanılanlar</string>
<string name="action_create">Not ekle</string>
<string name="welcome_text">%1$s uygulamasına hoş geldiniz</string>
<string name="action_settings">Ayarlar</string>
@@ -23,7 +23,7 @@
<string name="action_uncategorized">Kategorisiz</string>
<string name="menu_delete">Sil</string>
<string name="menu_change_category">Kategori</string>
- <string name="menu_favorite">Sık Kullanılanlara Ekle</string>
+ <string name="menu_favorite">Sık kullanılanlara ekle</string>
<string name="menu_preview">Ön izleme</string>
<string name="menu_share">Paylaş</string>
@@ -41,7 +41,7 @@
<string name="settings_note_mode">Notların görüntülenme kipi</string>
<string name="settings_theme_title">Tema</string>
- <string name="settings_font_title">Sabit aralıklı yazı tipi</string>
+ <string name="settings_font_title">Sabit aralıklı yazı türü</string>
<string name="settings_font_size">Yazı boyutu</string>
<string name="settings_wifi_only">Yalnız Wi-Fi ile eşitlensin</string>
<string name="settings_lock">Uygulama kilidi (Beta)</string>
@@ -52,6 +52,7 @@
<string name="error_sync">Eşitlenemedi: %1$s</string>
<string name="error_synchronization">Eşitlenemedi</string>
<string name="error_no_network">Ağ bağlantısı yok</string>
+ <string name="error_maintenance_mode">Sunucu bakım kipinde</string>
<string name="error_unknown">Bilinmeyen bir sorun çıktı.</string>
<string name="about_version_title">Sürüm</string>
@@ -68,11 +69,11 @@
<string name="about_translate_title">Çevirin</string>
<string name="about_translate">Transifex üzerindeki Nextcloud çeviri takımına katılarak uygulamanın çevrilmesine yardımcı olabilirsiniz: &lt;a href=\"%1$s\">%1$s&lt;/a></string>
<string name="about_app_license_title">Uygulama lisansı</string>
- <string name="about_app_license">Bu uygulama GNU GENEL KAMU LİSANSI v3+ koşulları altında dağıtılmaktadır.</string>
+ <string name="about_app_license">Bu uygulama GNU GENEL KAMU LİSANSI 3 sürüm ve üzeri koşulları altında dağıtılmaktadır.</string>
<string name="about_app_license_button">Lisansa bakın</string>
<string name="about_icons_disclaimer_title">Simgeler</string>
<string name="about_icons_disclaimer">&lt;p>Özgün simgeyi yapan %1$s&lt;/p>&lt;p>Bu uygulamanın kullandığı tüm diğer simgeler Google Inc. tarafından yapılmış ve Creative Commons Lisansı koşulları altında dağıtılan &lt;a href=\"https://materialdesignicons.com/\">Material Design Icons&lt;/a> kitaplığından sağlanmıştır.&lt;/p></string>
- <string name="about_credits_tab_title">Emeği Geçenler</string>
+ <string name="about_credits_tab_title">Emeği geçenler</string>
<string name="about_contribution_tab_title">Katkı</string>
<string name="about_license_tab_title">Lisans</string>
@@ -80,10 +81,9 @@
<string name="widget_note_list_placeholder">Herhangi bir not yok</string>
<string name="widget_single_note_title">Tek not</string>
<string name="widget_single_note_placeholder_tv">Not bulunamadı</string>
- <string name="widget_create_note">Not Ekle</string>
+ <string name="widget_create_note">Not ekle</string>
<string name="widget_not_logged_in">Bu pano bileşenini kullanabilmek için lütfen Notlar üzerinde oturum açın</string>
<string name="widget_entry_fav_contentDescription">Sık kullanılan ögeler için kullanılacak yıldız simgesi</string>
- <string name="widget_app_launcher_contentDescription">Uygulamayı başlatır</string>
<string name="activity_select_single_note">Not seçin</string>
@@ -107,7 +107,7 @@
<string name="account_already_imported">Hesap zaten içe aktarılmış</string>
<string name="no_notes_yet">Henüz bir not yok</string>
<string name="no_notes_yet_description">Yeni bir not eklemek için + tuşuna basın</string>
- <string name="simple_more">Daha Fazla</string>
+ <string name="simple_more">Diğer</string>
<string name="simple_move">Taşı</string>
<string name="category_readonly">Salt okunur</string>
<string name="no_category">Kategori yok</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">Nota ekle</string>
<string name="change_note_title">Not başlığını değiştir</string>
<string name="menu_edit_title">Başlığı düzenle</string>
- <string name="settings_branding">Markalama</string>
<string name="settings_gridview">Tablo görünümü</string>
<string name="simple_security">Güvenlik</string>
<string name="appearance_and_behavior">Görünüm ve davranış</string>
<string name="simple_synchronization">Eşitleme</string>
- <string name="simple_behavior">Davranış</string>
<string name="manage_accounts">Hesap yönetimi</string>
<string name="action_formatting_help">Biçimlendirme</string>
@@ -158,17 +156,10 @@
<item>Büyük</item>
</string-array>
- <string-array name="sync_entries">
- <item>Kapalı</item>
- <item>15 dakika</item>
- <item>1 saat</item>
- <item>6 saat</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Açık</item>
<item>Koyu</item>
- <item>Sistem Varsayılanı</item>
+ <item>Sistem varsayılanı</item>
</string-array>
<plurals name="ab_selected">
@@ -256,4 +247,8 @@
<string name="remove_account">%1$s sil</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Bir hesap ekleyebilmeniz için çalışan bir İnternet bağlantınız olmalı.</string>
-</resources>
+ <string name="simple_next">Sonraki</string>
+ <string name="simple_prev">Önceki</string>
+ <string name="simple_backup">Yedekle</string>
+ <string name="simple_repair">Onar</string>
+ </resources>
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 57f69257..1297e99e 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">Створити нотатку</string>
<string name="widget_not_logged_in">Будь ласка, увійдіть до Нотаток перед використанням цього віджету</string>
<string name="widget_entry_fav_contentDescription">Значок зірки використовується для позначення елемента як улюбленого</string>
- <string name="widget_app_launcher_contentDescription">Запускає застосунок</string>
<string name="activity_select_single_note">Оберіть нотатку</string>
@@ -130,10 +129,8 @@
<string name="append_to_note">Додати до нотатки</string>
<string name="change_note_title">Змінити заголовок нотатки</string>
<string name="menu_edit_title">Редагувати нотатку</string>
- <string name="settings_branding">Оформлення</string>
<string name="simple_security">Безпека</string>
<string name="simple_synchronization">Синхронізація</string>
- <string name="simple_behavior">Поведінка</string>
<string name="manage_accounts">Облікові записи</string>
<string name="action_formatting_help">Форматування</string>
@@ -149,13 +146,6 @@
<item>Великий</item>
</string-array>
- <string-array name="sync_entries">
- <item>Вимкнути</item>
- <item>15 хвилин</item>
- <item>1 година</item>
- <item>6 годин</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Світла</item>
<item>Темна</item>
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
new file mode 100644
index 00000000..b57ce4c7
--- /dev/null
+++ b/app/src/main/res/values-vi/strings.xml
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Ghi chú</string>
+ <string name="app_name_long">Nextcloud Ghi chú</string>
+ <string name="label_all_notes">Tất cả ghi chú</string>
+ <string name="label_favorites">Ưa thích</string>
+ <string name="action_create">Ghi chú mới</string>
+ <string name="welcome_text">Chào mừng đến %1$s</string>
+ <string name="action_settings">Cài đặt</string>
+ <string name="action_trashbin">Ghi chú đã xoá</string>
+ <string name="action_search">Tìm</string>
+ <string name="action_sorting_method">Phương pháp sắp xếp</string>
+ <string name="simple_cancel">Hủy</string>
+ <string name="simple_edit">Chỉnh sửa</string>
+ <string name="simple_remove">Xoá</string>
+ <string name="action_edit_save">Lưu</string>
+ <string name="simple_about">Giới thiệu</string>
+ <string name="simple_link">Liên kết</string>
+ <string name="action_note_deleted">Đã xóa %1$s</string>
+ <string name="action_note_restored">Đã khôi phục %1$s</string>
+ <string name="action_undo">Hoàn tác</string>
+ <string name="action_uncategorized">Chưa phân loại</string>
+ <string name="menu_delete">Xóa</string>
+ <string name="menu_change_category">Hạng mục</string>
+ <string name="menu_favorite">Ưa thích</string>
+ <string name="menu_preview">Xem trước</string>
+ <string name="menu_share">Chia sẻ</string>
+
+ <string name="search_in_category">Tìm kiếm trong %1$s</string>
+ <string name="search_in_all">Tìm kiếm tất cả ghi chú</string>
+
+ <string name="change_category_title">Chọn một hạng mục</string>
+
+ <string name="listview_updated_today">Hôm nay</string>
+ <string name="listview_updated_yesterday">Hôm qua</string>
+ <string name="listview_updated_this_week">Tuần này</string>
+ <string name="listview_updated_last_week">Tuần trước</string>
+ <string name="listview_updated_this_month">Tháng này</string>
+ <string name="listview_updated_last_month">Tháng trước</string>
+
+ <string name="settings_note_mode">Chế độ hiển thị cho các ghi chú</string>
+ <string name="settings_theme_title">Chủ đề</string>
+ <string name="settings_font_title">Phông chữ đơn cách</string>
+ <string name="settings_font_size">Cỡ chữ</string>
+ <string name="settings_wifi_only">Đồng bộ chỉ khi có Wi-Fi</string>
+ <string name="settings_lock">Khoá ứng dụng (Beta)</string>
+ <string name="settings_lock_summary">Thông tin thiết bị</string>
+ <string name="settings_background_sync">Đồng bộ hoá trong nền</string>
+ <string name="settings_prevent_screen_capture">Ngăn chặn chụp màn hình</string>
+
+ <string name="error_sync">Đồng bộ hoá thất bại: %1$s</string>
+ <string name="error_synchronization">Đồng bộ hoá thất bại</string>
+ <string name="error_no_network">Không có kết nối mạng</string>
+ <string name="error_maintenance_mode">Máy chủ đang trong chế độ bảo trì</string>
+ <string name="error_unknown">Đã xảy ra lỗi không xác định.</string>
+
+ <string name="about_version_title">Phiên bản</string>
+ <string name="about_version">Hiện tại bạn đang sử dụng &lt;strong> %1$s&lt;/strong></string>
+ <string name="about_maintainer_title">Người duy trì</string>
+ <string name="about_developers_title">Nhà phát triển</string>
+ <string name="about_translators_title">Người phiên dịch</string>
+ <string name="about_translators_transifex">Cộng đồng Nextcloud trên &lt;a href=\"%1$s\">Transifex&lt;/a></string>
+ <string name="about_testers_title">Người thử nghiệm</string>
+ <string name="about_source_title">Mã nguồn</string>
+ <string name="about_source">Dự án này được lưu trữ trên GitHub: &lt;a href=\"%1$s\">%1$s&lt;/a></string>
+ <string name="about_issues_title">Vấn đề</string>
+ <string name="about_issues">Bạn có thể báo cáo lỗi, đề nghị cải thiện và yêu cầu tính năng tại trình theo dõi vấn đề của GitHub: &lt;a href=\"%1$s\">%1$s&lt;/a></string>
+ <string name="about_translate_title">Dịch</string>
+ <string name="about_translate">Tham gia nhóm Nextcloud trên Transifex và giúp chúng tôi dịch ứng dụng này: &lt;a href=\"%1$s\">%1$s&lt;/a></string>
+ <string name="about_app_license_title">Giấy phép ứng dụng</string>
+ <string name="about_app_license">Ứng dụng này được cấp phép dưới GNU GENERAL PUBLIC LICENSE v3+.</string>
+ <string name="about_app_license_button">Xem giấy phép</string>
+ <string name="about_icons_disclaimer_title">Biểu tượng</string>
+ <string name="about_icons_disclaimer">&lt;p>Biểu tượng gốc được tạo bởi %1$s&lt;/p>&lt;p>Tất cả biểu tượng mới hơn được ứng dụng này sử dụng là &lt;a href=\"https://materialdesignicons.com/\">Biểu tượng thiết kế Material&lt;/a> được tạo bởi Google Inc. và được cấp phép dưới Apache 2.0 License.&lt;/p></string>
+ <string name="about_credits_tab_title">Ghi nhận</string>
+ <string name="about_contribution_tab_title">Đóng góp</string>
+ <string name="about_license_tab_title">Giấy phép</string>
+
+ <string name="widget_note_list_title">Danh sách ghi chú</string>
+ <string name="widget_note_list_placeholder">Không có ghi chú</string>
+ <string name="widget_single_note_title">Ghi chú đơn</string>
+ <string name="widget_single_note_placeholder_tv">Không tìm thấy ghi chú</string>
+ <string name="widget_create_note">Tạo ghi chú</string>
+ <string name="widget_not_logged_in">Vui lòng đăng nhập vào Ghi chú trước khi sử dụng tiện ích này</string>
+ <string name="widget_entry_fav_contentDescription">Biểu tượng sao được sử dụng để chỉ ra rằng một mục được yêu thích</string>
+
+ <string name="activity_select_single_note">Chọn ghi chú</string>
+
+ <string name="shortcut_create_long">Tạo một ghi chú mới</string>
+
+ <string name="pref_value_font_normal">Bình thường</string>
+ <string name="pref_value_wifi_and_mobile">Đồng bộ khi có Wi-Fi và dữ liệu di động</string>
+ <string name="pref_value_lock">Password protection</string>
+
+ <string name="simple_error">Lỗi</string>
+ <string name="simple_close">Đóng</string>
+ <string name="simple_copy">Sao chép</string>
+ <string name="simple_exception">Ngoại lệ</string>
+ <string name="pin_to_homescreen">Ghim vào màn hình chính</string>
+ <string name="note_has_been_deleted">Ghi chú này đã bị xoá</string>
+ <string name="add_account">Thêm tài khoản</string>
+ <string name="category_music">Nhạc</string>
+ <string name="category_movies">Phim</string>
+ <string name="category_movie">Xem phim</string>
+ <string name="category_work">Công việc</string>
+ <string name="account_already_imported">Tài khoản đã được nhập sẵn</string>
+ <string name="no_notes_yet">Chưa có ghi chú</string>
+ <string name="no_notes_yet_description">Nhấn nút + để tạo ghi chú mới</string>
+ <string name="simple_more">hơn</string>
+ <string name="simple_move">Dịch chuyển</string>
+ <string name="category_readonly">Chỉ xem</string>
+ <string name="no_category">Không có hạng mục</string>
+ <string name="add_category">Thêm %1$s</string>
+ <string name="simple_checkbox">Hộp chọn</string>
+ <string name="unlock_notes">Mở khoá ghi chú</string>
+ <string name="error_dialog_title">Ôi không - Làm gì bây giờ? 🙁</string>
+ <string name="error_dialog_tip_token_mismatch_retry">Vui lòng cố gắng buộc dừng và khởi động lại ứng dụng. Có thể đã có một kết nối không chính xác đến ứng dụng Nextcloud.</string>
+ <string name="error_dialog_tip_token_mismatch_clear_storage">Nếu vấn đề vẫn tiếp diễn, hãy cố gắng xoá bộ nhớ của cả hai ứng dụng: Nextcloud và Nextcloud Ghi chú để giải quyết vấn đề này.</string>
+ <string name="error_dialog_tip_clear_storage">Bạn có thể xoá bộ nhớ bằng cách mở thông tin ứng dụng và chọn Bộ nhớ → Xoá bộ nhớ. ⚠️ Cảnh báo: Việc này sẽ xoá các ghi chú chưa được đồng bộ hoá!</string>
+ <string name="error_dialog_tip_files_outdated">Ứng dụng Nextcloud của bạn có vẻ đã lỗi thời. Vui lòng đi đến CH Play hoặc F-Droid để lấy phiên bản mới nhất.</string>
+ <string name="error_dialog_tip_files_force_stop">Có gì đó có vẻ sai với ứng dụng Nextcloud của bạn. Vui lòng cố gắng buộc dừng cả hai, ứng dụng Nextcloud và ứng dụng Nextcloud Ghi chú.</string>
+ <string name="error_dialog_tip_files_delete_storage">Nếu việc buộc dừng chúng không giúp gì được, bạn có thể cố gắng xoá bộ nhớ của cả hai ứng dụng.</string>
+ <string name="error_dialog_timeout_instance">Không có phản hồi từ máy chủ của bạn trong thời gian được đưa ra. Vui lòng chắc chắn rằng bản sao của bạn đang chạy tốt.</string>
+ <string name="error_dialog_timeout_toggle">Hãy kiểm tra kết nối mạng của bạn. Đôi khi việc tắt và bật lại dữ liệu di động hoặc Wi-Fi có thể giúp.</string>
+ <string name="error_dialog_check_server">Phản hồi của máy chủ của bạn không chính xác. Vui lòng kiểm tra xem bạn có thể truy cập các ghi chú của bạn qua giao diện web không.</string>
+ <string name="error_dialog_check_server_logs">Có vấn đề với thiết lập Nextcloud của bạn. Vui lòng xem các tệp nhật ký của máy chủ.</string>
+ <string name="error_dialog_check_maintenance">Vui lòng kiểm tra xem bản sao Nextcloud của bạn hiện tại có đang không ở trong chế độ bảo trì không.</string>
+ <string name="error_dialog_insufficient_storage">Bản sao Nextcloud của bạn không còn bộ nhớ trống. Vui lòng xoá một số tệp để đồng bộ các thay đổi cục bộ của bạn vào đám mây của bạn.</string>
+ <string name="error_dialog_contact_us">Xin đừng chần chừ trong việc liên hệ với chúng tôi nếu vấn đề vẫn tiếp diễn. Bạn có thể tìm thông tin liên hệ của chúng tôi trong khu vực giới thiệu trong ngăn bên.</string>
+ <string name="error_dialog_we_need_info">Chúng tôi cần thông tin kỹ thuật sau đây để giúp bạn:</string>
+ <string name="error_dialog_server_app_enabled">Vui lòng chắc chắn rằng bạn đã cài đặt và bật ứng dụng \"Ghi chú\" trên máy chủ của bạn.</string>
+ <string name="error_dialog_redirect">Máy chủ của bạn đã phản hồi bằng mã trạng thái HTTP 302, nó có nghĩa là bạn chưa cài đặt ứng dụng Ghi chú trên máy chủ của bạn hoặc có gì đó bị thiết lập sai. Điều này có thể bị gây ra bởi những sự ghi đè tuỳ chỉnh trong tệp .htaccess hoặc bởi các ứng dụng Nextcloud như OID Client.</string>
+ <string name="error_dialog_tip_disable_battery_optimizations">Vui lòng tắt tất cả tối ưu hoá pin cho Nextcloud và ứng dụng Ghi chú.</string>
+ <string name="added_content">Đã thêm \"%1$s\"</string>
+ <string name="shared_text_empty">Văn bản được chia sẻ bị trống</string>
+ <string name="append_to_note">Thêm vào ghi chú</string>
+ <string name="change_note_title">Đổi tiêu đề ghi chú</string>
+ <string name="menu_edit_title">Chỉnh sửa tiêu đề</string>
+ <string name="settings_gridview">Hiển thị dạng lưới</string>
+ <string name="simple_security">Bảo mật</string>
+ <string name="appearance_and_behavior">Ngoại hình và hành vi</string>
+ <string name="simple_synchronization">Đồng bộ hoá</string>
+ <string name="manage_accounts">Quản lý tài khoản</string>
+ <string name="action_formatting_help">Định dạng</string>
+
+ <string-array name="noteMode_entries">
+ <item>Mở trong chế độ chỉnh sửa</item>
+ <item>Mở trong chế độ xem trước</item>
+ <item>Nhớ sự lựa chọn cuối của tôi</item>
+ </string-array>
+
+ <string-array name="fontSize_entries">
+ <item>Nhỏ</item>
+ <item>Trung bình</item>
+ <item>Lớn</item>
+ </string-array>
+
+ <string-array name="darkmode_entries">
+ <item>Sáng</item>
+ <item>Tối</item>
+ <item>Mặc định hệ thống</item>
+ </string-array>
+
+ <plurals name="ab_selected">
+ <item quantity="other">Đã chọn %d</item>
+ </plurals>
+ <plurals name="bulk_notes_deleted">
+ <item quantity="other">Đã xóa %1$d ghi chú</item>
+ </plurals>
+ <plurals name="bulk_notes_restored">
+ <item quantity="other">Đã khôi phục %1$d ghi chú</item>
+ </plurals>
+ <plurals name="share_multiple">
+ <item quantity="other">Chia sẻ nội dung của %1$d ghi chú</item>
+ </plurals>
+
+ <string name="formatting_help_divider" translateable="false">---</string>
+ <string name="formatting_help_codefence_inline" translateable="false">`%1$s`</string>
+ <string name="formatting_help_codefence_inline_escaped" translateable="false">\\`%1$s\\`</string>
+ <string name="formatting_help_codefence" translateable="false">```</string>
+ <string name="formatting_help_codefence_outer" translateable="false">````</string>
+ <string name="formatting_help_codefence_javascript" translateable="false">```javascript</string>
+ <string name="formatting_help_cbf_title">Định dạng dựa trên ngữ cảnh</string>
+ <string name="formatting_help_cbf_body_1">Một mục đích thiết kế lớn của ứng dụng Ghi chú là để cung cấp một công cụ không có sự xao nhãng. Tuy nhiên, bạn sẽ có thể định dạng văn bản của bạn bằng Markdown. Đối với các ví dụ khác nhau được nhắc đến dưới đây, bạn có thể sử dụng các lối tắt để bạn có thể định dạng các ghi chú mà không đánh các mã ở dưới.</string>
+ <string name="formatting_help_cbf_body_2">Chỉ cần chọn một phạm vi văn bản hoặc chạmvào con trỏ tại bất kỳ vị trí nào và bạn sẽ thấy một menu popup chứa các mục %4$s hoặc %5$s bên cạnh các mục mặc định %1$s, %2$s, %3$s.</string>
+
+ <string name="formatting_help_text_title">Văn bản</string>
+ <string name="formatting_help_text_body">Việc làm cho một số từ trở nên %1$sđậm%1$s và các từ khác %2$snghiêng%2$s bằng Markdown là rất dễ. Bạn có thể %3$sgạch bỏ%3$s một số từ và thậm chí là [liên kết đến Nextcloud](https://nextcloud.com).</string>
+
+ <string name="formatting_help_lists_title">Danh sách</string>
+ <string name="formatting_help_lists_body_1">Đôi khi bạn muốn các danh sách đánh số:</string>
+ <string name="formatting_help_lists_body_2">Một</string>
+ <string name="formatting_help_lists_body_3">Hai</string>
+ <string name="formatting_help_lists_body_4">Ba</string>
+ <string name="formatting_help_lists_body_5">Đôi khi bạn muốn gạch đầu dòng:</string>
+ <string name="formatting_help_lists_body_6">Bắt đầu một dòng bằng dấu gạch ngang</string>
+ <string name="formatting_help_lists_body_8">Như thế này</string>
+ <string name="formatting_help_lists_body_9">Và thế này</string>
+
+ <string name="formatting_help_checkboxes_title">Hộp chọn</string>
+ <string name="formatting_help_code_title">Mã</string>
+ <string name="formatting_help_tables_title">Bảng</string>
+ <!-- Column header of a sample table -->
+ <string name="formatting_help_tables_column">Cột %1d</string>
+ <!-- Table cell value of a sample table -->
+ <string name="formatting_help_tables_value">Giá trị %1d</string>
+
+ <string name="formatting_help_images_title">Hình ảnh</string>
+ <string name="formatting_help_images_alt">Hình ảnh đẹp</string>
+
+ <string name="simple_other">Khác</string>
+ <string name="sort_last_modified">Sắp xếp theo ngày sửa đổi</string>
+ <string name="sort_alphabetically">Sắp xếp theo bảng chữ cái</string>
+ <string name="error_action_open_battery_settings">Cài đặt pin</string>
+ <string name="error_action_open_deck_info">Mở Thông tin ứng dụng</string>
+ <string name="error_action_open_network">Cài đặt mạng</string>
+ <string name="no_account_configured_yet">Chưa có tài khoản nào được thiết lập</string>
+ <string name="no_other_accounts">Bạn chưa thiết lập tài khoản nào khác.</string>
+ <string name="choose_account">Chọn tài khoản</string>
+ <plurals name="remove_account_message">
+ <item quantity="other">Việc xoá tài khoản %1$s cũng sẽ xoá %2$d thay đổi chưa được đồng bộ hoá và không thể phục hồi.</item>
+ </plurals>
+ <string name="remove_account">Xoá %1$s</string>
+
+ <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Bạn phải có kết nối internet để thêm tài khoản.</string>
+ <string name="simple_next">Tiếp</string>
+ <string name="simple_prev">Trước</string>
+ </resources>
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index df7c0011..744c0b1c 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -52,6 +52,7 @@
<string name="error_sync">同步失败:%1$s</string>
<string name="error_synchronization">同步失败</string>
<string name="error_no_network">没有网络连接</string>
+ <string name="error_maintenance_mode">服务器处于维护模式下</string>
<string name="error_unknown">出现未知错误。</string>
<string name="about_version_title">版本</string>
@@ -83,7 +84,6 @@
<string name="widget_create_note">创建笔记</string>
<string name="widget_not_logged_in">请在使用此小部件之前登录 Notes</string>
<string name="widget_entry_fav_contentDescription">标星项目为收藏</string>
- <string name="widget_app_launcher_contentDescription">启动应用程序</string>
<string name="activity_select_single_note">选择笔记</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">追加笔记</string>
<string name="change_note_title">更改笔记标题</string>
<string name="menu_edit_title">编辑标题</string>
- <string name="settings_branding">创建标签</string>
<string name="settings_gridview">网格视图</string>
<string name="simple_security">安全</string>
<string name="appearance_and_behavior">外观和行为</string>
<string name="simple_synchronization">同步</string>
- <string name="simple_behavior">行为</string>
<string name="manage_accounts">管理账号</string>
<string name="action_formatting_help">格式</string>
@@ -158,13 +156,6 @@
<item>大</item>
</string-array>
- <string-array name="sync_entries">
- <item>关闭</item>
- <item>15 分钟</item>
- <item>1 小时</item>
- <item>6 小时</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>浅色</item>
<item>深色</item>
@@ -251,4 +242,8 @@
<string name="remove_account">移除 %1$s</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">要添加账号,你必须连接到互联网。</string>
-</resources>
+ <string name="simple_next">下一则 </string>
+ <string name="simple_prev">上一则</string>
+ <string name="simple_backup">备份</string>
+ <string name="simple_repair">修复</string>
+ </resources>
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 026d4682..b5a5c07e 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -52,6 +52,7 @@
<string name="error_sync">同步失敗:%1$s</string>
<string name="error_synchronization">同步失敗</string>
<string name="error_no_network">沒有網路連線</string>
+ <string name="error_maintenance_mode">伺服器正處於維護模式</string>
<string name="error_unknown">發生未知錯誤。</string>
<string name="about_version_title">版本</string>
@@ -83,7 +84,6 @@
<string name="widget_create_note">建立筆記</string>
<string name="widget_not_logged_in">在使用此小工具之前,請先登入到 Notes</string>
<string name="widget_entry_fav_contentDescription">星星圖示用來標示某則筆記在「我的最愛」中。</string>
- <string name="widget_app_launcher_contentDescription">啟動 App</string>
<string name="activity_select_single_note">選擇筆記</string>
@@ -137,12 +137,10 @@
<string name="append_to_note">附加到筆記</string>
<string name="change_note_title">改變筆記標題</string>
<string name="menu_edit_title">編輯標題</string>
- <string name="settings_branding">品牌化</string>
<string name="settings_gridview">網格檢視</string>
<string name="simple_security">安全性</string>
<string name="appearance_and_behavior">外觀與行為</string>
<string name="simple_synchronization">同步</string>
- <string name="simple_behavior">行為</string>
<string name="manage_accounts">賬戶管理</string>
<string name="action_formatting_help">正在格式化</string>
@@ -158,13 +156,6 @@
<item>大</item>
</string-array>
- <string-array name="sync_entries">
- <item>關閉</item>
- <item>15 分鐘</item>
- <item>1 小時</item>
- <item>6 小時</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>亮色</item>
<item>暗色</item>
@@ -251,4 +242,8 @@
<string name="remove_account">移除 %1$s</string>
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">您必須連線到互聯網才能新增賬戶。</string>
-</resources>
+ <string name="simple_next">下一步</string>
+ <string name="simple_prev">上一個</string>
+ <string name="simple_backup">備份</string>
+ <string name="simple_repair">修復</string>
+ </resources>
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 4804752c..a5b8bf96 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -77,7 +77,6 @@
<string name="widget_create_note">建立筆記</string>
<string name="widget_not_logged_in">在使用此小工具之前,請先登入到 Notes</string>
<string name="widget_entry_fav_contentDescription">星星圖示用來標示某則筆記在「我的最愛」中。</string>
- <string name="widget_app_launcher_contentDescription">啟動 App</string>
<string name="activity_select_single_note">選擇筆記</string>
@@ -131,7 +130,6 @@
<string name="menu_edit_title">編輯標題</string>
<string name="simple_security">安全性</string>
<string name="simple_synchronization">同步</string>
- <string name="simple_behavior">行為</string>
<string name="manage_accounts">帳戶管理</string>
<string name="action_formatting_help">正在格式化</string>
@@ -147,13 +145,6 @@
<item>大</item>
</string-array>
- <string-array name="sync_entries">
- <item>關閉</item>
- <item>15 分鐘</item>
- <item>1 小時</item>
- <item>6 小時</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>亮色</item>
<item>暗色</item>
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 3fd107e9..a7557c37 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -15,10 +15,4 @@
<item>@string/pref_value_theme_dark</item>
<item>@string/pref_value_theme_system_default</item>
</string-array>
- <string-array name="sync_values">
- <item>@string/pref_value_sync_off</item>
- <item>@string/pref_value_sync_15_minutes</item>
- <item>@string/pref_value_sync_1_hour</item>
- <item>@string/pref_value_sync_6_hours</item>
- </string-array>
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 8735993b..87944d9d 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -30,6 +30,4 @@
<color name="widget_background">#ddffffff</color>
<color name="widget_foreground">#222222</color>
- <color name="widget_foreground_strong">#121212</color>
- <color name="widget_divider">#32d1d1d1</color>
</resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index ec75a88d..4624e017 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -32,19 +32,9 @@
<!-- Widgets -->
<dimen name="widget_outer_radius">@dimen/spacer_1x</dimen>
- <dimen name="widget_inner_radius">@dimen/spacer_1hx</dimen>
- <dimen name="widget_divider">1dp</dimen>
-
<dimen name="widget_margin">@dimen/spacer_1x</dimen>
-
- <dimen name="widget_note_list_hdr_padding">2dp</dimen>
- <dimen name="widget_note_list_header_height">36dp</dimen>
- <dimen name="widget_note_list_icon_width">30dp</dimen>
-
<dimen name="widget_note_list_outer_padding">4dp</dimen>
<dimen name="widget_note_list_inner_padding">4dp</dimen>
-
- <dimen name="widget_note_list_entry_add_button">34dp</dimen>
<dimen name="widget_note_list_fav_icon_width">26dp</dimen>
<dimen name="widget_note_list_fav_icon_height">20dp</dimen>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 892bd442..ee2d67f1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -93,7 +93,6 @@
<string name="widget_create_note">Create Note</string>
<string name="widget_not_logged_in">Please login to Notes before using this widget</string>
<string name="widget_entry_fav_contentDescription">Star icon is used to denote an item as a favourite</string>
- <string name="widget_app_launcher_contentDescription">Launches app</string>
<string name="activity_select_single_note">Select note</string>
@@ -116,10 +115,6 @@
<string name="pref_value_font_size_small" translatable="false">small</string>
<string name="pref_value_font_size_medium" translatable="false">medium</string>
<string name="pref_value_font_size_large" translatable="false">large</string>
- <string name="pref_value_sync_off" translatable="false">off</string>
- <string name="pref_value_sync_15_minutes" translatable="false">15_minutes</string>
- <string name="pref_value_sync_1_hour" translatable="false">1_hour</string>
- <string name="pref_value_sync_6_hours" translatable="false">6_hours</string>
<string name="pref_value_theme_light" translatable="false">LIGHT</string>
<string name="pref_value_theme_dark" translatable="false">DARK</string>
<string name="pref_value_theme_system_default" translatable="false">SYSTEM_DEFAULT</string>
@@ -171,12 +166,10 @@
<string name="append_to_note">Append to note</string>
<string name="change_note_title">Change note title</string>
<string name="menu_edit_title">Edit title</string>
- <string name="settings_branding">Branding</string>
<string name="settings_gridview">Grid view</string>
<string name="simple_security">Security</string>
<string name="appearance_and_behavior">Appearance and behavior</string>
<string name="simple_synchronization">Synchronization</string>
- <string name="simple_behavior">Behavior</string>
<string name="manage_accounts">Manage accounts</string>
<string name="action_formatting_help">Formatting</string>
@@ -192,13 +185,6 @@
<item>Large</item>
</string-array>
- <string-array name="sync_entries">
- <item>Off</item>
- <item>15 minutes</item>
- <item>1 hour</item>
- <item>6 hours</item>
- </string-array>
-
<string-array name="darkmode_entries">
<item>Light</item>
<item>Dark</item>
@@ -310,10 +296,14 @@
<string name="settings_notes_path">Set folder</string>
<string name="settings_file_suffix">File extension</string>
-
<string-array name="settings_file_suffixes">
<item>.txt</item>
<item>.md</item>
</string-array>
+ <string name="simple_next">Next</string>
+ <string name="simple_prev">Previous</string>
+ <string name="simple_backup">Backup</string>
+ <string name="simple_repair">Repair</string>
+ <string name="backup">We detected an irrecoverably state of the app. Please backup your unsynchronized changes and clear the storage of the Notes app.</string>
</resources>
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 2794f79a..7ecfef1f 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -13,14 +13,11 @@
android:layout="@layout/item_pref"
android:title="@string/settings_wifi_only" />
- <ListPreference
- android:defaultValue="@string/pref_value_sync_off"
- android:entries="@array/sync_entries"
- android:entryValues="@array/sync_values"
+ <it.niedermann.owncloud.notes.branding.BrandedSwitchPreference
+ android:defaultValue="true"
android:icon="@drawable/ic_sync_black_24dp"
android:key="@string/pref_key_background_sync"
android:layout="@layout/item_pref"
- android:summary="%s"
android:title="@string/settings_background_sync" />
</it.niedermann.owncloud.notes.branding.BrandedPreferenceCategory>
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragmentTest.java b/app/src/test/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragmentTest.java
index 95a62cc0..881bde31 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragmentTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragmentTest.java
@@ -3,61 +3,49 @@ package it.niedermann.owncloud.notes.edit;
import org.junit.Assert;
import org.junit.Test;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.util.Arrays;
public class SearchableBaseNoteFragmentTest {
@SuppressWarnings("ConstantConditions")
@Test
- public void testCountOccurrencesFixed() {
- try {
- Method method = SearchableBaseNoteFragment.class.getDeclaredMethod("countOccurrences", String.class, String.class);
- method.setAccessible(true);
-
- for (int count = 0; count <= 15; ++count) {
- StringBuilder sb = new StringBuilder("Mike Chester Wang");
- for (int i = 0; i < count; ++i) {
- sb.append(sb);
- }
-
- long startTime = System.currentTimeMillis();
- int num = (int) method.invoke(null, sb.toString(), "Chester");
- long endTime = System.currentTimeMillis();
- System.out.println("Fixed Version");
- System.out.println("Total Time: " + (endTime - startTime) + " ms");
- System.out.println("Total Times: " + num);
- System.out.println("String Size: " + (sb.length() / 1024.0) + " K");
- Assert.assertEquals((int) Math.pow(2, count), num);
- System.out.println();
+ public void testCountOccurrencesFixed() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ final Method method = SearchableBaseNoteFragment.class.getDeclaredMethod("countOccurrences", String.class, String.class);
+ method.setAccessible(true);
+
+ for (int count = 0; count <= 15; ++count) {
+ final StringBuilder sb = new StringBuilder("Mike Chester Wang");
+ for (int i = 0; i < count; ++i) {
+ sb.append(sb);
}
- } catch (Exception e) {
- Assert.fail(Arrays.toString(e.getStackTrace()));
- System.out.println("Test Count Occurrences Fixed" + Arrays.toString(e.getStackTrace()));
+ final long startTime = System.currentTimeMillis();
+ final int num = (int) method.invoke(null, sb.toString(), "Chester");
+ final long endTime = System.currentTimeMillis();
+ System.out.println("Fixed Version");
+ System.out.println("Total Time: " + (endTime - startTime) + " ms");
+ System.out.println("Total Times: " + num);
+ System.out.println("String Size: " + (sb.length() / 1024.0) + " K");
+ Assert.assertEquals((int) Math.pow(2, count), num);
+ System.out.println();
}
}
@SuppressWarnings("ConstantConditions")
@Test
- public void testNullOrEmptyInput() {
- try {
- Method method = SearchableBaseNoteFragment.class.getDeclaredMethod("countOccurrences", String.class, String.class);
- method.setAccessible(true);
-
- int num;
- num = (int) method.invoke(null, null, "Hi");
- Assert.assertEquals(0, num);
- num = (int) method.invoke(null, "Hi my name is Mike Chester Wang", null);
- Assert.assertEquals(0, num);
- num = (int) method.invoke(null, "", "Hi");
- Assert.assertEquals(0, num);
- num = (int) method.invoke(null, "Hi my name is Mike Chester Wang", "");
- Assert.assertEquals(0, num);
-
- } catch (Exception e) {
- Assert.fail(Arrays.toString(e.getStackTrace()));
- System.out.println("Test Null Or Empty Input" + Arrays.toString(e.getStackTrace()));
- }
+ public void testNullOrEmptyInput() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ final Method method = SearchableBaseNoteFragment.class.getDeclaredMethod("countOccurrences", String.class, String.class);
+ method.setAccessible(true);
+
+ int num;
+ num = (int) method.invoke(null, null, "Hi");
+ Assert.assertEquals(0, num);
+ num = (int) method.invoke(null, "Hi my name is Mike Chester Wang", null);
+ Assert.assertEquals(0, num);
+ num = (int) method.invoke(null, "", "Hi");
+ Assert.assertEquals(0, num);
+ num = (int) method.invoke(null, "Hi my name is Mike Chester Wang", "");
+ Assert.assertEquals(0, num);
}
} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecorationTest.java b/app/src/test/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecorationTest.java
new file mode 100644
index 00000000..2369415f
--- /dev/null
+++ b/app/src/test/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecorationTest.java
@@ -0,0 +1,100 @@
+package it.niedermann.owncloud.notes.main.items.grid;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.StaggeredGridLayoutManager;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+
+import it.niedermann.owncloud.notes.main.items.ItemAdapter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(RobolectricTestRunner.class)
+public class GridItemDecorationTest {
+
+ private final ItemAdapter itemAdapter = mock(ItemAdapter.class);
+ private final RecyclerView recyclerView = mock(RecyclerView.class);
+ private final View view = mock(View.class);
+ private final StaggeredGridLayoutManager.LayoutParams layoutParams = mock(StaggeredGridLayoutManager.LayoutParams.class);
+
+ @Test
+ public void getItemOffsets() {
+ when(view.getLayoutParams()).thenReturn(layoutParams);
+ when(itemAdapter.getFirstPositionOfViewType(anyInt())).thenReturn(0);
+ when(itemAdapter.getItemViewType(anyInt())).then((arg) -> Arrays.asList(0, 4, 9).contains(arg.getArgument(0, Integer.class))
+ ? ItemAdapter.TYPE_SECTION
+ : ItemAdapter.TYPE_NOTE_ONLY_TITLE);
+
+ assertThrows("Requires at least one column", IllegalArgumentException.class, () -> new GridItemDecoration(itemAdapter, 0, 5, 5, 5, 5, 5));
+
+ final GridItemDecoration oneColumn = new GridItemDecoration(itemAdapter, 1, 5, 5, 5, 5, 5);
+
+ testAssertion(oneColumn, 0, 0, true, 5, 5, 5, 5);
+ testAssertion(oneColumn, 0, 1, false, 0, 5, 5, 5);
+ testAssertion(oneColumn, 0, 2, false, 0, 5, 5, 5);
+ testAssertion(oneColumn, 0, 3, false, 0, 5, 5, 5);
+ testAssertion(oneColumn, 0, 4, true, 5, 5, 5, 5);
+ testAssertion(oneColumn, 0, 5, false, 0, 5, 5, 5);
+ testAssertion(oneColumn, 0, 6, false, 0, 5, 5, 5);
+ testAssertion(oneColumn, 0, 7, false, 0, 5, 5, 5);
+ testAssertion(oneColumn, 0, 8, false, 0, 5, 5, 5);
+ testAssertion(oneColumn, 0, 9, true, 5, 5, 5, 5);
+
+ final GridItemDecoration twoColumns = new GridItemDecoration(itemAdapter, 2, 5, 5, 5, 5, 5);
+
+ testAssertion(twoColumns, 0, 0, true, 5, 5, 5, 5);
+ testAssertion(twoColumns, 0, 1, false, 0, 5, 5, 5);
+ testAssertion(twoColumns, 1, 2, false, 0, 0, 5, 5);
+ testAssertion(twoColumns, 0, 3, false, 0, 5, 5, 5);
+ testAssertion(twoColumns, 0, 4, true, 5, 5, 5, 5);
+ testAssertion(twoColumns, 0, 5, false, 0, 5, 5, 5);
+ testAssertion(twoColumns, 1, 6, false, 0, 0, 5, 5);
+ testAssertion(twoColumns, 0, 7, false, 0, 5, 5, 5);
+ testAssertion(twoColumns, 0, 8, false, 0, 5, 5, 5);
+ testAssertion(twoColumns, 0, 9, true, 5, 5, 5, 5);
+
+ final GridItemDecoration threeColumns = new GridItemDecoration(itemAdapter, 3, 5, 5, 5, 5, 5);
+
+ testAssertion(threeColumns, 0, 0, true, 5, 5, 5, 5);
+ testAssertion(threeColumns, 0, 1, false, 0, 5, 5, 5);
+ testAssertion(threeColumns, 1, 2, false, 0, 0, 5, 5);
+ testAssertion(threeColumns, 2, 3, false, 0, 0, 5, 5);
+ testAssertion(threeColumns, 0, 4, true, 5, 5, 5, 5);
+ testAssertion(threeColumns, 0, 5, false, 0, 5, 5, 5);
+ testAssertion(threeColumns, 1, 6, false, 0, 0, 5, 5);
+ testAssertion(threeColumns, 2, 7, false, 0, 0, 5, 5);
+ testAssertion(threeColumns, 0, 8, false, 0, 5, 5, 5);
+ testAssertion(threeColumns, 0, 9, true, 5, 5, 5, 5);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private void testAssertion(GridItemDecoration gid, int spanIndex, int position, boolean fullSpan, int top, int left, int right, int bottom) {
+ when(layoutParams.getSpanIndex()).thenReturn(spanIndex);
+ when(recyclerView.getChildAdapterPosition(any())).thenReturn(position);
+ final Rect result = new Rect();
+ gid.getItemOffsets(result, view, recyclerView, mock(RecyclerView.State.class));
+
+ if (fullSpan) {
+ verify(layoutParams).setFullSpan(true);
+ }
+ reset(layoutParams);
+ assertEquals(top, result.top);
+ assertEquals(left, result.left);
+ assertEquals(right, result.right);
+ assertEquals(bottom, result.bottom);
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java
index 7e8d49ea..eb21f312 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/AccountDaoTest.java
@@ -1,21 +1,16 @@
package it.niedermann.owncloud.notes.persistence;
-import android.os.Build;
-
import androidx.annotation.NonNull;
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
@@ -24,7 +19,6 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@RunWith(RobolectricTestRunner.class)
-@Config(sdk = {Build.VERSION_CODES.P})
public class AccountDaoTest {
@Rule
@@ -47,8 +41,8 @@ public class AccountDaoTest {
}
@Test
- public void insertAccount() throws NextcloudHttpRequestFailedException {
- final long createdId = db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null)));
+ public void insertAccount() {
+ final long createdId = db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", null, new Capabilities()));
final Account createdAccount = db.getAccountDao().getAccountById(createdId);
assertEquals("https://äöüß.example.com", createdAccount.getUrl());
assertEquals("彼得", createdAccount.getUserName());
@@ -56,8 +50,8 @@ public class AccountDaoTest {
}
@Test
- public void updateApiVersionFromNull() throws NextcloudHttpRequestFailedException {
- final Account account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null))));
+ public void updateApiVersionFromNull() {
+ final Account account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", null, new Capabilities())));
assertNull(account.getApiVersion());
assertEquals(0, db.getAccountDao().updateApiVersion(account.getId(), null));
@@ -66,8 +60,10 @@ public class AccountDaoTest {
}
@Test
- public void updateApiVersionFromExisting() throws NextcloudHttpRequestFailedException {
- final Account account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {data: {capabilities: {notes: {api_version: '[0.2]'}}}}}", null))));
+ public void updateApiVersionFromExisting() {
+ final Capabilities capabilities = new Capabilities();
+ capabilities.setApiVersion("[0.2]");
+ final Account account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", null, capabilities)));
assertEquals("[0.2]", account.getApiVersion());
assertEquals(0, db.getAccountDao().updateApiVersion(account.getId(), "[0.2]"));
@@ -75,4 +71,19 @@ public class AccountDaoTest {
assertEquals(1, db.getAccountDao().updateApiVersion(account.getId(), null));
}
+ @Test
+ public void updateDisplayName() {
+ final Account account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", null, new Capabilities())));
+ assertEquals("Should read userName in favor of displayName if displayName is NULL", "彼得", account.getDisplayName());
+
+ db.getAccountDao().updateDisplayName(account.getId(), "");
+ assertEquals("Should properly update the displayName, even if it is blank", "", db.getAccountDao().getAccountById(account.getId()).getDisplayName());
+
+ db.getAccountDao().updateDisplayName(account.getId(), "Foo Bar");
+ assertEquals("Foo Bar", db.getAccountDao().getAccountById(account.getId()).getDisplayName());
+
+ db.getAccountDao().updateDisplayName(account.getId(), null);
+ assertEquals("Should read userName in favor of displayName if displayName is NULL", "彼得", db.getAccountDao().getAccountById(account.getId()).getDisplayName());
+ }
+
} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/ApiProviderTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/ApiProviderTest.java
new file mode 100644
index 00000000..d23b8190
--- /dev/null
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/ApiProviderTest.java
@@ -0,0 +1,92 @@
+package it.niedermann.owncloud.notes.persistence;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
+import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+
+@RunWith(RobolectricTestRunner.class)
+public class ApiProviderTest {
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ private ApiProvider apiProvider;
+ private SingleSignOnAccount ssoAccount;
+ private SingleSignOnAccount secondSsoAccount;
+
+ @Before
+ public void setup() {
+ apiProvider = ApiProvider.getInstance();
+ ssoAccount = new SingleSignOnAccount("one", "eins", "1", "example.com", "");
+ secondSsoAccount = new SingleSignOnAccount("two", "zwei", "2", "example.org", "");
+ }
+
+ @Test
+ public void testGetOcsAPI() {
+ OcsAPI api = apiProvider.getOcsAPI(ApplicationProvider.getApplicationContext(), ssoAccount);
+
+ assertNotNull(api);
+ assertSame(api, apiProvider.getOcsAPI(ApplicationProvider.getApplicationContext(), ssoAccount));
+ assertNotSame(api, apiProvider.getOcsAPI(ApplicationProvider.getApplicationContext(), secondSsoAccount));
+
+ apiProvider.invalidateAPICache(ssoAccount);
+
+ final OcsAPI newApi = apiProvider.getOcsAPI(ApplicationProvider.getApplicationContext(), ssoAccount);
+
+ assertNotSame("After invalidating the cache, a new API instance is returned", api, newApi);
+
+ api = newApi;
+
+ assertSame(api, apiProvider.getOcsAPI(ApplicationProvider.getApplicationContext(), ssoAccount));
+ assertNotSame("A new instance should be returned when another SingleSignOn account is provided",
+ api, apiProvider.getOcsAPI(ApplicationProvider.getApplicationContext(), secondSsoAccount));
+
+ apiProvider.invalidateAPICache();
+
+ assertNotSame(api, apiProvider.getOcsAPI(ApplicationProvider.getApplicationContext(), ssoAccount));
+ assertNotSame(newApi, apiProvider.getOcsAPI(ApplicationProvider.getApplicationContext(), secondSsoAccount));
+ }
+
+ @Test
+ public void testGetNotesAPI() {
+ final NotesAPI notesAPI = apiProvider.getNotesAPI(ApplicationProvider.getApplicationContext(), ssoAccount, ApiVersion.API_VERSION_0_2);
+
+ assertNotNull(notesAPI);
+
+ assertSame("Before a manual invalidation, the returned Notes API will be the same instance",
+ notesAPI, apiProvider.getNotesAPI(ApplicationProvider.getApplicationContext(), ssoAccount, ApiVersion.API_VERSION_0_2));
+ assertSame("Before a manual invalidation, the returned Notes API will be the same instance, no matter which preferred version is passed",
+ notesAPI, apiProvider.getNotesAPI(ApplicationProvider.getApplicationContext(), ssoAccount, ApiVersion.API_VERSION_1_0));
+
+ apiProvider.invalidateAPICache();
+
+ final NotesAPI newNotesAPI = apiProvider.getNotesAPI(ApplicationProvider.getApplicationContext(), ssoAccount, ApiVersion.API_VERSION_1_0);
+
+ assertNotSame("After a manual invalidation, the returned Notes API will be a new instance",
+ notesAPI, newNotesAPI);
+ assertSame("Before a manual invalidation, the returned Notes API will be the same instance, no matter which preferred version is passed",
+ newNotesAPI, apiProvider.getNotesAPI(ApplicationProvider.getApplicationContext(), ssoAccount, ApiVersion.API_VERSION_0_2));
+ assertNotSame("Before a manual invalidation, the returned Notes API will be a different instance, even if the preferred version is the same, if the ssoAccount is different",
+ newNotesAPI, apiProvider.getNotesAPI(ApplicationProvider.getApplicationContext(), secondSsoAccount, ApiVersion.API_VERSION_0_2));
+
+ apiProvider.invalidateAPICache(ssoAccount);
+
+ assertNotSame("After a manual invalidation, the returned Notes API will be a new instance",
+ newNotesAPI, apiProvider.getNotesAPI(ApplicationProvider.getApplicationContext(), ssoAccount, ApiVersion.API_VERSION_0_2));
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClientTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClientTest.java
new file mode 100644
index 00000000..2ed59b05
--- /dev/null
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClientTest.java
@@ -0,0 +1,99 @@
+package it.niedermann.owncloud.notes.persistence;
+
+import android.accounts.NetworkErrorException;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.nextcloud.android.sso.api.ParsedResponse;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+import java.util.Map;
+
+import io.reactivex.Observable;
+import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import it.niedermann.owncloud.notes.shared.model.OcsResponse;
+import it.niedermann.owncloud.notes.shared.model.OcsUser;
+import retrofit2.Call;
+import retrofit2.Response;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(RobolectricTestRunner.class)
+public class CapabilitiesClientTest {
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ private final SingleSignOnAccount ssoAccount = mock(SingleSignOnAccount.class);
+ private final ApiProvider apiProvider = mock(ApiProvider.class);
+ private final OcsAPI ocsAPI = mock(OcsAPI.class);
+
+ @Before
+ public void setup() {
+ when(apiProvider.getOcsAPI(any(), any())).thenReturn(ocsAPI);
+ }
+
+ @Test
+ public void testGetCapabilities() throws Throwable {
+ //noinspection unchecked
+ final ParsedResponse<OcsResponse<Capabilities>> responseMock = mock(ParsedResponse.class);
+
+ final OcsResponse<Capabilities> mockOcs = new OcsResponse<>();
+ mockOcs.ocs = new OcsResponse.OcsWrapper<>();
+ mockOcs.ocs.data = new Capabilities();
+ mockOcs.ocs.data.setApiVersion("[1.0]");
+
+ when(responseMock.getResponse()).thenReturn(mockOcs);
+ when(responseMock.getHeaders()).thenReturn(Map.of("ETag", "1234"));
+ when(ocsAPI.getCapabilities(any())).thenReturn(Observable.just(responseMock));
+
+ final Capabilities capabilities = CapabilitiesClient.getCapabilities(ApplicationProvider.getApplicationContext(), ssoAccount, null, apiProvider);
+
+ assertEquals("[1.0]", capabilities.getApiVersion());
+ assertEquals("ETag should be read correctly from response but wasn't.", "1234", capabilities.getETag());
+
+ when(ocsAPI.getCapabilities(any())).thenReturn(Observable.error(new RuntimeException()));
+ assertThrows(RuntimeException.class, () -> CapabilitiesClient.getCapabilities(ApplicationProvider.getApplicationContext(), ssoAccount, null, apiProvider));
+
+ when(ocsAPI.getCapabilities(any())).thenReturn(Observable.error(new RuntimeException(new NetworkErrorException())));
+ assertThrows("Should unwrap exception cause if possible", NetworkErrorException.class, () -> CapabilitiesClient.getCapabilities(ApplicationProvider.getApplicationContext(), ssoAccount, null, apiProvider));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testGetDisplayName() throws IOException {
+ final OcsResponse<OcsUser> mockOcs = new OcsResponse<>();
+ mockOcs.ocs = new OcsResponse.OcsWrapper<>();
+ mockOcs.ocs.data = new OcsUser();
+ mockOcs.ocs.data.displayName = "Peter";
+ final Response<OcsResponse<OcsUser>> responseMock = Response.success(mockOcs);
+ final Call<OcsResponse<OcsUser>> callMock = mock(Call.class);
+
+ when(ocsAPI.getUser(any())).thenReturn(callMock);
+
+ when(callMock.execute()).thenReturn(responseMock);
+ assertEquals("Peter", CapabilitiesClient.getDisplayName(ApplicationProvider.getApplicationContext(), ssoAccount, apiProvider));
+
+ when(callMock.execute()).thenThrow(new RuntimeException() {
+ @Override
+ public void printStackTrace() {
+ // Do not spam console, this will be printed.
+ }
+ });
+ assertNull(CapabilitiesClient.getDisplayName(ApplicationProvider.getApplicationContext(), ssoAccount, apiProvider));
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java
index 17df5122..bd073054 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDaoTest.java
@@ -1,22 +1,18 @@
package it.niedermann.owncloud.notes.persistence;
import android.database.sqlite.SQLiteConstraintException;
-import android.os.Build;
import androidx.annotation.NonNull;
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
import java.util.Calendar;
import java.util.List;
@@ -37,7 +33,6 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
-@Config(sdk = {Build.VERSION_CODES.P})
public class NotesDaoTest {
@Rule
@@ -48,12 +43,12 @@ public class NotesDaoTest {
private Account account;
@Before
- public void setupDB() throws NextcloudHttpRequestFailedException {
+ public void setupDB() {
db = Room
.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), NotesDatabase.class)
.allowMainThreadQueries()
.build();
- db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null)));
+ db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", null, new Capabilities()));
account = db.getAccountDao().getAccountByName("彼得@äöüß.example.com");
}
@@ -67,12 +62,12 @@ public class NotesDaoTest {
db.getNoteDao().addNote(new Note(1, 1L, Calendar.getInstance(), "T", "C", "", false, "1", LOCAL_DELETED, account.getId(), "", 0));
db.getNoteDao().deleteByNoteId(1, LOCAL_DELETED);
assertNull(db.getNoteDao().getNoteById(1));
- assertNull(NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().getNoteById$(1)));
+ assertNull(NotesTestingUtil.getOrAwaitValue(db.getNoteDao().getNoteById$(1)));
db.getNoteDao().addNote(new Note(1, 1L, Calendar.getInstance(), "T", "C", "", false, "1", LOCAL_DELETED, account.getId(), "", 0));
db.getNoteDao().deleteByNoteId(1, VOID);
assertEquals(1, db.getNoteDao().getNoteById(1).getId());
- assertEquals(1, NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().getNoteById$(1)).getId());
+ assertEquals(1, NotesTestingUtil.getOrAwaitValue(db.getNoteDao().getNoteById$(1)).getId());
}
@Test
@@ -109,7 +104,7 @@ public class NotesDaoTest {
}
@Test
- public void getRemoteIds() throws NextcloudHttpRequestFailedException {
+ public void getRemoteIds() {
final Account secondAccount = setupSecondAccount();
db.getNoteDao().addNote(new Note(1, 4711L, Calendar.getInstance(), "T", "C", "", false, "1", VOID, account.getId(), "", 0));
@@ -150,29 +145,29 @@ public class NotesDaoTest {
}
@Test
- public void getFavoritesCount() throws NextcloudHttpRequestFailedException, InterruptedException {
+ public void getFavoritesCount() throws InterruptedException {
final Account secondAccount = setupSecondAccountAndTestNotes();
assertEquals(Integer.valueOf(1), db.getNoteDao().countFavorites(account.getId()));
assertEquals(Integer.valueOf(1), db.getNoteDao().countFavorites(secondAccount.getId()));
- assertEquals(Integer.valueOf(1), NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().countFavorites$(account.getId())));
- assertEquals(Integer.valueOf(1), NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().countFavorites$(secondAccount.getId())));
+ assertEquals(Integer.valueOf(1), NotesTestingUtil.getOrAwaitValue(db.getNoteDao().countFavorites$(account.getId())));
+ assertEquals(Integer.valueOf(1), NotesTestingUtil.getOrAwaitValue(db.getNoteDao().countFavorites$(secondAccount.getId())));
}
@Test
- public void count() throws NextcloudHttpRequestFailedException, InterruptedException {
+ public void count() throws InterruptedException {
final Account secondAccount = setupSecondAccountAndTestNotes();
assertEquals(Integer.valueOf(7), db.getNoteDao().count(account.getId()));
assertEquals(Integer.valueOf(5), db.getNoteDao().count(secondAccount.getId()));
- assertEquals(Integer.valueOf(7), NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().count$(account.getId())));
- assertEquals(Integer.valueOf(5), NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().count$(secondAccount.getId())));
+ assertEquals(Integer.valueOf(7), NotesTestingUtil.getOrAwaitValue(db.getNoteDao().count$(account.getId())));
+ assertEquals(Integer.valueOf(5), NotesTestingUtil.getOrAwaitValue(db.getNoteDao().count$(secondAccount.getId())));
}
@Test
- public void getLocalModifiedNotes() throws NextcloudHttpRequestFailedException {
+ public void getLocalModifiedNotes() {
final Account secondAccount = setupSecondAccountAndTestNotes();
final List<Note> accountNotes = db.getNoteDao().getLocalModifiedNotes(account.getId());
@@ -339,20 +334,10 @@ public class NotesDaoTest {
}
@Test
- public void getContent() throws InterruptedException {
- final Note note = new Note(1, 1L, Calendar.getInstance(), "My-Title", "My-Content", "", false, "1", LOCAL_DELETED, account.getId(), "", 0);
- db.getNoteDao().addNote(note);
- assertEquals("My-Content", db.getNoteDao().getContent(note.getId()));
- assertEquals("My-Content", NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().getContent$(note.getId())));
- assertNull(db.getNoteDao().getContent(note.getId() + 1));
- assertNull(NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().getContent$(note.getId() + 1)));
- }
-
- @Test
- public void getCategoriesLiveData() throws InterruptedException, NextcloudHttpRequestFailedException {
+ public void getCategoriesLiveData() throws InterruptedException {
final Account secondAccount = setupSecondAccountAndTestNotes();
- final List<CategoryWithNotesCount> accountCategories = NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().getCategories$(account.getId()));
+ final List<CategoryWithNotesCount> accountCategories = NotesTestingUtil.getOrAwaitValue(db.getNoteDao().getCategories$(account.getId()));
assertEquals(4, accountCategories.size());
for (CategoryWithNotesCount category : accountCategories) {
assertEquals(account.getId(), category.getAccountId());
@@ -363,7 +348,7 @@ public class NotesDaoTest {
assertTrue(accountCategories.stream().anyMatch(cat -> "ToDo".equals(cat.getCategory()) && Integer.valueOf(1).equals(cat.getTotalNotes())));
assertTrue(accountCategories.stream().anyMatch(cat -> "日记".equals(cat.getCategory()) && Integer.valueOf(1).equals(cat.getTotalNotes())));
- final List<CategoryWithNotesCount> secondAccountCategories = NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().getCategories$(secondAccount.getId()));
+ final List<CategoryWithNotesCount> secondAccountCategories = NotesTestingUtil.getOrAwaitValue(db.getNoteDao().getCategories$(secondAccount.getId()));
assertEquals(2, secondAccountCategories.size());
for (CategoryWithNotesCount category : secondAccountCategories) {
assertEquals(secondAccount.getId(), category.getAccountId());
@@ -375,21 +360,21 @@ public class NotesDaoTest {
}
@Test
- public void searchCategories() throws InterruptedException, NextcloudHttpRequestFailedException {
+ public void searchCategories() throws InterruptedException {
final Account secondAccount = setupSecondAccountAndTestNotes();
- assertEquals(2, NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "M%")).size());
- assertEquals(1, NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "Mo%")).size());
- assertEquals(1, NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "MO%")).size());
- assertEquals(1, NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "movie%")).size());
- assertEquals(1, NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "T%")).size());
- assertEquals(1, NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "日记")).size());
- assertEquals(2, NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(secondAccount.getId(), "M%")).size());
- assertEquals(0, NotesDatabaseTestUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(secondAccount.getId(), "T%")).size());
+ assertEquals(2, NotesTestingUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "M%")).size());
+ assertEquals(1, NotesTestingUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "Mo%")).size());
+ assertEquals(1, NotesTestingUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "MO%")).size());
+ assertEquals(1, NotesTestingUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "movie%")).size());
+ assertEquals(1, NotesTestingUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "T%")).size());
+ assertEquals(1, NotesTestingUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(account.getId(), "日记")).size());
+ assertEquals(2, NotesTestingUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(secondAccount.getId(), "M%")).size());
+ assertEquals(0, NotesTestingUtil.getOrAwaitValue(db.getNoteDao().searchCategories$(secondAccount.getId(), "T%")).size());
}
@Test
- public void searchRecentByModified() throws NextcloudHttpRequestFailedException {
+ public void searchRecentByModified() {
final Account secondAccount = setupSecondAccountAndTestNotes();
final List<Note> result = db.getNoteDao().searchRecentByModified(secondAccount.getId(), "T");
assertEquals(5, result.size());
@@ -409,12 +394,12 @@ public class NotesDaoTest {
return true;
}
- private Account setupSecondAccount() throws NextcloudHttpRequestFailedException {
- db.getAccountDao().insert(new Account("https://example.org", "test", "test@example.org", new Capabilities("{ocs: {}}", null)));
+ private Account setupSecondAccount() {
+ db.getAccountDao().insert(new Account("https://example.org", "test", "test@example.org", null, new Capabilities()));
return db.getAccountDao().getAccountByName("test@example.org");
}
- private Account setupSecondAccountAndTestNotes() throws NextcloudHttpRequestFailedException {
+ private Account setupSecondAccountAndTestNotes() {
final Account secondAccount = setupSecondAccount();
long uniqueId = 1;
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java
index 11c58dde..61a8069c 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesRepositoryTest.java
@@ -1,7 +1,7 @@
package it.niedermann.owncloud.notes.persistence;
import android.content.Context;
-import android.os.Build;
+import android.os.NetworkOnMainThreadException;
import androidx.annotation.NonNull;
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
@@ -9,23 +9,22 @@ import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.util.concurrent.MoreExecutors;
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
+import org.json.JSONException;
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
+import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Map;
-import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import it.niedermann.owncloud.notes.persistence.entity.Account;
@@ -33,17 +32,27 @@ import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
-import static it.niedermann.owncloud.notes.persistence.NotesDatabaseTestUtil.getOrAwaitValue;
+import static it.niedermann.owncloud.notes.persistence.NotesTestingUtil.getOrAwaitValue;
import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED;
import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_EDITED;
import static it.niedermann.owncloud.notes.shared.model.DBStatus.VOID;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
@RunWith(RobolectricTestRunner.class)
-@Config(sdk = {Build.VERSION_CODES.P})
public class NotesRepositoryTest {
@Rule
@@ -55,18 +64,18 @@ public class NotesRepositoryTest {
private NotesDatabase db;
@Before
- public void setupDB() throws NextcloudHttpRequestFailedException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ public void setupDB() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, JSONException {
final Context context = ApplicationProvider.getApplicationContext();
db = Room
.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), NotesDatabase.class)
.allowMainThreadQueries()
.build();
- final Constructor<NotesRepository> constructor = NotesRepository.class.getDeclaredConstructor(Context.class, NotesDatabase.class, ExecutorService.class);
+ final Constructor<NotesRepository> constructor = NotesRepository.class.getDeclaredConstructor(Context.class, NotesDatabase.class, ExecutorService.class, ApiProvider.class);
constructor.setAccessible(true);
- repo = constructor.newInstance(context, db, MoreExecutors.newDirectExecutorService());
+ repo = constructor.newInstance(context, db, MoreExecutors.newDirectExecutorService(), ApiProvider.getInstance());
- repo.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{\"ocs\":{\"meta\":{\"status\":\"ok\",\"statuscode\":200,\"message\":\"OK\"},\"data\":{\"version\":{\"major\":18,\"minor\":0,\"micro\":4,\"string\":\"18.0.4\",\"edition\":\"\",\"extendedSupport\":false},\"capabilities\":{\"core\":{\"pollinterval\":60,\"webdav-root\":\"remote.php\\/webdav\"},\"bruteforce\":{\"delay\":0},\"files\":{\"bigfilechunking\":true,\"blacklisted_files\":[\".htaccess\"],\"directEditing\":{\"url\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/files\\/api\\/v1\\/directEditing\",\"etag\":\"ed2b141af2a39b0e42666952ba60988d\"},\"versioning\":true,\"undelete\":true},\"activity\":{\"apiv2\":[\"filters\",\"filters-api\",\"previews\",\"rich-strings\"]},\"ocm\":{\"enabled\":true,\"apiVersion\":\"1.0-proposal1\",\"endPoint\":\"https:\\/\\/efss.qloud.my\\/index.php\\/ocm\",\"resourceTypes\":[{\"name\":\"file\",\"shareTypes\":[\"user\",\"group\"],\"protocols\":{\"webdav\":\"\\/public.php\\/webdav\\/\"}}]},\"deck\":{\"version\":\"0.8.2\"},\"richdocuments\":{\"mimetypes\":[\"application\\/vnd.oasis.opendocument.text\",\"application\\/vnd.oasis.opendocument.spreadsheet\",\"application\\/vnd.oasis.opendocument.graphics\",\"application\\/vnd.oasis.opendocument.presentation\",\"application\\/vnd.lotus-wordpro\",\"application\\/vnd.visio\",\"application\\/vnd.wordperfect\",\"application\\/msonenote\",\"application\\/msword\",\"application\\/rtf\",\"text\\/rtf\",\"application\\/vnd.openxmlformats-officedocument.wordprocessingml.document\",\"application\\/vnd.openxmlformats-officedocument.wordprocessingml.template\",\"application\\/vnd.ms-word.document.macroEnabled.12\",\"application\\/vnd.ms-word.template.macroEnabled.12\",\"application\\/vnd.ms-excel\",\"application\\/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\"application\\/vnd.openxmlformats-officedocument.spreadsheetml.template\",\"application\\/vnd.ms-excel.sheet.macroEnabled.12\",\"application\\/vnd.ms-excel.template.macroEnabled.12\",\"application\\/vnd.ms-excel.addin.macroEnabled.12\",\"application\\/vnd.ms-excel.sheet.binary.macroEnabled.12\",\"application\\/vnd.ms-powerpoint\",\"application\\/vnd.openxmlformats-officedocument.presentationml.presentation\",\"application\\/vnd.openxmlformats-officedocument.presentationml.template\",\"application\\/vnd.openxmlformats-officedocument.presentationml.slideshow\",\"application\\/vnd.ms-powerpoint.addin.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.presentation.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.template.macroEnabled.12\",\"application\\/vnd.ms-powerpoint.slideshow.macroEnabled.12\",\"text\\/csv\"],\"mimetypesNoDefaultOpen\":[\"image\\/svg+xml\",\"application\\/pdf\",\"text\\/plain\",\"text\\/spreadsheet\"],\"collabora\":[],\"direct_editing\":false,\"templates\":false,\"productName\":\"\\u5728\\u7ebf\\u534f\\u4f5c\"},\"dav\":{\"chunking\":\"1.0\"},\"files_sharing\":{\"api_enabled\":true,\"public\":{\"enabled\":true,\"password\":{\"enforced\":true,\"askForOptionalPassword\":false},\"expire_date\":{\"enabled\":true,\"days\":\"7\",\"enforced\":false},\"multiple_links\":true,\"expire_date_internal\":{\"enabled\":false},\"send_mail\":false,\"upload\":true,\"upload_files_drop\":true},\"resharing\":true,\"user\":{\"send_mail\":false,\"expire_date\":{\"enabled\":true}},\"group_sharing\":true,\"group\":{\"enabled\":true,\"expire_date\":{\"enabled\":true}},\"default_permissions\":31,\"federation\":{\"outgoing\":false,\"incoming\":false,\"expire_date\":{\"enabled\":true}},\"sharee\":{\"query_lookup_default\":false},\"sharebymail\":{\"enabled\":true,\"upload_files_drop\":{\"enabled\":true},\"password\":{\"enabled\":true},\"expire_date\":{\"enabled\":true}}},\"external\":{\"v1\":[\"sites\",\"device\",\"groups\",\"redirect\"]},\"notifications\":{\"ocs-endpoints\":[\"list\",\"get\",\"delete\",\"delete-all\",\"icons\",\"rich-strings\",\"action-web\"],\"push\":[\"devices\",\"object-data\",\"delete\"],\"admin-notifications\":[\"ocs\",\"cli\"]},\"password_policy\":{\"minLength\":8,\"enforceNonCommonPassword\":true,\"enforceNumericCharacters\":false,\"enforceSpecialCharacters\":false,\"enforceUpperLowerCase\":false,\"api\":{\"generate\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/password_policy\\/api\\/v1\\/generate\",\"validate\":\"https:\\/\\/efss.qloud.my\\/ocs\\/v2.php\\/apps\\/password_policy\\/api\\/v1\\/validate\"}},\"theming\":{\"name\":\"QloudData\",\"url\":\"https:\\/\\/www.qloud.my\\/qloud-data\\/\",\"slogan\":\"Powered by NextCloud\",\"color\":\"#1E4164\",\"color-text\":\"#ffffff\",\"color-element\":\"#1E4164\",\"logo\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\",\"background\":\"https:\\/\\/efss.qloud.my\\/core\\/img\\/background.png?v=47\",\"background-plain\":false,\"background-default\":true,\"logoheader\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\",\"favicon\":\"https:\\/\\/efss.qloud.my\\/index.php\\/apps\\/theming\\/image\\/logo?useSvg=1&v=47\"},\"registration\":{\"enabled\":true,\"apiRoot\":\"\\/ocs\\/v2.php\\/apps\\/registration\\/api\\/v1\\/\",\"apiLevel\":\"v1\"}}}}}", null), new IResponseCallback<Account>() {
+ repo.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities(), null, new IResponseCallback<Account>() {
@Override
public void onSuccess(Account result) {
@@ -79,7 +88,7 @@ public class NotesRepositoryTest {
});
account = repo.getAccountByName("彼得@äöüß.example.com");
- repo.addAccount("https://example.org", "test", "test@example.org", new Capabilities("{ocs: {}}", null), new IResponseCallback<Account>() {
+ repo.addAccount("https://example.org", "test", "test@example.org", new Capabilities(), "Herbert", new IResponseCallback<Account>() {
@Override
public void onSuccess(Account result) {
@@ -111,6 +120,13 @@ public class NotesRepositoryTest {
}
@Test
+ public void testGetInstance() {
+ final NotesRepository repo = NotesRepository.getInstance(ApplicationProvider.getApplicationContext());
+ assertNotNull("Result of NotesRepository.getInstance() must not be null", repo);
+ assertSame("Result of NotesRepository.getInstance() must always return the same instance", repo, NotesRepository.getInstance(ApplicationProvider.getApplicationContext()));
+ }
+
+ @Test
public void testGetIdMap() {
final Map<Long, Long> idMapOfFirstAccount = repo.getIdMap(account.getId());
assertEquals(3, idMapOfFirstAccount.size());
@@ -124,8 +140,8 @@ public class NotesRepositoryTest {
}
@Test
- public void testAddAccount() throws NextcloudHttpRequestFailedException, InterruptedException {
- repo.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities("{ocs: {}}", null), new IResponseCallback<Account>() {
+ public void testAddAccount() {
+ repo.addAccount("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", new Capabilities(), "", new IResponseCallback<Account>() {
@Override
public void onSuccess(Account createdAccount) {
assertEquals("https://äöüß.example.com", createdAccount.getUrl());
@@ -141,6 +157,17 @@ public class NotesRepositoryTest {
}
@Test
+ public void testDeleteAccount() throws IOException {
+ NotesTestingUtil.mockSingleSignOn(new SingleSignOnAccount(account.getAccountName(), account.getUserName(), "1337", account.getUrl(), ""));
+
+ assertNotNull(repo.getAccountById(account.getId()));
+
+ repo.deleteAccount(account);
+
+ assertNull(repo.getAccountById(account.getId()));
+ }
+
+ @Test
public void testAddNote() {
final Note localNote = new Note(null, Calendar.getInstance(), "Fancy Title", "MyContent", "Samples", false, "123");
localNote.setId(99);
@@ -155,33 +182,94 @@ public class NotesRepositoryTest {
@Test
public void updateApiVersion() {
- assertThrows(IllegalArgumentException.class, () -> repo.updateApiVersion(account.getId(), ""));
- assertThrows(IllegalArgumentException.class, () -> repo.updateApiVersion(account.getId(), "asdf"));
- assertThrows(IllegalArgumentException.class, () -> repo.updateApiVersion(account.getId(), "{}"));
+ repo.updateApiVersion(account.getId(), "");
+ assertNull(repo.getAccountById(account.getId()).getApiVersion());
+
+ repo.updateApiVersion(account.getId(), "foo");
+ assertNull(repo.getAccountById(account.getId()).getApiVersion());
+
+ repo.updateApiVersion(account.getId(), "{}");
+ assertNull(repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), null);
assertNull(repo.getAccountById(account.getId()).getApiVersion());
+
repo.updateApiVersion(account.getId(), "[]");
assertNull(repo.getAccountById(account.getId()).getApiVersion());
repo.updateApiVersion(account.getId(), "[1.0]");
assertEquals("[1.0]", repo.getAccountById(account.getId()).getApiVersion());
+
repo.updateApiVersion(account.getId(), "[0.2, 1.0]");
- assertEquals("[0.2, 1.0]", repo.getAccountById(account.getId()).getApiVersion());
+ assertEquals("[0.2,1.0]", repo.getAccountById(account.getId()).getApiVersion());
- // TODO is this really indented?
- repo.updateApiVersion(account.getId(), "[0.2, abc]");
- assertEquals("[0.2, abc]", repo.getAccountById(account.getId()).getApiVersion());
+ repo.updateApiVersion(account.getId(), "[0.2, foo]");
+ assertEquals("[0.2]", repo.getAccountById(account.getId()).getApiVersion());
}
@Test
- @Ignore("Need to find a way to stub deleteAndSync method")
public void moveNoteToAnotherAccount() throws InterruptedException {
- final Note noteToMove = repo.getNoteById(1);
- assertEquals(3, repo.getLocalModifiedNotes(secondAccount.getId()).size());
- final Note movedNote = getOrAwaitValue(repo.moveNoteToAnotherAccount(secondAccount, noteToMove));
- assertEquals(4, repo.getLocalModifiedNotes(secondAccount.getId()).size());
+ final NotesRepository repoSpy = spy(repo);
+ final Note noteToMove = repoSpy.getNoteById(1);
+
+ assertEquals(VOID, noteToMove.getStatus());
+ assertEquals(3, repoSpy.getLocalModifiedNotes(secondAccount.getId()).size());
+
+ doNothing().when(repoSpy).deleteNoteAndSync(any(), anyLong());
+ doNothing().when(repoSpy).scheduleSync(any(), anyBoolean());
+
+ final Note movedNote = getOrAwaitValue(repoSpy.moveNoteToAnotherAccount(secondAccount, noteToMove));
+
+ assertEquals(4, repoSpy.getLocalModifiedNotes(secondAccount.getId()).size());
+ assertEquals("美好的一天", movedNote.getTitle());
+ assertEquals("C", movedNote.getContent());
+ assertEquals("Movies", movedNote.getCategory());
assertEquals(LOCAL_EDITED, movedNote.getStatus());
- // TODO assert deleteAndSync has been called
+
+ verify(repoSpy, times(1)).deleteNoteAndSync(any(), anyLong());
+ verify(repoSpy, times(1)).addNoteAndSync(any(), any());
+ }
+
+ @Test
+ public void testSyncStatusLiveData() throws InterruptedException, IOException {
+ NotesTestingUtil.mockSingleSignOn(new SingleSignOnAccount(account.getAccountName(), account.getUserName(), "1337", account.getUrl(), ""));
+
+ assertFalse(NotesTestingUtil.getOrAwaitValue(repo.getSyncStatus()));
+ repo.addCallbackPull(account, () -> {
+ try {
+ assertTrue(NotesTestingUtil.getOrAwaitValue(repo.getSyncStatus()));
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ });
+ repo.scheduleSync(account, false);
+ assertFalse(NotesTestingUtil.getOrAwaitValue(repo.getSyncStatus()));
+ }
+
+ @Test
+ public void testSyncErrorsLiveData() throws InterruptedException, IOException {
+ NotesTestingUtil.mockSingleSignOn(new SingleSignOnAccount(account.getAccountName(), account.getUserName(), "1337", account.getUrl(), ""));
+
+ assertThrows("The very first time, this LiveData should never have been set", RuntimeException.class, () -> NotesTestingUtil.getOrAwaitValue(repo.getSyncErrors()));
+ repo.scheduleSync(account, false);
+ //noinspection ConstantConditions
+ assertTrue("In our scenario, we expect an exception of type " + NetworkOnMainThreadException.class.getSimpleName() + " to be handeled.", getOrAwaitValue(repo.getSyncErrors()).stream()
+ .anyMatch(e -> e.getMessage().contains(NetworkOnMainThreadException.class.getSimpleName())));
+ }
+
+ @Test
+ public void updateDisplayName() {
+ final Account account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account("https://äöüß.example.com", "彼得", "彼得@äöüß.example.com", null, new Capabilities())));
+ assertEquals("Should read userName in favor of displayName if displayName is NULL", "彼得", account.getDisplayName());
+
+ repo.updateDisplayName(account.getId(), "");
+ assertEquals("Should properly update the displayName, even if it is blank", "", db.getAccountDao().getAccountById(account.getId()).getDisplayName());
+
+ repo.updateDisplayName(account.getId(), "Foo Bar");
+ assertEquals("Foo Bar", db.getAccountDao().getAccountById(account.getId()).getDisplayName());
+
+ repo.updateDisplayName(account.getId(), null);
+ assertEquals("Should read userName in favor of displayName if displayName is NULL", "彼得", db.getAccountDao().getAccountById(account.getId()).getDisplayName());
}
} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTaskTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTaskTest.java
new file mode 100644
index 00000000..c0dcb682
--- /dev/null
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTaskTest.java
@@ -0,0 +1,98 @@
+package it.niedermann.owncloud.notes.persistence;
+
+import android.content.Context;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+
+import com.nextcloud.android.sso.api.ParsedResponse;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Map;
+
+import io.reactivex.Observable;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
+import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
+
+import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_EDITED;
+import static it.niedermann.owncloud.notes.shared.model.DBStatus.VOID;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@SuppressWarnings("CallToThreadRun")
+@RunWith(RobolectricTestRunner.class)
+public class NotesServerSyncTaskTest {
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ private NotesServerSyncTask task;
+
+ private final Account account = mock(Account.class);
+ private final NotesRepository repo = mock(NotesRepository.class);
+ private final NotesAPI notesAPI = mock(NotesAPI.class);
+ private final ApiProvider apiProvider = mock(ApiProvider.class);
+
+ @Before
+ public void setup() throws NextcloudFilesAppAccountNotFoundException, IOException {
+ when(apiProvider.getNotesAPI(any(), any(), any())).thenReturn(notesAPI);
+ NotesTestingUtil.mockSingleSignOn(new SingleSignOnAccount(account.getAccountName(), account.getUserName(), "", account.getUrl(), ""));
+ this.task = new NotesServerSyncTask(mock(Context.class), repo, account, false, apiProvider) {
+ @Override
+ void onPreExecute() {
+
+ }
+
+ @Override
+ void onPostExecute(SyncResultStatus status) {
+
+ }
+ };
+ }
+
+ @Test
+ public void testPushLocalChanges() {
+ when(repo.getLocalModifiedNotes(anyLong())).thenReturn(Arrays.asList(
+ new Note(1, null, Calendar.getInstance(), "Does not has a remoteId yet, therefore", "This note should be created on the server", "", false, "1", LOCAL_EDITED, 0, "", 0),
+ new Note(1, 2L, Calendar.getInstance(), "Has already a remoteId, therefore", "This note should be updated on the server", "", false, "1", LOCAL_EDITED, 0, "", 0)
+ ));
+
+ this.task.run();
+
+ verify(notesAPI).createNote(argThat(argument -> "This note should be created on the server".equals(argument.getContent())));
+ verify(notesAPI).editNote(argThat(argument -> "This note should be updated on the server".equals(argument.getContent())));
+ }
+
+ @Test
+ public void testPullRemoteChanges() {
+ when(repo.getAccountById(anyLong())).thenReturn(account);
+ when(repo.getIdMap(anyLong())).thenReturn(Map.of(1000L, 1L, 2000L, 2L));
+ when(repo.updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(anyLong(), anyLong(), anyString(), anyBoolean(), anyString(), anyString(), anyString(), anyString())).thenReturn(1);
+ when(notesAPI.getNotes(any(), any())).thenReturn(Observable.just(ParsedResponse.of(Arrays.asList(
+ new Note(0, 1000L, Calendar.getInstance(), "RemoteId is in the idMap, therefore", "This note should be updated locally", "", false, "1", VOID, 0, "", 0),
+ new Note(0, 3000L, Calendar.getInstance(), "Is a new RemoteId, therefore", "This note should be created locally", "", false, "1", VOID, 0, "", 0)
+ ))));
+
+ this.task.run();
+
+ verify(repo).addNote(anyLong(), argThat(argument -> "This note should be created locally".equals(argument.getContent())));
+ verify(repo).updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(anyLong(), anyLong(), anyString(), anyBoolean(), anyString(), anyString(), argThat("This note should be updated locally"::equals), anyString());
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDatabaseTestUtil.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesTestingUtil.java
index 26ec47c3..9ec8caa3 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesDatabaseTestUtil.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/NotesTestingUtil.java
@@ -1,18 +1,26 @@
package it.niedermann.owncloud.notes.persistence;
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.nextcloud.android.sso.AccountImporter;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
-import java.util.Random;
+import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-public class NotesDatabaseTestUtil {
+public class NotesTestingUtil {
private static long currentLong = 1;
- private NotesDatabaseTestUtil() {
+ private NotesTestingUtil() {
// Util class
}
@@ -39,18 +47,14 @@ public class NotesDatabaseTestUtil {
return (T) data[0];
}
- public static String randomString(int length) {
- final int leftLimit = 48; // numeral '0'
- final int rightLimit = 122; // letter 'z'
-
- return new Random().ints(leftLimit, rightLimit + 1)
- .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
- .limit(length)
- .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
- .toString();
- }
-
- public static long uniqueLong() {
- return currentLong++;
+ /**
+ * Pretends managing {@link SingleSignOnAccount}s by using own private {@link SharedPreferences}.
+ *
+ * @param ssoAccount this account will be added
+ */
+ public static void mockSingleSignOn(@NonNull SingleSignOnAccount ssoAccount) throws IOException {
+ final SharedPreferences sharedPrefs = ApplicationProvider.getApplicationContext().getSharedPreferences("TEMP_SHARED_PREFS_" + currentLong++, Context.MODE_PRIVATE);
+ sharedPrefs.edit().putString("PREF_ACCOUNT_STRING" + ssoAccount.name, SingleSignOnAccount.toString(ssoAccount)).commit();
+ AccountImporter.setSharedPreferences(sharedPrefs);
}
}
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializerTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializerTest.java
new file mode 100644
index 00000000..657c3e3e
--- /dev/null
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializerTest.java
@@ -0,0 +1,309 @@
+package it.niedermann.owncloud.notes.persistence.sync;
+
+import android.graphics.Color;
+
+import com.google.gson.JsonParser;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+@RunWith(RobolectricTestRunner.class)
+public class CapabilitiesDeserializerTest {
+
+ private final CapabilitiesDeserializer deserializer = new CapabilitiesDeserializer();
+
+ @Test
+ public void testDefaultWithoutApiVersion() {
+ //language=json
+ final String response = "" +
+ "{" +
+ " \"version\":{" +
+ " \"major\":18," +
+ " \"minor\":0," +
+ " \"micro\":4," +
+ " \"string\":\"18.0.4\"," +
+ " \"edition\":\"\"," +
+ " \"extendedSupport\":false" +
+ " }," +
+ " \"capabilities\":{" +
+ " \"theming\":{" +
+ " \"color\":\"#1E4164\"," +
+ " \"color-text\":\"#ffffff\"" +
+ " }" +
+ " }" +
+ "}";
+ final Capabilities capabilities = deserializer.deserialize(JsonParser.parseString(response), null, null);
+ assertNull(capabilities.getETag());
+ assertNull(capabilities.getApiVersion());
+ assertEquals(Color.parseColor("#1E4164"), capabilities.getColor());
+ assertEquals(Color.parseColor("#ffffff"), capabilities.getTextColor());
+ }
+
+ @Test
+ public void testDefaultWithApiVersion() {
+ //language=json
+ final String response = "" +
+ "{" +
+ " \"version\":{" +
+ " \"major\":18," +
+ " \"minor\":0," +
+ " \"micro\":4," +
+ " \"string\":\"18.0.4\"," +
+ " \"edition\":\"\"," +
+ " \"extendedSupport\":false" +
+ " }," +
+ " \"capabilities\":{" +
+ " \"notes\":{" +
+ " \"api_version\": [" +
+ " \"0.2\"," +
+ " \"1.1\"" +
+ " ]" +
+ " }," +
+ " \"theming\":{" +
+ " \"color\":\"#1E4164\"," +
+ " \"color-text\":\"#ffffff\"" +
+ " }" +
+ " }" +
+ "}";
+ final Capabilities capabilities = deserializer.deserialize(JsonParser.parseString(response), null, null);
+ assertNull(capabilities.getETag());
+ assertEquals("[\"0.2\",\"1.1\"]", capabilities.getApiVersion());
+ assertEquals(Color.parseColor("#1E4164"), capabilities.getColor());
+ assertEquals(Color.parseColor("#ffffff"), capabilities.getTextColor());
+ }
+
+ /**
+ * According to the <a href="https://github.com/nextcloud/notes/blob/master/docs/api/README.md#capabilites">REST-API documentation</a>, the <code>api_version</code> property is a "list of strings", so a plain string is not allowed.
+ */
+ @Test
+ public void testDefaultWithInvalidApiVersion() {
+ //language=json
+ final String response = "" +
+ "{" +
+ " \"version\":{" +
+ " \"major\":18," +
+ " \"minor\":0," +
+ " \"micro\":4," +
+ " \"string\":\"18.0.4\"," +
+ " \"edition\":\"\"," +
+ " \"extendedSupport\":false" +
+ " }," +
+ " \"capabilities\":{" +
+ " \"notes\":{" +
+ " \"api_version\": \"1.0\"" +
+ " }," +
+ " \"theming\":{" +
+ " \"color\":\"#1E4164\"," +
+ " \"color-text\":\"#ffffff\"" +
+ " }" +
+ " }" +
+ "}";
+ final Capabilities capabilities = deserializer.deserialize(JsonParser.parseString(response), null, null);
+ assertNull(capabilities.getETag());
+ assertEquals("\"1.0\"", capabilities.getApiVersion());
+ assertEquals(Color.parseColor("#1E4164"), capabilities.getColor());
+ assertEquals(Color.parseColor("#ffffff"), capabilities.getTextColor());
+ }
+
+
+ @Test
+ public void testRealisticSample() {
+ //language=json
+ final String response = "" +
+ "{" +
+ " \"version\": {" +
+ " \"major\": 20," +
+ " \"minor\": 0," +
+ " \"micro\": 7," +
+ " \"string\": \"20.0.7\"," +
+ " \"edition\": \"\"," +
+ " \"extendedSupport\": false" +
+ " }," +
+ " \"capabilities\": {" +
+ " \"core\": {" +
+ " \"pollinterval\": 60," +
+ " \"webdav-root\": \"remote.php/webdav\"" +
+ " }," +
+ " \"bruteforce\": {" +
+ " \"delay\": 0" +
+ " }," +
+ " \"files\": {" +
+ " \"bigfilechunking\": true," +
+ " \"blacklisted_files\": [" +
+ " \".htaccess\"" +
+ " ]," +
+ " \"directEditing\": {" +
+ " \"url\": \"https://nextcloud.example.com/ocs/v2.php/apps/files/api/v1/directEditing\"," +
+ " \"etag\": \"6226ba873373f5e73a3ef504107523f7\"" +
+ " }," +
+ " \"comments\": true," +
+ " \"undelete\": true," +
+ " \"versioning\": true" +
+ " }," +
+ " \"activity\": {" +
+ " \"apiv2\": [" +
+ " \"filters\"," +
+ " \"filters-api\"," +
+ " \"previews\"," +
+ " \"rich-strings\"" +
+ " ]" +
+ " }," +
+ " \"ocm\": {" +
+ " \"enabled\": true," +
+ " \"apiVersion\": \"1.0-proposal1\"," +
+ " \"endPoint\": \"https://nextcloud.example.com/index.php/ocm\"," +
+ " \"resourceTypes\": [" +
+ " {" +
+ " \"name\": \"file\"," +
+ " \"shareTypes\": [" +
+ " \"user\"," +
+ " \"group\"" +
+ " ]," +
+ " \"protocols\": {" +
+ " \"webdav\": \"/public.php/webdav/\"" +
+ " }" +
+ " }" +
+ " ]" +
+ " }," +
+ " \"dav\": {" +
+ " \"chunking\": \"1.0\"" +
+ " }," +
+ " \"deck\": {" +
+ " \"version\": \"1.2.5\"," +
+ " \"canCreateBoards\": true" +
+ " }," +
+ " \"notes\": {" +
+ " \"api_version\": [" +
+ " \"0.2\"," +
+ " \"1.1\"" +
+ " ]," +
+ " \"version\": \"4.0.4\"" +
+ " }," +
+ " \"notifications\": {" +
+ " \"ocs-endpoints\": [" +
+ " \"list\"," +
+ " \"get\"," +
+ " \"delete\"," +
+ " \"delete-all\"," +
+ " \"icons\"," +
+ " \"rich-strings\"," +
+ " \"action-web\"," +
+ " \"user-status\"" +
+ " ]," +
+ " \"push\": [" +
+ " \"devices\"," +
+ " \"object-data\"," +
+ " \"delete\"" +
+ " ]," +
+ " \"admin-notifications\": [" +
+ " \"ocs\"," +
+ " \"cli\"" +
+ " ]" +
+ " }," +
+ " \"password_policy\": {" +
+ " \"minLength\": 8," +
+ " \"enforceNonCommonPassword\": true," +
+ " \"enforceNumericCharacters\": false," +
+ " \"enforceSpecialCharacters\": false," +
+ " \"enforceUpperLowerCase\": false," +
+ " \"api\": {" +
+ " \"generate\": \"https://nextcloud.example.com/ocs/v2.php/apps/password_policy/api/v1/generate\"," +
+ " \"validate\": \"https://nextcloud.example.com/ocs/v2.php/apps/password_policy/api/v1/validate\"" +
+ " }" +
+ " }," +
+ " \"files_sharing\": {" +
+ " \"sharebymail\": {" +
+ " \"enabled\": true," +
+ " \"upload_files_drop\": {" +
+ " \"enabled\": true" +
+ " }," +
+ " \"password\": {" +
+ " \"enabled\": true," +
+ " \"enforced\": false" +
+ " }," +
+ " \"expire_date\": {" +
+ " \"enabled\": true" +
+ " }" +
+ " }," +
+ " \"api_enabled\": true," +
+ " \"public\": {" +
+ " \"enabled\": true," +
+ " \"password\": {" +
+ " \"enforced\": false," +
+ " \"askForOptionalPassword\": false" +
+ " }," +
+ " \"expire_date\": {" +
+ " \"enabled\": false" +
+ " }," +
+ " \"multiple_links\": true," +
+ " \"expire_date_internal\": {" +
+ " \"enabled\": false" +
+ " }," +
+ " \"send_mail\": false," +
+ " \"upload\": true," +
+ " \"upload_files_drop\": true" +
+ " }," +
+ " \"resharing\": true," +
+ " \"user\": {" +
+ " \"send_mail\": false," +
+ " \"expire_date\": {" +
+ " \"enabled\": true" +
+ " }" +
+ " }," +
+ " \"group_sharing\": true," +
+ " \"group\": {" +
+ " \"enabled\": true," +
+ " \"expire_date\": {" +
+ " \"enabled\": true" +
+ " }" +
+ " }," +
+ " \"default_permissions\": 31," +
+ " \"federation\": {" +
+ " \"outgoing\": true," +
+ " \"incoming\": true," +
+ " \"expire_date\": {" +
+ " \"enabled\": true" +
+ " }" +
+ " }," +
+ " \"sharee\": {" +
+ " \"query_lookup_default\": false" +
+ " }" +
+ " }," +
+ " \"theming\": {" +
+ " \"name\": \"Sample name\"," +
+ " \"url\": \"https://nextcloud.com\"," +
+ " \"slogan\": \"a safe home for all your data\"," +
+ " \"color\": \"#44616B\"," +
+ " \"color-text\": \"#ffffff\"," +
+ " \"color-element\": \"#44616B\"," +
+ " \"color-element-bright\": \"#44616B\"," +
+ " \"color-element-dark\": \"#44616B\"," +
+ " \"logo\": \"https://nextcloud.example.com/core/img/logo/logo.svg?v=8\"," +
+ " \"background\": \"#44616B\"," +
+ " \"background-plain\": true," +
+ " \"background-default\": true," +
+ " \"logoheader\": \"https://nextcloud.example.com/core/img/logo/logo.svg?v=8\"," +
+ " \"favicon\": \"https://nextcloud.example.com/core/img/logo/logo.svg?v=8\"" +
+ " }," +
+ " \"user_status\": {" +
+ " \"enabled\": true," +
+ " \"supports_emoji\": true" +
+ " }," +
+ " \"weather_status\": {" +
+ " \"enabled\": true" +
+ " }" +
+ " }" +
+ "}";
+ final Capabilities capabilities = deserializer.deserialize(JsonParser.parseString(response), null, null);
+ assertNull(capabilities.getETag());
+ assertEquals("[\"0.2\",\"1.1\"]", capabilities.getApiVersion());
+ assertEquals(Color.parseColor("#44616B"), capabilities.getColor());
+ assertEquals(Color.parseColor("#ffffff"), capabilities.getTextColor());
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/util/NotesColorUtilTest.java b/app/src/test/java/it/niedermann/owncloud/notes/persistence/util/NotesColorUtilTest.java
index 8645370c..f11a3519 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/persistence/util/NotesColorUtilTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/util/NotesColorUtilTest.java
@@ -1,14 +1,12 @@
package it.niedermann.owncloud.notes.persistence.util;
import android.graphics.Color;
-import android.os.Build;
import androidx.core.util.Pair;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.List;
@@ -19,7 +17,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
-@Config(sdk = {Build.VERSION_CODES.P})
public class NotesColorUtilTest {
@Test
public void testContrastRatioIsSufficient() {
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/model/CapabilitiesTest.java b/app/src/test/java/it/niedermann/owncloud/notes/shared/model/CapabilitiesTest.java
deleted file mode 100644
index 5a30bd2a..00000000
--- a/app/src/test/java/it/niedermann/owncloud/notes/shared/model/CapabilitiesTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package it.niedermann.owncloud.notes.shared.model;
-
-import android.graphics.Color;
-import android.os.Build;
-
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
-@RunWith(RobolectricTestRunner.class)
-@Config(sdk = {Build.VERSION_CODES.P})
-public class CapabilitiesTest {
-
- @Test
- public void testDefaultWithoutApiVersion() throws NextcloudHttpRequestFailedException {
- final String response = "" +
- "{" +
- " 'ocs':{" +
- " 'meta':{" +
- " 'status':'ok'," +
- " 'statuscode':200," +
- " 'message':'OK'" +
- " }," +
- " 'data':{" +
- " 'version':{" +
- " 'major':18," +
- " 'minor':0," +
- " 'micro':4," +
- " 'string':'18.0.4'," +
- " 'edition':''," +
- " 'extendedSupport':false" +
- " }," +
- " 'capabilities':{" +
- " 'theming':{" +
- " 'color':'#1E4164'," +
- " 'color-text':'#ffffff'" +
- " }" +
- " }" +
- " }" +
- " }" +
- "}";
- final Capabilities capabilities = new Capabilities(response, null);
- assertNull(capabilities.getETag());
- assertNull(capabilities.getApiVersion());
- assertEquals(Color.parseColor("#1E4164"), capabilities.getColor());
- assertEquals(Color.parseColor("#ffffff"), capabilities.getTextColor());
- }
-
- @Test
- public void testDefaultWithApiVersion() throws NextcloudHttpRequestFailedException {
- final String response = "" +
- "{" +
- " 'ocs':{" +
- " 'meta':{" +
- " 'status':'ok'," +
- " 'statuscode':200," +
- " 'message':'OK'" +
- " }," +
- " 'data':{" +
- " 'version':{" +
- " 'major':18," +
- " 'minor':0," +
- " 'micro':4," +
- " 'string':'18.0.4'," +
- " 'edition':''," +
- " 'extendedSupport':false" +
- " }," +
- " 'capabilities':{" +
- " 'notes':{" +
- " 'api_version': '1.0'" +
- " }," +
- " 'theming':{" +
- " 'color':'#1E4164'," +
- " 'color-text':'#ffffff'" +
- " }" +
- " }" +
- " }" +
- " }" +
- "}";
- final Capabilities capabilities = new Capabilities(response, null);
- assertNull(capabilities.getETag());
- assertEquals("1.0", capabilities.getApiVersion());
- assertEquals(Color.parseColor("#1E4164"), capabilities.getColor());
- assertEquals(Color.parseColor("#ffffff"), capabilities.getTextColor());
- }
-
- @Test
- public void etagShouldAlwaysBeStored() throws NextcloudHttpRequestFailedException {
- final Capabilities capabilities = new Capabilities("{ocs: {}}", "ed38bf28-e429-4231-84be-35d166acfb6d ");
- assertEquals("ed38bf28-e429-4231-84be-35d166acfb6d ", capabilities.getETag());
- }
-
- @Test(expected = NextcloudHttpRequestFailedException.class)
- public void throwsExceptionOnMaintenanceMode() throws NextcloudHttpRequestFailedException {
- final String response = "" +
- "{" +
- " 'ocs':{" +
- " 'meta':{" +
- " 'status':'ok'," +
- " 'statuscode':503," +
- " 'message':'OK'" +
- " }" +
- " }" +
- "}";
- new Capabilities(response, null);
- }
-} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtilTest.java b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtilTest.java
new file mode 100644
index 00000000..68f39125
--- /dev/null
+++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtilTest.java
@@ -0,0 +1,229 @@
+package it.niedermann.owncloud.notes.shared.util;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
+
+@RunWith(RobolectricTestRunner.class)
+public class ApiVersionUtilTest extends TestCase {
+
+ @Test
+ public void testParse_invalid_one() {
+ assertEquals(0, ApiVersionUtil.parse(null).size());
+ assertEquals(0, ApiVersionUtil.parse("").size());
+ assertEquals(0, ApiVersionUtil.parse(" ").size());
+ assertEquals(0, ApiVersionUtil.parse("{}").size());
+ assertEquals(0, ApiVersionUtil.parse("[]").size());
+ assertEquals(0, ApiVersionUtil.parse("[foo").size());
+ }
+
+ @Test
+ public void testParse_valid_one() {
+ Collection<ApiVersion> result;
+ ApiVersion current;
+
+ result = ApiVersionUtil.parse("[0.2]");
+ assertEquals(1, result.size());
+ current = result.iterator().next();
+ assertEquals(0, current.getMajor());
+ assertEquals(2, current.getMinor());
+
+ result = ApiVersionUtil.parse("[1.0]");
+ assertEquals(1, result.size());
+ current = result.iterator().next();
+ assertEquals(1, current.getMajor());
+ assertEquals(0, current.getMinor());
+
+ result = ApiVersionUtil.parse("[\"0.2\"]");
+ assertEquals(1, result.size());
+ current = result.iterator().next();
+ assertEquals(0, current.getMajor());
+ assertEquals(2, current.getMinor());
+
+ result = ApiVersionUtil.parse("[\"1.0\"]");
+ assertEquals(1, result.size());
+ current = result.iterator().next();
+ assertEquals(1, current.getMajor());
+ assertEquals(0, current.getMinor());
+
+ result = ApiVersionUtil.parse("1.0");
+ assertEquals(1, result.size());
+ current = result.iterator().next();
+ assertEquals(1, current.getMajor());
+ assertEquals(0, current.getMinor());
+ }
+
+ @Test
+ public void testParse_invalid_many() {
+ Collection<ApiVersion> result;
+ ApiVersion current;
+ Iterator<ApiVersion> iterator;
+
+ result = ApiVersionUtil.parse("[0.2, foo]");
+ assertEquals(1, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(0, current.getMajor());
+ assertEquals(2, current.getMinor());
+
+ result = ApiVersionUtil.parse("[foo, 1.1]");
+ assertEquals(1, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(1, current.getMajor());
+ assertEquals(1, current.getMinor());
+
+ assertEquals(0, ApiVersionUtil.parse("[foo, bar]").size());
+
+ result = ApiVersionUtil.parse("[, 1.1]");
+ assertEquals(1, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(1, current.getMajor());
+ assertEquals(1, current.getMinor());
+
+ result = ApiVersionUtil.parse("[1.1, ?]");
+ assertEquals(1, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(1, current.getMajor());
+ assertEquals(1, current.getMinor());
+ }
+
+ @Test
+ public void testParse_valid_many() {
+ Collection<ApiVersion> result;
+ ApiVersion current;
+ Iterator<ApiVersion> iterator;
+
+ result = ApiVersionUtil.parse("[0.2, 1.0]");
+ assertEquals(2, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(0, current.getMajor());
+ assertEquals(2, current.getMinor());
+ current = iterator.next();
+ assertEquals(1, current.getMajor());
+ assertEquals(0, current.getMinor());
+
+ result = ApiVersionUtil.parse("[\"0.2\", \"1.0\"]");
+ assertEquals(2, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(0, current.getMajor());
+ assertEquals(2, current.getMinor());
+ current = iterator.next();
+ assertEquals(1, current.getMajor());
+ assertEquals(0, current.getMinor());
+
+ result = ApiVersionUtil.parse("[0.2,1.0]");
+ assertEquals(2, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(0, current.getMajor());
+ assertEquals(2, current.getMinor());
+ current = iterator.next();
+ assertEquals(1, current.getMajor());
+ assertEquals(0, current.getMinor());
+
+ result = ApiVersionUtil.parse("[\"0.2\",\"1.0\"]");
+ assertEquals(2, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(0, current.getMajor());
+ assertEquals(2, current.getMinor());
+ current = iterator.next();
+ assertEquals(1, current.getMajor());
+ assertEquals(0, current.getMinor());
+
+ result = ApiVersionUtil.parse("[0.2, \"1.0\"]");
+ assertEquals(2, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(0, current.getMajor());
+ assertEquals(2, current.getMinor());
+ current = iterator.next();
+ assertEquals(1, current.getMajor());
+ assertEquals(0, current.getMinor());
+
+ result = ApiVersionUtil.parse("[0.2,\"1.0\"]");
+ assertEquals(2, result.size());
+ iterator = result.iterator();
+ current = iterator.next();
+ assertEquals(0, current.getMajor());
+ assertEquals(2, current.getMinor());
+ current = iterator.next();
+ assertEquals(1, current.getMajor());
+ assertEquals(0, current.getMinor());
+ }
+
+ @Test
+ public void testSerialize() {
+ assertNull(ApiVersionUtil.serialize(null));
+ assertNull(ApiVersionUtil.serialize(Collections.emptyList()));
+
+ assertEquals("[0.2]", ApiVersionUtil.serialize(Collections.singleton(ApiVersion.API_VERSION_0_2)));
+ assertEquals("[1.0]", ApiVersionUtil.serialize(Collections.singleton(ApiVersion.API_VERSION_1_0)));
+
+ assertEquals("[1.0]", ApiVersionUtil.serialize(Arrays.asList(ApiVersion.API_VERSION_1_0, null)));
+ assertEquals("[1.0]", ApiVersionUtil.serialize(Arrays.asList(null, ApiVersion.API_VERSION_1_0)));
+
+ assertEquals("[0.2,1.0]", ApiVersionUtil.serialize(Arrays.asList(ApiVersion.API_VERSION_0_2, ApiVersion.API_VERSION_1_0)));
+
+ // TODO sure...?
+ assertEquals("[1.0,1.0]", ApiVersionUtil.serialize(Arrays.asList(ApiVersion.API_VERSION_1_0, ApiVersion.API_VERSION_1_0)));
+ }
+
+ @Test
+ public void testSanitize() {
+ assertEquals("[1.1,1.1,1.2,0.2]", ApiVersionUtil.sanitize("[null, 1.1, 1.1,,1.2, 0.2]"));
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Test
+ public void testGetPreferredApiVersion() {
+ assertNull(ApiVersionUtil.getPreferredApiVersion(null));
+ assertNull(ApiVersionUtil.getPreferredApiVersion(""));
+ assertNull(ApiVersionUtil.getPreferredApiVersion("[]"));
+ assertNull(ApiVersionUtil.getPreferredApiVersion("foo"));
+
+ ApiVersion result;
+
+ result = ApiVersionUtil.getPreferredApiVersion("[0.2]");
+ assertEquals(0, result.getMajor());
+ assertEquals(2, result.getMinor());
+
+ result = ApiVersionUtil.getPreferredApiVersion("[1.1]");
+ assertEquals(1, result.getMajor());
+ assertEquals(1, result.getMinor());
+
+ result = ApiVersionUtil.getPreferredApiVersion("[0.2,1.1]");
+ assertEquals(1, result.getMajor());
+ assertEquals(1, result.getMinor());
+
+ result = ApiVersionUtil.getPreferredApiVersion("[1.1,0.2]");
+ assertEquals(1, result.getMajor());
+ assertEquals(1, result.getMinor());
+
+ result = ApiVersionUtil.getPreferredApiVersion("[10.0,1.1,1.0,0.2]");
+ assertEquals(1, result.getMajor());
+ assertEquals(1, result.getMinor());
+
+ result = ApiVersionUtil.getPreferredApiVersion("[1.1,1.5,1.0]");
+ assertEquals(1, result.getMajor());
+ assertEquals(5, result.getMinor());
+
+ result = ApiVersionUtil.getPreferredApiVersion("[1.1,,foo,1.0]");
+ assertEquals(1, result.getMajor());
+ assertEquals(1, result.getMinor());
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java
index 7a264fdd..72c7d957 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java
+++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.java
@@ -1,13 +1,10 @@
package it.niedermann.owncloud.notes.shared.util;
-import android.os.Build;
-
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
import it.niedermann.android.markdown.MarkdownUtil;
@@ -16,7 +13,6 @@ import it.niedermann.android.markdown.MarkdownUtil;
* Created by stefan on 06.10.15.
*/
@RunWith(RobolectricTestRunner.class)
-@Config(sdk = {Build.VERSION_CODES.P})
public class NoteUtilTest extends TestCase {
@Test
@@ -35,6 +31,9 @@ public class NoteUtilTest extends TestCase {
assertEquals("Test", NoteUtil.getLineWithoutMarkdown("\nTest", 0));
assertEquals("Foo", NoteUtil.getLineWithoutMarkdown("Foo\nBar", 0));
assertEquals("Bar", NoteUtil.getLineWithoutMarkdown("Foo\nBar", 1));
+ assertEquals("Foo", NoteUtil.getLineWithoutMarkdown("* Foo\n* Bar", 0));
+ assertEquals("Bar", NoteUtil.getLineWithoutMarkdown("- Foo\nBar", 1));
+ assertEquals("Foo", NoteUtil.getLineWithoutMarkdown("# Foo", 0));
}
@Test
diff --git a/build.gradle b/build.gradle
index 0f7db75e..125ef7ba 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,12 +2,12 @@
buildscript {
repositories {
+ mavenCentral()
google()
- jcenter()
}
dependencies {
apply plugin: 'maven'
- classpath 'com.android.tools.build:gradle:4.1.3'
+ classpath 'com.android.tools.build:gradle:4.2.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -16,8 +16,8 @@ buildscript {
allprojects {
repositories {
+ mavenCentral()
google()
- jcenter()
maven { url "https://jitpack.io" }
}
}
diff --git a/fastlane/metadata/android/en-US/changelogs/3004002.txt b/fastlane/metadata/android/en-US/changelogs/3004002.txt
index 3eb3160a..252f5bd4 100644
--- a/fastlane/metadata/android/en-US/changelogs/3004002.txt
+++ b/fastlane/metadata/android/en-US/changelogs/3004002.txt
@@ -1,2 +1,5 @@
- 🌐 Enhanced linkifying - by @Cui-Yusong
-- ⚙️ Use Retrofit for API calls (#1167) \ No newline at end of file
+- ⚙️ Use Retrofit for API calls (#1167)
+- ⚙ Switched based for markdown rendering in widgets
+- ⚙ Enable background synchronization by default (#1168) - by @MasterWanna
+- 🐞 Fix multiple created notes when creating a new note and wait or toggle between edit and preview mode (#1198) \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004003.txt b/fastlane/metadata/android/en-US/changelogs/3004003.txt
new file mode 100644
index 00000000..0461bd55
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004003.txt
@@ -0,0 +1,4 @@
+- ⚙️ Enhancing toggling of existing punctuation in the editor (#1193) - by @Jimmy-7664
+- 🐞 Also remove markdown from title when the note only has one line (#1205)
+- 🌓 Fix menu text color in dark mode (#1215)
+- ⚙️ Enhanced removal of markdown (for generating title & excerpt) - by @Cui-Yusong \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004004.txt b/fastlane/metadata/android/en-US/changelogs/3004004.txt
new file mode 100644
index 00000000..66e48fb0
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004004.txt
@@ -0,0 +1,3 @@
+- 🎨 Fix theming issue when theming app is disabled on the server
+- 🐞 Fix potential ConcurrentModificationException
+- 🐞 Fix moving notes to another account \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004005.txt b/fastlane/metadata/android/en-US/changelogs/3004005.txt
new file mode 100644
index 00000000..47b6d2e6
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004005.txt
@@ -0,0 +1 @@
+- 🔒 Remove content of notes and search terms from LogCat (#1243) \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004006.txt b/fastlane/metadata/android/en-US/changelogs/3004006.txt
new file mode 100644
index 00000000..7232b7a4
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004006.txt
@@ -0,0 +1,8 @@
+v3.4.6
+
+- 🖼 Support displaying images hosted at Nextcloud in preview mode (#1018)
+- 🌎 Updated translations
+
+v3.4.5
+
+- 🔒 Remove content of notes and search terms from LogCat (#1243) \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004007.txt b/fastlane/metadata/android/en-US/changelogs/3004007.txt
new file mode 100644
index 00000000..7726631d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004007.txt
@@ -0,0 +1,8 @@
+v3.4.6 - v3.4.7
+
+- 🖼 Support displaying images hosted at Nextcloud in preview mode (#1018)
+- 🌎 Updated translations
+
+v3.4.5
+
+- 🔒 Remove content of notes and search terms from LogCat (#1243) \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004008.txt b/fastlane/metadata/android/en-US/changelogs/3004008.txt
new file mode 100644
index 00000000..8cf2383d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004008.txt
@@ -0,0 +1,8 @@
+v3.4.6 - v3.4.8
+
+- 🖼 Support displaying images hosted at Nextcloud in preview mode (#1018)
+- 🌎 Updated translations
+
+v3.4.5
+
+- 🔒 Remove content of notes and search terms from LogCat (#1243) \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004009.txt b/fastlane/metadata/android/en-US/changelogs/3004009.txt
new file mode 100644
index 00000000..06053cc0
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004009.txt
@@ -0,0 +1,13 @@
+v3.4.9
+
+- 🐞 Fix crash on rotating device (#1239)
+- 🐞 Add more verbose log output in case of a NextcloudFilesAppAccountNotFoundException (#1256)
+
+v3.4.6 - v3.4.8
+
+- 🖼 Support displaying images hosted at Nextcloud in preview mode (#1018)
+- 🌎 Updated translations
+
+v3.4.5
+
+- 🔒 Remove content of notes and search terms from LogCat (#1243) \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004010.txt b/fastlane/metadata/android/en-US/changelogs/3004010.txt
new file mode 100644
index 00000000..92da1c45
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004010.txt
@@ -0,0 +1,3 @@
+- 🗑 Open trashbin of files app instead of Web UI (#238)
+- ✅ Make links in checkbox list items clickable
+- 🐞 NextcloudFilesAppAccountNotFoundException - Trying to provide backup & repair steps (#1256) \ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/3004011.txt b/fastlane/metadata/android/en-US/changelogs/3004011.txt
new file mode 100644
index 00000000..5af56e89
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3004011.txt
@@ -0,0 +1,3 @@
+- ✅ Checkboxes using an uppercase X can not be toggled (#1276)
+- 🐞 Search field does not immediately focus (#1282)
+- 🐞 Change grid view option not always applied directly (#1254) \ No newline at end of file
diff --git a/markdown/build.gradle b/markdown/build.gradle
index 47e219fa..adbe36d8 100644
--- a/markdown/build.gradle
+++ b/markdown/build.gradle
@@ -15,24 +15,26 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
-}
-ext {
- markwonVersion = "4.6.2"
- rxMarkdownVersion = "0.1.3"
+ testOptions {
+ unitTests {
+ includeAndroidResources true
+ }
+ }
}
dependencies {
implementation 'com.github.nextcloud:Android-SingleSignOn:0.5.6'
implementation 'com.github.stefan-niedermann:android-commons:0.2.0'
- implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation "androidx.lifecycle:lifecycle-livedata:2.3.1"
+ implementation 'androidx.appcompat:appcompat:1.3.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata:2.3.1'
+ implementation 'org.jsoup:jsoup:1.13.1'
+
+ def markwonVersion = '4.6.2'
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:editor:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
@@ -47,18 +49,17 @@ dependencies {
implementation("io.noties.markwon:syntax-highlight:$markwonVersion") {
exclude group: 'org.jetbrains', module: 'annotations-java5'
}
- implementation("io.noties:prism4j:2.0.0") {
+ implementation('io.noties:prism4j:2.0.0') {
exclude group: 'org.jetbrains', module: 'annotations-java5'
}
- annotationProcessor "io.noties:prism4j-bundler:2.0.0"
- implementation 'org.jetbrains:annotations:20.1.0'
-
- implementation "com.yydcdut:markdown-processor:$rxMarkdownVersion"
- implementation "com.yydcdut:rxmarkdown-wrapper:$rxMarkdownVersion"
+ annotationProcessor 'io.noties:prism4j-bundler:2.0.0'
+ implementation 'org.jetbrains:annotations:21.0.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
+ testImplementation 'androidx.test:core:1.3.0'
+ testImplementation 'androidx.arch.core:core-testing:2.1.0'
testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.2'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+ testImplementation 'org.mockito:mockito-core:3.11.2'
+ testImplementation 'org.robolectric:robolectric:4.5.1'
} \ No newline at end of file
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/ListTagHandler.java b/markdown/src/main/java/it/niedermann/android/markdown/ListTagHandler.java
new file mode 100644
index 00000000..93e5d80a
--- /dev/null
+++ b/markdown/src/main/java/it/niedermann/android/markdown/ListTagHandler.java
@@ -0,0 +1,80 @@
+package it.niedermann.android.markdown;
+
+import android.text.Editable;
+import android.text.Html;
+import android.text.style.BulletSpan;
+
+import androidx.annotation.NonNull;
+import androidx.core.text.HtmlCompat;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.xml.sax.XMLReader;
+
+import java.util.Stack;
+
+/**
+ * Adds <code>•</code> to unordered list items and a counter to ordered list items.
+ * Call {@link #prepareTagHandling(String)}, so the default handler of {@link Html#fromHtml(String, int)} does not prevent the handling.
+ */
+public class ListTagHandler implements Html.TagHandler {
+
+ private static final String X_OL = "x-ol";
+ private static final String X_UL = "x-ul";
+ private static final String X_LI = "x-li";
+
+ private final Stack<String> parents = new Stack<>();
+ private final Stack<Integer> listItemIndex = new Stack<>();
+
+ @Override
+ public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
+ if (X_OL.equals(tag)) {
+ if (opening) {
+ parents.push(X_OL);
+ listItemIndex.push(1);
+ } else {
+ parents.pop();
+ listItemIndex.pop();
+ }
+ } else if (X_UL.equals(tag)) {
+ if (opening) {
+ parents.push(X_UL);
+ } else {
+ parents.pop();
+ }
+ } else if (X_LI.equals(tag)) {
+ if (X_OL.equals(parents.peek())) {
+ if (opening) {
+ output.append("\n");
+ for (int nestingLevel = 1; nestingLevel < parents.size(); nestingLevel++) {
+ output.append("\t\t");
+ }
+ output.append(String.valueOf(listItemIndex.peek())).append(". ");
+ listItemIndex.push(listItemIndex.pop() + 1);
+ }
+ } else if (X_UL.equals(parents.peek())) {
+ if (opening) {
+ output.append("\n");
+ for (int nestingLevel = 1; nestingLevel < parents.size(); nestingLevel++) {
+ output.append("\t\t");
+ }
+ output.append("•");
+ }
+ }
+ }
+ }
+
+ /**
+ * Replace the list tags with custom tags to prevent them being handeled by {@link HtmlCompat}.
+ * Otherwise, all <code>li</code> tags will be replaced with {@link BulletSpan} which is not the
+ * desired behavior of ordered list items.
+ */
+ @NonNull
+ public static String prepareTagHandling(@NonNull String html) {
+ final Document document = Jsoup.parse(html);
+ document.getElementsByTag("ol").tagName(X_OL);
+ document.getElementsByTag("ul").tagName(X_UL);
+ document.getElementsByTag("li").tagName(X_LI);
+ return document.outerHtml();
+ }
+} \ No newline at end of file
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java
index 097acd76..99d8bdc9 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownUtil.java
@@ -6,22 +6,29 @@ import android.os.Build;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
+import android.text.style.QuoteSpan;
import android.util.Log;
+import android.util.Pair;
import android.widget.RemoteViews.RemoteView;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat;
-import com.yydcdut.markdown.MarkdownProcessor;
-import com.yydcdut.markdown.syntax.text.TextFactory;
-import com.yydcdut.rxmarkdown.RxMarkdown;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
-import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,31 +37,23 @@ import io.noties.markwon.Markwon;
import it.niedermann.android.markdown.model.EListType;
import it.niedermann.android.markdown.model.SearchSpan;
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class MarkdownUtil {
private static final String TAG = MarkdownUtil.class.getSimpleName();
- 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};
-
- private static final Pattern PATTERN_LISTS = Pattern.compile("^\\s*[*+-]\\s+", Pattern.MULTILINE);
- private static final Pattern PATTERN_HEADINGS = Pattern.compile("^#+\\s+(.*?)\\s*#*$", Pattern.MULTILINE);
- private static final Pattern PATTERN_HEADING_LINE = Pattern.compile("^(?:=*|-*)$", Pattern.MULTILINE);
- private static final Pattern PATTERN_EMPHASIS = Pattern.compile("(\\*+|_+)(.*?)\\1", Pattern.MULTILINE);
- private static final Pattern PATTERN_SPACE_1 = Pattern.compile("^\\s+", Pattern.MULTILINE);
- private static final Pattern PATTERN_SPACE_2 = Pattern.compile("\\s+$", Pattern.MULTILINE);
+ private static final Parser PARSER = Parser.builder().build();
+ private static final HtmlRenderer RENDERER = HtmlRenderer.builder().softbreak("<br>").build();
private static final Pattern PATTERN_CODE_FENCE = Pattern.compile("^(`{3,})");
private static final Pattern PATTERN_ORDERED_LIST_ITEM = Pattern.compile("^(\\d+).\\s.+$");
private static final Pattern PATTERN_ORDERED_LIST_ITEM_EMPTY = Pattern.compile("^(\\d+).\\s$");
private static final Pattern PATTERN_MARKDOWN_LINK = Pattern.compile("\\[(.+)?]\\(([^ ]+?)?( \"(.+)\")?\\)");
- @Nullable
- private static final String checkboxCheckedEmoji = getCheckboxEmoji(true);
- @Nullable
- private static final String checkboxUncheckedEmoji = getCheckboxEmoji(false);
+ private static final String PATTERN_QUOTE_BOLD_PUNCTUATION = Pattern.quote("**");
+
+ private static final Optional<String> CHECKBOX_CHECKED_EMOJI = getCheckboxEmoji(true);
+ private static final Optional<String> CHECKBOX_UNCHECKED_EMOJI = getCheckboxEmoji(false);
private MarkdownUtil() {
// Util class
@@ -64,52 +63,75 @@ public class MarkdownUtil {
* {@link RemoteView}s have a limited subset of supported classes to maintain compatibility with many different launchers.
* <p>
* Since {@link Markwon} makes heavy use of custom spans, this won't look nice e. g. at app widgets, because they simply won't be rendered.
- * Therefore we currently fall back on {@link RxMarkdown} as the results will look better in this special case.
- * We might change this in the future by utilizing {@link Markwon} and creating a {@link Spanned} from an {@link HtmlCompat} interemediate.
+ * Therefore we currently use {@link HtmlCompat} to filter supported spans from the output of {@link HtmlRenderer} as an intermediate step.
*/
public static CharSequence renderForRemoteView(@NonNull Context context, @NonNull String content) {
- final MarkdownProcessor markdownProcessor = new MarkdownProcessor(context);
- markdownProcessor.factory(TextFactory.create());
- return parseCompat(markdownProcessor, replaceCheckboxesWithEmojis(content));
+ // Create HTML string from Markup
+ final String html = RENDERER.render(PARSER.parse(replaceCheckboxesWithEmojis(content)));
+
+ // Create Spanned from HTML, with special handling for ordered list items
+ final Spanned spanned = HtmlCompat.fromHtml(ListTagHandler.prepareTagHandling(html), 0, null, new ListTagHandler());
+
+ // Enhance colors and margins of the Spanned
+ return customizeQuoteSpanAppearance(context, spanned, 5, 30);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static Spanned customizeQuoteSpanAppearance(@NonNull Context context, @NonNull Spanned input, int stripeWidth, int gapWidth) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ return input;
+ }
+ final SpannableStringBuilder ssb = new SpannableStringBuilder(input);
+ final QuoteSpan[] originalQuoteSpans = ssb.getSpans(0, ssb.length(), QuoteSpan.class);
+ @ColorInt final int colorBlockQuote = ContextCompat.getColor(context, R.color.block_quote);
+ for (QuoteSpan originalQuoteSpan : originalQuoteSpans) {
+ final int start = ssb.getSpanStart(originalQuoteSpan);
+ final int end = ssb.getSpanEnd(originalQuoteSpan);
+ ssb.removeSpan(originalQuoteSpan);
+ ssb.setSpan(new QuoteSpan(colorBlockQuote, stripeWidth, gapWidth), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ return ssb;
}
@NonNull
public static String replaceCheckboxesWithEmojis(@NonNull String content) {
return runForEachCheckbox(content, (line) -> {
for (EListType listType : EListType.values()) {
- if (checkboxCheckedEmoji != null) {
- line = line.replace(listType.checkboxChecked, checkboxCheckedEmoji);
+ if (CHECKBOX_CHECKED_EMOJI.isPresent()) {
+ line = line.replace(listType.checkboxChecked, CHECKBOX_CHECKED_EMOJI.get());
+ line = line.replace(listType.checkboxCheckedUpperCase, CHECKBOX_CHECKED_EMOJI.get());
}
- if (checkboxUncheckedEmoji != null) {
- line = line.replace(listType.checkboxUnchecked, checkboxUncheckedEmoji);
+ if (CHECKBOX_UNCHECKED_EMOJI.isPresent()) {
+ line = line.replace(listType.checkboxUnchecked, CHECKBOX_UNCHECKED_EMOJI.get());
}
}
return line;
});
}
- @Nullable
- private static String getCheckboxEmoji(boolean checked) {
- final String[] checkedEmojis;
- final String[] uncheckedEmojis;
- // Seriously what the fuck, Samsung?
- // https://emojipedia.org/ballot-box-with-x/
- if (Build.MANUFACTURER != null && Build.MANUFACTURER.toLowerCase().contains("samsung")) {
- checkedEmojis = new String[]{"✅", "☑️", "✔️"};
- uncheckedEmojis = new String[]{"❌", "\uD83D\uDD32️", "☐️"};
- } else {
- checkedEmojis = new String[]{"☒", "✅", "☑️", "✔️"};
- uncheckedEmojis = new String[]{"☐", "❌", "\uD83D\uDD32️", "☐️"};
- }
- final Paint paint = new Paint();
+ @NonNull
+ private static Optional<String> getCheckboxEmoji(boolean checked) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- for (String emoji : checked ? checkedEmojis : uncheckedEmojis) {
+ final String[] emojis;
+ // Seriously what the fuck, Samsung?
+ // https://emojipedia.org/ballot-box-with-x/
+ if (Build.MANUFACTURER != null && Build.MANUFACTURER.toLowerCase(Locale.getDefault()).contains("samsung")) {
+ emojis = checked
+ ? new String[]{"✅", "☑️", "✔️"}
+ : new String[]{"❌", "\uD83D\uDD32️", "☐️"};
+ } else {
+ emojis = checked
+ ? new String[]{"☒", "✅", "☑️", "✔️"}
+ : new String[]{"☐", "❌", "\uD83D\uDD32️", "☐️"};
+ }
+ final Paint paint = new Paint();
+ for (String emoji : emojis) {
if (paint.hasGlyph(emoji)) {
- return emoji;
+ return Optional.of(emoji);
}
}
}
- return null;
+ return Optional.empty();
}
/**
@@ -146,28 +168,6 @@ public class MarkdownUtil {
return TextUtils.join("\n", lines);
}
- /**
- * This is a compatibility-method that provides workarounds for several bugs in RxMarkdown
- * <p>
- * 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
- private 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 int getStartOfLine(@NonNull CharSequence s, int cursorPosition) {
int startOfLine = cursorPosition;
while (startOfLine > 0 && s.charAt(startOfLine - 1) != '\n') {
@@ -249,7 +249,7 @@ public class MarkdownUtil {
public static boolean lineStartsWithCheckbox(@NonNull String line, @NonNull EListType listType) {
final String trimmedLine = line.trim();
- return (trimmedLine.startsWith(listType.checkboxUnchecked) || trimmedLine.startsWith(listType.checkboxChecked));
+ return (trimmedLine.startsWith(listType.checkboxUnchecked) || trimmedLine.startsWith(listType.checkboxChecked) || trimmedLine.startsWith(listType.checkboxCheckedUpperCase));
}
/**
@@ -278,34 +278,101 @@ public class MarkdownUtil {
* @return the new cursor position
*/
public static int togglePunctuation(@NonNull Editable editable, int selectionStart, int selectionEnd, @NonNull String punctuation) {
- switch (punctuation) {
- case "**":
- case "__":
- case "*":
- case "_":
- case "~~": {
- final boolean selectionIsSurroundedByPunctuation = selectionIsSurroundedByPunctuation(editable, selectionStart, selectionEnd, punctuation);
- if (selectionIsSurroundedByPunctuation) {
- editable.delete(selectionEnd, selectionEnd + punctuation.length());
- editable.delete(selectionStart - punctuation.length(), selectionStart);
- return selectionEnd - punctuation.length();
- } else {
- final int containedPunctuationCount = getContainedPunctuationCount(editable, selectionStart, selectionEnd, punctuation);
- if (containedPunctuationCount == 0) {
- editable.insert(selectionEnd, punctuation);
- editable.insert(selectionStart, punctuation);
- return selectionEnd + punctuation.length() * 2;
- } else if (containedPunctuationCount % 2 > 0) {
- return selectionEnd;
- } else {
- removeContainingPunctuation(editable, selectionStart, selectionEnd, punctuation);
- return selectionEnd - containedPunctuationCount * punctuation.length();
- }
- }
+ final String initialString = editable.toString();
+ if (selectionStart < 0 || selectionStart > initialString.length() || selectionEnd < 0 || selectionEnd > initialString.length()) {
+ return 0;
+ }
+
+ // handle special case: italic (that damn thing will match like ANYTHING (regarding bold / bold+italic)....)
+ final boolean isItalic = punctuation.length() == 1 && punctuation.charAt(0) == '*';
+ if (isItalic) {
+ final Optional<Integer> result = handleItalicEdgeCase(editable, initialString, selectionStart, selectionEnd);
+ // The result is only present if this actually was an edge case
+ if (result.isPresent()) {
+ return result.get();
+ }
+ }
+
+ // handle the simple cases
+ final String wildcardRex = "([^" + punctuation.charAt(0) + "])+";
+ final String punctuationRex = Pattern.quote(punctuation);
+ final String pattern = isItalic
+ // in this case let's make optional asterisks around it, so it wont match anything between two (bold+italic)s
+ ? "\\*?\\*?" + punctuationRex + wildcardRex + punctuationRex + "\\*?\\*?"
+ : punctuationRex + wildcardRex + punctuationRex;
+ final Pattern searchPattern = Pattern.compile(pattern);
+ int relevantStart = selectionStart - 2;
+ relevantStart = Math.max(relevantStart, 0);
+ int relevantEnd = selectionEnd + 2;
+ relevantEnd = Math.min(relevantEnd, initialString.length());
+ final Matcher matcher = searchPattern.matcher(initialString).region(relevantStart, relevantEnd);
+
+ // if the matcher matches, it's a remove
+ if (matcher.find()) {
+ // this resets the matcher, while keeping the required region
+ matcher.region(relevantStart, relevantEnd);
+ final int punctuationLength = punctuation.length();
+ final List<Pair<Integer, Integer>> startEnd = new LinkedList<>();
+ int removedCount = 0;
+ while (matcher.find()) {
+ startEnd.add(new Pair<>(matcher.start(), matcher.end()));
+ removedCount += punctuationLength;
+ }
+ // start from the end
+ Collections.reverse(startEnd);
+ for (Pair<Integer, Integer> item : startEnd) {
+ deletePunctuation(editable, punctuationLength, item.first, item.second);
+ }
+ int offsetAtEnd = 0;
+ // depending on if the user has selected the markdown chars, we might need to add an offset to the resulting cursor position
+ if (initialString.substring(Math.max(selectionEnd - punctuationLength + 1, 0), Math.min(selectionEnd + 1, initialString.length())).equals(punctuation) ||
+ initialString.substring(selectionEnd, Math.min(selectionEnd + punctuationLength, initialString.length())).equals(punctuation)) {
+ offsetAtEnd = punctuationLength;
}
- default:
- throw new UnsupportedOperationException("This kind of punctuation is not yet supported: " + punctuation);
+ return selectionEnd - removedCount * 2 + offsetAtEnd;
+ // ^
+ // start+end, need to double
}
+
+ // do nothing when punctuation is contained only once
+ if (Pattern.compile(punctuationRex).matcher(initialString).region(selectionStart, selectionEnd).find()) {
+ return selectionEnd;
+ }
+
+ // nothing returned so far, so it has to be an insertion
+ return insertPunctuation(editable, selectionStart, selectionEnd, punctuation);
+ }
+
+ private static void deletePunctuation(Editable editable, int punctuationLength, int start, int end) {
+ editable.delete(end - punctuationLength, end);
+ editable.delete(start, start + punctuationLength);
+ }
+
+ /**
+ * @return an {@link Optional<Integer>} of the new cursor position.
+ * The return value is only {@link Optional#isPresent()}, if this is an italic edge case.
+ */
+ @NonNull
+ private static Optional<Integer> handleItalicEdgeCase(Editable editable, String editableAsString, int selectionStart, int selectionEnd) {
+ // look if selection is bold, this is the only edge case afaik
+ final Pattern searchPattern = Pattern.compile("(^|[^*])" + PATTERN_QUOTE_BOLD_PUNCTUATION + "([^*])*" + PATTERN_QUOTE_BOLD_PUNCTUATION + "([^*]|$)");
+ // look the selection expansion by 1 is intended, so the NOT '*' has a chance to match. we don't want to match ***blah***
+ final Matcher matcher = searchPattern.matcher(editableAsString)
+ .region(Math.max(selectionStart - 1, 0), Math.min(selectionEnd + 1, editableAsString.length()));
+ if (matcher.find()) {
+ return Optional.of(insertPunctuation(editable, selectionStart, selectionEnd, "*"));
+ }
+ // look around (3 chars) (NOT '*' + "**"). User might have selected the text only
+ if (matcher.region(Math.max(selectionStart - 3, 0), Math.min(selectionEnd + 3, editableAsString.length())).find()) {
+ return Optional.of(insertPunctuation(editable, selectionStart, selectionEnd, "*"));
+ }
+ return Optional.empty();
+ }
+
+ private static int insertPunctuation(Editable editable, int firstPosition, int secondPosition, String punctuation) {
+ editable.insert(secondPosition, punctuation);
+ editable.insert(firstPosition, punctuation);
+ return secondPosition + punctuation.length();
}
/**
@@ -313,10 +380,9 @@ public class MarkdownUtil {
*
* @return the new cursor position
*/
- // CS304 issue link: https://github.com/stefan-niedermann/nextcloud-notes/issues/1186
public static int insertLink(@NonNull Editable editable, int selectionStart, int selectionEnd, @Nullable String clipboardUrl) {
if (selectionStart == selectionEnd) {
- if (selectionStart>0 && selectionEnd<editable.length()) {
+ if (selectionStart > 0 && selectionEnd < editable.length()) {
char start = editable.charAt(selectionStart - 1);
char end = editable.charAt(selectionEnd);
if (start == ' ' || end == ' ') {
@@ -328,9 +394,6 @@ public class MarkdownUtil {
editable.insert(selectionEnd, " ");
}
editable.insert(selectionStart, "[](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
- if (clipboardUrl != null) {
- selectionEnd += clipboardUrl.length();
- }
return selectionStart + 1;
} else {
@@ -351,8 +414,7 @@ public class MarkdownUtil {
}
return selectionEnd + 2;
}
- }
- else {
+ } else {
editable.insert(selectionStart, "[](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
return selectionStart + 1;
}
@@ -383,38 +445,6 @@ public class MarkdownUtil {
}
}
- /**
- * @return whether or not the selection of {@param text} from {@param start} to {@param end} is
- * surrounded or not by the given {@param punctuation}.
- */
- private static boolean selectionIsSurroundedByPunctuation(@NonNull CharSequence text, int start, int end, @NonNull String punctuation) {
- if (text.length() < end + punctuation.length()) {
- return false;
- }
- if (start - punctuation.length() < 0 || end + punctuation.length() > text.length()) {
- return false;
- }
- return punctuation.contentEquals(text.subSequence(start - punctuation.length(), start))
- && punctuation.contentEquals(text.subSequence(end, end + punctuation.length()));
- }
-
- private static int getContainedPunctuationCount(@NonNull CharSequence text, int start, int end, @NonNull String punctuation) {
- final Matcher matcher = Pattern.compile(Pattern.quote(punctuation)).matcher(text.subSequence(start, end));
- int counter = 0;
- while (matcher.find()) {
- counter++;
- }
- return counter;
- }
-
- private static void removeContainingPunctuation(@NonNull Editable editable, int start, int end, @NonNull String punctuation) {
- final Matcher matcher = Pattern.compile(Pattern.quote(punctuation)).matcher(editable.subSequence(start, end));
- int countDeletedPunctuations = 0;
- while (matcher.find()) {
- editable.delete(start + matcher.start() - countDeletedPunctuations * punctuation.length(), start + matcher.end() - countDeletedPunctuations * punctuation.length());
- countDeletedPunctuations++;
- }
- }
public static boolean selectionIsInLink(@NonNull CharSequence text, int start, int end) {
final Matcher matcher = PATTERN_MARKDOWN_LINK.matcher(text);
@@ -479,23 +509,12 @@ public class MarkdownUtil {
*/
@NonNull
public static String removeMarkdown(@Nullable String s) {
- if (s == null)
+ if (TextUtils.isEmpty(s)) {
return "";
- // TODO maybe we can utilize the markwon renderer?
-
- for (EListType listType : EListType.values()) {
- for (String item : Arrays.asList(listType.checkboxChecked, listType.checkboxUnchecked, listType.listSymbolWithTrailingSpace)) {
- if (s.startsWith(item)) {
- s = s.substring(item.length());
- }
- }
}
- s = PATTERN_LISTS.matcher(s).replaceAll("");
- s = PATTERN_HEADINGS.matcher(s).replaceAll("$1");
- s = PATTERN_HEADING_LINE.matcher(s).replaceAll("");
- s = PATTERN_EMPHASIS.matcher(s).replaceAll("$2");
- s = PATTERN_SPACE_1.matcher(s).replaceAll("");
- s = PATTERN_SPACE_2.matcher(s).replaceAll("");
- return s;
+ assert s != null;
+ final String html = RENDERER.render(PARSER.parse(replaceCheckboxesWithEmojis(s)));
+ final Spanned spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT);
+ return spanned.toString().trim();
}
}
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java
index d41681c3..55ec5367 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java
@@ -25,8 +25,7 @@ import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.ext.tables.TableAwareMovementMethod;
import io.noties.markwon.ext.tables.TablePlugin;
import io.noties.markwon.ext.tasklist.TaskListPlugin;
-import io.noties.markwon.image.DefaultDownScalingMediaDecoder;
-import io.noties.markwon.image.ImagesPlugin;
+import io.noties.markwon.image.glide.GlideImagesPlugin;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.linkify.LinkifyPlugin;
import io.noties.markwon.movement.MovementMethodPlugin;
@@ -39,6 +38,7 @@ import io.noties.prism4j.Prism4j;
import io.noties.prism4j.annotations.PrismBundle;
import it.niedermann.android.markdown.MarkdownEditor;
import it.niedermann.android.markdown.MarkdownUtil;
+import it.niedermann.android.markdown.markwon.plugins.CustomGlideStore;
import it.niedermann.android.markdown.markwon.plugins.LinkClickInterceptorPlugin;
import it.niedermann.android.markdown.markwon.plugins.NextcloudMentionsPlugin;
import it.niedermann.android.markdown.markwon.plugins.SearchHighlightPlugin;
@@ -90,7 +90,7 @@ public class MarkwonMarkdownViewer extends AppCompatTextView implements Markdown
.usePlugin(LinkifyPlugin.create(true))
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
.usePlugin(LinkClickInterceptorPlugin.create())
- .usePlugin(ImagesPlugin.create(plugin -> plugin.defaultMediaDecoder(DefaultDownScalingMediaDecoder.create(context.getResources().getDisplayMetrics().widthPixels, 0))))
+ .usePlugin(GlideImagesPlugin.create(new CustomGlideStore(context)))
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
.usePlugin(new ToggleableTaskListPlugin((toggledCheckboxPosition, newCheckedState) -> {
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/glide/DownsampleWithMaxWidth.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/glide/DownsampleWithMaxWidth.java
new file mode 100644
index 00000000..479a14f5
--- /dev/null
+++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/glide/DownsampleWithMaxWidth.java
@@ -0,0 +1,33 @@
+package it.niedermann.android.markdown.markwon.glide;
+
+import androidx.annotation.Px;
+
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
+
+/**
+ * @see <a href="https://github.com/noties/Markwon/issues/329#issuecomment-855220315">Source</a>
+ */
+public class DownsampleWithMaxWidth extends DownsampleStrategy {
+
+ @Px
+ private final int maxWidth;
+
+ public DownsampleWithMaxWidth(@Px int maxWidth) {
+ this.maxWidth = maxWidth;
+ }
+
+ @Override
+ public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
+ // do not scale down if fits requested dimension
+ if (sourceWidth < maxWidth) {
+ return 1F;
+ }
+ return (float) maxWidth / sourceWidth;
+ }
+
+ @Override
+ public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
+ // go figure
+ return SampleSizeRounding.MEMORY;
+ }
+} \ No newline at end of file
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/CustomGlideStore.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/CustomGlideStore.java
new file mode 100644
index 00000000..6e58c931
--- /dev/null
+++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/CustomGlideStore.java
@@ -0,0 +1,48 @@
+package it.niedermann.android.markdown.markwon.plugins;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.RequestManager;
+import com.bumptech.glide.request.target.Target;
+
+import io.noties.markwon.image.AsyncDrawable;
+import io.noties.markwon.image.glide.GlideImagesPlugin;
+import it.niedermann.android.markdown.R;
+import it.niedermann.android.markdown.markwon.glide.DownsampleWithMaxWidth;
+
+/**
+ * <ul>
+ * <li>Applies downscaling via {@link DownsampleWithMaxWidth} to avoid <a href="https://github.com/stefan-niedermann/nextcloud-notes/issues/1034">issues with large images</a></li>
+ * <li>Adds a placeholder while loading an image</li>
+ * <li>Adds a "broken image" placeholder in case of an error</li>
+ * </ul>
+ */
+public class CustomGlideStore implements GlideImagesPlugin.GlideStore {
+ private final RequestManager requestManager;
+ private final DownsampleWithMaxWidth downsampleWithMaxWidth;
+
+ public CustomGlideStore(@NonNull Context context) {
+ this.requestManager = Glide.with(context);
+ downsampleWithMaxWidth = new DownsampleWithMaxWidth(context.getResources().getDisplayMetrics().widthPixels);
+ }
+
+ @NonNull
+ @Override
+ public RequestBuilder<Drawable> load(@NonNull AsyncDrawable drawable) {
+ return requestManager
+ .load(drawable.getDestination())
+ .downsample(downsampleWithMaxWidth)
+ .placeholder(R.drawable.ic_baseline_image_24)
+ .error(R.drawable.ic_baseline_broken_image_24);
+ }
+
+ @Override
+ public void cancel(@NonNull Target<?> target) {
+ requestManager.clear(target);
+ }
+}
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java
index e815b54b..81c48aa2 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/LinkClickInterceptorPlugin.java
@@ -1,22 +1,19 @@
package it.niedermann.android.markdown.markwon.plugins;
-import android.text.Spannable;
-import android.text.style.URLSpan;
-import android.widget.TextView;
-
import androidx.annotation.NonNull;
+import org.commonmark.node.Link;
+
import java.util.Collection;
import java.util.LinkedList;
import java.util.function.Function;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonPlugin;
+import io.noties.markwon.MarkwonSpansFactory;
+import io.noties.markwon.core.CoreProps;
import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan;
-import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
-import static it.niedermann.android.markdown.MarkdownUtil.getContentAsSpannable;
-
public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {
@NonNull
@@ -27,20 +24,9 @@ public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {
}
@Override
- public void afterSetText(@NonNull TextView textView) {
- super.afterSetText(textView);
- if (onLinkClickCallbacks.size() > 0) {
- final Spannable spannable = getContentAsSpannable(textView);
- final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
-
- for (URLSpan originalSpan : spans) {
- final InterceptedURLSpan interceptedSpan = new InterceptedURLSpan(onLinkClickCallbacks, originalSpan.getURL());
- final int start = spannable.getSpanStart(originalSpan);
- final int end = spannable.getSpanEnd(originalSpan);
- spannable.removeSpan(originalSpan);
- spannable.setSpan(interceptedSpan, start, end, SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- }
+ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
+ super.configureSpansFactory(builder);
+ builder.setFactory(Link.class, (configuration, props) -> new InterceptedURLSpan(onLinkClickCallbacks, CoreProps.LINK_DESTINATION.get(props)));
}
public void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) {
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java
index 3d5f66b6..7aee5a27 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java
@@ -1,8 +1,12 @@
package it.niedermann.android.markdown.markwon.plugins;
-import android.util.Log;
+import android.text.Spannable;
+import android.text.style.ClickableSpan;
+import android.util.Range;
+import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.Block;
@@ -12,16 +16,23 @@ import org.commonmark.node.Paragraph;
import org.commonmark.node.SoftLineBreak;
import org.commonmark.node.Text;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.SpannableBuilder;
+import io.noties.markwon.SpannableBuilder.Span;
import io.noties.markwon.ext.tasklist.TaskListItem;
import io.noties.markwon.ext.tasklist.TaskListProps;
import io.noties.markwon.ext.tasklist.TaskListSpan;
+import it.niedermann.android.markdown.MarkdownUtil;
import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
/**
@@ -30,8 +41,6 @@ import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
*/
public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
- private static final String TAG = ToggleableTaskListPlugin.class.getSimpleName();
-
@NonNull
private final AtomicBoolean enabled = new AtomicBoolean(true);
@NonNull
@@ -45,6 +54,10 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
this.enabled.set(enabled);
}
+ /**
+ * Prepares {@link TaskListSpan}s and marks each one with a {@link ToggleMarkerSpan} in the first step.
+ * The {@link ToggleMarkerSpan} are different from {@link TaskListSpan}s as they will stop on nested tasks instead of spanning the whole tasks including its subtasks.
+ */
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(TaskListItem.class, (visitor, node) -> {
@@ -56,7 +69,6 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
.get(TaskListItem.class);
final Object spans = spanFactory == null ? null :
spanFactory.getSpans(visitor.configuration(), visitor.renderProps());
-
if (spans != null) {
final TaskListSpan taskListSpan;
if (spans instanceof TaskListSpan[]) {
@@ -71,20 +83,16 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
taskListSpan = null;
}
- Log.i(TAG, visitor.builder().subSequence(length, visitor.builder().length()).toString());
- int content = TaskListContextVisitor.contentLength(node);
- Log.i(TAG, "content: " + content + ", '" + visitor.builder().subSequence(length, length + content) + "'");
-
+ final int content = TaskListContextVisitor.contentLength(node);
if (content > 0 && taskListSpan != null) {
// maybe additionally identify this task list (for persistence)
visitor.builder().setSpan(
- new ToggleTaskListSpan(enabled, toggleListener, taskListSpan, visitor.builder().subSequence(length, length + content).toString()),
+ new ToggleMarkerSpan(taskListSpan),
length,
length + content
);
}
}
-
SpannableBuilder.setSpans(
visitor.builder(),
spans,
@@ -98,7 +106,86 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
});
}
- static class TaskListContextVisitor extends AbstractVisitor {
+
+ /**
+ * Adds for each symbolic {@link ToggleMarkerSpan} an actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s.
+ */
+ @Override
+ public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
+ super.afterRender(node, visitor);
+
+ final List<Span> markerSpans = getSortedSpans(visitor.builder(), ToggleMarkerSpan.class, 0, visitor.builder().length());
+
+ for (int position = 0; position < markerSpans.size(); position++) {
+ final Span markerSpan = markerSpans.get(position);
+ final int start = markerSpan.start;
+ final int end = markerSpan.end;
+ final Collection<Range<Integer>> freeRanges = findFreeRanges(visitor.builder(), start, end);
+ for (Range<Integer> freeRange : freeRanges) {
+ visitor.builder().setSpan(
+ new ToggleTaskListSpan(enabled, toggleListener, ((ToggleMarkerSpan) markerSpan.what).getTaskListSpan(), position),
+ freeRange.getLower(), freeRange.getUpper());
+ }
+ }
+ }
+
+ /**
+ * Removes {@link ToggleMarkerSpan}s from {@param textView}.
+ */
+ @Override
+ public void afterSetText(@NonNull TextView textView) {
+ super.afterSetText(textView);
+ final Spannable spannable = MarkdownUtil.getContentAsSpannable(textView);
+ for (ToggleMarkerSpan span : spannable.getSpans(0, spannable.length(), ToggleMarkerSpan.class)) {
+ spannable.removeSpan(span);
+ }
+ textView.setText(spannable);
+ }
+
+ /**
+ * @return a {@link List} of {@link Range}s in the given {@param spanned} from {@param start} to {@param end} which is <strong>not</strong> taken for a {@link ClickableSpan}.
+ */
+ @NonNull
+ private static Collection<Range<Integer>> findFreeRanges(@NonNull SpannableBuilder builder, int start, int end) {
+ final List<Range<Integer>> freeRanges;
+ final List<Span> clickableSpans = getSortedSpans(builder, ClickableSpan.class, start, end);
+ if (clickableSpans.size() > 0) {
+ freeRanges = new LinkedList<>();
+ int from = start;
+ for (Span clickableSpan : clickableSpans) {
+ final int clickableStart = clickableSpan.start;
+ final int clickableEnd = clickableSpan.end;
+ if (from != clickableStart) {
+ freeRanges.add(new Range<>(from, clickableStart));
+ }
+ from = clickableEnd;
+ }
+ if (clickableSpans.size() > 0) {
+ final int lastUpperBlocker = clickableSpans.get(clickableSpans.size() - 1).end;
+ if (lastUpperBlocker < end) {
+ freeRanges.add(new Range<>(lastUpperBlocker, end));
+ }
+ }
+ } else if (start == end) {
+ freeRanges = Collections.emptyList();
+ } else {
+ freeRanges = Collections.singletonList(new Range<>(start, end));
+ }
+ return freeRanges;
+ }
+
+ /**
+ * @return a {@link List} of {@link Span}s holding {@param type}s, sorted ascending by the span start.
+ */
+ private static <T> List<Span> getSortedSpans(@NonNull SpannableBuilder builder, @NonNull Class<T> type, int start, int end) {
+ return builder.getSpans(start, end)
+ .stream()
+ .filter(span -> type.isInstance(span.what))
+ .sorted((o1, o2) -> o1.start - o2.start)
+ .collect(Collectors.toList());
+ }
+
+ private static final class TaskListContextVisitor extends AbstractVisitor {
private int contentLength = 0;
static int contentLength(Node node) {
@@ -142,4 +229,23 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
}
}
}
+
+ /**
+ * Helper class which holds an {@link TaskListSpan} but does not include the range of child {@link TaskListSpan}s.
+ */
+ @VisibleForTesting
+ static final class ToggleMarkerSpan {
+
+ @NonNull
+ private final TaskListSpan taskListSpan;
+
+ private ToggleMarkerSpan(@NonNull TaskListSpan taskListSpan) {
+ this.taskListSpan = taskListSpan;
+ }
+
+ @NonNull
+ private TaskListSpan getTaskListSpan() {
+ return taskListSpan;
+ }
+ }
}
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/InterceptedURLSpan.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/InterceptedURLSpan.java
index d6c57659..f370c006 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/InterceptedURLSpan.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/InterceptedURLSpan.java
@@ -1,12 +1,16 @@
package it.niedermann.android.markdown.markwon.span;
+import android.text.Spanned;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.View;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import java.util.Collection;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.function.Function;
public class InterceptedURLSpan extends URLSpan {
@@ -14,16 +18,16 @@ public class InterceptedURLSpan extends URLSpan {
private static final String TAG = InterceptedURLSpan.class.getSimpleName();
@NonNull
private final Collection<Function<String, Boolean>> onLinkClickCallbacks;
+ private final ExecutorService executor = Executors.newCachedThreadPool();
public InterceptedURLSpan(@NonNull Collection<Function<String, Boolean>> onLinkClickCallbacks, String url) {
super(url);
this.onLinkClickCallbacks = onLinkClickCallbacks;
}
-
@Override
public void onClick(View widget) {
if (onLinkClickCallbacks.size() > 0) {
- new Thread(() -> {
+ executor.submit(() -> {
for (Function<String, Boolean> callback : onLinkClickCallbacks) {
try {
if (callback.apply(getURL())) {
@@ -34,7 +38,7 @@ public class InterceptedURLSpan extends URLSpan {
}
}
super.onClick(widget);
- }).start();
+ });
} else {
super.onClick(widget);
}
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/ToggleTaskListSpan.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/ToggleTaskListSpan.java
index 2f755bab..2b432602 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/ToggleTaskListSpan.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/span/ToggleTaskListSpan.java
@@ -1,15 +1,12 @@
package it.niedermann.android.markdown.markwon.span;
-import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.View;
-import android.widget.TextView;
import androidx.annotation.NonNull;
-import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
@@ -19,43 +16,24 @@ public class ToggleTaskListSpan extends ClickableSpan {
private static final String TAG = ToggleTaskListSpan.class.getSimpleName();
- final AtomicBoolean enabled;
- final BiConsumer<Integer, Boolean> toggleListener;
- final TaskListSpan span;
- final String content;
+ private final AtomicBoolean enabled;
+ private final BiConsumer<Integer, Boolean> toggleListener;
+ private final TaskListSpan span;
+ private final int position;
- public ToggleTaskListSpan(@NonNull AtomicBoolean enabled, @NonNull BiConsumer<Integer, Boolean> toggleListener, @NonNull TaskListSpan span, String content) {
+ public ToggleTaskListSpan(@NonNull AtomicBoolean enabled, @NonNull BiConsumer<Integer, Boolean> toggleListener, @NonNull TaskListSpan span, int position) {
this.enabled = enabled;
this.toggleListener = toggleListener;
this.span = span;
- this.content = content;
+ this.position = position;
}
@Override
public void onClick(@NonNull View widget) {
- if(enabled.get()) {
+ if (enabled.get()) {
span.setDone(!span.isDone());
widget.invalidate();
- Log.v(TAG, "task-list click, isDone: " + span.isDone() + ", content: '" + content + "'");
-
- // it must be a TextView
- final TextView textView = (TextView) widget;
- // it must be spanned
- // TODO what if textView is not a spanned?
- final Spanned spanned = (Spanned) textView.getText();
-
- final ClickableSpan[] toggles = spanned.getSpans(0, spanned.length(), getClass());
- Arrays.sort(toggles, (o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2));
-
- int currentTogglePosition = -1;
- for (int i = 0; i < toggles.length; i++) {
- if (spanned.getSpanStart(toggles[i]) == spanned.getSpanStart(this) && spanned.getSpanEnd(toggles[i]) == spanned.getSpanEnd(this)) {
- currentTogglePosition = i;
- break;
- }
- }
-
- toggleListener.accept(currentTogglePosition, span.isDone());
+ toggleListener.accept(position, span.isDone());
} else {
Log.w(TAG, "Prevented toggling checkbox because the view is disabled");
}
diff --git a/markdown/src/main/java/it/niedermann/android/markdown/model/EListType.java b/markdown/src/main/java/it/niedermann/android/markdown/model/EListType.java
index 0bd7d7a9..db21b631 100644
--- a/markdown/src/main/java/it/niedermann/android/markdown/model/EListType.java
+++ b/markdown/src/main/java/it/niedermann/android/markdown/model/EListType.java
@@ -8,6 +8,7 @@ public enum EListType {
public final String listSymbol;
public final String listSymbolWithTrailingSpace;
public final String checkboxChecked;
+ public final String checkboxCheckedUpperCase;
public final String checkboxUnchecked;
public final String checkboxUncheckedWithTrailingSpace;
@@ -15,6 +16,7 @@ public enum EListType {
this.listSymbol = String.valueOf(listSymbol);
this.listSymbolWithTrailingSpace = listSymbol + " ";
this.checkboxChecked = listSymbolWithTrailingSpace + "[x]";
+ this.checkboxCheckedUpperCase = listSymbolWithTrailingSpace + "[X]";
this.checkboxUnchecked = listSymbolWithTrailingSpace + "[ ]";
this.checkboxUncheckedWithTrailingSpace = checkboxUnchecked + " ";
}
diff --git a/markdown/src/main/res/drawable/ic_baseline_broken_image_24.xml b/markdown/src/main/res/drawable/ic_baseline_broken_image_24.xml
new file mode 100644
index 00000000..9c054846
--- /dev/null
+++ b/markdown/src/main/res/drawable/ic_baseline_broken_image_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#757575"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M21,5v6.59l-3,-3.01 -4,4.01 -4,-4 -4,4 -3,-3.01L3,5c0,-1.1 0.9,-2 2,-2h14c1.1,0 2,0.9 2,2zM18,11.42l3,3.01L21,19c0,1.1 -0.9,2 -2,2L5,21c-1.1,0 -2,-0.9 -2,-2v-6.58l3,2.99 4,-4 4,4 4,-3.99z"/>
+</vector>
diff --git a/markdown/src/main/res/drawable/ic_baseline_image_24.xml b/markdown/src/main/res/drawable/ic_baseline_image_24.xml
new file mode 100644
index 00000000..135402e4
--- /dev/null
+++ b/markdown/src/main/res/drawable/ic_baseline_image_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#757575"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
+</vector>
diff --git a/markdown/src/main/res/values-night/colors.xml b/markdown/src/main/res/values-night/colors.xml
index c9468045..315bccef 100644
--- a/markdown/src/main/res/values-night/colors.xml
+++ b/markdown/src/main/res/values-night/colors.xml
@@ -2,4 +2,5 @@
<resources>
<color name="bg_highlighted">#2a2a2a</color>
<color name="bg_code">#11ffffff</color>
+ <color name="block_quote">#666</color>
</resources> \ No newline at end of file
diff --git a/markdown/src/main/res/values/colors.xml b/markdown/src/main/res/values/colors.xml
index 72c77310..fbc6dd76 100644
--- a/markdown/src/main/res/values/colors.xml
+++ b/markdown/src/main/res/values/colors.xml
@@ -3,4 +3,5 @@
<color name="search_color">#0082C9</color>
<color name="bg_highlighted">#eee</color>
<color name="bg_code">#0e000000</color>
+ <color name="block_quote">#aaa</color>
</resources> \ No newline at end of file
diff --git a/markdown/src/test/java/it/niedermann/android/markdown/ExampleUnitTest.java b/markdown/src/test/java/it/niedermann/android/markdown/ExampleUnitTest.java
deleted file mode 100644
index 0fa5d232..00000000
--- a/markdown/src/test/java/it/niedermann/android/markdown/ExampleUnitTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package it.niedermann.android.markdown;
-
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
- */
-public class ExampleUnitTest {
- @Test
- public void addition_isCorrect() {
- assertEquals(4, 2 + 2);
- }
-} \ No newline at end of file
diff --git a/markdown/src/test/java/it/niedermann/android/markdown/ListTagHandlerTest.java b/markdown/src/test/java/it/niedermann/android/markdown/ListTagHandlerTest.java
new file mode 100644
index 00000000..57649701
--- /dev/null
+++ b/markdown/src/test/java/it/niedermann/android/markdown/ListTagHandlerTest.java
@@ -0,0 +1,142 @@
+package it.niedermann.android.markdown;
+
+import androidx.core.text.HtmlCompat;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ListTagHandlerTest extends TestCase {
+
+ //language=html
+ private static final String SAMPLE_HTML_WIDGET_TEST = "<h1>Widget-Test</h1>" +
+ "<p><a href=\"https://nextcloud.com\">Link</a></p>" +
+ "<p><strong>bold</strong> <em>italic</em> <em><strong>itabold</strong></em> ~~strike~~</p>" +
+ "<ol start=\"3\">" +
+ "<li>Item" +
+ "<ol>" +
+ "<li>Subitem</li>" +
+ "<li>Subitem" +
+ "<ul>" +
+ "<li>Subi</li>" +
+ "<li>Subi</li>" +
+ "</ul>" +
+ "</li>" +
+ "<li>Test</li>" +
+ "</ol>" +
+ "</li>" +
+ "<li>Item</li>" +
+ "<li>Item</li>" +
+ "</ol>" +
+ "<ul>" +
+ "<li>Unordered</li>" +
+ "<li>Unordered</li>" +
+ "</ul>" +
+ "<p>☐ Unchecked<br>☒ Checked</p>";
+
+ //language=html
+ private static final String SAMPLE_HTML_MARKDOWN_SYNTAX =
+ "<h2>This is a header.</h2>" +
+ "<ol>" +
+ "<li>This is the first list item.</li>" +
+ "<li>This is the second list item.</li>" +
+ "</ol>" +
+ "<p>Here's some example code:</p>" +
+ "<pre><code>return shell_exec(&quot;echo $input | $markdown_script&quot;);" +
+ "</code></pre>" +
+ "</blockquote>" +
+ "<p>Any decent text editor should make email-style quoting easy. For<br>example, with BBEdit, you can make a selection and choose Increase<br>Quote Level from the Text menu.</p>" +
+ "<h3>Lists</h3>" +
+ "<p>Markdown supports ordered (numbered) and unordered (bulleted) lists.</p>" +
+ "<p>Unordered lists use asterisks, pluses, and hyphens -- interchangably<br>-- as list markers:</p>" +
+ "<ul>" +
+ "<li>Red</li>" +
+ "<li>Green</li>" +
+ "<li>Blue</li>" +
+ "</ul>" +
+ "<p>is equivalent to:</p>" +
+ "<ul>" +
+ "<li>Red</li>" +
+ "<li>Green</li>" +
+ "<li>Blue</li>" +
+ "</ul>" +
+ "<p>and:</p>" +
+ "<ul>" +
+ "<li>Red</li>" +
+ "<li>Green</li>" +
+ "<li>Blue</li>" +
+ "</ul>" +
+ "<p>Ordered lists use numbers followed by periods:</p>" +
+ "<ol>" +
+ "<li>Bird</li>" +
+ "<li>McHale</li>" +
+ "<li>Parish</li>" +
+ "</ol>" +
+ "<p>It's important to note that the actual numbers you use to mark the<br>list have no effect on the HTML output Markdown produces. The HTML<br>Markdown produces from the above list is:</p>" +
+ "<p>If you instead wrote the list in Markdown like this:</p>" +
+ "<ol>" +
+ "<li>Bird</li>" +
+ "<li>McHale</li>" +
+ "<li>Parish</li>" +
+ "</ol>" +
+ "<p>or even:</p>" +
+ "<ol start=\"3\">" +
+ "<li>Bird</li>" +
+ "<li>McHale</li>" +
+ "<li>Parish</li>" +
+ "</ol>";
+
+ @Test
+ public void testMarkOrderedListTags() {
+ assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("<ol start=\"3\">"));
+ assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</ol>"));
+ assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</ul>"));
+ assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</ul>"));
+ assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</li>"));
+ assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</li>"));
+
+ final String markedSampleHtml = ListTagHandler.prepareTagHandling(SAMPLE_HTML_WIDGET_TEST);
+
+ assertFalse(markedSampleHtml.contains("<ol start=\"3\">"));
+ assertFalse(markedSampleHtml.contains("</ol>"));
+ assertFalse(markedSampleHtml.contains("</ul>"));
+ assertFalse(markedSampleHtml.contains("</ul>"));
+ assertFalse(markedSampleHtml.contains("</li>"));
+ assertFalse(markedSampleHtml.contains("</li>"));
+ }
+
+ @Test
+ public void testHandleTag() {
+ final ListTagHandler handler = new ListTagHandler();
+
+ assertEquals("\n• Item ", HtmlCompat.fromHtml(ListTagHandler.prepareTagHandling("<ul><li>Item</li></ul>"), 0, null, handler).toString());
+
+ final String[] lines = HtmlCompat.fromHtml(ListTagHandler.prepareTagHandling(SAMPLE_HTML_WIDGET_TEST), 0, null, handler).toString().split("\n");
+
+ assertEquals("Widget-Test", lines[0]);
+ assertEquals("", lines[1]);
+ assertEquals("Link", lines[2]);
+ assertEquals("", lines[3]);
+ assertEquals("bold italic itabold ~~strike~~", lines[4]);
+ assertEquals("", lines[5]);
+ assertEquals("", lines[6]);
+ assertEquals("1. Item ", lines[7]);
+ assertEquals("\t\t1. Subitem ", lines[8]);
+ assertEquals("\t\t2. Subitem ", lines[9]);
+ assertEquals("\t\t\t\t• Subi ", lines[10]);
+ assertEquals("\t\t\t\t• Subi ", lines[11]);
+ assertEquals("\t\t3. Test ", lines[12]);
+ assertEquals("2. Item ", lines[13]);
+ assertEquals("3. Item ", lines[14]);
+ assertEquals("• Unordered ", lines[15]);
+ assertEquals("• Unordered ", lines[16]);
+ assertEquals("", lines[17]);
+ assertEquals("☐ Unchecked", lines[18]);
+ assertEquals("☒ Checked", lines[19]);
+
+ HtmlCompat.fromHtml(ListTagHandler.prepareTagHandling(SAMPLE_HTML_MARKDOWN_SYNTAX), 0, null, handler);
+ }
+} \ No newline at end of file
diff --git a/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java b/markdown/src/test/java/it/niedermann/android/markdown/MarkdownUtilTest.java
index 7c3f9774..19d9b189 100644
--- a/markdown/src/androidTest/java/it/niedermann/android/markdown/MarkdownUtilTest.java
+++ b/markdown/src/test/java/it/niedermann/android/markdown/MarkdownUtilTest.java
@@ -1,6 +1,5 @@
package it.niedermann.android.markdown;
-import android.content.Context;
import android.graphics.Color;
import android.text.Editable;
import android.text.Spannable;
@@ -8,13 +7,11 @@ import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -24,13 +21,13 @@ import java.util.Map;
import it.niedermann.android.markdown.model.EListType;
import it.niedermann.android.markdown.model.SearchSpan;
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
public class MarkdownUtilTest extends TestCase {
@Test
public void testGetStartOfLine() {
//language=md
- StringBuilder test = new StringBuilder(
+ final StringBuilder test = new StringBuilder(
"# Test-Note\n" + // line start 0
"\n" + // line start 12
"- [ ] this is a test note\n" + // line start 13
@@ -92,57 +89,86 @@ public class MarkdownUtilTest extends TestCase {
}
@Test
+ public void testGetMarkdownLink() {
+ assertEquals("[Foo](https://bar)", MarkdownUtil.getMarkdownLink("Foo", "https://bar"));
+ }
+
+ @Test
public void testLineStartsWithCheckbox() {
final Map<String, Boolean> lines = new HashMap<>();
lines.put(" - [ ] a", true);
lines.put(" - [x] a", true);
+ lines.put(" - [X] a", true);
lines.put(" * [ ] a", true);
lines.put(" * [x] a", true);
+ lines.put(" * [X] a", true);
lines.put(" + [ ] a", true);
lines.put(" + [x] a", true);
+ lines.put(" + [X] a", true);
lines.put("- [ ] a", true);
lines.put("- [x] a", true);
+ lines.put("- [X] a", true);
lines.put("* [ ] a", true);
lines.put("* [x] a", true);
+ lines.put("* [X] a", true);
lines.put("+ [ ] a", true);
lines.put("+ [x] a", true);
+ lines.put("+ [X] a", true);
lines.put(" - [ ] ", true);
lines.put(" - [x] ", true);
+ lines.put(" - [X] ", true);
lines.put(" * [ ] ", true);
lines.put(" * [x] ", true);
+ lines.put(" * [X] ", true);
lines.put(" + [ ] ", true);
lines.put(" + [x] ", true);
+ lines.put(" + [X] ", true);
lines.put(" - [ ]", true);
lines.put(" - [x]", true);
+ lines.put(" - [X]", true);
lines.put(" * [ ]", true);
lines.put(" * [x]", true);
+ lines.put(" * [X]", true);
lines.put(" + [ ]", true);
lines.put(" + [x]", true);
+ lines.put(" + [X]", true);
lines.put("- [ ] ", true);
lines.put("- [x] ", true);
+ lines.put("- [X] ", true);
lines.put("* [ ] ", true);
lines.put("* [x] ", true);
+ lines.put("* [X] ", true);
lines.put("+ [ ] ", true);
lines.put("+ [x] ", true);
+ lines.put("+ [X] ", true);
lines.put("- [ ]", true);
lines.put("- [x]", true);
+ lines.put("- [X]", true);
lines.put("* [ ]", true);
lines.put("* [x]", true);
+ lines.put("* [X]", true);
lines.put("+ [ ]", true);
lines.put("+ [x]", true);
+ lines.put("+ [X]", true);
lines.put("-[ ] ", false);
lines.put("-[x] ", false);
+ lines.put("-[X] ", false);
lines.put("*[ ] ", false);
lines.put("*[x] ", false);
+ lines.put("*[X] ", false);
lines.put("+[ ] ", false);
lines.put("+[x] ", false);
+ lines.put("+[X] ", false);
lines.put("-[ ]", false);
lines.put("-[x]", false);
+ lines.put("-[X]", false);
lines.put("*[ ]", false);
lines.put("*[x]", false);
+ lines.put("*[X]", false);
lines.put("+[ ]", false);
lines.put("+[x]", false);
+ lines.put("+[X]", false);
lines.put("- [] ", false);
lines.put("* [] ", false);
@@ -167,7 +193,7 @@ public class MarkdownUtilTest extends TestCase {
// Add italic
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
- assertEquals(13, MarkdownUtil.togglePunctuation(builder, 6, 11, "*"));
+ assertEquals(12, MarkdownUtil.togglePunctuation(builder, 6, 11, "*"));
assertEquals("Lorem *ipsum* dolor sit amet.", builder.toString());
// Remove italic
@@ -177,7 +203,7 @@ public class MarkdownUtilTest extends TestCase {
// Add bold
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
- assertEquals(15, MarkdownUtil.togglePunctuation(builder, 6, 11, "**"));
+ assertEquals(13, MarkdownUtil.togglePunctuation(builder, 6, 11, "**"));
assertEquals("Lorem **ipsum** dolor sit amet.", builder.toString());
// Remove bold
@@ -187,7 +213,7 @@ public class MarkdownUtilTest extends TestCase {
// Add strike
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
- assertEquals(15, MarkdownUtil.togglePunctuation(builder, 6, 11, "~~"));
+ assertEquals(13, MarkdownUtil.togglePunctuation(builder, 6, 11, "~~"));
assertEquals("Lorem ~~ipsum~~ dolor sit amet.", builder.toString());
// Remove strike
@@ -197,7 +223,7 @@ public class MarkdownUtilTest extends TestCase {
// Add italic at first position
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
- assertEquals(7, MarkdownUtil.togglePunctuation(builder, 0, 5, "*"));
+ assertEquals(6, MarkdownUtil.togglePunctuation(builder, 0, 5, "*"));
assertEquals("*Lorem* ipsum dolor sit amet.", builder.toString());
// Remove italic from first position
@@ -207,7 +233,7 @@ public class MarkdownUtilTest extends TestCase {
// Add italic at last position
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
- assertEquals(29, MarkdownUtil.togglePunctuation(builder, 22, 27, "*"));
+ assertEquals(28, MarkdownUtil.togglePunctuation(builder, 22, 27, "*"));
assertEquals("Lorem ipsum dolor sit *amet.*", builder.toString());
// Remove italic from last position
@@ -242,47 +268,145 @@ public class MarkdownUtilTest extends TestCase {
assertEquals(27, MarkdownUtil.togglePunctuation(builder, 0, 31, "*"));
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
- // Special use-case: toggle from italic to bold and back
-
- // TODO Toggle italic on bold text
-// builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
-// assertEquals(17, MarkdownUtil.togglePunctuation(builder, 8, 13, "*"));
-// assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
-
- // TODO Toggle bold on italic text
-// builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
-// assertEquals(17, MarkdownUtil.togglePunctuation(builder, 7, 12, "**"));
-// assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
-
- // TODO Toggle bold to italic
-// builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
-// assertEquals(33, MarkdownUtil.togglePunctuation(builder, 0, 31, "*"));
-// assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
-
- // TODO Toggle multiple bold parts to italic
-// builder = new SpannableStringBuilder("Lorem **ipsum** dolor **sit** amet.");
-// assertEquals(38, MarkdownUtil.togglePunctuation(builder, 0, 34, "*"));
-// assertEquals("Lorem ***ipsum*** dolor ***sit*** amet.", builder.toString());
-
- // TODO Toggle italic and bold to bold
-// builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor sit amet.");
-// assertEquals(13, MarkdownUtil.togglePunctuation(builder, 0, 14, "*"));
-// assertEquals("Lorem **ipsum** dolor sit amet.", builder.toString());
-
- // TODO Toggle italic and bold to italic
-// builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor sit amet.");
-// assertEquals(12, MarkdownUtil.togglePunctuation(builder, 9, 14, "**"));
-// assertEquals("Lorem *ipsum* dolor sit amet.", builder.toString());
-
- // TODO Toggle multiple italic and bold to bold
-// builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor ***sit*** amet.");
-// assertEquals(34, MarkdownUtil.togglePunctuation(builder, 0, 38, "*"));
-// assertEquals("Lorem **ipsum** dolor **sit** amet.", builder.toString());
-
- // TODO Toggle multiple italic and bold to italic
-// builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor ***sit*** amet.");
-// assertEquals(30, MarkdownUtil.togglePunctuation(builder, 0, 38, "**"));
-// assertEquals("Lorem *ipsum* dolor *sit* amet.", builder.toString());
+ // Toggle italic on bold text
+ builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
+ assertEquals(14, MarkdownUtil.togglePunctuation(builder, 8, 13, "*"));
+ assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
+
+ // Toggle italic on bold text
+ builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
+ assertEquals(16, MarkdownUtil.togglePunctuation(builder, 6, 15, "*"));
+ assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
+
+ // Toggle bold on italic text
+ builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
+ assertEquals(14, MarkdownUtil.togglePunctuation(builder, 7, 12, "**"));
+ assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
+
+ // Toggle bold to italic
+ builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
+ assertEquals(32, MarkdownUtil.togglePunctuation(builder, 0, 31, "*"));
+ assertEquals("*Lorem **ipsum** dolor sit amet.*", builder.toString());
+
+ // Toggle italic and bold to bold
+ builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor sit amet.");
+ assertEquals(13, MarkdownUtil.togglePunctuation(builder, 0, 14, "*"));
+ assertEquals("Lorem **ipsum** dolor sit amet.", builder.toString());
+
+ // toggle italic around multiple existing bolds
+ builder = new SpannableStringBuilder("Lorem **ipsum** dolor **sit** amet.");
+ assertEquals(35, MarkdownUtil.togglePunctuation(builder, 0, 34, "*"));
+ assertEquals("*Lorem **ipsum** dolor **sit** amet*.", builder.toString());
+
+ // Toggle italic and bold to italic
+ builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor sit amet.");
+ assertEquals(12, MarkdownUtil.togglePunctuation(builder, 9, 14, "**"));
+ assertEquals("Lorem *ipsum* dolor sit amet.", builder.toString());
+
+ // Toggle multiple italic and bold to bold
+ builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor ***sit*** amet.");
+ assertEquals(34, MarkdownUtil.togglePunctuation(builder, 0, 38, "*"));
+ assertEquals("Lorem **ipsum** dolor **sit** amet.", builder.toString());
+
+ // Toggle multiple italic and bold to italic
+ builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor ***sit*** amet.");
+ assertEquals(30, MarkdownUtil.togglePunctuation(builder, 0, 38, "**"));
+ assertEquals("Lorem *ipsum* dolor *sit* amet.", builder.toString());
+
+ // Toggle italic on an empty text
+ builder = new SpannableStringBuilder("");
+ assertEquals(1, MarkdownUtil.togglePunctuation(builder, 0, 0, "*"));
+ assertEquals("**", builder.toString());
+
+ // Toggle italic on a blank selection
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(2, MarkdownUtil.togglePunctuation(builder, 0, 1, "*"));
+ assertEquals("* *", builder.toString());
+
+ // Toggle italic on a partial blank selection
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(3, MarkdownUtil.togglePunctuation(builder, 1, 2, "*"));
+ assertEquals(" * * ", builder.toString());
+
+ // Toggle bold on an empty text
+ builder = new SpannableStringBuilder("");
+ assertEquals(2, MarkdownUtil.togglePunctuation(builder, 0, 0, "**"));
+ assertEquals("****", builder.toString());
+
+ // Toggle bold on a blank selection
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(3, MarkdownUtil.togglePunctuation(builder, 0, 1, "**"));
+ assertEquals("** **", builder.toString());
+
+ // Toggle bold on a partial blank selection
+ builder = new SpannableStringBuilder(" ");
+ assertEquals(4, MarkdownUtil.togglePunctuation(builder, 1, 2, "**"));
+ assertEquals(" ** ** ", builder.toString());
+
+ // Toggle italic right after bold
+ builder = new SpannableStringBuilder("**Bold**Italic");
+ assertEquals(15, MarkdownUtil.togglePunctuation(builder, 8, 14, "*"));
+ assertEquals("**Bold***Italic*", builder.toString());
+
+ // Toggle italic for last of many bolds in one line
+ builder = new SpannableStringBuilder("Lorem **Ipsum** **Dolor**");
+ assertEquals(24, MarkdownUtil.togglePunctuation(builder, 18, 23, "*"));
+ assertEquals("Lorem **Ipsum** ***Dolor***", builder.toString());
+
+ builder = new SpannableStringBuilder("Lorem **Ipsum** **Dolor**");
+ assertEquals(14, MarkdownUtil.togglePunctuation(builder, 8, 13, "*"));
+ assertEquals("Lorem ***Ipsum*** **Dolor**", builder.toString());
+
+ builder = new SpannableStringBuilder("Lorem **Ipsum** **Dolor**");
+ assertEquals(16, MarkdownUtil.togglePunctuation(builder, 6, 15, "*"));
+ assertEquals("Lorem ***Ipsum*** **Dolor**", builder.toString());
+
+ // Toggle italic for last bold + italic in a row of multiple marked elements
+ builder = new SpannableStringBuilder("Lorem **Ipsum** ***Dolor***");
+ assertEquals(23, MarkdownUtil.togglePunctuation(builder, 19, 24, "*"));
+ assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
+
+ builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
+ assertEquals(13, MarkdownUtil.togglePunctuation(builder, 9, 14, "*"));
+ assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
+
+ builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
+ assertEquals(15, MarkdownUtil.togglePunctuation(builder, 6, 17, "*"));
+ assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
+
+ builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
+ assertEquals(15, MarkdownUtil.togglePunctuation(builder, 7, 16, "*"));
+ assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
+
+ builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
+ assertEquals(15, MarkdownUtil.togglePunctuation(builder, 7, 17, "*"));
+ assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
+
+ builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
+ assertEquals(15, MarkdownUtil.togglePunctuation(builder, 8, 16, "*"));
+ assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
+
+ // Multiline
+
+ builder = new SpannableStringBuilder("Lorem ***Ipsum***\n **Dolor**");
+ assertEquals(29, MarkdownUtil.togglePunctuation(builder, 0, 28, "*"));
+ assertEquals("*Lorem ***Ipsum***\n **Dolor***", builder.toString());
+
+ builder = new SpannableStringBuilder("**Bold**\nItalic");
+ assertEquals(16, MarkdownUtil.togglePunctuation(builder, 9, 15, "*"));
+ assertEquals("**Bold**\n*Italic*", builder.toString());
+
+ builder = new SpannableStringBuilder("Bold\n*Italic*");
+ assertEquals(6, MarkdownUtil.togglePunctuation(builder, 0, 4, "**"));
+ assertEquals("**Bold**\n*Italic*", builder.toString());
+
+ builder = new SpannableStringBuilder("*Italic*\nBold");
+ assertEquals(15, MarkdownUtil.togglePunctuation(builder, 9, 13, "**"));
+ assertEquals("*Italic*\n**Bold**", builder.toString());
+
+ builder = new SpannableStringBuilder("Italic\n**Bold**");
+ assertEquals(7, MarkdownUtil.togglePunctuation(builder, 0, 6, "*"));
+ assertEquals("*Italic*\n**Bold**", builder.toString());
}
@Test
@@ -421,80 +545,6 @@ public class MarkdownUtilTest extends TestCase {
}
@Test
- public void testRemoveContainingPunctuation() {
- try {
- final Method m = MarkdownUtil.class.getDeclaredMethod("removeContainingPunctuation", Editable.class, int.class, int.class, String.class);
- m.setAccessible(true);
- Editable builder;
-
- builder = new SpannableStringBuilder("Lorem *ipsum* dolor");
- m.invoke(null, builder, 0, 19, "*");
- assertEquals("Lorem ipsum dolor", builder.toString());
-
- builder = new SpannableStringBuilder("*Lorem ipsum dolor*");
- m.invoke(null, builder, 0, 19, "*");
- assertEquals("Lorem ipsum dolor", builder.toString());
-
- builder = new SpannableStringBuilder("**Lorem ipsum**");
- m.invoke(null, builder, 0, 15, "**");
- assertEquals("Lorem ipsum", builder.toString());
-
- builder = new SpannableStringBuilder("*Lorem* *ipsum*");
- m.invoke(null, builder, 0, 15, "*");
- assertEquals("Lorem ipsum", builder.toString());
-
- builder = new SpannableStringBuilder("Lorem* ipsum");
- m.invoke(null, builder, 0, 12, "*");
- assertEquals("Lorem ipsum", builder.toString());
-
- builder = new SpannableStringBuilder("*Lorem* ipsum*");
- m.invoke(null, builder, 0, 14, "*");
- assertEquals("Lorem ipsum", builder.toString());
-
- builder = new SpannableStringBuilder("**Lorem ipsum**");
- m.invoke(null, builder, 0, 15, "*");
- assertEquals("Lorem ipsum", builder.toString());
- } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
- e.printStackTrace();
- }
- }
-
- @Test
- @SuppressWarnings("ConstantConditions")
- public void testSelectionIsSurroundedByPunctuation() {
- try {
- final Method m = MarkdownUtil.class.getDeclaredMethod("selectionIsSurroundedByPunctuation", CharSequence.class, int.class, int.class, String.class);
- m.setAccessible(true);
- assertTrue((Boolean) m.invoke(null, "*Lorem ipsum*", 1, 12, "*"));
- assertTrue((Boolean) m.invoke(null, "**Lorem ipsum**", 2, 13, "*"));
- assertTrue((Boolean) m.invoke(null, "**Lorem ipsum**", 2, 13, "**"));
-
- assertFalse((Boolean) m.invoke(null, "*Lorem ipsum*", 0, 12, "*"));
- assertFalse((Boolean) m.invoke(null, "*Lorem ipsum*", 1, 13, "*"));
- assertFalse((Boolean) m.invoke(null, "*Lorem ipsum*", 0, 13, "*"));
- } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
- e.printStackTrace();
- }
- }
-
- @Test
- @SuppressWarnings("ConstantConditions")
- public void testGetContainedPunctuationCount() {
- try {
- final Method m = MarkdownUtil.class.getDeclaredMethod("getContainedPunctuationCount", CharSequence.class, int.class, int.class, String.class);
- m.setAccessible(true);
- assertEquals(0, (int) m.invoke(null, "*Lorem ipsum*", 1, 12, "*"));
- assertEquals(1, (int) m.invoke(null, "*Lorem ipsum*", 1, 13, "*"));
- assertEquals(2, (int) m.invoke(null, "*Lorem ipsum*", 0, 13, "*"));
- assertEquals(0, (int) m.invoke(null, "*Lorem ipsum*", 0, 13, "**"));
- assertEquals(0, (int) m.invoke(null, "*Lorem ipsum**", 0, 13, "**"));
- assertEquals(1, (int) m.invoke(null, "*Lorem ipsum**", 0, 14, "**"));
- } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
- e.printStackTrace();
- }
- }
-
- @Test
@SuppressWarnings("ConstantConditions")
public void testSelectionIsInLink() {
try {
@@ -665,6 +715,14 @@ public class MarkdownUtilTest extends TestCase {
listType.checkboxUnchecked + " \n" +
listType.checkboxChecked + " Item";
assertEquals(expected_9, MarkdownUtil.setCheckboxStatus(origin_9, 1, true));
+
+ final String origin_10 = "" +
+ listType.checkboxChecked + " Item\n" +
+ listType.checkboxCheckedUpperCase + " Item";
+ final String expected_10 = "" +
+ listType.checkboxChecked + " Item\n" +
+ listType.checkboxUnchecked + " Item";
+ assertEquals(expected_10, MarkdownUtil.setCheckboxStatus(origin_10, 1, false));
}
}
@@ -674,8 +732,6 @@ public class MarkdownUtilTest extends TestCase {
final Method removeSpans = MarkdownUtil.class.getDeclaredMethod("removeSpans", Spannable.class, Class.class);
removeSpans.setAccessible(true);
- final Context context = ApplicationProvider.getApplicationContext();
-
final Editable editable_1 = new SpannableStringBuilder("Lorem Ipsum dolor sit amet");
editable_1.setSpan(new SearchSpan(Color.RED, Color.GRAY, false, false), 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
editable_1.setSpan(new ForegroundColorSpan(Color.BLUE), 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -719,11 +775,11 @@ public class MarkdownUtilTest extends TestCase {
assertEquals("FooTestBar", MarkdownUtil.removeMarkdown("Foo*Test*Bar"));
assertEquals("FooTestBar", MarkdownUtil.removeMarkdown("Foo**Test**Bar"));
assertEquals("FooTestBar", MarkdownUtil.removeMarkdown("Foo***Test***Bar"));
- assertEquals("FooTest*Bar", MarkdownUtil.removeMarkdown("Foo*Test**Bar"));
+ assertEquals("Foo*Test**Bar", MarkdownUtil.removeMarkdown("Foo*Test**Bar"));
assertEquals("Foo*TestBar", MarkdownUtil.removeMarkdown("Foo***Test**Bar"));
- assertEquals("FooTestBar", MarkdownUtil.removeMarkdown("Foo_Test_Bar"));
- assertEquals("FooTestBar", MarkdownUtil.removeMarkdown("Foo__Test__Bar"));
- assertEquals("FooTestBar", MarkdownUtil.removeMarkdown("Foo___Test___Bar"));
+ assertEquals("Foo_Test_Bar", MarkdownUtil.removeMarkdown("Foo_Test_Bar"));
+ assertEquals("Foo__Test__Bar", MarkdownUtil.removeMarkdown("Foo__Test__Bar"));
+ assertEquals("Foo___Test___Bar", MarkdownUtil.removeMarkdown("Foo___Test___Bar"));
assertEquals("Foo\nHeader\nBar", MarkdownUtil.removeMarkdown("Foo\n# Header\nBar"));
assertEquals("Foo\nHeader\nBar", MarkdownUtil.removeMarkdown("Foo\n### Header\nBar"));
assertEquals("Foo\nHeader\nBar", MarkdownUtil.removeMarkdown("Foo\n# Header #\nBar"));
@@ -735,15 +791,17 @@ public class MarkdownUtilTest extends TestCase {
assertEquals("Foo\nAufzählung\nBar", MarkdownUtil.removeMarkdown("Foo\n* Aufzählung\nBar"));
assertEquals("Foo\nAufzählung\nBar", MarkdownUtil.removeMarkdown("Foo\n+ Aufzählung\nBar"));
assertEquals("Foo\nAufzählung\nBar", MarkdownUtil.removeMarkdown("Foo\n- Aufzählung\nBar"));
- assertEquals("Foo\nAufzählung\nBar", MarkdownUtil.removeMarkdown("Foo\n - Aufzählung\nBar"));
+ assertEquals("Foo\n- Aufzählung\nBar", MarkdownUtil.removeMarkdown("Foo\n - Aufzählung\nBar"));
assertEquals("Foo\nAufzählung *\nBar", MarkdownUtil.removeMarkdown("Foo\n* Aufzählung *\nBar"));
assertEquals("Title", MarkdownUtil.removeMarkdown("# Title"));
assertEquals("Aufzählung", MarkdownUtil.removeMarkdown("* Aufzählung"));
// assertEquals("Foo Link Bar", MarkdownUtil.removeMarkdown("Foo [Link](https://example.com) Bar"));
assertFalse(MarkdownUtil.removeMarkdown("- [ ] Test").contains("- [ ]"));
assertTrue(MarkdownUtil.removeMarkdown("- [ ] Test").endsWith("Test"));
+ assertEquals("", MarkdownUtil.removeMarkdown(null));
+ assertEquals("", MarkdownUtil.removeMarkdown(""));
// https://github.com/stefan-niedermann/nextcloud-notes/issues/1104
assertEquals("2021-03-24 - Example text", MarkdownUtil.removeMarkdown("2021-03-24 - Example text"));
}
-} \ No newline at end of file
+}
diff --git a/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java b/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java
new file mode 100644
index 00000000..d864aea4
--- /dev/null
+++ b/markdown/src/test/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPluginTest.java
@@ -0,0 +1,204 @@
+package it.niedermann.android.markdown.markwon.plugins;
+
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.URLSpan;
+import android.util.Range;
+import android.widget.TextView;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import junit.framework.TestCase;
+
+import org.commonmark.node.Node;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import io.noties.markwon.MarkwonVisitor;
+import io.noties.markwon.SpannableBuilder;
+import io.noties.markwon.ext.tasklist.TaskListSpan;
+import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan;
+import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(RobolectricTestRunner.class)
+public class ToggleableTaskListPluginTest extends TestCase {
+
+ @Test
+ public void testAfterRender() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
+ final Node node = mock(Node.class);
+ final MarkwonVisitor visitor = mock(MarkwonVisitor.class);
+
+ final Constructor<ToggleableTaskListPlugin.ToggleMarkerSpan> markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class);
+ markerSpanConstructor.setAccessible(true);
+
+ final SpannableBuilder builder = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
+ builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6);
+ builder.setSpan(new URLSpan(""), 6, 11);
+ builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19);
+ builder.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22);
+ builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27);
+
+ when(visitor.builder()).thenReturn(builder);
+
+ final ToggleableTaskListPlugin plugin = new ToggleableTaskListPlugin((i, b) -> {
+ // Do nothing...
+ });
+ plugin.afterRender(node, visitor);
+
+ // We ignore marker spans in this test. They will be removed in another step
+ final List<SpannableBuilder.Span> spans = builder.getSpans(0, builder.length())
+ .stream()
+ .filter(span -> span.what.getClass() != ToggleableTaskListPlugin.ToggleMarkerSpan.class)
+ .sorted((o1, o2) -> o1.start - o2.start)
+ .collect(Collectors.toList());
+
+ assertEquals(5, spans.size());
+ assertEquals(ToggleTaskListSpan.class, spans.get(0).what.getClass());
+ assertEquals(0, spans.get(0).start);
+ assertEquals(6, spans.get(0).end);
+ assertEquals(URLSpan.class, spans.get(1).what.getClass());
+ assertEquals(6, spans.get(1).start);
+ assertEquals(11, spans.get(1).end);
+ assertEquals(ToggleTaskListSpan.class, spans.get(2).what.getClass());
+ assertEquals(11, spans.get(2).start);
+ assertEquals(19, spans.get(2).end);
+ assertEquals(InterceptedURLSpan.class, spans.get(3).what.getClass());
+ assertEquals(19, spans.get(3).start);
+ assertEquals(22, spans.get(3).end);
+ assertEquals(ToggleTaskListSpan.class, spans.get(4).what.getClass());
+ assertEquals(22, spans.get(4).start);
+ assertEquals(27, spans.get(4).end);
+ }
+
+ @Test
+ public void testAfterSetText() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
+ final Constructor<ToggleableTaskListPlugin.ToggleMarkerSpan> markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class);
+ markerSpanConstructor.setAccessible(true);
+
+ final Editable editable = new SpannableStringBuilder("Lorem Ipsum Dolor \nSit Amet");
+ editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editable.setSpan(new URLSpan(""), 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editable.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ final TextView textView = new TextView(ApplicationProvider.getApplicationContext());
+ textView.setText(editable);
+
+ assertEquals(3, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length);
+
+ final ToggleableTaskListPlugin plugin = new ToggleableTaskListPlugin((i, b) -> {
+ // Do nothing...
+ });
+ plugin.afterSetText(textView);
+
+ assertEquals(0, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length);
+ }
+
+ @Test
+ @SuppressWarnings({"unchecked", "ConstantConditions"})
+ public void testGetSortedSpans() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("getSortedSpans", SpannableBuilder.class, Class.class, int.class, int.class);
+ m.setAccessible(true);
+
+ final Object firstClickableSpan = new URLSpan("");
+ final Object secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), "");
+ final Object unclickableSpan = new ForegroundColorSpan(android.R.color.white);
+
+ final SpannableBuilder spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
+ spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spannable.setSpan(unclickableSpan, 3, 20, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ List<SpannableBuilder.Span> clickableSpans;
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 0, 0);
+ assertEquals(0, clickableSpans.size());
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, spannable.length() - 1, spannable.length() - 1);
+ assertEquals(0, clickableSpans.size());
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 0, 5);
+ assertEquals(0, clickableSpans.size());
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 0, spannable.length());
+ assertEquals(2, clickableSpans.size());
+ assertEquals(firstClickableSpan, clickableSpans.get(0).what);
+ assertEquals(secondClickableSpan, clickableSpans.get(1).what);
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 0, 17);
+ assertEquals(1, clickableSpans.size());
+ assertEquals(firstClickableSpan, clickableSpans.get(0).what);
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 12, 22);
+ assertEquals(1, clickableSpans.size());
+ assertEquals(secondClickableSpan, clickableSpans.get(0).what);
+
+ clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 9, 20);
+ assertEquals(2, clickableSpans.size());
+ assertEquals(firstClickableSpan, clickableSpans.get(0).what);
+ assertEquals(secondClickableSpan, clickableSpans.get(1).what);
+ }
+
+ @Test
+ @SuppressWarnings({"unchecked", "ConstantConditions"})
+ public void testFindFreeRanges() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("findFreeRanges", SpannableBuilder.class, int.class, int.class);
+ m.setAccessible(true);
+
+ final Object firstClickableSpan = new URLSpan("");
+ final Object secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), "");
+ final SpannableBuilder spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
+ spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ List<Range<Integer>> freeRanges;
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, 0);
+ assertEquals(0, freeRanges.size());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, spannable.length() - 1, spannable.length() - 1);
+ assertEquals(0, freeRanges.size());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, 6);
+ assertEquals(1, freeRanges.size());
+ assertEquals(0, (int) freeRanges.get(0).getLower());
+ assertEquals(6, (int) freeRanges.get(0).getUpper());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, 6);
+ assertEquals(1, freeRanges.size());
+ assertEquals(0, (int) freeRanges.get(0).getLower());
+ assertEquals(6, (int) freeRanges.get(0).getUpper());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 3, 15);
+ assertEquals(2, freeRanges.size());
+ assertEquals(3, (int) freeRanges.get(0).getLower());
+ assertEquals(6, (int) freeRanges.get(0).getUpper());
+ assertEquals(11, (int) freeRanges.get(1).getLower());
+ assertEquals(15, (int) freeRanges.get(1).getUpper());
+
+ freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, spannable.length());
+ assertEquals(3, freeRanges.size());
+ assertEquals(0, (int) freeRanges.get(0).getLower());
+ assertEquals(6, (int) freeRanges.get(0).getUpper());
+ assertEquals(11, (int) freeRanges.get(1).getLower());
+ assertEquals(19, (int) freeRanges.get(1).getUpper());
+ assertEquals(22, (int) freeRanges.get(2).getLower());
+ assertEquals(27, (int) freeRanges.get(2).getUpper());
+ }
+}