diff --git a/.github/ISSUE_TEMPLATE/release_template.md b/.github/ISSUE_TEMPLATE/release_template.md index 51710388725..780307f8e2a 100644 --- a/.github/ISSUE_TEMPLATE/release_template.md +++ b/.github/ISSUE_TEMPLATE/release_template.md @@ -18,29 +18,45 @@ For OEM releases, keep the OEM Release template and remove the Open Release one - [ ] [DOC] Ping in #documentation-internal about the new release - [ ] [GIT] Create branch `release/M.m.p` in owncloud/android from master + - [ ] [GIT] Rebase `release/M.m.p` against `stable` in owncloud/android + - [ ] [GIT] Create branch `release/x.y.z` in owncloud/android-library from master + - [ ] [GIT] Rebase `release/x.y.z` against `stable` in owncloud/android-library - [ ] [DEV] Update version number and name in build.gradle in owncloudApp module + - [ ] [DEV] Update [SBOM](https://cloud.owncloud.com/f/6072870) - [ ] [DIS] Create a folder for the new version like `M.m.p_YYYY-MM-DD` inside the `changelog` folder - [ ] [DIS] Move all changelog files from the unreleased folder to the new version folder - [ ] [DIS] Update screenshots, if needed, in README.md - - [ ] [DIS] Add ReleaseNotes replacing `emptyList` with `listOf` and adding inside `ReleaseNote()` with String resources - - [ ] [QA] Design Test plan - - [ ] [QA] Regression Test plan - - [ ] [GIT] Create and sign tag `oc-android-M.m.p` in HEAD commit of release branch, in owncloud/android - - [ ] [GIT] Create and sign tag `x.y.z` in HEAD commit of release branch, in owncloud/android-library + - [ ] [DEV] Add ReleaseNotes replacing `emptyList` with `listOf` and adding inside `ReleaseNote()` with String resources + - [ ] [DIS] Prepare post in central.owncloud.org ([`Category:News + Tag:android`](https://central.owncloud.org/tags/c/news/5/android)) - [ ] [DIS] Generate final bundle from signed commit in owncloud/android - - [ ] [GIT] Merge branch `release/M.m.p` in owncloud/android, into master + - [ ] [QA] Design Test plan + - [ ] [DEV] Code Review + - [ ] [QA] Regression Test execution + - [ ] [QA] QA Approval + - [ ] [DIS] Upload release APK and bundle to internal owncloud instance + - [ ] [DOC] Ping in #documentation-internal that we are close to sign the new tags + - [ ] [GIT] Create and sign tag `vM.m.p` in HEAD commit of release branch, in owncloud/android + - [ ] [GIT] Create and sign tag `vx.y.z` in HEAD commit of release branch, in owncloud/android-library - [ ] [DIS] Upload & publish release bundle and changelog in Play Store - [ ] [DIS] Update screenshots and store listing, if needed, in Play Store - - [ ] [GIT] Publish a new release in owncloud/android - - [ ] [DIS] Create post in central.owncloud.org ([`Category:News + Tag:android`](https://central.owncloud.org/tags/c/news/5/android)) - - [ ] [COM] Inform `#updates` and `#marketing` in internal chat - - [ ] [DIS] Upload release APK and bundle to internal owncloud instance - - [ ] [GIT] Merge master branch into stable, in owncloud/android-library - - [ ] [GIT] Merge master branch into stable, in owncloud/android + - [ ] [GIT] Publish a new release in [owncloud/android](https://github.com/owncloud/android/releases) + - [ ] [DIS] Release published in Play Store + - [ ] [DIS] Publish post in central.owncloud.org ([`Category:News + Tag:android`](https://central.owncloud.org/tags/c/news/5/android)) + - [ ] [COM] Inform `#updates` and `#marketing` in internal chat that release is out + - [ ] [GIT] Merge `release/M.m.p` branch into `stable`, in owncloud/android-library + - [ ] [GIT] Merge `release/M.m.p` branch into `stable`, in owncloud/android + - [ ] [GIT] Merge `release/M.m.p` branch into `master`, in owncloud/android-library + - [ ] [GIT] Merge `release/M.m.p` branch into `master`, in owncloud/android - [ ] [DOC] Update documentation with new stuff by creating [issue](https://github.com/owncloud/docs-client-android/issues) -### BUGS & IMPROVEMENTS +### QA + +Regression test: + +Bugs & improvements: + +- [ ] (1) ... _____ diff --git a/.github/workflows/calens.yml b/.github/workflows/calens.yml index 477d6d9bf67..fa20ce7ff39 100644 --- a/.github/workflows/calens.yml +++ b/.github/workflows/calens.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-22.04 name: Generate Calens Changelog steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run Calens Docker uses: addnab/docker-run-action@v3 with: diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 3e5d285167f..ff81e849752 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -9,5 +9,5 @@ jobs: name: "Validation" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2827e3347c9..596b01f06fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,290 @@ Changelog for ownCloud Android Client [unreleased] (UNRELEASED) The following sections list the changes in ownCloud Android Client unreleased relevant to ownCloud admins and users. -[unreleased]: https://github.com/owncloud/android/compare/v3.0.4...master +[unreleased]: https://github.com/owncloud/android/compare/v4.1.0...master Summary ------- +* Bugfix - Some Null Pointer Exceptions avoided: [#4158](https://github.com/owncloud/android/issues/4158) +* Enhancement - "Apply to all" when many name conflicts arise: [#4078](https://github.com/owncloud/android/issues/4078) + +Details +------- + +* Bugfix - Some Null Pointer Exceptions avoided: [#4158](https://github.com/owncloud/android/issues/4158) + + In the detail screen, in the main file list ViewModel and in the OCFile repository the app has + been prevented from crashing when a null is found. + + https://github.com/owncloud/android/issues/4158 + https://github.com/owncloud/android/pull/4170 + +* Enhancement - "Apply to all" when many name conflicts arise: [#4078](https://github.com/owncloud/android/issues/4078) + + A new dialog has been created where a checkbox has been added to be able to select all the folders + or files that have conflicts. + + https://github.com/owncloud/android/issues/4078 + https://github.com/owncloud/android/pull/4138 + +Changelog for ownCloud Android Client [4.1.0] (2023-08-23) +======================================= +The following sections list the changes in ownCloud Android Client 4.1.0 relevant to +ownCloud admins and users. + +[4.1.0]: https://github.com/owncloud/android/compare/v4.0.0...v4.1.0 + +Summary +------- + +* Bugfix - Spaces' thumbnails not loaded the first time: [#3959](https://github.com/owncloud/android/issues/3959) +* Bugfix - Bad error message when copying/moving with server down: [#4044](https://github.com/owncloud/android/issues/4044) +* Bugfix - Unnecessary or wrong call: [#4074](https://github.com/owncloud/android/issues/4074) +* Bugfix - Menu option unset av. offline shown when shouldn't: [#4077](https://github.com/owncloud/android/issues/4077) +* Bugfix - List of accounts empty after removing all accounts and adding new ones: [#4114](https://github.com/owncloud/android/issues/4114) +* Bugfix - Crash when the token is expired: [#4116](https://github.com/owncloud/android/issues/4116) +* Change - Upgrade min SDK to Android 6 (API 23): [#3245](https://github.com/owncloud/android/issues/3245) +* Change - Move file menu options filter to use case: [#4009](https://github.com/owncloud/android/issues/4009) +* Change - Gradle Version Catalog: [#4035](https://github.com/owncloud/android/pull/4035) +* Change - Remove "ignore" from the debug flavour Android manifest: [#4064](https://github.com/owncloud/android/pull/4064) +* Change - Not opening browser automatically in login: [#4067](https://github.com/owncloud/android/issues/4067) +* Change - Added new unit tests for providers: [#4073](https://github.com/owncloud/android/issues/4073) +* Change - New detail screen file design: [#4098](https://github.com/owncloud/android/pull/4098) +* Enhancement - Show "More" button for every file list item: [#2885](https://github.com/owncloud/android/issues/2885) +* Enhancement - Added "Open in web" options to main file list: [#3860](https://github.com/owncloud/android/issues/3860) +* Enhancement - Copy/move conflict solved by users: [#3935](https://github.com/owncloud/android/issues/3935) +* Enhancement - Improve grid mode: [#4027](https://github.com/owncloud/android/issues/4027) +* Enhancement - Improve UX of creation dialog: [#4031](https://github.com/owncloud/android/issues/4031) +* Enhancement - File name conflict starting by (1): [#4040](https://github.com/owncloud/android/pull/4040) +* Enhancement - Force security if not protected: [#4061](https://github.com/owncloud/android/issues/4061) +* Enhancement - Prevent http traffic with branding options: [#4066](https://github.com/owncloud/android/issues/4066) +* Enhancement - Unit tests for datasources classes - Part 2: [#4071](https://github.com/owncloud/android/issues/4071) +* Enhancement - Respect app_providers_appsUrl value from capabilities: [#4075](https://github.com/owncloud/android/issues/4075) +* Enhancement - Apply (1) to uploads' name conflicts: [#4079](https://github.com/owncloud/android/issues/4079) +* Enhancement - Support "per app" language change on Android 13+: [#4082](https://github.com/owncloud/android/issues/4082) +* Enhancement - Align Sharing icons with other platforms: [#4101](https://github.com/owncloud/android/issues/4101) + +Details +------- + +* Bugfix - Spaces' thumbnails not loaded the first time: [#3959](https://github.com/owncloud/android/issues/3959) + + Changing our own lazy image loading with coil library in spaces and file list. + + https://github.com/owncloud/android/issues/3959 + https://github.com/owncloud/android/pull/4084 + +* Bugfix - Bad error message when copying/moving with server down: [#4044](https://github.com/owncloud/android/issues/4044) + + Right now, when we are trying to copy a file to another folder and the server is downwe receive a + correct message. Before the issue the message shown code from the application. + + https://github.com/owncloud/android/issues/4044 + https://github.com/owncloud/android/pull/4127 + +* Bugfix - Unnecessary or wrong call: [#4074](https://github.com/owncloud/android/issues/4074) + + Removed added path when checking path existence. + + https://github.com/owncloud/android/issues/4074 + https://github.com/owncloud/android/pull/4131 + https://github.com/owncloud/android-library/pull/578 + +* Bugfix - Menu option unset av. offline shown when shouldn't: [#4077](https://github.com/owncloud/android/issues/4077) + + Unset available offline menu option is not shown in files inside an available offline folder + anymore, because content inside an available offline folder cannot be changed its status, + only if the folder changes it. + + https://github.com/owncloud/android/issues/4077 + https://github.com/owncloud/android/pull/4093 + +* Bugfix - List of accounts empty after removing all accounts and adding new ones: [#4114](https://github.com/owncloud/android/issues/4114) + + Now, the account list is shown when User opens the app and was added a new account. + + https://github.com/owncloud/android/issues/4114 + https://github.com/owncloud/android/pull/4122 + +* Bugfix - Crash when the token is expired: [#4116](https://github.com/owncloud/android/issues/4116) + + Now when the token expires and we switch from grid to list mode on the main screen the app doesn't + crash. + + https://github.com/owncloud/android/issues/4116 + https://github.com/owncloud/android/pull/4132 + +* Change - Upgrade min SDK to Android 6 (API 23): [#3245](https://github.com/owncloud/android/issues/3245) + + The minimum SDK has been updated to API 23, which means that the minimum version of Android we'll + support from now on is Android 6 Marshmallow. + + https://github.com/owncloud/android/issues/3245 + https://github.com/owncloud/android/pull/4036 + https://github.com/owncloud/android-library/pull/566 + +* Change - Move file menu options filter to use case: [#4009](https://github.com/owncloud/android/issues/4009) + + The old class where the menu options for a file or group or files were filtered has been replaced + by a new use case which fits in the architecture of the app. + + https://github.com/owncloud/android/issues/4009 + https://github.com/owncloud/android/pull/4039 + +* Change - Gradle Version Catalog: [#4035](https://github.com/owncloud/android/pull/4035) + + Introduces the Gradle Version Catalog to manage the dependencies in a scalable way. Now, all + the dependencies are declared inside toml file. + + https://github.com/owncloud/android/pull/4035 + +* Change - Remove "ignore" from the debug flavour Android manifest: [#4064](https://github.com/owncloud/android/pull/4064) + + A `tools:ignore` property from the Android manifest specific for the debug flavour was + removed as it is not needed anymore. + + https://github.com/owncloud/android/pull/4064 + +* Change - Not opening browser automatically in login: [#4067](https://github.com/owncloud/android/issues/4067) + + When there is a fixed bearer auth server URL via a branded parameter, the login screen won't + redirect automatically to the browser so that some problems in the authentication flow are + solved. + + https://github.com/owncloud/android/issues/4067 + https://github.com/owncloud/android/pull/4106 + +* Change - Added new unit tests for providers: [#4073](https://github.com/owncloud/android/issues/4073) + + Implementation of tests for the functions within ScopedStorageProvider and + OCSharedPreferencesProvider. + + https://github.com/owncloud/android/issues/4073 + https://github.com/owncloud/android/pull/4091 + +* Change - New detail screen file design: [#4098](https://github.com/owncloud/android/pull/4098) + + The detail view ha been improved. It added new properties like last sync, status icon on + thumbnail, path and creation date + + https://github.com/owncloud/android/issues/4092 + https://github.com/owncloud/android/pull/4098 + +* Enhancement - Show "More" button for every file list item: [#2885](https://github.com/owncloud/android/issues/2885) + + A 3-dot button has been added to every file, where the options that we have in the 3-dot menu in + multiselection for that single file have been added for a quicker access to them. Also, some + options have been reordered. + + https://github.com/owncloud/android/issues/2885 + https://github.com/owncloud/android/pull/4076 + +* Enhancement - Added "Open in web" options to main file list: [#3860](https://github.com/owncloud/android/issues/3860) + + "Open in web" dynamic options (depending on the providers available) are now shown in the main + file list as well, when selecting one single file which has providers to open it in web. + + https://github.com/owncloud/android/issues/3860 + https://github.com/owncloud/android/pull/4058 + +* Enhancement - Copy/move conflict solved by users: [#3935](https://github.com/owncloud/android/issues/3935) + + A pop-up is displayed in case there is a name conflict with the files been moved or copied. The + pop-up has the options to Skip, Replace and Keep both, to be consistent with the web client. + + https://github.com/owncloud/android/issues/3935 + https://github.com/owncloud/android/pull/4062 + +* Enhancement - Improve grid mode: [#4027](https://github.com/owncloud/android/issues/4027) + + Grid mode has been improved to show bigger thumbnails in images files. + + https://github.com/owncloud/android/issues/4027 + https://github.com/owncloud/android/pull/4089 + +* Enhancement - Improve UX of creation dialog: [#4031](https://github.com/owncloud/android/issues/4031) + + Creation dialog now shows an error message and disables the confirmation button when + forbidden characters are typed + + https://github.com/owncloud/android/issues/4031 + https://github.com/owncloud/android/pull/4097 + +* Enhancement - File name conflict starting by (1): [#4040](https://github.com/owncloud/android/pull/4040) + + File conflicts now are named with suffix starting in (1) instead of (2). + + https://github.com/owncloud/android/issues/3946 + https://github.com/owncloud/android/pull/4040 + +* Enhancement - Force security if not protected: [#4061](https://github.com/owncloud/android/issues/4061) + + A new branding parameter was created to enforce security protection in the app if device + protection is not enabled. + + https://github.com/owncloud/android/issues/4061 + https://github.com/owncloud/android/pull/4087 + +* Enhancement - Prevent http traffic with branding options: [#4066](https://github.com/owncloud/android/issues/4066) + + Adding branding option for prevent http traffic. + + https://github.com/owncloud/android/issues/4066 + https://github.com/owncloud/android/pull/4110 + +* Enhancement - Unit tests for datasources classes - Part 2: [#4071](https://github.com/owncloud/android/issues/4071) + + Unit tests of the OCLocalFileDataSource and OCRemoteFileDataSource classes have been done. + + https://github.com/owncloud/android/issues/4071 + https://github.com/owncloud/android/pull/4123 + +* Enhancement - Respect app_providers_appsUrl value from capabilities: [#4075](https://github.com/owncloud/android/issues/4075) + + Now, the app receives the app_providers_appsUrl from the local database. Before of this + issue, the value was hardcoded. + + https://github.com/owncloud/android/issues/4075 + https://github.com/owncloud/android/pull/4113 + +* Enhancement - Apply (1) to uploads' name conflicts: [#4079](https://github.com/owncloud/android/issues/4079) + + When new files were uploaded manually to pC, shared from a 3rd party app or text shared with oC + name conflict happens, (2) was added to the file name instead of (1). + + Right now if we upload a file with a repeated name, the new file name will end with (1). + + https://github.com/owncloud/android/issues/4079 + https://github.com/owncloud/android/pull/4129 + +* Enhancement - Support "per app" language change on Android 13+: [#4082](https://github.com/owncloud/android/issues/4082) + + The locales_config.xml file has been created for the application to detect the language that + the user wishes to choose. + + https://github.com/owncloud/android/issues/4082 + https://github.com/owncloud/android/pull/4099 + +* Enhancement - Align Sharing icons with other platforms: [#4101](https://github.com/owncloud/android/issues/4101) + + The share icon has been changed on the screens where it appears to be synchronized with other + platforms. + + https://github.com/owncloud/android/issues/4101 + https://github.com/owncloud/android/pull/4112 + +Changelog for ownCloud Android Client [4.0.0] (2023-05-29) +======================================= +The following sections list the changes in ownCloud Android Client 4.0.0 relevant to +ownCloud admins and users. + +[4.0.0]: https://github.com/owncloud/android/compare/v3.0.4...v4.0.0 + +Summary +------- + +* Security - Make ShareActivity not-exported: [#4038](https://github.com/owncloud/android/pull/4038) * Bugfix - Error message for protocol exception: [#3948](https://github.com/owncloud/android/issues/3948) * Bugfix - Incorrect list of files in av. offline when browsing from details: [#3986](https://github.com/owncloud/android/issues/3986) * Change - Bump target SDK to 33: [#3617](https://github.com/owncloud/android/issues/3617) @@ -29,6 +308,13 @@ Summary Details ------- +* Security - Make ShareActivity not-exported: [#4038](https://github.com/owncloud/android/pull/4038) + + ShareActivity was made not-exported in the manifest since this property is only needed for + those activities that need to be launched from other external apps, which is not the case. + + https://github.com/owncloud/android/pull/4038 + * Bugfix - Error message for protocol exception: [#3948](https://github.com/owncloud/android/issues/3948) Previously, when the network connection is lost while uploading a file, "Unknown error" was diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b77dab94f97..139803fa07b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,7 @@ Before we're able to merge your code into the ownCloud app for Android, please, Please, use the mentioned prefixes because CI system is ready to match with them. Be sure your feature, fix, improvement or technical branches are updated with latest changes in official `android/master`, it will give us a better chance to test your code before merging it with stable code. * Once you are done with your code, start a pull request to merge your contribution into official `android/master`. * Keep on using pull requests for your next contributions although you own write permissions. +* Important to mention that ownCloud Android team uses [GitFlow](https://datasift.github.io/gitflow/IntroducingGitFlow.html) as branching model. Please take a look to the link to understand better what it is and how it works. It's something as useful as easy. [contribution]: https://owncloud.com/contribute/ diff --git a/README.md b/README.md index ed75324c060..53fd4ad6d47 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ |master (Unit tests and data instrumented tests)| ![](https://app.bitrise.io/app/7c4fbbdb2c1c0a20/status.svg?token=t2kBlsAf8d8yZftuohQnTw&branch=master)| | :----- | :------ | |**master (UI tests)**| ![](https://app.bitrise.io/app/a2a0b888408d15d8/status.svg?token=6Fz1YAJL944eJLwmmbkQ9A&branch=master)| -|**stable**| ![](https://app.bitrise.io/app/a2a0b888408d15d8/status.svg?token=6Fz1YAJL944eJLwmmbkQ9A&branch=stable)| + **Start contributing:** Make sure you read [SETUP.md](https://github.com/owncloud/android/blob/master/SETUP.md) when you start working on this project. Basically: Fork this repository and contribute back using pull requests to the master branch. Easy starting points are also reviewing [pull requests](https://github.com/owncloud/android/pulls) and working on [contributions are welcome](https://github.com/owncloud/android/issues?q=is%3Aopen+is%3Aissue+label%3A%22Contributions+are+welcome%22). diff --git a/androidx.databinding_viewbinding_7.4.2@aar b/androidx.databinding_viewbinding_7.4.2@aar new file mode 100644 index 00000000000..e69de29bb2d diff --git a/build.gradle b/build.gradle index 7792634d9f8..6295e58088d 100644 --- a/build.gradle +++ b/build.gradle @@ -2,39 +2,8 @@ buildscript { ext { // SDK sdkCompileVersion = 33 - sdkMinVersion = 21 + sdkMinVersion = 23 sdkTargetVersion = 33 - - // Android jetpack - androidxArchCore = "2.2.0" - androidxLifecycle = "2.5.1" - androidxRoom = "2.5.1" - androidxCore = "1.9.0" - androidxFragment = "1.5.5" - androidxAppcompat = "1.5.1" - androidxLegacy = "1.0.0" - - // Kotlin - orgJetbrainsKotlin = "1.8.10" - orgJetbrainsKotlinx = "1.6.4" - - // Koin - ioInsertKoin = "3.3.3" - - // Markwon - markwon = "4.6.2" - - // Moshi - comSquareupMoshi = '1.14.0' - - // Testing - ioMockk = "1.13.3" - junitVersion = "4.13.2" - androidxTestExt = "1.1.5" - androidxTest = "1.4.0" - androidxTestEspresso = "3.5.1" - androidxTestUiautomator = "2.2.0" - androidxAnnotation = "1.6.0" } repositories { @@ -43,15 +12,15 @@ buildscript { } dependencies { - classpath "com.android.tools.build:gradle:7.4.2" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$orgJetbrainsKotlin" - classpath "org.jlleitschuh.gradle:ktlint-gradle:11.1.0" + classpath libs.android.gradlePlugin + classpath libs.kotlin.gradlePlugin + classpath libs.ktlint.gradlePlugin } } plugins { - id "org.sonarqube" version "4.0.0.2929" - id 'com.google.devtools.ksp' version '1.8.10-1.0.9' apply false + alias libs.plugins.sonarqube + alias libs.plugins.ksp apply false } allprojects { diff --git a/changelog/unreleased/3851 b/changelog/4.0.0_2023-05-29/3851 similarity index 100% rename from changelog/unreleased/3851 rename to changelog/4.0.0_2023-05-29/3851 diff --git a/changelog/unreleased/3930 b/changelog/4.0.0_2023-05-29/3930 similarity index 100% rename from changelog/unreleased/3930 rename to changelog/4.0.0_2023-05-29/3930 diff --git a/changelog/unreleased/3945 b/changelog/4.0.0_2023-05-29/3945 similarity index 100% rename from changelog/unreleased/3945 rename to changelog/4.0.0_2023-05-29/3945 diff --git a/changelog/unreleased/3949 b/changelog/4.0.0_2023-05-29/3949 similarity index 100% rename from changelog/unreleased/3949 rename to changelog/4.0.0_2023-05-29/3949 diff --git a/changelog/unreleased/3973 b/changelog/4.0.0_2023-05-29/3973 similarity index 100% rename from changelog/unreleased/3973 rename to changelog/4.0.0_2023-05-29/3973 diff --git a/changelog/unreleased/3982 b/changelog/4.0.0_2023-05-29/3982 similarity index 100% rename from changelog/unreleased/3982 rename to changelog/4.0.0_2023-05-29/3982 diff --git a/changelog/unreleased/3990 b/changelog/4.0.0_2023-05-29/3990 similarity index 100% rename from changelog/unreleased/3990 rename to changelog/4.0.0_2023-05-29/3990 diff --git a/changelog/unreleased/4000 b/changelog/4.0.0_2023-05-29/4000 similarity index 100% rename from changelog/unreleased/4000 rename to changelog/4.0.0_2023-05-29/4000 diff --git a/changelog/unreleased/4001 b/changelog/4.0.0_2023-05-29/4001 similarity index 100% rename from changelog/unreleased/4001 rename to changelog/4.0.0_2023-05-29/4001 diff --git a/changelog/unreleased/4011 b/changelog/4.0.0_2023-05-29/4011 similarity index 100% rename from changelog/unreleased/4011 rename to changelog/4.0.0_2023-05-29/4011 diff --git a/changelog/unreleased/4013 b/changelog/4.0.0_2023-05-29/4013 similarity index 100% rename from changelog/unreleased/4013 rename to changelog/4.0.0_2023-05-29/4013 diff --git a/changelog/unreleased/4014 b/changelog/4.0.0_2023-05-29/4014 similarity index 100% rename from changelog/unreleased/4014 rename to changelog/4.0.0_2023-05-29/4014 diff --git a/changelog/unreleased/4017 b/changelog/4.0.0_2023-05-29/4017 similarity index 100% rename from changelog/unreleased/4017 rename to changelog/4.0.0_2023-05-29/4017 diff --git a/changelog/unreleased/4021 b/changelog/4.0.0_2023-05-29/4021 similarity index 100% rename from changelog/unreleased/4021 rename to changelog/4.0.0_2023-05-29/4021 diff --git a/changelog/unreleased/4023 b/changelog/4.0.0_2023-05-29/4023 similarity index 100% rename from changelog/unreleased/4023 rename to changelog/4.0.0_2023-05-29/4023 diff --git a/changelog/unreleased/4026 b/changelog/4.0.0_2023-05-29/4026 similarity index 100% rename from changelog/unreleased/4026 rename to changelog/4.0.0_2023-05-29/4026 diff --git a/changelog/unreleased/4032 b/changelog/4.0.0_2023-05-29/4032 similarity index 100% rename from changelog/unreleased/4032 rename to changelog/4.0.0_2023-05-29/4032 diff --git a/changelog/4.0.0_2023-05-29/4038 b/changelog/4.0.0_2023-05-29/4038 new file mode 100644 index 00000000000..8f18510da70 --- /dev/null +++ b/changelog/4.0.0_2023-05-29/4038 @@ -0,0 +1,7 @@ +Security: Make ShareActivity not-exported + +ShareActivity was made not-exported in the manifest since this property is only +needed for those activities that need to be launched from other external apps, which +is not the case. + +https://github.com/owncloud/android/pull/4038 diff --git a/changelog/4.1.0_2023-08-23/4035 b/changelog/4.1.0_2023-08-23/4035 new file mode 100644 index 00000000000..6151699e693 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4035 @@ -0,0 +1,6 @@ +Change: Gradle Version Catalog + +Introduces the Gradle Version Catalog to manage the dependencies in a scalable way. +Now, all the dependencies are declared inside toml file. + +https://github.com/owncloud/android/pull/4035 diff --git a/changelog/4.1.0_2023-08-23/4036 b/changelog/4.1.0_2023-08-23/4036 new file mode 100644 index 00000000000..5dce06738e8 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4036 @@ -0,0 +1,8 @@ +Change: Upgrade min SDK to Android 6 (API 23) + +The minimum SDK has been updated to API 23, which means that the minimum +version of Android we'll support from now on is Android 6 Marshmallow. + +https://github.com/owncloud/android/issues/3245 +https://github.com/owncloud/android/pull/4036 +https://github.com/owncloud/android-library/pull/566 diff --git a/changelog/4.1.0_2023-08-23/4039 b/changelog/4.1.0_2023-08-23/4039 new file mode 100644 index 00000000000..3182ba645f4 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4039 @@ -0,0 +1,7 @@ +Change: Move file menu options filter to use case + +The old class where the menu options for a file or group or files were filtered +has been replaced by a new use case which fits in the architecture of the app. + +https://github.com/owncloud/android/issues/4009 +https://github.com/owncloud/android/pull/4039 diff --git a/changelog/4.1.0_2023-08-23/4040 b/changelog/4.1.0_2023-08-23/4040 new file mode 100644 index 00000000000..007d6ae2eca --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4040 @@ -0,0 +1,6 @@ +Enhancement: File name conflict starting by (1) + +File conflicts now are named with suffix starting in (1) instead of (2). + +https://github.com/owncloud/android/pull/4040 +https://github.com/owncloud/android/issues/3946 diff --git a/changelog/4.1.0_2023-08-23/4058 b/changelog/4.1.0_2023-08-23/4058 new file mode 100644 index 00000000000..18c98f38c53 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4058 @@ -0,0 +1,8 @@ +Enhancement: Added "Open in web" options to main file list + +"Open in web" dynamic options (depending on the providers available) are now shown +in the main file list as well, when selecting one single file which has providers +to open it in web. + +https://github.com/owncloud/android/issues/3860 +https://github.com/owncloud/android/pull/4058 diff --git a/changelog/4.1.0_2023-08-23/4062 b/changelog/4.1.0_2023-08-23/4062 new file mode 100644 index 00000000000..3a83f66c1fe --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4062 @@ -0,0 +1,7 @@ +Enhancement: Copy/move conflict solved by users + +A pop-up is displayed in case there is a name conflict with the files been moved or copied. +The pop-up has the options to Skip, Replace and Keep both, to be consistent with the web client. + +https://github.com/owncloud/android/issues/3935 +https://github.com/owncloud/android/pull/4062 diff --git a/changelog/4.1.0_2023-08-23/4064 b/changelog/4.1.0_2023-08-23/4064 new file mode 100644 index 00000000000..a7ef93331bf --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4064 @@ -0,0 +1,6 @@ +Change: Remove "ignore" from the debug flavour Android manifest + +A `tools:ignore` property from the Android manifest specific for the debug flavour +was removed as it is not needed anymore. + +https://github.com/owncloud/android/pull/4064 diff --git a/changelog/4.1.0_2023-08-23/4076 b/changelog/4.1.0_2023-08-23/4076 new file mode 100644 index 00000000000..b8aaf479beb --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4076 @@ -0,0 +1,8 @@ +Enhancement: Show "More" button for every file list item + +A 3-dot button has been added to every file, where the options that we have +in the 3-dot menu in multiselection for that single file have been added for a +quicker access to them. Also, some options have been reordered. + +https://github.com/owncloud/android/issues/2885 +https://github.com/owncloud/android/pull/4076 diff --git a/changelog/4.1.0_2023-08-23/4084 b/changelog/4.1.0_2023-08-23/4084 new file mode 100644 index 00000000000..c84ac614fc9 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4084 @@ -0,0 +1,6 @@ +Bugfix: Spaces' thumbnails not loaded the first time + +Changing our own lazy image loading with coil library in spaces and file list. + +https://github.com/owncloud/android/issues/3959 +https://github.com/owncloud/android/pull/4084 diff --git a/changelog/4.1.0_2023-08-23/4087 b/changelog/4.1.0_2023-08-23/4087 new file mode 100644 index 00000000000..3b96933dcea --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4087 @@ -0,0 +1,6 @@ +Enhancement: Force security if not protected + +A new branding parameter was created to enforce security protection in the app if device protection is not enabled. + +https://github.com/owncloud/android/issues/4061 +https://github.com/owncloud/android/pull/4087 diff --git a/changelog/4.1.0_2023-08-23/4089 b/changelog/4.1.0_2023-08-23/4089 new file mode 100644 index 00000000000..17eb324ebde --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4089 @@ -0,0 +1,6 @@ +Enhancement: Improve grid mode + +Grid mode has been improved to show bigger thumbnails in images files. + +https://github.com/owncloud/android/issues/4027 +https://github.com/owncloud/android/pull/4089 diff --git a/changelog/4.1.0_2023-08-23/4091 b/changelog/4.1.0_2023-08-23/4091 new file mode 100644 index 00000000000..9fa42ecdf92 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4091 @@ -0,0 +1,6 @@ +Change: Added new unit tests for providers + +Implementation of tests for the functions within ScopedStorageProvider and OCSharedPreferencesProvider. + +https://github.com/owncloud/android/issues/4073 +https://github.com/owncloud/android/pull/4091 diff --git a/changelog/4.1.0_2023-08-23/4092 b/changelog/4.1.0_2023-08-23/4092 new file mode 100644 index 00000000000..c87f5ea0a28 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4092 @@ -0,0 +1,6 @@ +Change: New detail screen file design + +the detail view ha been improved. It added new properties like last sync, status icon on thumbnail, path and creation date + +https://github.com/owncloud/android/pull/4098 +https://github.com/owncloud/android/issues/4092 diff --git a/changelog/4.1.0_2023-08-23/4093 b/changelog/4.1.0_2023-08-23/4093 new file mode 100644 index 00000000000..ecdb3ff209a --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4093 @@ -0,0 +1,8 @@ +Bugfix: Menu option unset av. offline shown when shouldn't + +Unset available offline menu option is not shown in files inside an available +offline folder anymore, because content inside an available offline folder +cannot be changed its status, only if the folder changes it. + +https://github.com/owncloud/android/issues/4077 +https://github.com/owncloud/android/pull/4093 diff --git a/changelog/4.1.0_2023-08-23/4097 b/changelog/4.1.0_2023-08-23/4097 new file mode 100644 index 00000000000..3235d0908a7 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4097 @@ -0,0 +1,6 @@ +Enhancement: Improve UX of creation dialog + +Creation dialog now shows an error message and disables the confirmation button when forbidden characters are typed + +https://github.com/owncloud/android/issues/4031 +https://github.com/owncloud/android/pull/4097 diff --git a/changelog/4.1.0_2023-08-23/4099 b/changelog/4.1.0_2023-08-23/4099 new file mode 100644 index 00000000000..92c9b4c34a8 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4099 @@ -0,0 +1,6 @@ +Enhancement: Support "per app" language change on Android 13+ + +The locales_config.xml file has been created for the application to detect the language that the user wishes to choose. + +https://github.com/owncloud/android/issues/4082 +https://github.com/owncloud/android/pull/4099 diff --git a/changelog/4.1.0_2023-08-23/4106 b/changelog/4.1.0_2023-08-23/4106 new file mode 100644 index 00000000000..673852c4777 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4106 @@ -0,0 +1,7 @@ +Change: Not opening browser automatically in login + +When there is a fixed bearer auth server URL via a branded parameter, the login screen won't redirect +automatically to the browser so that some problems in the authentication flow are solved. + +https://github.com/owncloud/android/issues/4067 +https://github.com/owncloud/android/pull/4106 diff --git a/changelog/4.1.0_2023-08-23/4110 b/changelog/4.1.0_2023-08-23/4110 new file mode 100644 index 00000000000..6fa55b21cb1 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4110 @@ -0,0 +1,6 @@ +Enhancement: Prevent http traffic with branding options + +Adding branding option for prevent http traffic. + +https://github.com/owncloud/android/issues/4066 +https://github.com/owncloud/android/pull/4110 \ No newline at end of file diff --git a/changelog/4.1.0_2023-08-23/4112 b/changelog/4.1.0_2023-08-23/4112 new file mode 100644 index 00000000000..63c2b483326 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4112 @@ -0,0 +1,6 @@ +Enhancement: Align Sharing icons with other platforms + +The share icon has been changed on the screens where it appears to be synchronized with other platforms. + +https://github.com/owncloud/android/issues/4101 +https://github.com/owncloud/android/pull/4112 diff --git a/changelog/4.1.0_2023-08-23/4113 b/changelog/4.1.0_2023-08-23/4113 new file mode 100644 index 00000000000..e91b92287f0 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4113 @@ -0,0 +1,6 @@ +Enhancement: Respect app_providers_appsUrl value from capabilities + +Now, the app receives the app_providers_appsUrl from the local database. Before of this issue, the value was hardcoded. + +https://github.com/owncloud/android/issues/4075 +https://github.com/owncloud/android/pull/4113 diff --git a/changelog/4.1.0_2023-08-23/4122 b/changelog/4.1.0_2023-08-23/4122 new file mode 100644 index 00000000000..cc918b83578 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4122 @@ -0,0 +1,6 @@ +Bugfix: List of accounts empty after removing all accounts and adding new ones + +Now, the account list is shown when User opens the app and was added a new account. + +https://github.com/owncloud/android/issues/4114 +https://github.com/owncloud/android/pull/4122 diff --git a/changelog/4.1.0_2023-08-23/4123 b/changelog/4.1.0_2023-08-23/4123 new file mode 100644 index 00000000000..8563f587224 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4123 @@ -0,0 +1,6 @@ +Enhancement: Unit tests for datasources classes - Part 2 + +Unit tests of the OCLocalFileDataSource and OCRemoteFileDataSource classes have been done. + +https://github.com/owncloud/android/issues/4071 +https://github.com/owncloud/android/pull/4123 \ No newline at end of file diff --git a/changelog/4.1.0_2023-08-23/4127 b/changelog/4.1.0_2023-08-23/4127 new file mode 100644 index 00000000000..f8a296dcc29 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4127 @@ -0,0 +1,7 @@ +Bugfix: Bad error message when copying/moving with server down + +Right now, when we are trying to copy a file to another folder and the server is downwe receive a correct message. +Before the issue the message shown code from the application. + +https://github.com/owncloud/android/issues/4044 +https://github.com/owncloud/android/pull/4127 diff --git a/changelog/4.1.0_2023-08-23/4129 b/changelog/4.1.0_2023-08-23/4129 new file mode 100644 index 00000000000..7b52d37f790 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4129 @@ -0,0 +1,10 @@ +Enhancement: Apply (1) to uploads' name conflicts + +When new files were uploaded manually to pC, shared from a 3rd party app or text shared with oC +name conflict happens, (2) was added to the file name instead of (1). + +Right now if we upload a file with a repeated name, the new file name will end with (1). + + +https://github.com/owncloud/android/issues/4079 +https://github.com/owncloud/android/pull/4129 diff --git a/changelog/4.1.0_2023-08-23/4131 b/changelog/4.1.0_2023-08-23/4131 new file mode 100644 index 00000000000..05e3dfbafcd --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4131 @@ -0,0 +1,7 @@ +Bugfix: unnecessary or wrong call + +Removed added path when checking path existence. + +https://github.com/owncloud/android/issues/4074 +https://github.com/owncloud/android/pull/4131 +https://github.com/owncloud/android-library/pull/578 diff --git a/changelog/4.1.0_2023-08-23/4132 b/changelog/4.1.0_2023-08-23/4132 new file mode 100644 index 00000000000..60a41a29f01 --- /dev/null +++ b/changelog/4.1.0_2023-08-23/4132 @@ -0,0 +1,8 @@ +Bugfix: Crash when the token is expired + + +Now when the token expires and we switch +from grid to list mode on the main screen the app doesn't crash. + +https://github.com/owncloud/android/issues/4116 +https://github.com/owncloud/android/pull/4132 diff --git a/changelog/unreleased/4138 b/changelog/unreleased/4138 new file mode 100644 index 00000000000..bdfd816ab3a --- /dev/null +++ b/changelog/unreleased/4138 @@ -0,0 +1,6 @@ +Enhancement: "Apply to all" when many name conflicts arise + +A new dialog has been created where a checkbox has been added to be able to select all the folders or files that have conflicts. + +https://github.com/owncloud/android/issues/4078 +https://github.com/owncloud/android/pull/4138 \ No newline at end of file diff --git a/changelog/unreleased/4170 b/changelog/unreleased/4170 new file mode 100644 index 00000000000..e7ad28c99a7 --- /dev/null +++ b/changelog/unreleased/4170 @@ -0,0 +1,6 @@ +Bugfix: Some Null Pointer Exceptions avoided + +in the detail screen, in the main file list ViewModel and in the OCFile repository the app has been prevented from crashing when a null is found. + +https://github.com/owncloud/android/issues/4158 +https://github.com/owncloud/android/pull/4170 diff --git a/dependencies.txt b/dependencies.txt new file mode 100644 index 00000000000..5bdf68c0c55 --- /dev/null +++ b/dependencies.txt @@ -0,0 +1,12 @@ + +Welcome to Gradle 7.5! + +Here are the highlights of this release: + - Support for Java 18 + - Support for building with Groovy 4 + - Much more responsive continuous builds + - Improved diagnostics for dependency resolution + +For more details see https://docs.gradle.org/7.5/release-notes.html + +Starting a Gradle Daemon (subsequent builds will be faster) diff --git a/docs_resources/detail_view_device.png b/docs_resources/detail_view_device.png new file mode 100644 index 00000000000..2278023bd68 Binary files /dev/null and b/docs_resources/detail_view_device.png differ diff --git a/docs_resources/filelist_device.png b/docs_resources/filelist_device.png index 55943a5924d..03a011e34e1 100644 Binary files a/docs_resources/filelist_device.png and b/docs_resources/filelist_device.png differ diff --git a/docs_resources/photos_device.png b/docs_resources/photos_device.png index dcb01b79225..4f9ace2ddfc 100644 Binary files a/docs_resources/photos_device.png and b/docs_resources/photos_device.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 3c385dfd5dc..398460dcc31 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index c1cf09119e6..dabab662942 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index dc68b5e4107..3ab4bc544a8 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index a460da82f4e..13bb67cf66e 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png index 02b58965ed8..fdc481cff78 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png index b5a62faa75e..bc3009aa2a0 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png index d3f2d0fc539..37bb67c97db 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png index b3dd3eada08..8ef93ebd72e 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000000..c71b46e72d1 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,123 @@ +[versions] +androidGradlePlugin = "7.4.2" +androidxActivity = "1.6.1" +androidxAnnotation = "1.6.0" +androidxAppCompat = "1.5.1" +androidxArch = "2.2.0" +androidxBiometric = "1.1.0" +androidxBrowser = "1.5.0" +androidxContraintLayout = "2.1.4" +androidxCore = "1.10.1" +androidxEnterpriseFeedback = "1.1.0" +androidxEspresso = "3.5.1" +androidxFragment = "1.5.7" +androidxLegacy = "1.0.0" +androidxLifecycle = "2.5.1" +androidxLifecycleExtensions = "2.2.0" +androidxRoom = "2.5.1" +androidxSqlite = "2.3.1" +androidxTest = "1.4.0" +androidxTestExt = "1.1.5" +androidxTestMonitor = "1.6.1" +androidxTestUiAutomator ="2.2.0" +androidxWork = "2.8.1" +coil = "2.2.2" +dexopener = "2.0.5" +disklrucache = "2.0.2" +exoplayer ="2.16.1" +floatingactionbutton = "1.10.1" +glide = "4.15.1" +glideToVectorYou = "v2.0.0" +junit4 = "4.13.2" +koin = "3.3.3" +kotlin = "1.8.10" +kotlinxCoroutines = "1.6.4" +ksp = "1.8.10-1.0.9" +ktlint = "11.1.0" +markwon = "4.6.2" +material = "1.8.0" +mockk = "1.13.3" +moshi = "1.15.0" +patternlockview = "a90b0d4bf0" +photoView = "2.3.0" +preference = "1.2.0" +sonarqube = "4.0.0.2929" +stetho = "1.6.0" + +[libraries] +androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidxAnnotation" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } +androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidxArch" } +androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidxBiometric" } +androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidxContraintLayout" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } +androidx-enterprise-feedback = { group = "androidx.enterprise", name = "enterprise-feedback", version.ref = "androidxEnterpriseFeedback" } +androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidxFragment" } +androidx-fragment-testing = { group = "androidx.fragment", name = "fragment-testing", version.ref = "androidxFragment" } +androidx-legacy-support = { group = "androidx.legacy", name = "legacy-support-v4", version.ref = "androidxLegacy" } +androidx-lifecycle-common-java8 = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "androidxLifecycle" } +androidx-lifecycle-extensions = { group = "androidx.lifecycle", name = "lifecycle-extensions", version.ref = "androidxLifecycleExtensions" } +androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "androidxLifecycle" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } +androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference" } +androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidxRoom" } +androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" } +androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "androidxRoom" } +androidx-sqlite-ktx = { group = "androidx.sqlite", name = "sqlite-ktx", version.ref = "androidxSqlite" } +androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTest" } +androidx-test-espresso-contrib = { group = "androidx.test.espresso", name = "espresso-contrib", version.ref = "androidxEspresso" } +androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" } +androidx-test-espresso-intents = { group = "androidx.test.espresso", name = "espresso-intents", version.ref = "androidxEspresso" } +androidx-test-espresso-web = { group = "androidx.test.espresso", name = "espresso-web", version.ref = "androidxEspresso" } +androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxTestExt" } +androidx-test-monitor = { group = "androidx.test", name = "monitor", version.ref = "androidxTestMonitor" } +androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTest" } +androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTest" } +androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxTestUiAutomator" } +androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" } +coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } +dexopener = { group = "com.github.tmurakami", name = "dexopener", version.ref = "dexopener" } +disklrucache = { group = "com.jakewharton", name = "disklrucache", version.ref = "disklrucache" } +exoplayer = { group = "com.google.android.exoplayer", name = "exoplayer", version.ref = "exoplayer" } +floatingactionbutton = { group = "com.getbase", name = "floatingactionbutton", version.ref = "floatingactionbutton" } +glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } +glide-vector = { group = "com.github.2coffees1team", name = "GlideToVectorYou", version.ref = "glideToVectorYou" } +junit4 = { group = "junit", name = "junit", version.ref = "junit4" } +koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } +koin-androidx-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" } +koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } +kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } +markwon-core = { group = "io.noties.markwon", name = "core", version.ref = "markwon" } +markwon-ext-strikethrough = { group = "io.noties.markwon", name = "ext-strikethrough", version.ref = "markwon" } +markwon-ext-tables = { group = "io.noties.markwon", name = "ext-tables", version.ref = "markwon" } +markwon-ext-tasklist = { group = "io.noties.markwon", name = "ext-tasklist", version.ref = "markwon" } +markwon-html = { group = "io.noties.markwon", name = "html", version.ref = "markwon" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } +moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" } +moshi-kotlin-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" } +patternlockview = { group = "com.github.aritraroy.PatternLockView", name = "patternlockview", version.ref = "patternlockview" } +photoview = { group = "com.github.chrisbanes", name = "PhotoView", version.ref = "photoView" } +stetho = { group = "com.facebook.stetho", name = "stetho", version.ref = "stetho" } + +# Dependencies of the included build-logic +android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } +kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } +ktlint-gradlePlugin = { group = "org.jlleitschuh.gradle", name = "ktlint-gradle", version.ref = "ktlint" } + +[bundles] +espresso = ["androidx-test-espresso-contrib", "androidx-test-espresso-core", "androidx-test-espresso-intents", "androidx-test-espresso-web"] +markwon = ["markwon-core", "markwon-ext-tables", "markwon-ext-strikethrough", "markwon-ext-tasklist", "markwon-html"] + +[plugins] +kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" } diff --git a/owncloud-android-library b/owncloud-android-library index ab3a594e5c2..c82d75b1d8a 160000 --- a/owncloud-android-library +++ b/owncloud-android-library @@ -1 +1 @@ -Subproject commit ab3a594e5c2ecd0d39781ebe0fae5cde2876bff3 +Subproject commit c82d75b1d8a987b924c1bdea51fb228a6d134956 diff --git a/owncloudApp/build.gradle b/owncloudApp/build.gradle index a97b3fc16f1..3e61d9d2882 100644 --- a/owncloudApp/build.gradle +++ b/owncloudApp/build.gradle @@ -12,83 +12,75 @@ dependencies { implementation project(':owncloudData') // Kotlin - implementation "org.jetbrains.kotlin:kotlin-stdlib:$orgJetbrainsKotlin" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$orgJetbrainsKotlinx" + implementation libs.kotlin.stdlib + implementation libs.kotlinx.coroutines.core // Android X - implementation "androidx.annotation:annotation:1.6.0" - implementation "androidx.appcompat:appcompat:$androidxAppcompat" - implementation "androidx.biometric:biometric:1.1.0" - implementation "androidx.constraintlayout:constraintlayout:2.1.4" - implementation "androidx.legacy:legacy-support-v4:$androidxLegacy" - implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycle" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:$androidxLifecycle" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidxLifecycle" - implementation "androidx.lifecycle:lifecycle-common-java8:$androidxLifecycle" - implementation "androidx.preference:preference-ktx:1.2.0" - implementation "androidx.room:room-runtime:$androidxRoom" - implementation "androidx.sqlite:sqlite-ktx:2.3.1" - implementation "androidx.work:work-runtime-ktx:2.8.1" - implementation("androidx.browser:browser:1.5.0") { because "CustomTabs required for OAuth2 and OIDC" } - implementation("androidx.enterprise:enterprise-feedback:1.1.0") { because "MDM feedback" } - - // KTX extensions, see https://developer.android.com/kotlin/ktx.html - implementation "androidx.core:core-ktx:$androidxCore" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidxLifecycle" - implementation "androidx.fragment:fragment-ktx:$androidxFragment" + implementation libs.androidx.annotation + implementation libs.androidx.appcompat + implementation libs.androidx.biometric + implementation libs.androidx.constraintlayout + implementation libs.androidx.core.ktx + implementation libs.androidx.fragment.ktx + implementation libs.androidx.legacy.support + implementation libs.androidx.lifecycle.common.java8 + implementation libs.androidx.lifecycle.extensions + implementation libs.androidx.lifecycle.livedata.ktx + implementation libs.androidx.lifecycle.runtime.ktx + implementation libs.androidx.lifecycle.viewmodel.ktx + implementation libs.androidx.preference.ktx + implementation libs.androidx.room.runtime + implementation libs.androidx.sqlite.ktx + implementation libs.androidx.work.runtime.ktx + implementation(libs.androidx.browser) { because "CustomTabs required for OAuth2 and OIDC" } + implementation(libs.androidx.enterprise.feedback) { because "MDM feedback" } // Image loading - implementation "com.github.bumptech.glide:glide:4.15.1" - implementation "com.github.2coffees1team:GlideToVectorYou:v2.0.0" + implementation libs.coil + implementation libs.glide + implementation libs.glide.vector // Zooming Android ImageView. - implementation "com.github.chrisbanes:PhotoView:2.3.0" + implementation libs.photoview // Koin dependency injector - implementation "io.insert-koin:koin-core:$ioInsertKoin" - implementation "io.insert-koin:koin-androidx-workmanager:$ioInsertKoin" + implementation libs.koin.androidx.workmanager + implementation libs.koin.core // Miscellaneous - implementation "com.getbase:floatingactionbutton:1.10.1" - implementation "com.github.aritraroy.PatternLockView:patternlockview:a90b0d4bf0" - implementation "com.google.android.exoplayer:exoplayer:2.16.1" - implementation "com.google.android.material:material:1.8.0" - implementation "com.jakewharton:disklrucache:2.0.2" + implementation libs.disklrucache + implementation libs.exoplayer + implementation libs.floatingactionbutton + implementation libs.material + implementation libs.patternlockview // Markdown Preview - implementation "io.noties.markwon:core:$markwon" - implementation "io.noties.markwon:ext-tables:$markwon" - implementation "io.noties.markwon:ext-strikethrough:$markwon" - implementation "io.noties.markwon:ext-tasklist:$markwon" - implementation "io.noties.markwon:html:$markwon" + implementation libs.bundles.markwon // Tests testImplementation project(":owncloudTestUtil") - testImplementation "androidx.arch.core:core-testing:$androidxArchCore" - testImplementation "io.mockk:mockk:$ioMockk" - testImplementation "junit:junit:$junitVersion" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$orgJetbrainsKotlinx" + testImplementation libs.androidx.arch.core.testing + testImplementation libs.junit4 + testImplementation libs.kotlinx.coroutines.test + testImplementation libs.mockk // Instrumented tests androidTestImplementation project(":owncloudTestUtil") - androidTestImplementation "androidx.annotation:annotation:$androidxAnnotation" - androidTestImplementation "androidx.arch.core:core-testing:2.2.0" - androidTestImplementation "androidx.test.espresso:espresso-contrib:$androidxTestEspresso" - androidTestImplementation "androidx.test.espresso:espresso-core:$androidxTestEspresso" - androidTestImplementation "androidx.test.espresso:espresso-intents:$androidxTestEspresso" - androidTestImplementation "androidx.test.espresso:espresso-web:$androidxTestEspresso" - androidTestImplementation "androidx.test.ext:junit:$androidxTestExt" - androidTestImplementation "androidx.test.uiautomator:uiautomator:$androidxTestUiautomator" - androidTestImplementation "androidx.test:core:$androidxTest" - androidTestImplementation "androidx.test:rules:$androidxTest" - androidTestImplementation "androidx.test:runner:$androidxTest" - androidTestImplementation "com.github.tmurakami:dexopener:2.0.5" - androidTestImplementation("io.mockk:mockk-android:$ioMockk") { exclude module: "objenesis" } - debugImplementation "androidx.test:monitor:1.6.1" - - debugImplementation "androidx.fragment:fragment-testing:$androidxFragment" - debugImplementation "com.facebook.stetho:stetho:1.6.0" + androidTestImplementation libs.androidx.annotation + androidTestImplementation libs.androidx.arch.core.testing + androidTestImplementation libs.androidx.test.core + androidTestImplementation libs.androidx.test.ext.junit + androidTestImplementation libs.androidx.test.rules + androidTestImplementation libs.androidx.test.runner + androidTestImplementation libs.androidx.test.uiautomator + androidTestImplementation libs.bundles.espresso + androidTestImplementation libs.dexopener + androidTestImplementation(libs.mockk.android) { exclude module: "objenesis" } + + // Debug + debugImplementation libs.androidx.fragment.testing + debugImplementation libs.androidx.test.monitor + debugImplementation libs.stetho } android { @@ -100,8 +92,8 @@ android { testInstrumentationRunner "com.owncloud.android.utils.OCTestAndroidJUnitRunner" - versionCode = 30000404 - versionName = "4.0-beta.4" + versionCode = 41000000 + versionName = "4.1.0" buildConfigField "String", gitRemote, "\"" + getGitOriginRemote() + "\"" buildConfigField "String", commitSHA1, "\"" + getLatestGitHash() + "\"" diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt index 17adfc8aacc..88638726835 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt @@ -219,8 +219,6 @@ class LoginActivityTest { showCenteredRefreshButton = true, showEmbeddedCheckServerButton = false ) - - verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) } } @Test @@ -233,7 +231,7 @@ class LoginActivityTest { R.id.centeredRefreshButton.isDisplayed(true) R.id.centeredRefreshButton.scrollAndClick() - verify(exactly = 2) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) } + verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) } serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC))) R.id.centeredRefreshButton.isDisplayed(false) diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/files/details/FileDetailsFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/files/details/FileDetailsFragmentTest.kt new file mode 100644 index 00000000000..4ca930508eb --- /dev/null +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/files/details/FileDetailsFragmentTest.kt @@ -0,0 +1,214 @@ +package com.owncloud.android.files.details + +import android.content.Context +import androidx.test.core.app.ActivityScenario.launch +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withId +import com.owncloud.android.R +import com.owncloud.android.domain.files.model.OCFileWithSyncInfo +import com.owncloud.android.presentation.files.details.FileDetailsFragment +import com.owncloud.android.presentation.files.details.FileDetailsViewModel +import com.owncloud.android.presentation.files.operations.FileOperationsViewModel +import com.owncloud.android.sharing.shares.ui.TestShareFileActivity +import com.owncloud.android.testutil.OC_ACCOUNT +import com.owncloud.android.testutil.OC_FILE +import com.owncloud.android.testutil.OC_FILE_OC_AVAILABLE_OFFLINE_FILE +import com.owncloud.android.testutil.OC_FILE_WITH_SYNC_INFO +import com.owncloud.android.testutil.OC_FILE_WITH_SYNC_INFO_AND_SPACE +import com.owncloud.android.testutil.OC_FILE_WITH_SYNC_INFO_AND_WITHOUT_PERSONAL_SPACE +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.matchers.assertVisibility +import com.owncloud.android.utils.matchers.isDisplayed +import com.owncloud.android.utils.matchers.withDrawable +import com.owncloud.android.utils.matchers.withText +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Test +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module + +class FileDetailsFragmentTest { + + private lateinit var fileDetailsViewModel: FileDetailsViewModel + private lateinit var fileOperationsViewModel: FileOperationsViewModel + private lateinit var context: Context + + private var currentFile: MutableStateFlow = MutableStateFlow(OC_FILE_WITH_SYNC_INFO_AND_SPACE) + private var currentFileWithoutPersonalSpace: MutableStateFlow = + MutableStateFlow(OC_FILE_WITH_SYNC_INFO_AND_WITHOUT_PERSONAL_SPACE) + private var currentFileSyncInfo: MutableStateFlow = MutableStateFlow(OC_FILE_WITH_SYNC_INFO) + private var currentFileAvailableOffline: MutableStateFlow = MutableStateFlow(OC_FILE_OC_AVAILABLE_OFFLINE_FILE) + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + fileDetailsViewModel = mockk(relaxed = true) + fileOperationsViewModel = mockk(relaxed = true) + every { fileDetailsViewModel.currentFile } returns currentFile + stopKoin() + + startKoin { + context + allowOverride(override = true) + modules( + module { + viewModel { + fileDetailsViewModel + } + viewModel { + fileOperationsViewModel + } + } + ) + + } + + val fileDetailsFragment = FileDetailsFragment.newInstance( + OC_FILE, + OC_ACCOUNT, + syncFileAtOpen = false + ) + + launch(TestShareFileActivity::class.java).onActivity { + it.startFragment(fileDetailsFragment) + } + } + + @Test + fun display_visibility_of_detail_view_when_it_is_displayed() { + assertViewsDisplayed() + } + + @Test + fun show_space_personal_when_it_has_value() { + R.id.fdSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE) + R.id.fdSpaceLabel.assertVisibility(ViewMatchers.Visibility.VISIBLE) + R.id.fdIconSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE) + + R.id.fdSpace.withText(R.string.bottom_nav_personal) + R.id.fdSpaceLabel.withText(R.string.space_label) + onView(withId(R.id.fdIconSpace)) + .check(matches(withDrawable(R.drawable.ic_spaces))) + } + + @Test + fun hide_space_when_it_has_no_value() { + every { fileDetailsViewModel.currentFile } returns currentFileSyncInfo + + R.id.fdSpace.assertVisibility(ViewMatchers.Visibility.GONE) + R.id.fdSpaceLabel.assertVisibility(ViewMatchers.Visibility.GONE) + R.id.fdIconSpace.assertVisibility(ViewMatchers.Visibility.GONE) + } + + @Test + fun show_space_not_personal_when_it_has_value() { + every { fileDetailsViewModel.currentFile } returns currentFileWithoutPersonalSpace + + R.id.fdSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE) + R.id.fdSpaceLabel.assertVisibility(ViewMatchers.Visibility.VISIBLE) + R.id.fdIconSpace.assertVisibility(ViewMatchers.Visibility.VISIBLE) + + R.id.fdSpace.withText(currentFileWithoutPersonalSpace.value?.space?.name.toString()) + R.id.fdSpaceLabel.withText(R.string.space_label) + onView(withId(R.id.fdIconSpace)) + .check(matches(withDrawable(R.drawable.ic_spaces))) + } + + @Test + fun show_last_sync_when_it_has_value() { + currentFile.value?.file?.lastSyncDateForData = 1212121212212 + R.id.fdLastSync.assertVisibility(ViewMatchers.Visibility.VISIBLE) + R.id.fdLastSyncLabel.assertVisibility(ViewMatchers.Visibility.VISIBLE) + + R.id.fdLastSyncLabel.withText(R.string.filedetails_last_sync) + R.id.fdLastSync.withText(DisplayUtils.unixTimeToHumanReadable(currentFile.value?.file?.lastSyncDateForData!!)) + } + + @Test + fun hide_last_sync_when_it_has_no_value() { + every { fileDetailsViewModel.currentFile } returns currentFile + + R.id.fdLastSync.assertVisibility(ViewMatchers.Visibility.GONE) + R.id.fdLastSyncLabel.assertVisibility(ViewMatchers.Visibility.GONE) + } + + @Test + fun verifyTests() { + R.id.fdCreatedLabel.withText(R.string.filedetails_created) + R.id.fdCreated.withText(DisplayUtils.unixTimeToHumanReadable(currentFile.value?.file?.creationTimestamp!!)) + + R.id.fdModifiedLabel.withText(R.string.filedetails_modified) + R.id.fdModified.withText(DisplayUtils.unixTimeToHumanReadable(currentFile.value?.file?.modificationTimestamp!!)) + + R.id.fdPathLabel.withText(R.string.ssl_validator_label_L) + R.id.fdPath.withText(currentFile.value?.file?.getParentRemotePath()!!) + + R.id.fdname.withText(currentFile.value?.file?.fileName!!) + } + + @Test + fun badge_available_offline_in_image_is_not_viewed_when_file_does_not_change_state() { + every { fileDetailsViewModel.currentFile } returns currentFileAvailableOffline + + R.id.badgeDetailFile.assertVisibility(ViewMatchers.Visibility.VISIBLE) + onView(withId(R.id.badgeDetailFile)) + .check(matches(withDrawable(R.drawable.offline_available_pin))) + + } + + @Test + fun show_badge_isAvailableLocally_in_image_when_file_change_state() { + currentFile.value?.file?.etagInConflict = "error" + + R.id.badgeDetailFile.assertVisibility(ViewMatchers.Visibility.VISIBLE) + onView(withId(R.id.badgeDetailFile)) + .check(matches(withDrawable(R.drawable.error_pin))) + } + + private fun assertViewsDisplayed( + showImage: Boolean = true, + showFdName: Boolean = true, + showFdProgressText: Boolean = false, + showFdProgressBar: Boolean = false, + showFdCancelBtn: Boolean = false, + showDivider: Boolean = true, + showDivider2: Boolean = true, + showFdTypeLabel: Boolean = true, + showFdType: Boolean = true, + showFdSizeLabel: Boolean = true, + showFdSize: Boolean = true, + showFdModifiedLabel: Boolean = true, + showFdModified: Boolean = true, + showFdCreatedLabel: Boolean = true, + showFdCreated: Boolean = true, + showDivider3: Boolean = true, + showFdPathLabel: Boolean = true, + showFdPath: Boolean = true + ) { + R.id.fdImageDetailFile.isDisplayed(displayed = showImage) + R.id.fdname.isDisplayed(displayed = showFdName) + R.id.fdProgressText.isDisplayed(displayed = showFdProgressText) + R.id.fdProgressBar.isDisplayed(displayed = showFdProgressBar) + R.id.fdCancelBtn.isDisplayed(displayed = showFdCancelBtn) + R.id.divider.isDisplayed(displayed = showDivider) + R.id.fdTypeLabel.isDisplayed(displayed = showFdTypeLabel) + R.id.fdType.isDisplayed(displayed = showFdType) + R.id.fdSizeLabel.isDisplayed(displayed = showFdSizeLabel) + R.id.fdSize.isDisplayed(displayed = showFdSize) + R.id.divider2.isDisplayed(displayed = showDivider2) + R.id.fdModifiedLabel.isDisplayed(displayed = showFdModifiedLabel) + R.id.fdModified.isDisplayed(displayed = showFdModified) + R.id.fdCreatedLabel.isDisplayed(displayed = showFdCreatedLabel) + R.id.fdCreated.isDisplayed(displayed = showFdCreated) + R.id.divider3.isDisplayed(displayed = showDivider3) + R.id.fdPathLabel.isDisplayed(displayed = showFdPathLabel) + R.id.fdPath.isDisplayed(displayed = showFdPath) + } +} \ No newline at end of file diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/TestShareFileActivity.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/TestShareFileActivity.kt index ee29471cb72..f4c4ae95864 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/TestShareFileActivity.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/TestShareFileActivity.kt @@ -26,9 +26,12 @@ import com.owncloud.android.R import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.sharing.shares.model.OCShare import com.owncloud.android.presentation.sharing.ShareFragmentListener +import com.owncloud.android.services.OperationsService import com.owncloud.android.testing.SingleFragmentActivity +import com.owncloud.android.ui.fragment.FileFragment.ContainerActivity +import com.owncloud.android.ui.helpers.FileOperationsHelper -class TestShareFileActivity : SingleFragmentActivity(), ShareFragmentListener { +class TestShareFileActivity : SingleFragmentActivity(), ShareFragmentListener, ContainerActivity { fun startFragment(fragment: Fragment) { supportFragmentManager.commit(allowStateLoss = true) { add(R.id.container, fragment, TEST_FRAGMENT_TAG) @@ -70,4 +73,15 @@ class TestShareFileActivity : SingleFragmentActivity(), ShareFragmentListener { companion object { private const val TEST_FRAGMENT_TAG = "TEST FRAGMENT" } + + override fun getOperationsServiceBinder(): OperationsService.OperationsServiceBinder { + TODO("Not yet implemented") + } + + override fun getFileOperationsHelper(): FileOperationsHelper { + TODO("Not yet implemented") + } + + override fun showDetails(file: OCFile?) { + } } diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/ui/activity/ReleaseNotesActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/ui/activity/ReleaseNotesActivityTest.kt index 192fe47067d..203ae672e11 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/ui/activity/ReleaseNotesActivityTest.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/ui/activity/ReleaseNotesActivityTest.kt @@ -25,13 +25,14 @@ import android.content.Intent import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import com.owncloud.android.R +import com.owncloud.android.presentation.releasenotes.ReleaseNote +import com.owncloud.android.presentation.releasenotes.ReleaseNoteType import com.owncloud.android.presentation.releasenotes.ReleaseNotesActivity import com.owncloud.android.presentation.releasenotes.ReleaseNotesViewModel import com.owncloud.android.utils.click import com.owncloud.android.utils.matchers.assertChildCount import com.owncloud.android.utils.matchers.isDisplayed import com.owncloud.android.utils.matchers.withText -import com.owncloud.android.utils.releaseNotesList import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertEquals @@ -48,6 +49,23 @@ class ReleaseNotesActivityTest { private lateinit var releaseNotesViewModel: ReleaseNotesViewModel + private val releaseNotesList = listOf( + ReleaseNote( + title = R.string.release_notes_4_1_title_1, + subtitle = R.string.release_notes_4_1_subtitle_1, + type = ReleaseNoteType.ENHANCEMENT, + ), + ReleaseNote( + title = R.string.release_notes_4_1_title_2, + subtitle = R.string.release_notes_4_1_subtitle_2, + type = ReleaseNoteType.ENHANCEMENT, + ), + ReleaseNote( + title = R.string.release_notes_4_1_title_3, + subtitle = R.string.release_notes_4_1_subtitle_3, + type = ReleaseNoteType.ENHANCEMENT, + ), + ) @Before fun setUp() { context = ApplicationProvider.getApplicationContext() @@ -109,6 +127,6 @@ class ReleaseNotesActivityTest { @Test fun test_childCount() { - R.id.releaseNotes.assertChildCount(3) + R.id.releaseNotes.assertChildCount(releaseNotesList.size) } } diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ReleaseNotesList.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ReleaseNotesList.kt index 16a2cde32a7..dec200fba1e 100644 --- a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ReleaseNotesList.kt +++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ReleaseNotesList.kt @@ -25,18 +25,38 @@ import com.owncloud.android.presentation.releasenotes.ReleaseNoteType val releaseNotesList = listOf( ReleaseNote( - title = R.string.release_notes_header, - subtitle = R.string.release_notes_footer, - type = ReleaseNoteType.BUGFIX + title = R.string.release_notes_4_1_title_1, + subtitle = R.string.release_notes_4_1_subtitle_1, + type = ReleaseNoteType.ENHANCEMENT, ), ReleaseNote( - title = R.string.release_notes_header, - subtitle = R.string.release_notes_footer, - type = ReleaseNoteType.BUGFIX + title = R.string.release_notes_4_1_title_2, + subtitle = R.string.release_notes_4_1_subtitle_2, + type = ReleaseNoteType.ENHANCEMENT, ), ReleaseNote( - title = R.string.release_notes_header, - subtitle = R.string.release_notes_footer, - type = ReleaseNoteType.ENHANCEMENT - ) + title = R.string.release_notes_4_1_title_3, + subtitle = R.string.release_notes_4_1_subtitle_3, + type = ReleaseNoteType.ENHANCEMENT, + ), + ReleaseNote( + title = R.string.release_notes_4_1_title_4, + subtitle = R.string.release_notes_4_1_subtitle_4, + type = ReleaseNoteType.BUGFIX, + ), + ReleaseNote( + title = R.string.release_notes_4_1_title_5, + subtitle = R.string.release_notes_4_1_subtitle_5, + type = ReleaseNoteType.ENHANCEMENT, + ), + ReleaseNote( + title = R.string.release_notes_4_1_title_6, + subtitle = R.string.release_notes_4_1_subtitle_6, + type = ReleaseNoteType.ENHANCEMENT, + ), + ReleaseNote( + title = R.string.release_notes_4_1_title_7, + subtitle = R.string.release_notes_4_1_subtitle_7, + type = ReleaseNoteType.BUGFIX, + ), ) diff --git a/owncloudApp/src/debug/AndroidManifest.xml b/owncloudApp/src/debug/AndroidManifest.xml index 8c6c35ca099..4f378cd5eb9 100644 --- a/owncloudApp/src/debug/AndroidManifest.xml +++ b/owncloudApp/src/debug/AndroidManifest.xml @@ -14,12 +14,11 @@ ~ limitations under the License. --> - + - - + + diff --git a/owncloudApp/src/main/AndroidManifest.xml b/owncloudApp/src/main/AndroidManifest.xml index a98b3b1389b..c9e586193e2 100644 --- a/owncloudApp/src/main/AndroidManifest.xml +++ b/owncloudApp/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ ownCloud Android client application Copyright (C) 2012 Bartek Przybylski - Copyright (C) 2016 ownCloud GmbH. + Copyright (C) 2023 ownCloud GmbH. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2, @@ -19,23 +19,7 @@ - - - - - - @@ -45,7 +29,7 @@ --> @@ -63,12 +47,14 @@ android:allowBackup="false" android:icon="@mipmap/icon" android:label="@string/app_name" + android:localeConfig="@xml/locales_config" android:manageSpaceActivity="com.owncloud.android.ui.activity.ManageSpaceActivity" android:networkSecurityConfig="@xml/network_security_config" android:preserveLegacyExternalStorage="true" android:requestLegacyExternalStorage="true" android:resizeableActivity="true" android:supportsPictureInPicture="false" + android:taskAffinity="" android:theme="@style/Theme.ownCloud.Toolbar"> + android:exported="false"> diff --git a/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt b/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt index cda286d545a..1705e6a6fc2 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt @@ -1,4 +1,4 @@ -/* +/** * ownCloud Android client application * * @author masensio @@ -7,7 +7,8 @@ * @author Christian Schabesberger * @author David Crespo Ríos * @author Juan Carlos Garrote Gascón - * Copyright (C) 2022 ownCloud GmbH. + * + * Copyright (C) 2023 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -21,6 +22,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package com.owncloud.android import android.app.Activity @@ -32,9 +34,11 @@ import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.view.WindowManager +import android.widget.CheckBox +import androidx.appcompat.app.AlertDialog import androidx.core.content.pm.PackageInfoCompat import com.owncloud.android.presentation.authentication.AccountUtils -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.db.PreferenceManager import com.owncloud.android.dependecyinjection.commonModule @@ -43,6 +47,9 @@ import com.owncloud.android.dependecyinjection.remoteDataSourceModule import com.owncloud.android.dependecyinjection.repositoryModule import com.owncloud.android.dependecyinjection.useCaseModule import com.owncloud.android.dependecyinjection.viewModelModule +import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.usecases.GetPersonalSpaceForAccountUseCase import com.owncloud.android.extensions.createNotificationChannel import com.owncloud.android.lib.common.SingleSessionManager import com.owncloud.android.presentation.migration.StorageMigrationActivity @@ -54,8 +61,10 @@ import com.owncloud.android.presentation.security.pattern.PatternManager import com.owncloud.android.presentation.security.passcode.PassCodeActivity import com.owncloud.android.presentation.security.passcode.PassCodeManager import com.owncloud.android.presentation.settings.logging.SettingsLogsFragment.Companion.PREFERENCE_ENABLE_LOGGING +import com.owncloud.android.providers.CoroutinesDispatcherProvider import com.owncloud.android.providers.LogsProvider import com.owncloud.android.providers.MdmProvider +import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.activity.WhatsNewActivity import com.owncloud.android.utils.CONFIGURATION_ALLOW_SCREENSHOTS import com.owncloud.android.utils.DOWNLOAD_NOTIFICATION_CHANNEL_ID @@ -64,6 +73,10 @@ import com.owncloud.android.utils.FILE_SYNC_CONFLICT_NOTIFICATION_CHANNEL_ID import com.owncloud.android.utils.FILE_SYNC_NOTIFICATION_CHANNEL_ID import com.owncloud.android.utils.MEDIA_SERVICE_NOTIFICATION_CHANNEL_ID import com.owncloud.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.koin.android.ext.android.inject import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin import org.koin.core.context.stopKoin @@ -118,6 +131,27 @@ class MainApp : Application() { } else { ReleaseNotesActivity.runIfNeeded(activity) + + val pref = PreferenceManager.getDefaultSharedPreferences(appContext) + val dontShowAgainDialogPref = pref.getBoolean(PREFERENCE_KEY_DONT_SHOW_OCIS_ACCOUNT_WARNING_DIALOG, false) + if (!dontShowAgainDialogPref && shouldShowDialog(activity)) { + val checkboxDialog = activity.layoutInflater.inflate(R.layout.checkbox_dialog, null) + val checkbox = checkboxDialog.findViewById(R.id.checkbox_dialog) + checkbox.setText(R.string.ocis_accounts_warning_checkbox_message) + val builder = AlertDialog.Builder(activity).apply { + setView(checkboxDialog) + setTitle(R.string.ocis_accounts_warning_title) + setMessage(R.string.ocis_accounts_warning_message) + setCancelable(false) + setPositiveButton(R.string.ocis_accounts_warning_button) { _, _ -> + if (checkbox.isChecked) { + pref.edit().putBoolean(PREFERENCE_KEY_DONT_SHOW_OCIS_ACCOUNT_WARNING_DIALOG, true).apply() + } + } + } + val alertDialog = builder.create() + alertDialog.show() + } } } @@ -125,13 +159,41 @@ class MainApp : Application() { PreferenceManager.deleteOldSettingsPreferences(applicationContext) } + private fun shouldShowDialog(activity: Activity) = + runBlocking(CoroutinesDispatcherProvider().io) { + if (activity !is FileDisplayActivity) return@runBlocking false + val account = AccountUtils.getCurrentOwnCloudAccount(appContext) ?: return@runBlocking false + + val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase by inject() + val capabilities = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) { + getStoredCapabilitiesUseCase.execute( + GetStoredCapabilitiesUseCase.Params( + accountName = account.name + ) + ) + } + val spacesAllowed = capabilities != null && capabilities.isSpacesAllowed() + + var personalSpace: OCSpace? = null + if (spacesAllowed) { + val getPersonalSpaceForAccountUseCase: GetPersonalSpaceForAccountUseCase by inject() + personalSpace = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) { + getPersonalSpaceForAccountUseCase.execute( + GetPersonalSpaceForAccountUseCase.Params( + accountName = account.name + ) + ) + } + } + + spacesAllowed && personalSpace == null + } + override fun onActivityStarted(activity: Activity) { Timber.v("${activity.javaClass.simpleName} onStart() starting") PassCodeManager.onActivityStarted(activity) PatternManager.onActivityStarted(activity) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - BiometricManager.onActivityStarted(activity) - } + BiometricManager.onActivityStarted(activity) } override fun onActivityResumed(activity: Activity) { @@ -146,9 +208,7 @@ class MainApp : Application() { Timber.v("${activity.javaClass.simpleName} onStop() ending") PassCodeManager.onActivityStopped(activity) PatternManager.onActivityStopped(activity) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - BiometricManager.onActivityStopped(activity) - } + BiometricManager.onActivityStopped(activity) } override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { @@ -248,6 +308,8 @@ class MainApp : Application() { const val PREFERENCE_KEY_LAST_SEEN_VERSION_CODE = "lastSeenVersionCode" + const val PREFERENCE_KEY_DONT_SHOW_OCIS_ACCOUNT_WARNING_DIALOG = "PREFERENCE_KEY_DONT_SHOW_OCIS_ACCOUNT_WARNING_DIALOG" + /** * Next methods give access in code to some constants that need to be defined in string resources to be referred * in AndroidManifest.xml file or other xml resource files; or that need to be easy to modify in build time. diff --git a/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt b/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt index 595bbc7eff7..08fbe8a3815 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt @@ -26,8 +26,6 @@ package com.owncloud.android.datamodel import android.accounts.Account -import com.owncloud.android.domain.capabilities.model.OCCapability -import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PATH import com.owncloud.android.domain.files.usecases.GetFileByIdUseCase @@ -36,8 +34,6 @@ import com.owncloud.android.domain.files.usecases.GetFolderContentUseCase import com.owncloud.android.domain.files.usecases.GetFolderImagesUseCase import com.owncloud.android.domain.files.usecases.GetPersonalRootFolderForAccountUseCase import com.owncloud.android.domain.files.usecases.GetSharesRootFolderForAccount -import com.owncloud.android.domain.spaces.model.OCSpace -import com.owncloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAccountUseCase import com.owncloud.android.providers.CoroutinesDispatcherProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.runBlocking @@ -124,26 +120,4 @@ class FileDataStorageManager( }.getDataOrNull() result ?: listOf() } - - fun getCapability(accountName: String): OCCapability? = runBlocking(CoroutinesDispatcherProvider().io) { - val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase by inject() - - val capability = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) { - getStoredCapabilitiesUseCase.execute(GetStoredCapabilitiesUseCase.Params(accountName)) - } - capability - } - - fun getSpace(spaceId: String?, accountName: String): OCSpace? = runBlocking(CoroutinesDispatcherProvider().io) { - if (spaceId == null) return@runBlocking null - val getSpaceWithSpecialsByIdForAccountUseCase: GetSpaceWithSpecialsByIdForAccountUseCase by inject() - - val space = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) { - getSpaceWithSpecialsByIdForAccountUseCase.execute(GetSpaceWithSpecialsByIdForAccountUseCase.Params( - spaceId = spaceId, - accountName = accountName, - )) - } - return@runBlocking space - } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt index 69ec8d2b6f6..e3cc9f7eb18 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt @@ -36,14 +36,14 @@ import com.owncloud.android.data.files.datasources.LocalFileDataSource import com.owncloud.android.data.files.datasources.implementation.OCLocalFileDataSource import com.owncloud.android.data.folderbackup.datasources.FolderBackupLocalDataSource import com.owncloud.android.data.folderbackup.datasources.implementation.OCFolderBackupLocalDataSource -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.data.sharing.shares.datasources.LocalShareDataSource import com.owncloud.android.data.sharing.shares.datasources.implementation.OCLocalShareDataSource import com.owncloud.android.data.spaces.datasources.LocalSpacesDataSource import com.owncloud.android.data.spaces.datasources.implementation.OCLocalSpacesDataSource -import com.owncloud.android.data.storage.LocalStorageProvider -import com.owncloud.android.data.storage.ScopedStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider +import com.owncloud.android.data.providers.ScopedStorageProvider import com.owncloud.android.data.transfers.datasources.LocalTransferDataSource import com.owncloud.android.data.transfers.datasources.implementation.OCLocalTransferDataSource import com.owncloud.android.data.user.datasources.LocalUserDataSource diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt index f06323e4859..ee741111e89 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt @@ -51,7 +51,7 @@ import com.owncloud.android.domain.webfinger.WebFingerRepository import org.koin.dsl.module val repositoryModule = module { - factory { OCAppRegistryRepository(get(), get()) } + factory { OCAppRegistryRepository(get(), get(), get()) } factory { OCAuthenticationRepository(get(), get()) } factory { OCCapabilityRepository(get(), get(), get()) } factory { OCFileRepository(get(), get(), get(), get()) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index dcfb6dd37e0..7bfe12549f1 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -73,6 +73,7 @@ import com.owncloud.android.domain.files.usecases.SaveFileOrFolderUseCase import com.owncloud.android.domain.files.usecases.SortFilesUseCase import com.owncloud.android.domain.files.usecases.SortFilesWithSyncInfoUseCase import com.owncloud.android.domain.files.usecases.UpdateAlreadyDownloadedFilesPathUseCase +import com.owncloud.android.domain.files.usecases.GetFileWithSyncInfoByIdUseCase import com.owncloud.android.domain.server.usecases.GetServerInfoAsyncUseCase import com.owncloud.android.domain.sharing.sharees.GetShareesAsyncUseCase import com.owncloud.android.domain.sharing.shares.usecases.CreatePrivateShareAsyncUseCase @@ -85,6 +86,7 @@ import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUs import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase +import com.owncloud.android.domain.spaces.usecases.GetPersonalSpaceForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase import com.owncloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAccountUseCase import com.owncloud.android.domain.spaces.usecases.GetSpacesFromEveryAccountUseCaseAsStream @@ -101,6 +103,7 @@ import com.owncloud.android.domain.user.usecases.RefreshUserQuotaFromServerAsync import com.owncloud.android.domain.webfinger.usecases.GetOwnCloudInstanceFromWebFingerUseCase import com.owncloud.android.domain.webfinger.usecases.GetOwnCloudInstancesFromAuthenticatedWebFingerUseCase import com.owncloud.android.usecases.accounts.RemoveAccountUseCase +import com.owncloud.android.usecases.files.FilterFileMenuOptionsUseCase import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase import com.owncloud.android.usecases.synchronization.SynchronizeFolderUseCase import com.owncloud.android.usecases.transfers.downloads.CancelDownloadForFileUseCase @@ -169,6 +172,8 @@ val useCaseModule = module { factory { CleanConflictUseCase(get()) } factory { SaveDownloadWorkerUUIDUseCase(get()) } factory { CleanWorkersUUIDUseCase(get()) } + factory { FilterFileMenuOptionsUseCase(get(), get(), get()) } + factory { GetFileWithSyncInfoByIdUseCase(get()) } // Open in web factory { GetUrlToOpenInWebUseCase(get(), get()) } @@ -196,6 +201,7 @@ val useCaseModule = module { // Spaces factory { GetSpacesFromEveryAccountUseCaseAsStream(get()) } + factory { GetPersonalSpaceForAccountUseCase(get()) } factory { GetPersonalAndProjectSpacesForAccountUseCase(get()) } factory { GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase(get()) } factory { GetProjectSpacesWithSpecialsForAccountAsStreamUseCase(get()) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index b9a49d3561a..d56393871b9 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -39,6 +39,9 @@ import com.owncloud.android.presentation.files.filelist.MainFileListViewModel import com.owncloud.android.presentation.files.operations.FileOperationsViewModel import com.owncloud.android.presentation.logging.LogListViewModel import com.owncloud.android.presentation.migration.MigrationViewModel +import com.owncloud.android.presentation.previews.PreviewAudioViewModel +import com.owncloud.android.presentation.previews.PreviewTextViewModel +import com.owncloud.android.presentation.previews.PreviewVideoViewModel import com.owncloud.android.presentation.releasenotes.ReleaseNotesViewModel import com.owncloud.android.presentation.security.biometric.BiometricViewModel import com.owncloud.android.presentation.security.passcode.PassCodeViewModel @@ -75,7 +78,7 @@ val viewModelModule = module { PassCodeViewModel(get(), get(), action) } - viewModel { AuthenticationViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { AuthenticationViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } viewModel { OAuthViewModel(get(), get(), get(), get()) } viewModel { SettingsViewModel(get()) } viewModel { SettingsSecurityViewModel(get(), get()) } @@ -90,9 +93,12 @@ val viewModelModule = module { viewModel { PatternViewModel(get()) } viewModel { BiometricViewModel(get(), get()) } viewModel { ReleaseNotesViewModel(get(), get()) } - viewModel { FileDetailsViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { FileDetailsViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } - viewModel { PreviewImageViewModel(get(), get(), get()) } + viewModel { PreviewImageViewModel(get(), get(), get(), get(), get()) } + viewModel { PreviewAudioViewModel(get(), get(), get()) } + viewModel { PreviewTextViewModel(get(), get(), get()) } + viewModel { PreviewVideoViewModel(get(), get(), get()) } viewModel { FileOperationsViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } viewModel { (initialFolderToDisplay: OCFile, fileListOption: FileListOption) -> MainFileListViewModel( @@ -108,6 +114,9 @@ val viewModelModule = module { get(), get(), get(), + get(), + get(), + get(), initialFolderToDisplay, fileListOption, ) diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt index e5171ef72a3..8396154e4c9 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt @@ -39,7 +39,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentActivity import com.google.android.material.snackbar.Snackbar import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.lib.common.network.WebdavUtils import com.owncloud.android.presentation.common.ShareSheetHelper @@ -50,14 +50,18 @@ import com.owncloud.android.presentation.security.SecurityEnforced import com.owncloud.android.presentation.security.biometric.BiometricActivity import com.owncloud.android.presentation.security.biometric.BiometricStatus import com.owncloud.android.presentation.security.biometric.EnableBiometrics +import com.owncloud.android.presentation.security.isDeviceSecure import com.owncloud.android.presentation.security.passcode.PassCodeActivity import com.owncloud.android.presentation.security.pattern.PatternActivity import com.owncloud.android.presentation.settings.privacypolicy.PrivacyPolicyActivity import com.owncloud.android.presentation.settings.security.SettingsSecurityFragment.Companion.EXTRAS_LOCK_ENFORCED +import com.owncloud.android.providers.MdmProvider import com.owncloud.android.ui.activity.FileDisplayActivity.Companion.ALL_FILES_SAF_REGEX import com.owncloud.android.ui.dialog.ShareLinkToDialog +import com.owncloud.android.utils.CONFIGURATION_DEVICE_PROTECTION import com.owncloud.android.utils.MimetypeIconUtil import com.owncloud.android.utils.UriUtilsKt.getExposedFileUriForOCFile +import org.koin.android.ext.android.inject import timber.log.Timber import java.io.File @@ -284,37 +288,32 @@ fun Activity.hideSoftKeyboard() { fun Activity.checkPasscodeEnforced(securityEnforced: SecurityEnforced) { val sharedPreferencesProvider = OCSharedPreferencesProvider(this) + val mdmProvider by inject() + // If device protection is false, launch the previous behaviour (check the lockEnforced). + // If device protection is true, ask for security only if device is not secure. + val showDeviceProtectionForced: Boolean = + mdmProvider.getBrandingBoolean(CONFIGURATION_DEVICE_PROTECTION, R.bool.device_protection) && !isDeviceSecure() val lockEnforced: Int = this.resources.getInteger(R.integer.lock_enforced) val passcodeConfigured = sharedPreferencesProvider.getBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false) val patternConfigured = sharedPreferencesProvider.getBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false) when (parseFromInteger(lockEnforced)) { - LockEnforcedType.DISABLED -> {} - LockEnforcedType.EITHER_ENFORCED -> { - if (!passcodeConfigured && !patternConfigured) { - val options = arrayOf(getString(R.string.security_enforced_first_option), getString(R.string.security_enforced_second_option)) - var optionSelected = 0 - - AlertDialog.Builder(this) - .setCancelable(false) - .setTitle(getString(R.string.security_enforced_title)) - .setSingleChoiceItems(options, LockType.PASSCODE.ordinal) { _, which -> optionSelected = which } - .setPositiveButton(android.R.string.ok) { dialog, _ -> - when (LockType.parseFromInteger(optionSelected)) { - LockType.PASSCODE -> securityEnforced.optionLockSelected(LockType.PASSCODE) - LockType.PATTERN -> securityEnforced.optionLockSelected(LockType.PATTERN) - } - dialog.dismiss() - } - .show() + LockEnforcedType.DISABLED -> { + if (showDeviceProtectionForced) { + showSelectSecurityDialog(passcodeConfigured, patternConfigured, securityEnforced) } } + LockEnforcedType.EITHER_ENFORCED -> { + showSelectSecurityDialog(passcodeConfigured, patternConfigured, securityEnforced) + } + LockEnforcedType.PASSCODE_ENFORCED -> { if (!passcodeConfigured) { manageOptionLockSelected(LockType.PASSCODE) } } + LockEnforcedType.PATTERN_ENFORCED -> { if (!patternConfigured) { manageOptionLockSelected(LockType.PATTERN) @@ -323,6 +322,30 @@ fun Activity.checkPasscodeEnforced(securityEnforced: SecurityEnforced) { } } +private fun Activity.showSelectSecurityDialog( + passcodeConfigured: Boolean, + patternConfigured: Boolean, + securityEnforced: SecurityEnforced +) { + if (!passcodeConfigured && !patternConfigured) { + val options = arrayOf(getString(R.string.security_enforced_first_option), getString(R.string.security_enforced_second_option)) + var optionSelected = 0 + + AlertDialog.Builder(this) + .setCancelable(false) + .setTitle(getString(R.string.security_enforced_title)) + .setSingleChoiceItems(options, LockType.PASSCODE.ordinal) { _, which -> optionSelected = which } + .setPositiveButton(android.R.string.ok) { dialog, _ -> + when (LockType.parseFromInteger(optionSelected)) { + LockType.PASSCODE -> securityEnforced.optionLockSelected(LockType.PASSCODE) + LockType.PATTERN -> securityEnforced.optionLockSelected(LockType.PATTERN) + } + dialog.dismiss() + } + .show() + } +} + fun Activity.manageOptionLockSelected(type: LockType) { OCSharedPreferencesProvider(this).let { @@ -344,6 +367,7 @@ fun Activity.manageOptionLockSelected(type: LockType) { flags = FLAG_ACTIVITY_NO_HISTORY putExtra(EXTRAS_LOCK_ENFORCED, true) }) + LockType.PATTERN -> startActivity(Intent(this, PatternActivity::class.java).apply { action = PatternActivity.ACTION_REQUEST_WITH_RESULT flags = FLAG_ACTIVITY_NO_HISTORY diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FileMenuOptionExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FileMenuOptionExt.kt new file mode 100644 index 00000000000..7a6e5bdc1a1 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FileMenuOptionExt.kt @@ -0,0 +1,81 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.extensions + +import com.owncloud.android.R +import com.owncloud.android.domain.files.model.FileMenuOption + +fun FileMenuOption.toResId() = + when (this) { + FileMenuOption.SELECT_ALL -> R.id.file_action_select_all + FileMenuOption.SELECT_INVERSE -> R.id.action_select_inverse + FileMenuOption.DOWNLOAD -> R.id.action_download_file + FileMenuOption.RENAME -> R.id.action_rename_file + FileMenuOption.MOVE -> R.id.action_move + FileMenuOption.COPY -> R.id.action_copy + FileMenuOption.REMOVE -> R.id.action_remove_file + FileMenuOption.OPEN_WITH -> R.id.action_open_file_with + FileMenuOption.SYNC -> R.id.action_sync_file + FileMenuOption.CANCEL_SYNC -> R.id.action_cancel_sync + FileMenuOption.SHARE -> R.id.action_share_file + FileMenuOption.DETAILS -> R.id.action_see_details + FileMenuOption.SEND -> R.id.action_send_file + FileMenuOption.SET_AV_OFFLINE -> R.id.action_set_available_offline + FileMenuOption.UNSET_AV_OFFLINE -> R.id.action_unset_available_offline + } + +fun FileMenuOption.toStringResId() = + when (this) { + FileMenuOption.SELECT_ALL -> R.string.actionbar_select_all + FileMenuOption.SELECT_INVERSE -> R.string.actionbar_select_inverse + FileMenuOption.DOWNLOAD -> R.string.filedetails_download + FileMenuOption.RENAME -> R.string.common_rename + FileMenuOption.MOVE -> R.string.actionbar_move + FileMenuOption.COPY -> android.R.string.copy + FileMenuOption.REMOVE -> R.string.common_remove + FileMenuOption.OPEN_WITH -> R.string.actionbar_open_with + FileMenuOption.SYNC -> R.string.filedetails_sync_file + FileMenuOption.CANCEL_SYNC -> R.string.common_cancel_sync + FileMenuOption.SHARE -> R.string.action_share + FileMenuOption.DETAILS -> R.string.actionbar_see_details + FileMenuOption.SEND -> R.string.actionbar_send_file + FileMenuOption.SET_AV_OFFLINE -> R.string.set_available_offline + FileMenuOption.UNSET_AV_OFFLINE -> R.string.unset_available_offline + } + +fun FileMenuOption.toDrawableResId() = + when (this) { + FileMenuOption.SELECT_ALL -> R.drawable.ic_select_all + FileMenuOption.SELECT_INVERSE -> R.drawable.ic_select_inverse + FileMenuOption.DOWNLOAD -> R.drawable.ic_action_download + FileMenuOption.RENAME -> R.drawable.ic_pencil + FileMenuOption.MOVE -> R.drawable.ic_action_move + FileMenuOption.COPY -> R.drawable.ic_action_copy + FileMenuOption.REMOVE -> R.drawable.ic_action_delete_white + FileMenuOption.OPEN_WITH -> R.drawable.ic_open_in_app + FileMenuOption.SYNC -> R.drawable.ic_action_refresh + FileMenuOption.CANCEL_SYNC -> R.drawable.ic_action_cancel_white + FileMenuOption.SHARE -> R.drawable.ic_share_generic_white + FileMenuOption.DETAILS -> R.drawable.ic_info_white + FileMenuOption.SEND -> R.drawable.ic_send_white + FileMenuOption.SET_AV_OFFLINE -> R.drawable.ic_action_set_available_offline + FileMenuOption.UNSET_AV_OFFLINE -> R.drawable.ic_action_unset_available_offline + } diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt index bf8a88f8743..14db6e7f9da 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt @@ -2,7 +2,9 @@ * ownCloud Android client application * * @author David González Verdugo - * Copyright (C) 2020 ownCloud GmbH. + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -22,12 +24,16 @@ package com.owncloud.android.extensions import android.app.AlertDialog import android.content.Context import android.content.DialogInterface +import android.view.Menu +import android.view.MenuItem.SHOW_AS_ACTION_NEVER import android.view.inputmethod.InputMethodManager import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.snackbar.Snackbar +import com.owncloud.android.R +import com.owncloud.android.domain.appregistry.model.AppRegistryProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -85,3 +91,22 @@ fun Fragment.collectLatestLifecycleFlow( } } } + +fun Fragment.addOpenInWebMenuOptions( + menu: Menu, + openInWebProviders: Map = emptyMap(), + appRegistryProviders: List? = emptyList(), +): Map { + val newOpenInWebProviders = emptyMap().toMutableMap() + // Remove "open in web" dynamic menu items and add them again to avoid duplications + openInWebProviders.forEach { (_, menuItemId) -> + menu.removeItem(menuItemId) + } + appRegistryProviders?.forEachIndexed { index, appRegistryProvider -> + menu.add(Menu.NONE, index, 0, getString(R.string.ic_action_open_with_web, appRegistryProvider.name)).also { + it.setShowAsAction(SHOW_AS_ACTION_NEVER) + newOpenInWebProviders[appRegistryProvider.name] = it.itemId + } + } + return newOpenInWebProviders +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/MenuExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/MenuExt.kt new file mode 100644 index 00000000000..728cd1e7b6d --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/MenuExt.kt @@ -0,0 +1,51 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.extensions + +import android.view.Menu +import com.owncloud.android.R +import com.owncloud.android.domain.files.model.FileMenuOption + +fun Menu.filterMenuOptions( + optionsToShow: List, + hasWritePermission: Boolean, +) { + FileMenuOption.values().forEach { fileMenuOption -> + val item = this.findItem(fileMenuOption.toResId()) + item?.let { + if (optionsToShow.contains(fileMenuOption)) { + it.isVisible = true + it.isEnabled = true + if (fileMenuOption.toResId() == R.id.action_open_file_with) { + if (!hasWritePermission) { + item.setTitle(R.string.actionbar_open_with_read_only) + } else { + item.setTitle(R.string.actionbar_open_with) + } + } + } else { + it.isVisible = false + it.isEnabled = false + } + } + + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/files/FileMenuFilter.java b/owncloudApp/src/main/java/com/owncloud/android/files/FileMenuFilter.java deleted file mode 100644 index 4f68f486475..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/files/FileMenuFilter.java +++ /dev/null @@ -1,435 +0,0 @@ -/** - * ownCloud Android client application - * - * @author David A. Velasco - * @author Christian Schabesberger - * @author Abel García de Prada - * @author Shashvat Kedia - * @author David Crespo Rios - * @author Juan Carlos Garrote Gascón - * - * Copyright (C) 2023 ownCloud GmbH. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.files; - -import android.accounts.Account; -import android.content.Context; -import android.view.Menu; -import android.view.MenuItem; - -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.work.WorkInfo; -import androidx.work.WorkManager; -import com.google.common.collect.Iterables; -import com.owncloud.android.R; -import com.owncloud.android.domain.availableoffline.model.AvailableOfflineStatus; -import com.owncloud.android.domain.capabilities.model.OCCapability; -import com.owncloud.android.domain.files.model.OCFile; -import com.owncloud.android.domain.files.model.OCFileSyncInfo; -import com.owncloud.android.domain.spaces.model.OCSpace; -import com.owncloud.android.extensions.WorkManagerExtKt; -import com.owncloud.android.ui.activity.ComponentsGetter; -import com.owncloud.android.ui.preview.PreviewVideoFragment; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import static com.owncloud.android.usecases.transfers.TransferConstantsKt.TRANSFER_TAG_DOWNLOAD; - -/** - * Filters out the file actions available in a given {@link Menu} for a given {@link OCFile} - * according to the current state of the latest. - */ -public class FileMenuFilter { - - private static final int SINGLE_SELECT_ITEMS = 1; - private static final String TAG_SECOND_FRAGMENT = "SECOND_FRAGMENT"; - - private List mFiles; - private ComponentsGetter mComponentsGetter; - private Account mAccount; - private Context mContext; - private List mFilesSync; - - /** - * Constructor - * - * @param targetFiles List of {@link OCFile} file targets of the action to filter in the {@link Menu}. - * @param account ownCloud {@link Account} holding targetFile. - * @param cg Accessor to app components, needed to access synchronization services - * @param context Android {@link Context}, needed to access build setup resources. - */ - public FileMenuFilter(List targetFiles, Account account, ComponentsGetter cg, - Context context) { - mFiles = targetFiles; - mAccount = account; - mComponentsGetter = cg; - mContext = context; - mFilesSync = new ArrayList<>(); - } - - /** - * Constructor - * - * @param targetFile {@link OCFile} target of the action to filter in the {@link Menu}. - * @param account ownCloud {@link Account} holding targetFile. - * @param cg Accessor to app components, needed to access synchronization services - * @param context Android {@link Context}, needed to access build setup resources. - */ - public FileMenuFilter(OCFile targetFile, Account account, ComponentsGetter cg, - Context context) { - this(Arrays.asList(targetFile), account, cg, context); - } - - /** - * Constructor - * - * @param targetFiles List of {@link OCFile} file targets of the action to filter in the {@link Menu}. - * @param account ownCloud {@link Account} holding targetFile. - * @param cg Accessor to app components, needed to access synchronization services - * @param context Android {@link Context}, needed to access build setup resources. - * @param filesSync List of {@link OCFileSyncInfo} with info about the sync status of each target file. - */ - public FileMenuFilter(List targetFiles, Account account, ComponentsGetter cg, - Context context, List filesSync) { - this(targetFiles, account, cg, context); - mFilesSync = filesSync; - } - - /** - * Filters out the file actions available in the passed {@link Menu} taken into account - * the state of the {@link OCFile} held by the filter. - * - * @param menu Options or context menu to filter. - */ - public void filter(Menu menu, boolean displaySelectAll, boolean displaySelectInverse, - boolean onlyAvailableOffline, boolean sharedByLinkFiles) { - if (mFiles == null || mFiles.size() <= 0) { - hideAll(menu); - } else { - List toShow = new ArrayList<>(); - List toHide = new ArrayList<>(); - - filter(toShow, toHide, displaySelectAll, displaySelectInverse, onlyAvailableOffline, sharedByLinkFiles); - - boolean hasWritePermission; - if (mFiles.size() == 1) { - hasWritePermission = mFiles.get(0).getHasWritePermission(); - } else { - hasWritePermission = false; - } - MenuItem item; - for (int i : toShow) { - item = menu.findItem(i); - if (item != null) { - item.setVisible(true); - item.setEnabled(true); - if (i == R.id.action_open_file_with) { - if (!hasWritePermission) { - item.setTitle(R.string.actionbar_open_with_read_only); - } else { - item.setTitle(R.string.actionbar_open_with); - } - } - } - } - - for (int i : toHide) { - item = menu.findItem(i); - if (item != null) { - item.setVisible(false); - item.setEnabled(false); - } - } - } - } - - private void hideAll(Menu menu) { - MenuItem item; - for (int i = 0; i < menu.size(); i++) { - item = menu.getItem(i); - item.setVisible(false); - item.setEnabled(false); - } - } - - /** - * Performs the real filtering, to be applied in the {@link Menu} by the caller methods. - *

- * Decides what actions must be shown and hidden. - * - * @param toShow List to save the options that must be shown in the menu. - * @param toHide List to save the options that must be shown in the menu. - */ - - private void filter(List toShow, List toHide, boolean displaySelectAll, - boolean displaySelectInverse, boolean onlyAvailableOffline, boolean sharedByLinkFiles) { - - boolean synchronizing; - if (mFilesSync.isEmpty()) { - synchronizing = anyFileSynchronizingLookingIntoWorkers(); - } else { - synchronizing = anyFileSynchronizingLookingIntoFilesSync(); - } - - boolean videoPreviewing = anyFileVideoPreviewing(); - - boolean videoStreaming = !anyFileDown() && anyFileVideoPreviewing(); - - /// decision is taken for each possible action on a file in the menu - - if (displaySelectAll) { - toShow.add(R.id.file_action_select_all); - } else { - toHide.add(R.id.file_action_select_all); - } - if (displaySelectInverse) { - toShow.add(R.id.action_select_inverse); - } else { - toHide.add(R.id.action_select_inverse); - } - - // DOWNLOAD - if (mFiles.isEmpty() || containsFolder() || anyFileDown() || synchronizing || videoPreviewing || - onlyAvailableOffline || sharedByLinkFiles) { - toHide.add(R.id.action_download_file); - - } else { - toShow.add(R.id.action_download_file); - } - - // RENAME - boolean hasRenamePermission; - if (mFiles.size() == 1) { - hasRenamePermission = mFiles.get(0).getHasRenamePermission(); - } else { - hasRenamePermission = false; - } - if (!isSingleSelection() || synchronizing || videoPreviewing || onlyAvailableOffline || sharedByLinkFiles || !hasRenamePermission) { - toHide.add(R.id.action_rename_file); - } else { - toShow.add(R.id.action_rename_file); - } - - // MOVE - boolean hasMovePermission = Iterables.all(mFiles, OCFile::getHasMovePermission); - if (mFiles.isEmpty() || synchronizing || videoPreviewing || onlyAvailableOffline || sharedByLinkFiles || !hasMovePermission) { - toHide.add(R.id.action_move); - } else { - toShow.add(R.id.action_move); - } - - // COPY - if (mFiles.isEmpty() || synchronizing || videoPreviewing || onlyAvailableOffline || sharedByLinkFiles) { - toHide.add(R.id.action_copy); - } else { - toShow.add(R.id.action_copy); - } - - // REMOVE - boolean hasDeletePermission = Iterables.all(mFiles, OCFile::getHasDeletePermission); - if (mFiles.isEmpty() || synchronizing || onlyAvailableOffline || sharedByLinkFiles || !hasDeletePermission) { - toHide.add(R.id.action_remove_file); - } else { - toShow.add(R.id.action_remove_file); - } - - // OPEN WITH (different to preview!) - if (!isSingleFile() || synchronizing) { - toHide.add(R.id.action_open_file_with); - } else { - toShow.add(R.id.action_open_file_with); - } - - // CANCEL SYNCHRONIZATION - if (mFiles.isEmpty() || !synchronizing || anyFavorite() || onlyAvailableOffline || sharedByLinkFiles) { - toHide.add(R.id.action_cancel_sync); - - } else { - toShow.add(R.id.action_cancel_sync); - } - - // SYNC CONTENTS (BOTH FILE AND FOLDER) - if (mFiles.isEmpty() || (!anyFileDown() && !containsFolder()) || synchronizing || onlyAvailableOffline || sharedByLinkFiles) { - toHide.add(R.id.action_sync_file); - - } else { - toShow.add(R.id.action_sync_file); - } - - // SHARE FILE - boolean shareViaLinkAllowed = (mContext != null && - mContext.getResources().getBoolean(R.bool.share_via_link_feature)); - boolean shareWithUsersAllowed = (mContext != null && - mContext.getResources().getBoolean(R.bool.share_with_users_feature)); - - OCCapability capability = mComponentsGetter.getStorageManager().getCapability(mAccount.name); - - boolean notAllowResharing = anyFileSharedWithMe() && - capability != null && capability.getFilesSharingResharing().isFalse(); - - OCSpace space = mComponentsGetter.getStorageManager().getSpace(mFiles.get(0).getSpaceId(), mAccount.name); - boolean notPersonalSpace = space != null && !space.isPersonal(); - - boolean hasResharePermission; - if (mFiles.size() == 1) { - hasResharePermission = mFiles.get(0).getHasResharePermission(); - } else { - hasResharePermission = false; - } - if ((!shareViaLinkAllowed && !shareWithUsersAllowed) || !isSingleSelection() || - notAllowResharing || onlyAvailableOffline || notPersonalSpace || !hasResharePermission) { - toHide.add(R.id.action_share_file); - } else { - toShow.add(R.id.action_share_file); - } - - // SEE DETAILS - if (!isSingleFile()) { - toHide.add(R.id.action_see_details); - } else { - toShow.add(R.id.action_see_details); - } - - // SEND - boolean sendAllowed = (mContext != null && - mContext.getString(R.string.send_files_to_other_apps).equalsIgnoreCase("on")); - if (containsFolder() || (!areDownloaded() && !isSingleFile()) || !sendAllowed || synchronizing || videoStreaming || onlyAvailableOffline) { - toHide.add(R.id.action_send_file); - } else { - toShow.add(R.id.action_send_file); - } - - // SET AS AVAILABLE OFFLINE - if (synchronizing || !anyUnfavorite() || videoStreaming) { - toHide.add(R.id.action_set_available_offline); - } else { - toShow.add(R.id.action_set_available_offline); - } - - // UNSET AS AVAILABLE OFFLINE - if (!anyFavorite() || videoStreaming) { - toHide.add(R.id.action_unset_available_offline); - } else { - toShow.add(R.id.action_unset_available_offline); - } - - } - - private boolean anyFileSynchronizingLookingIntoWorkers() { - boolean synchronizing = false; - if (!mFiles.isEmpty() && mAccount != null) { - WorkManager workManager = WorkManager.getInstance(mContext); - List workInfos = WorkManagerExtKt.getRunningWorkInfosByTags(workManager, Arrays.asList(TRANSFER_TAG_DOWNLOAD, mAccount.name)); - for (int i = 0; !synchronizing && i < workInfos.size(); i++) { - if (!workInfos.get(i).getState().isFinished()) { - for (int j = 0; !synchronizing && j < mFiles.size(); j++) { - synchronizing = workInfos.get(i).getTags().contains(mFiles.get(j).getId().toString()); - } - } - } - } - return synchronizing; - } - - private boolean anyFileSynchronizingLookingIntoFilesSync() { - boolean synchronizing = false; - if (!mFilesSync.isEmpty()) { - for (int i = 0; !synchronizing && i < mFilesSync.size(); i++) { - synchronizing = mFilesSync.get(i).isSynchronizing(); - } - } - return synchronizing; - } - - private boolean anyFileVideoPreviewing() { - final FragmentActivity activity = (FragmentActivity) mContext; - Fragment secondFragment = activity.getSupportFragmentManager().findFragmentByTag( - TAG_SECOND_FRAGMENT); - boolean videoPreviewing = false; - if (secondFragment instanceof PreviewVideoFragment) { - for (int i = 0; !videoPreviewing && i < mFiles.size(); i++) { - videoPreviewing = ((PreviewVideoFragment) secondFragment). - getFile().equals(mFiles.get(i)); - } - } - return videoPreviewing; - } - - private boolean isSingleSelection() { - return mFiles.size() == SINGLE_SELECT_ITEMS; - } - - private boolean isSingleFile() { - return isSingleSelection() && !mFiles.get(0).isFolder(); - } - - private boolean areDownloaded() { - for (OCFile file : mFiles) { - if (!file.isAvailableLocally()) { - return false; - } - } - return true; - } - - private boolean containsFolder() { - for (OCFile file : mFiles) { - if (file.isFolder()) { - return true; - } - } - return false; - } - - private boolean anyFileDown() { - for (OCFile file : mFiles) { - if (file.isAvailableLocally()) { - return true; - } - } - return false; - } - - private boolean anyFavorite() { - for (OCFile file : mFiles) { - if (file.getAvailableOfflineStatus() == AvailableOfflineStatus.AVAILABLE_OFFLINE) { - return true; - } - } - return false; - } - - private boolean anyUnfavorite() { - for (OCFile file : mFiles) { - if (file.getAvailableOfflineStatus() == AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE) { - return true; - } - } - return false; - } - - private boolean anyFileSharedWithMe() { - for (OCFile file : mFiles) { - if (file.isSharedWithMe()) { - return true; - } - } - return false; - } -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/media/MediaService.java b/owncloudApp/src/main/java/com/owncloud/android/media/MediaService.java index 1dd510ba48d..33bf50e0020 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/media/MediaService.java +++ b/owncloudApp/src/main/java/com/owncloud/android/media/MediaService.java @@ -583,7 +583,7 @@ private void updateNotification(String content) { mNotificationBuilder.setContentIntent(PendingIntent.getActivity(getApplicationContext(), (int) System.currentTimeMillis(), showDetailsIntent, - NotificationUtils.INSTANCE.getPendingIntentFlags())); + NotificationUtils.pendingIntentFlags)); mNotificationBuilder.setWhen(System.currentTimeMillis()); mNotificationBuilder.setTicker(ticker); mNotificationBuilder.setContentTitle(ticker); @@ -618,7 +618,7 @@ private void setUpAsForeground(String content) { mNotificationBuilder.setContentIntent(PendingIntent.getActivity(getApplicationContext(), (int) System.currentTimeMillis(), showDetailsIntent, - NotificationUtils.INSTANCE.getPendingIntentFlags())); + NotificationUtils.pendingIntentFlags)); mNotificationBuilder.setContentTitle(ticker); mNotificationBuilder.setContentText(content); mNotificationBuilder.setChannelId(MEDIA_SERVICE_NOTIFICATION_CHANNEL_ID); diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/AccountsManagementActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/AccountsManagementActivity.kt index 21460238761..dffb9937bb9 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/AccountsManagementActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/AccountsManagementActivity.kt @@ -80,6 +80,11 @@ class AccountsManagementActivity : FileActivity(), AccountsManagementAdapter.Acc displayShowTitleEnabled = true ) + } + + override fun onStart() { + super.onStart() + val accountList = accountsManagementViewModel.getLoggedAccounts() originalAccounts = toAccountNameSet(accountList) originalCurrentAccount = accountsManagementViewModel.getCurrentAccount()?.name.toString() @@ -88,6 +93,7 @@ class AccountsManagementActivity : FileActivity(), AccountsManagementAdapter.Acc account = accountsManagementViewModel.getCurrentAccount() onAccountSet(false) + } /** diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticationViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticationViewModel.kt index c8114fc8ea5..770fa31c70a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticationViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticationViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.owncloud.android.MainApp +import com.owncloud.android.R import com.owncloud.android.domain.authentication.oauth.RegisterClientUseCase import com.owncloud.android.domain.authentication.oauth.RequestTokenUseCase import com.owncloud.android.domain.authentication.oauth.model.ClientRegistrationInfo @@ -45,6 +46,7 @@ import com.owncloud.android.domain.webfinger.usecases.GetOwnCloudInstancesFromAu import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.presentation.authentication.oauth.OAuthUtils import com.owncloud.android.presentation.common.UIResult +import com.owncloud.android.providers.ContextProvider import com.owncloud.android.providers.CoroutinesDispatcherProvider import com.owncloud.android.providers.WorkManagerProvider import kotlinx.coroutines.launch @@ -65,6 +67,7 @@ class AuthenticationViewModel( private val requestTokenUseCase: RequestTokenUseCase, private val registerClientUseCase: RegisterClientUseCase, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, + private val contextProvider: ContextProvider, ) : ViewModel() { val codeVerifier: String = OAuthUtils().generateRandomCodeVerifier() @@ -99,7 +102,11 @@ class AuthenticationViewModel( showLoading = true, liveData = _serverInfo, useCase = getServerInfoAsyncUseCase, - useCaseParams = GetServerInfoAsyncUseCase.Params(serverPath = serverUrl, creatingAccount = creatingAccount) + useCaseParams = GetServerInfoAsyncUseCase.Params( + serverPath = serverUrl, + creatingAccount = creatingAccount, + secureConnectionEnforced = contextProvider.getBoolean(R.bool.enforce_secure_connection), + ) ) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/LoginActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/LoginActivity.kt index 0fbb114091d..8fb58fb4b85 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/LoginActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/LoginActivity.kt @@ -50,6 +50,8 @@ import com.owncloud.android.domain.authentication.oauth.model.ResponseType import com.owncloud.android.domain.authentication.oauth.model.TokenRequest import com.owncloud.android.domain.exceptions.NoNetworkConnectionException import com.owncloud.android.domain.exceptions.OwncloudVersionNotSupportedException +import com.owncloud.android.domain.exceptions.SSLErrorCode +import com.owncloud.android.domain.exceptions.SSLErrorException import com.owncloud.android.domain.exceptions.ServerNotReachableException import com.owncloud.android.domain.exceptions.StateMismatchException import com.owncloud.android.domain.exceptions.UnauthorizedException @@ -272,6 +274,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted text = getString(R.string.error_no_network_connection) setCompoundDrawablesWithIntrinsicBounds(R.drawable.no_network, 0, 0, 0) } + else -> binding.webfingerStatusText.run { text = uiResult.getThrowableOrNull()?.parseError("", resources, true) setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0) @@ -355,6 +358,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted performGetAuthorizationCodeRequest(oidcServerConfiguration.authorizationEndpoint.toUri()) } } + else -> { binding.serverStatusText.run { text = getString(R.string.auth_unsupported_auth_method) @@ -376,17 +380,25 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted private fun getServerInfoIsError(uiResult: UIResult.Error) { updateCenteredRefreshButtonVisibility(shouldBeVisible = true) - when (uiResult.error) { - is CertificateCombinedException -> + when { + uiResult.error is CertificateCombinedException -> showUntrustedCertDialog(uiResult.error) - is OwncloudVersionNotSupportedException -> binding.serverStatusText.run { + + uiResult.error is OwncloudVersionNotSupportedException -> binding.serverStatusText.run { text = getString(R.string.server_not_supported) setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0) } - is NoNetworkConnectionException -> binding.serverStatusText.run { + + uiResult.error is NoNetworkConnectionException -> binding.serverStatusText.run { text = getString(R.string.error_no_network_connection) setCompoundDrawablesWithIntrinsicBounds(R.drawable.no_network, 0, 0, 0) } + + uiResult.error is SSLErrorException && uiResult.error.code == SSLErrorCode.NOT_HTTP_ALLOWED -> binding.serverStatusText.run { + text = getString(R.string.ssl_connection_not_secure) + setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0) + } + else -> binding.serverStatusText.run { text = uiResult.error?.parseError("", resources, true) setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0) @@ -430,6 +442,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted } showOrHideBasicAuthFields(shouldBeVisible = false) } + else -> { binding.serverStatusText.isVisible = false binding.authStatusText.run { @@ -461,6 +474,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted ) } } + is UIResult.Error -> { Timber.e(uiResult.error, "Client registration failed.") performGetAuthorizationCodeRequest(authorizationEndpoint) @@ -585,6 +599,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted clientRegistrationInfo = clientRegistrationInfo ) } + is UIResult.Error -> { Timber.e(uiResult.error, "OAuth request to exchange authorization code for tokens failed") updateOAuthStatusIconAndText(uiResult.error) @@ -703,7 +718,6 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted val url = mdmProvider.getBrandingString(mdmKey = CONFIGURATION_SERVER_URL, stringKey = R.string.server_url) if (url.isNotEmpty()) { binding.hostUrlInput.setText(url) - checkOcServer() } binding.loginLayout.run { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/common/DrawerViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/common/DrawerViewModel.kt index af3af310863..435ca8f8eb1 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/common/DrawerViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/common/DrawerViewModel.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.user.model.UserQuota import com.owncloud.android.domain.user.usecases.GetStoredQuotaUseCase import com.owncloud.android.domain.user.usecases.GetUserQuotasUseCase diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/documentsprovider/DocumentsStorageProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/documentsprovider/DocumentsStorageProvider.kt index e68cbe58771..39aa3f1bd8e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/documentsprovider/DocumentsStorageProvider.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/documentsprovider/DocumentsStorageProvider.kt @@ -38,7 +38,7 @@ import android.provider.DocumentsContract import android.provider.DocumentsProvider import com.owncloud.android.MainApp import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.domain.UseCaseResult import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase import com.owncloud.android.domain.exceptions.NoConnectionWithServerException @@ -389,7 +389,10 @@ class DocumentsStorageProvider : DocumentsProvider() { copyFileUseCase.execute( CopyFileUseCase.Params( - listOfFilesToCopy = listOf(sourceFile), targetFolder = targetParentFile + listOfFilesToCopy = listOf(sourceFile), + targetFolder = targetParentFile, + replace = listOf(false), + isUserLogged = AccountUtils.getCurrentOwnCloudAccount(context) != null, ) ).also { result -> syncRequired = false @@ -416,7 +419,10 @@ class DocumentsStorageProvider : DocumentsProvider() { moveFileUseCase.execute( MoveFileUseCase.Params( - listOfFilesToMove = listOf(sourceFile), targetFolder = targetParentFile + listOfFilesToMove = listOf(sourceFile), + targetFolder = targetParentFile, + replace = listOf(false), + isUserLogged = AccountUtils.getCurrentOwnCloudAccount(context) != null, ) ).also { result -> syncRequired = false @@ -468,7 +474,11 @@ class DocumentsStorageProvider : DocumentsProvider() { val newFile = File(tempDir, displayName) newFile.parentFile?.mkdirs() fileToUpload = OCFile( - remotePath = parentDocument.remotePath + displayName, mimeType = mimeType, parentId = parentDocument.id, owner = parentDocument.owner, spaceId = parentDocument.spaceId + remotePath = parentDocument.remotePath + displayName, + mimeType = mimeType, + parentId = parentDocument.id, + owner = parentDocument.owner, + spaceId = parentDocument.spaceId ).apply { storagePath = newFile.path } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/SortOptionsView.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/SortOptionsView.kt index 98f884aaad0..6676559e99b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/SortOptionsView.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/SortOptionsView.kt @@ -24,8 +24,8 @@ import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.databinding.SortOptionsLayoutBinding import com.owncloud.android.presentation.files.SortOrder.Companion.PREF_FILE_LIST_SORT_ORDER import com.owncloud.android.presentation.files.SortType.Companion.PREF_FILE_LIST_SORT_TYPE diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/createfolder/CreateFolderDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/createfolder/CreateFolderDialogFragment.kt index 3fff1a1aa4b..a96258bb6ee 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/createfolder/CreateFolderDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/createfolder/CreateFolderDialogFragment.kt @@ -27,7 +27,9 @@ import android.view.WindowManager import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.DialogFragment +import com.google.android.material.textfield.TextInputLayout import com.owncloud.android.R import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.utils.PreferenceUtils @@ -41,9 +43,15 @@ import com.owncloud.android.utils.PreferenceUtils class CreateFolderDialogFragment : DialogFragment() { private lateinit var parentFolder: OCFile private lateinit var createFolderListener: CreateFolderListener + private var isButtonEnabled: Boolean = false + private val MAX_FILENAME_LENGTH = 223 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + if (savedInstanceState != null) { + isButtonEnabled = savedInstanceState.getBoolean(IS_BUTTON_ENABLED_FLAG_KEY) + } + // Inflate the layout for the dialog val inflater = requireActivity().layoutInflater val view = inflater.inflate(R.layout.edit_box_dialog, null) @@ -57,6 +65,9 @@ class CreateFolderDialogFragment : DialogFragment() { // Request focus val inputText: EditText = view.findViewById(R.id.user_input) + val inputLayout: TextInputLayout = view.findViewById(R.id.edit_box_input_text_layout) + var error: String? = null + inputText.requestFocus() // Build the dialog @@ -71,17 +82,62 @@ class CreateFolderDialogFragment : DialogFragment() { } .setNegativeButton(android.R.string.cancel, null) .setTitle(R.string.uploader_info_dirname) - val alertDialog: Dialog = builder.create() + val alertDialog = builder.create() + + alertDialog.setOnShowListener { + val okButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) + okButton.isEnabled = isButtonEnabled + + okButton.setOnClickListener { + var fileName: String = inputText.text.toString() + createFolderListener.onFolderNameSet(fileName, parentFolder) + dialog?.dismiss() + } + } + + inputText.doOnTextChanged { text, _, _, _ -> + val okButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) + + if (text.isNullOrBlank()) { + okButton.isEnabled = false + error = getString(R.string.uploader_upload_text_dialog_filename_error_empty) + } else if (text.length > MAX_FILENAME_LENGTH) { + error = String.format( + getString(R.string.uploader_upload_text_dialog_filename_error_length_max), + MAX_FILENAME_LENGTH + ) + } else if (forbiddenChars.any { text.contains(it) }) { + error = getString(R.string.filename_forbidden_characters) + } else { + okButton.isEnabled = true + error = null + inputLayout.error = error + } + + if (error != null) { + okButton.isEnabled = false + inputLayout.error = error + } + } + + alertDialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) return alertDialog } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(IS_BUTTON_ENABLED_FLAG_KEY, isButtonEnabled) + } + interface CreateFolderListener { fun onFolderNameSet(newFolderName: String, parentFolder: OCFile) } companion object { const val CREATE_FOLDER_FRAGMENT = "CREATE_FOLDER_FRAGMENT" + private const val IS_BUTTON_ENABLED_FLAG_KEY = "IS_BUTTON_ENABLED_FLAG_KEY" + private val forbiddenChars = listOf('/', '\\') /** * Public factory method to create new CreateFolderDialogFragment instances. diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt index 6a40ea764ec..f1611f7aabb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsFragment.kt @@ -2,8 +2,9 @@ * ownCloud Android client application * * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón * - * Copyright (C) 2022 ownCloud GmbH. + * Copyright (C) 2023 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -17,6 +18,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package com.owncloud.android.presentation.files.details import android.accounts.Account @@ -30,6 +32,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.widget.ImageView import androidx.browser.customtabs.CustomTabsIntent import androidx.core.view.isVisible import androidx.work.WorkInfo @@ -41,14 +44,16 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.domain.exceptions.InstanceNotConfiguredException import com.owncloud.android.domain.exceptions.TooEarlyException import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.domain.files.model.OCFileWithSyncInfo import com.owncloud.android.domain.utils.Event +import com.owncloud.android.extensions.addOpenInWebMenuOptions import com.owncloud.android.extensions.collectLatestLifecycleFlow +import com.owncloud.android.extensions.filterMenuOptions import com.owncloud.android.extensions.isDownload import com.owncloud.android.extensions.openOCFile import com.owncloud.android.extensions.sendDownloadedFilesByShareSheet import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showMessageInSnackbar -import com.owncloud.android.files.FileMenuFilter import com.owncloud.android.presentation.common.UIResult import com.owncloud.android.presentation.conflicts.ConflictsResolveActivity import com.owncloud.android.presentation.files.details.FileDetailsViewModel.ActionsInDetailsView.NONE @@ -93,7 +98,7 @@ class FileDetailsFragment : FileFragment() { private var _binding: FileDetailsFragmentBinding? = null private val binding get() = _binding!! - private val mutableOpenInWebProviders: MutableMap = hashMapOf() + private var openInWebProviders: Map = hashMapOf() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -110,10 +115,10 @@ class FileDetailsFragment : FileFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - collectLatestLifecycleFlow(fileDetailsViewModel.currentFile) { ocFile: OCFile? -> - if (ocFile != null) { - file = ocFile - updateDetails(ocFile) + collectLatestLifecycleFlow(fileDetailsViewModel.currentFile) { ocFileWithSyncInfo: OCFileWithSyncInfo? -> + if (ocFileWithSyncInfo != null) { + file = ocFileWithSyncInfo.file + updateDetails(ocFileWithSyncInfo) } else { requireActivity().onBackPressed() } @@ -157,6 +162,7 @@ class FileDetailsFragment : FileFragment() { fileDetailsViewModel.updateActionInDetailsView(NONE) requireActivity().invalidateOptionsMenu() } + is UIResult.Loading -> {} is UIResult.Success -> when (uiResult.data) { SynchronizeFileUseCase.SyncType.AlreadySynchronized -> showMessageInSnackbar(getString(R.string.sync_file_nothing_to_do_msg)) @@ -165,9 +171,11 @@ class FileDetailsFragment : FileFragment() { showConflictActivityIntent.putExtra(ConflictsResolveActivity.EXTRA_FILE, file) startActivity(showConflictActivityIntent) } + is SynchronizeFileUseCase.SyncType.DownloadEnqueued -> { fileDetailsViewModel.startListeningToWorkInfo(uiResult.data.workerId) } + SynchronizeFileUseCase.SyncType.FileNotFound -> showMessageInSnackbar("FILE NOT FOUND") is SynchronizeFileUseCase.SyncType.UploadEnqueued -> fileDetailsViewModel.startListeningToWorkInfo(uiResult.data.workerId) null -> showMessageInSnackbar("NULL") @@ -180,7 +188,7 @@ class FileDetailsFragment : FileFragment() { if (actions.requiresSync() && safeFile != null) fileOperationsViewModel.performOperation( SynchronizeFileOperation( - fileToSync = safeFile, + fileToSync = safeFile.file, accountName = fileDetailsViewModel.getAccount().name ) ) @@ -197,33 +205,11 @@ class FileDetailsFragment : FileFragment() { override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) val safeFile = fileDetailsViewModel.getCurrentFile() ?: return - val fileMenuFilter = FileMenuFilter( - safeFile, - fileDetailsViewModel.getAccount(), - mContainerActivity, - activity - ) - fileMenuFilter.filter( - menu, - false, - false, - false, - false, - ) - - menu.findItem(R.id.action_see_details)?.apply { - isVisible = false - isEnabled = false - } + fileDetailsViewModel.filterMenuOptions(safeFile.file) - menu.findItem(R.id.action_move)?.apply { - isVisible = false - isEnabled = false - } - - menu.findItem(R.id.action_copy)?.apply { - isVisible = false - isEnabled = false + collectLatestLifecycleFlow(fileDetailsViewModel.menuOptions) { menuOptions -> + val hasWritePermission = safeFile.file.hasWritePermission + menu.filterMenuOptions(menuOptions, hasWritePermission) } menu.findItem(R.id.action_search)?.apply { @@ -231,96 +217,161 @@ class FileDetailsFragment : FileFragment() { isEnabled = false } - // Remove items and then add them again. Otherwise we can get duplications or missing some app providers... - mutableOpenInWebProviders.forEach { (_, menuItem) -> - menu.removeItem(menuItem.itemId) - } - val appRegistryProviders = fileDetailsViewModel.appRegistryMimeType.value?.appProviders - appRegistryProviders?.forEachIndexed { index, appRegistryProvider -> - menu.add(Menu.NONE, index, 0, getString(R.string.ic_action_open_with_web, appRegistryProvider.name)).also { - mutableOpenInWebProviders[appRegistryProvider.name] = it - } - } + openInWebProviders = addOpenInWebMenuOptions(menu, openInWebProviders, appRegistryProviders) } override fun onOptionsItemSelected(item: MenuItem): Boolean { val safeFile = fileDetailsViewModel.getCurrentFile() ?: return false // Let's match the ones that are dynamic first. - mutableOpenInWebProviders.forEach { (openInWebProviderName, menuItem) -> - if (menuItem == item) { - fileDetailsViewModel.openInWeb(safeFile.remoteId!!, openInWebProviderName) + openInWebProviders.forEach { (openInWebProviderName, menuItemId) -> + if (menuItemId == item.itemId) { + fileDetailsViewModel.openInWeb(safeFile.file.remoteId!!, openInWebProviderName) return true } } return when (item.itemId) { R.id.action_share_file -> { - mContainerActivity.fileOperationsHelper.showShareFile(fileDetailsViewModel.getCurrentFile()) + mContainerActivity.fileOperationsHelper.showShareFile(safeFile.file) true } + R.id.action_open_file_with -> { - if (!safeFile.isAvailableLocally) { // Download the file - Timber.d("%s : File must be downloaded before opening it", safeFile.remotePath) + if (!safeFile.file.isAvailableLocally) { // Download the file + Timber.d("%s : File must be downloaded before opening it", safeFile.file.remotePath) fileDetailsViewModel.updateActionInDetailsView(SYNC_AND_OPEN_WITH) } else { // Already downloaded -> Open it - requireActivity().openOCFile(safeFile) + requireActivity().openOCFile(safeFile.file) } true } + R.id.action_remove_file -> { - val dialog = RemoveFilesDialogFragment.newInstance(safeFile) + val dialog = RemoveFilesDialogFragment.newInstance(safeFile.file) dialog.show(parentFragmentManager, FRAGMENT_TAG_CONFIRMATION) true } + R.id.action_rename_file -> { - val dialog = RenameFileDialogFragment.newInstance(safeFile) + val dialog = RenameFileDialogFragment.newInstance(safeFile.file) dialog.show(parentFragmentManager, FRAGMENT_TAG_RENAME_FILE) true } + R.id.action_cancel_sync -> { fileDetailsViewModel.cancelCurrentTransfer() true } + R.id.action_download_file, R.id.action_sync_file -> { fileDetailsViewModel.updateActionInDetailsView(SYNC) true } + R.id.action_send_file -> { - if (!safeFile.isAvailableLocally) { // Download the file - Timber.d("%s : File must be downloaded before sending it", safeFile.remotePath) + if (!safeFile.file.isAvailableLocally) { // Download the file + Timber.d("%s : File must be downloaded before sending it", safeFile.file.remotePath) fileDetailsViewModel.updateActionInDetailsView(SYNC_AND_SEND) } else { // Already downloaded -> Send it - requireActivity().sendDownloadedFilesByShareSheet(listOf(safeFile)) + requireActivity().sendDownloadedFilesByShareSheet(listOf(safeFile.file)) } true } + R.id.action_set_available_offline -> { - fileOperationsViewModel.performOperation(SetFilesAsAvailableOffline(listOf(safeFile))) - fileOperationsViewModel.performOperation(SynchronizeFileOperation(safeFile, safeFile.owner)) + fileOperationsViewModel.performOperation(SetFilesAsAvailableOffline(listOf(safeFile.file))) + fileOperationsViewModel.performOperation(SynchronizeFileOperation(safeFile.file, safeFile.file.owner)) true } + R.id.action_unset_available_offline -> { - fileOperationsViewModel.performOperation(UnsetFilesAsAvailableOffline(listOf(safeFile))) + fileOperationsViewModel.performOperation(UnsetFilesAsAvailableOffline(listOf(safeFile.file))) true } + else -> super.onOptionsItemSelected(item) } } - private fun updateDetails(ocFile: OCFile) { - binding.fdFilename.text = ocFile.fileName - binding.fdSize.text = DisplayUtils.bytesToHumanReadable(ocFile.length, requireContext()) - binding.fdModified.text = DisplayUtils.unixTimeToHumanReadable(ocFile.modificationTimestamp) - setMimeType(ocFile) + private fun updateDetails(ocFileWithSyncInfo: OCFileWithSyncInfo) { + binding.fdname.text = ocFileWithSyncInfo.file.fileName + binding.fdSize.text = DisplayUtils.bytesToHumanReadable(ocFileWithSyncInfo.file.length, requireContext()) + binding.fdPath.text = ocFileWithSyncInfo.file.getParentRemotePath() + setLastSync(ocFileWithSyncInfo.file) + setModified(ocFileWithSyncInfo.file) + setCreated(ocFileWithSyncInfo.file) + setIconPinAccordingToFilesLocalState(binding.badgeDetailFile, ocFileWithSyncInfo) + setMimeType(ocFileWithSyncInfo.file) + setSpaceName(ocFileWithSyncInfo) requireActivity().invalidateOptionsMenu() } + private fun setLastSync(ocFile: OCFile) { + if (ocFile.lastSyncDateForData?.let { it > ZERO_MILLISECOND_TIME } == true) { + binding.fdLastSync.visibility = View.VISIBLE + binding.fdLastSyncLabel.visibility = View.VISIBLE + binding.fdLastSync.text = DisplayUtils.unixTimeToHumanReadable(ocFile.lastSyncDateForData!!) + } + } + + private fun setModified(ocFile: OCFile) { + if (ocFile.modificationTimestamp?.let { it > ZERO_MILLISECOND_TIME } == true) { + binding.fdModified.visibility = View.VISIBLE + binding.fdModifiedLabel.visibility = View.VISIBLE + binding.fdModified.text = DisplayUtils.unixTimeToHumanReadable(ocFile.modificationTimestamp) + } + } + + private fun setCreated(ocFile: OCFile) { + if (ocFile.creationTimestamp?.let { it > ZERO_MILLISECOND_TIME } == true) { + binding.fdCreated.visibility = View.VISIBLE + binding.fdCreatedLabel.visibility = View.VISIBLE + binding.fdCreated.text = DisplayUtils.unixTimeToHumanReadable(ocFile.creationTimestamp!!) + } + } + + private fun setSpaceName(ocFileWithSyncInfo: OCFileWithSyncInfo) { + val space = ocFileWithSyncInfo.space + if (space != null) { + binding.fdSpace.visibility = View.VISIBLE + binding.fdSpaceLabel.visibility = View.VISIBLE + binding.fdIconSpace.visibility = View.VISIBLE + if (space.isPersonal) { + binding.fdSpace.text = getString(R.string.bottom_nav_personal) + } else { + binding.fdSpace.text = space.name + } + } + } + + private fun setIconPinAccordingToFilesLocalState(thumbnailImageView: ImageView, ocFileWithSyncInfo: OCFileWithSyncInfo) { + // local state + thumbnailImageView.bringToFront() + thumbnailImageView.isVisible = false + + val file = ocFileWithSyncInfo.file + if (ocFileWithSyncInfo.isSynchronizing) { + thumbnailImageView.setImageResource(R.drawable.sync_pin) + thumbnailImageView.visibility = View.VISIBLE + } else if (file.etagInConflict != null) { + // conflict + thumbnailImageView.setImageResource(R.drawable.error_pin) + thumbnailImageView.visibility = View.VISIBLE + } else if (file.isAvailableOffline) { + thumbnailImageView.setImageResource(R.drawable.offline_available_pin) + thumbnailImageView.visibility = View.VISIBLE + } else if (file.isAvailableLocally) { + thumbnailImageView.setImageResource(R.drawable.downloaded_pin) + thumbnailImageView.visibility = View.VISIBLE + } + } + private fun setMimeType(ocFile: OCFile) { binding.fdType.text = DisplayUtils.convertMIMEtoPrettyPrint(ocFile.mimeType) - binding.fdIcon.let { imageView -> + binding.fdImageDetailFile.let { imageView -> imageView.apply { tag = ocFile.id setOnClickListener { @@ -376,9 +427,9 @@ class FileDetailsFragment : FileFragment() { showProgressView(isTransferGoingOn = true) binding.fdProgressText.text = if (workInfo.isDownload()) { - getString(R.string.downloader_download_enqueued_ticker, safeFile.fileName) + getString(R.string.downloader_download_enqueued_ticker, safeFile.file.fileName) } else { // Transfer is upload (?) - getString(R.string.uploader_upload_enqueued_ticker, safeFile.fileName) + getString(R.string.uploader_upload_enqueued_ticker, safeFile.file.fileName) } binding.fdProgressBar.apply { progress = 0 @@ -413,16 +464,19 @@ class FileDetailsFragment : FileFragment() { SYNC -> { fileDetailsViewModel.updateActionInDetailsView(NONE) } + SYNC_AND_OPEN -> { navigateToPreviewOrOpenFile(file) fileDetailsViewModel.updateActionInDetailsView(NONE) } + SYNC_AND_OPEN_WITH -> { - requireActivity().openOCFile(safeFile) + requireActivity().openOCFile(safeFile.file) fileDetailsViewModel.updateActionInDetailsView(NONE) } + SYNC_AND_SEND -> { - requireActivity().sendDownloadedFilesByShareSheet(listOf(safeFile)) + requireActivity().sendDownloadedFilesByShareSheet(listOf(safeFile.file)) fileDetailsViewModel.updateActionInDetailsView(NONE) } } @@ -474,15 +528,19 @@ class FileDetailsFragment : FileFragment() { PreviewImageFragment.canBePreviewed(fileWaitingToPreview) -> { fileDisplayActivity.startImagePreview(fileWaitingToPreview) } + PreviewAudioFragment.canBePreviewed(fileWaitingToPreview) -> { fileDisplayActivity.startAudioPreview(fileWaitingToPreview, 0) } + PreviewVideoFragment.canBePreviewed(fileWaitingToPreview) -> { fileDisplayActivity.startVideoPreview(fileWaitingToPreview, 0) } + PreviewTextFragment.canBePreviewed(fileWaitingToPreview) -> { fileDisplayActivity.startTextPreview(fileWaitingToPreview) } + else -> fileDisplayActivity.openOCFile(fileWaitingToPreview) } } @@ -511,6 +569,7 @@ class FileDetailsFragment : FileFragment() { private const val ARG_FILE = "FILE" private const val ARG_ACCOUNT = "ACCOUNT" private const val ARG_SYNC_FILE_AT_OPEN = "SYNC_FILE_AT_OPEN" + private const val ZERO_MILLISECOND_TIME = 0 /** * Public factory method to create new FileDetailsFragment instances. diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsViewModel.kt index 37f2c34664e..8fd8d379c10 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/details/FileDetailsViewModel.kt @@ -1,21 +1,24 @@ -/* +/** * ownCloud Android client application * * @author Abel García de Prada - * Copyright (C) 2022 ownCloud GmbH. - *

+ * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. - *

+ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - *

+ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package com.owncloud.android.presentation.files.details import android.accounts.Account @@ -28,13 +31,16 @@ import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import androidx.work.WorkManager +import com.owncloud.android.R import com.owncloud.android.domain.appregistry.model.AppRegistryMimeType import com.owncloud.android.domain.appregistry.usecases.GetAppRegistryForMimeTypeAsStreamUseCase import com.owncloud.android.domain.appregistry.usecases.GetUrlToOpenInWebUseCase import com.owncloud.android.domain.capabilities.usecases.RefreshCapabilitiesFromServerAsyncUseCase import com.owncloud.android.domain.extensions.isOneOf +import com.owncloud.android.domain.files.model.FileMenuOption import com.owncloud.android.domain.files.model.OCFile -import com.owncloud.android.domain.files.usecases.GetFileByIdAsStreamUseCase +import com.owncloud.android.domain.files.model.OCFileWithSyncInfo +import com.owncloud.android.domain.files.usecases.GetFileWithSyncInfoByIdUseCase import com.owncloud.android.domain.utils.Event import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.extensions.getRunningWorkInfosByTags @@ -45,6 +51,7 @@ import com.owncloud.android.presentation.files.details.FileDetailsViewModel.Acti import com.owncloud.android.presentation.files.details.FileDetailsViewModel.ActionsInDetailsView.SYNC_AND_OPEN import com.owncloud.android.providers.ContextProvider import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.usecases.files.FilterFileMenuOptionsUseCase import com.owncloud.android.usecases.transfers.downloads.CancelDownloadForFileUseCase import com.owncloud.android.usecases.transfers.uploads.CancelUploadForFileUseCase import com.owncloud.android.workers.DownloadFileWorker @@ -60,10 +67,11 @@ class FileDetailsViewModel( private val openInWebUseCase: GetUrlToOpenInWebUseCase, refreshCapabilitiesFromServerAsyncUseCase: RefreshCapabilitiesFromServerAsyncUseCase, getAppRegistryForMimeTypeAsStreamUseCase: GetAppRegistryForMimeTypeAsStreamUseCase, - val contextProvider: ContextProvider, private val cancelDownloadForFileUseCase: CancelDownloadForFileUseCase, - getFileByIdAsStreamUseCase: GetFileByIdAsStreamUseCase, private val cancelUploadForFileUseCase: CancelUploadForFileUseCase, + private val filterFileMenuOptionsUseCase: FilterFileMenuOptionsUseCase, + getFileWithSyncInfoByIdUseCase: GetFileWithSyncInfoByIdUseCase, + val contextProvider: ContextProvider, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, private val workManager: WorkManager, account: Account, @@ -84,12 +92,20 @@ class FileDetailsViewModel( ) private val account: StateFlow = MutableStateFlow(account) - val currentFile: StateFlow = - getFileByIdAsStreamUseCase.execute(GetFileByIdAsStreamUseCase.Params(ocFile.id!!)) + private val ocFileWithSyncInfo = OCFileWithSyncInfo( + file = ocFile, + uploadWorkerUuid = UUID.randomUUID(), + downloadWorkerUuid = UUID.randomUUID(), + isSynchronizing = true, + space = null + ) + + val currentFile: StateFlow = + getFileWithSyncInfoByIdUseCase.execute(GetFileWithSyncInfoByIdUseCase.Params(ocFile.id!!)) .stateIn( viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = ocFile + initialValue = ocFileWithSyncInfo ) private val _ongoingTransferUUID = MutableLiveData() @@ -107,7 +123,10 @@ class FileDetailsViewModel( private val _actionsInDetailsView: MutableStateFlow = MutableStateFlow(if (shouldSyncFile) SYNC_AND_OPEN else NONE) val actionsInDetailsView: StateFlow = _actionsInDetailsView - fun getCurrentFile(): OCFile? = currentFile.value + private val _menuOptions: MutableStateFlow> = MutableStateFlow(emptyList()) + val menuOptions: StateFlow> = _menuOptions + + fun getCurrentFile(): OCFileWithSyncInfo? = currentFile.value fun getAccount() = account.value fun updateActionInDetailsView(actionsInDetailsView: ActionsInDetailsView) { @@ -123,7 +142,7 @@ class FileDetailsViewModel( fun checkOnGoingTransfersWhenOpening() { val safeFile = currentFile.value ?: return val listOfWorkers = - workManager.getRunningWorkInfosByTags(listOf(safeFile.id!!.toString(), getAccount().name, DownloadFileWorker::class.java.name)) + workManager.getRunningWorkInfosByTags(listOf(safeFile.file.id!!.toString(), getAccount().name, DownloadFileWorker::class.java.name)) listOfWorkers.firstOrNull()?.let { workInfo -> _ongoingTransferUUID.postValue(workInfo.id) } @@ -134,14 +153,13 @@ class FileDetailsViewModel( val currentTransfer = ongoingTransfer.value?.peekContent() ?: return@launch val safeFile = currentFile.value ?: return@launch if (currentTransfer.isUpload()) { - cancelUploadForFileUseCase.execute(CancelUploadForFileUseCase.Params(safeFile)) + cancelUploadForFileUseCase.execute(CancelUploadForFileUseCase.Params(safeFile.file)) } else if (currentTransfer.isDownload()) { - cancelDownloadForFileUseCase.execute(CancelDownloadForFileUseCase.Params(safeFile)) + cancelDownloadForFileUseCase.execute(CancelDownloadForFileUseCase.Params(safeFile.file)) } } } - // TODO: Use MainFileListViewModel's openInWeb method and remove this one fun openInWeb(fileId: String, appName: String) { runUseCaseWithResult( coroutineDispatcher = coroutinesDispatcherProvider.io, @@ -157,6 +175,32 @@ class FileDetailsViewModel( ) } + fun filterMenuOptions(file: OCFile) { + val shareViaLinkAllowed = contextProvider.getBoolean(R.bool.share_via_link_feature) + val shareWithUsersAllowed = contextProvider.getBoolean(R.bool.share_with_users_feature) + val sendAllowed = contextProvider.getString(R.string.send_files_to_other_apps).equals("on", ignoreCase = true) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val result = filterFileMenuOptionsUseCase.execute(FilterFileMenuOptionsUseCase.Params( + files = listOf(file), + accountName = getAccount().name, + isAnyFileVideoPreviewing = false, + displaySelectAll = false, + displaySelectInverse = false, + onlyAvailableOfflineFiles = false, + onlySharedByLinkFiles = false, + shareViaLinkAllowed = shareViaLinkAllowed, + shareWithUsersAllowed = shareWithUsersAllowed, + sendAllowed = sendAllowed, + )) + result.apply { + remove(FileMenuOption.DETAILS) + remove(FileMenuOption.MOVE) + remove(FileMenuOption.COPY) + } + _menuOptions.update { result } + } + } + enum class ActionsInDetailsView { NONE, SYNC, SYNC_AND_OPEN, SYNC_AND_OPEN_WITH, SYNC_AND_SEND; diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt index aa8bae15038..6b78086da0f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt @@ -3,6 +3,7 @@ * * @author Fernando Sanz Velasco * @author Juan Carlos Garrote Gascón + * @author Manuel Plazas Palacio * * Copyright (C) 2023 ownCloud GmbH. * @@ -31,6 +32,7 @@ import android.widget.ImageView import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.core.view.setMargins import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager @@ -92,6 +94,7 @@ class FileListAdapter( } ListViewHolder(binding) } + ViewType.GRID_IMAGE.ordinal -> { val binding = GridItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding.root.apply { @@ -100,6 +103,7 @@ class FileListAdapter( } GridImageViewHolder(binding) } + ViewType.GRID_ITEM.ordinal -> { val binding = GridItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding.root.apply { @@ -108,6 +112,7 @@ class FileListAdapter( } GridViewHolder(binding) } + else -> { val binding = ListFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding.root.apply { @@ -134,9 +139,11 @@ class FileListAdapter( layoutManager.spanCount == 1 -> { ViewType.LIST_ITEM.ordinal } + (files[position] as OCFileWithSyncInfo).file.isImage -> { ViewType.GRID_IMAGE.ordinal } + else -> { ViewType.GRID_ITEM.ordinal } @@ -188,14 +195,11 @@ class FileListAdapter( filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context) } - holder.itemView.findViewById(R.id.sharedIcon).apply { - isVisible = file.sharedByLink || file.sharedWithSharee == true || file.isSharedWithMe - if (file.sharedByLink) { - setImageResource(R.drawable.ic_shared_by_link) - } else if (file.sharedWithSharee == true || file.isSharedWithMe) { - setImageResource(R.drawable.shared_via_users) - } - } + holder.itemView.findViewById(R.id.share_icons_layout).isVisible = + file.sharedByLink || file.sharedWithSharee == true || file.isSharedWithMe + holder.itemView.findViewById(R.id.shared_by_link_icon).isVisible = file.sharedByLink + holder.itemView.findViewById(R.id.shared_via_users_icon).isVisible = + file.sharedWithSharee == true || file.isSharedWithMe when (viewType) { ViewType.LIST_ITEM.ordinal -> { @@ -205,6 +209,7 @@ class FileListAdapter( it.Filename.text = file.fileName it.fileListSize.text = DisplayUtils.bytesToHumanReadable(file.length, context) it.fileListLastMod.text = DisplayUtils.getRelativeTimestamp(context, file.modificationTimestamp) + it.threeDotMenu.isVisible = getCheckedItems().isEmpty() if (fileListOption.isAvailableOffline() || (fileListOption.isSharedByLink() && fileWithSyncInfo.space == null)) { it.spacePathLine.path.apply { text = file.getParentRemotePath() @@ -225,13 +230,24 @@ class FileListAdapter( it.spacePathLine.spaceIcon.isVisible = false it.spacePathLine.spaceName.isVisible = false } + it.threeDotMenu.setOnClickListener { + listener.onThreeDotButtonClick(fileWithSyncInfo = fileWithSyncInfo) + } } } + ViewType.GRID_ITEM.ordinal -> { // Filename val view = holder as GridViewHolder view.binding.Filename.text = file.fileName } + ViewType.GRID_IMAGE.ordinal -> { + val layoutParams = fileIcon.layoutParams as ViewGroup.MarginLayoutParams + val marginImage = 4 + layoutParams.setMargins(marginImage) + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + } } setIconPinAccordingToFilesLocalState(holder.itemView.findViewById(R.id.localFileIndicator), fileWithSyncInfo) @@ -248,11 +264,11 @@ class FileListAdapter( position = position ) } + holder.itemView.setBackgroundColor(Color.WHITE) val checkBoxV = holder.itemView.findViewById(R.id.custom_checkbox).apply { - isVisible = false + isVisible = getCheckedItems().isNotEmpty() } - holder.itemView.setBackgroundColor(Color.WHITE) if (isSelected(position)) { holder.itemView.setBackgroundColor(ContextCompat.getColor(context, R.color.selected_item_background)) @@ -261,16 +277,10 @@ class FileListAdapter( holder.itemView.setBackgroundColor(Color.WHITE) checkBoxV.setImageResource(R.drawable.ic_checkbox_blank_outline) } - checkBoxV.isVisible = getCheckedItems().isNotEmpty() if (file.isFolder) { // Folder - fileIcon.setImageResource( - MimetypeIconUtil.getFolderTypeIconId( - file.isSharedWithMe || file.sharedWithSharee == true, - file.sharedByLink - ) - ) + fileIcon.setImageResource(R.drawable.ic_menu_archive) } else { // Set file icon depending on its mimetype. Ask for thumbnail later. fileIcon.setImageResource(MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName)) @@ -356,35 +366,43 @@ class FileListAdapter( foldersCount <= 0 -> { "" } + foldersCount == 1 -> { context.getString(R.string.file_list__footer__folder) } + else -> { // foldersCount > 1 context.getString(R.string.file_list__footer__folders, foldersCount) } } } + filesCount == 1 -> { return when { foldersCount <= 0 -> { context.getString(R.string.file_list__footer__file) } + foldersCount == 1 -> { context.getString(R.string.file_list__footer__file_and_folder) } + else -> { // foldersCount > 1 context.getString(R.string.file_list__footer__file_and_folders, foldersCount) } } } + else -> { // filesCount > 1 return when { foldersCount <= 0 -> { context.getString(R.string.file_list__footer__files, filesCount) } + foldersCount == 1 -> { context.getString(R.string.file_list__footer__files_and_folder, filesCount) } + else -> { // foldersCount > 1 context.getString( R.string.file_list__footer__files_and_folders, filesCount, foldersCount @@ -398,6 +416,7 @@ class FileListAdapter( interface FileListAdapterListener { fun onItemClick(ocFileWithSyncInfo: OCFileWithSyncInfo, position: Int) fun onLongItemClick(position: Int): Boolean = true + fun onThreeDotButtonClick(fileWithSyncInfo: OCFileWithSyncInfo) } inner class GridViewHolder(val binding: GridItemBinding) : RecyclerView.ViewHolder(binding.root) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt index 8af5b7149ba..d225c01de53 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt @@ -4,6 +4,7 @@ * @author Fernando Sanz Velasco * @author Jose Antonio Barros Ramos * @author Juan Carlos Garrote Gascón + * @author Manuel Plazas Palacio * * Copyright (C) 2023 ownCloud GmbH. * @@ -44,14 +45,17 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager +import coil.load import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout import com.owncloud.android.R import com.owncloud.android.databinding.MainFileListFragmentBinding import com.owncloud.android.datamodel.ThumbnailsCacheManager @@ -59,21 +63,26 @@ import com.owncloud.android.domain.appregistry.model.AppRegistryMimeType import com.owncloud.android.domain.exceptions.InstanceNotConfiguredException import com.owncloud.android.domain.exceptions.TooEarlyException import com.owncloud.android.domain.files.model.FileListOption +import com.owncloud.android.domain.files.model.FileMenuOption import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PATH import com.owncloud.android.domain.files.model.OCFileSyncInfo import com.owncloud.android.domain.files.model.OCFileWithSyncInfo import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.domain.utils.Event +import com.owncloud.android.extensions.addOpenInWebMenuOptions import com.owncloud.android.extensions.collectLatestLifecycleFlow +import com.owncloud.android.extensions.filterMenuOptions import com.owncloud.android.extensions.parseError import com.owncloud.android.extensions.sendDownloadedFilesByShareSheet import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showMessageInSnackbar import com.owncloud.android.extensions.toDrawableRes +import com.owncloud.android.extensions.toDrawableResId +import com.owncloud.android.extensions.toResId +import com.owncloud.android.extensions.toStringResId import com.owncloud.android.extensions.toSubtitleStringRes import com.owncloud.android.extensions.toTitleStringRes -import com.owncloud.android.files.FileMenuFilter import com.owncloud.android.presentation.authentication.AccountUtils import com.owncloud.android.presentation.common.BottomSheetFragmentItemView import com.owncloud.android.presentation.common.UIResult @@ -90,10 +99,13 @@ import com.owncloud.android.presentation.files.operations.FileOperationsViewMode import com.owncloud.android.presentation.files.removefile.RemoveFilesDialogFragment import com.owncloud.android.presentation.files.renamefile.RenameFileDialogFragment import com.owncloud.android.presentation.files.renamefile.RenameFileDialogFragment.Companion.FRAGMENT_TAG_RENAME_FILE +import com.owncloud.android.presentation.thumbnails.ThumbnailsRequester import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.activity.FolderPickerActivity import com.owncloud.android.ui.dialog.ConfirmationDialogFragment +import com.owncloud.android.ui.preview.PreviewVideoFragment +import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.MimetypeIconUtil import com.owncloud.android.utils.PreferenceUtils import org.koin.androidx.viewmodel.ext.android.sharedViewModel @@ -135,6 +147,13 @@ class MainFileListFragment : Fragment(), private var currentDefaultApplication: String? = null private var browserOpened = false + private var openInWebProviders: Map = hashMapOf() + + private var menu: Menu? = null + private var checkedFiles: List = emptyList() + private var fileSingleFile: OCFile? = null + private var fileOptionsBottomSheetSingleFileLayout: LinearLayout? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -242,6 +261,7 @@ class MainFileListFragment : Fragment(), } private fun subscribeToViewModels() { + /* MainFileListViewModel observables */ // Observe the current folder displayed collectLatestLifecycleFlow(mainFileListViewModel.currentFolderDisplayed) { currentFolderDisplayed: OCFile -> fileActions?.onCurrentFolderUpdated(currentFolderDisplayed, mainFileListViewModel.getSpace()) @@ -263,18 +283,21 @@ class MainFileListFragment : Fragment(), setViewTypeSelector(SortOptionsView.AdditionalView.HIDDEN) } } - // Observe the current space to update the toolbar. - // We cant rely exclusively on the [currentFolderDisplayed] because sometimes retrieving the space takes more time + + // Observe the current space to update the toolbar + // We can't rely exclusively on the [currentFolderDisplayed] because sometimes retrieving the space takes more time collectLatestLifecycleFlow(mainFileListViewModel.space) { currentSpace: OCSpace? -> currentSpace?.let { fileActions?.onCurrentFolderUpdated(mainFileListViewModel.getFile(), currentSpace) } } + // Observe the list of app registries that allow creating new files collectLatestLifecycleFlow(mainFileListViewModel.appRegistryToCreateFiles) { listAppRegistry -> binding.fabNewfile.isVisible = listAppRegistry.isNotEmpty() registerFabNewFileListener(listAppRegistry) } + // Observe the open in web action to trigger browser collectLatestLifecycleFlow(mainFileListViewModel.openInWebFlow) { if (it != null) { @@ -306,6 +329,210 @@ class MainFileListFragment : Fragment(), } } + // Observe the menu filtered options in multiselection + collectLatestLifecycleFlow(mainFileListViewModel.menuOptions) { menuOptions -> + val hasWritePermission = if (checkedFiles.size == 1) { + checkedFiles.first().hasWritePermission + } else { + false + } + menu?.filterMenuOptions(menuOptions, hasWritePermission) + } + + // Observe the app registry in multiselection + collectLatestLifecycleFlow(mainFileListViewModel.appRegistryMimeType) { appRegistryMimeType -> + val appProviders = appRegistryMimeType?.appProviders + menu?.let { + openInWebProviders = addOpenInWebMenuOptions(it, openInWebProviders, appProviders) + } + } + + // Observe the menu filtered options for a single file + collectLatestLifecycleFlow(mainFileListViewModel.menuOptionsSingleFile) { menuOptions -> + fileSingleFile?.let { file -> + val fileOptionsBottomSheetSingleFile = layoutInflater.inflate(R.layout.file_options_bottom_sheet_fragment, null) + val dialog = BottomSheetDialog(requireContext()) + dialog.setContentView(fileOptionsBottomSheetSingleFile) + + val fileOptionsBottomSheetSingleFileBehavior: BottomSheetBehavior<*> = + BottomSheetBehavior.from(fileOptionsBottomSheetSingleFile.parent as View) + val closeBottomSheetButton = fileOptionsBottomSheetSingleFile.findViewById(R.id.close_bottom_sheet) + closeBottomSheetButton.setOnClickListener { + dialog.hide() + dialog.dismiss() + } + + val thumbnailBottomSheet = fileOptionsBottomSheetSingleFile.findViewById(R.id.thumbnail_bottom_sheet) + if (file.isFolder) { + // Folder + thumbnailBottomSheet.setImageResource(R.drawable.ic_menu_archive) + } else { + // Set file icon depending on its mimetype. Ask for thumbnail later. + thumbnailBottomSheet.setImageResource(MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName)) + if (file.remoteId != null) { + val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(file.remoteId) + if (thumbnail != null) { + thumbnailBottomSheet.setImageBitmap(thumbnail) + } + if (file.needsToUpdateThumbnail) { + // generate new Thumbnail + if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailBottomSheet)) { + val task = ThumbnailsCacheManager.ThumbnailGenerationTask( + thumbnailBottomSheet, + AccountUtils.getCurrentOwnCloudAccount(requireContext()) + ) + val asyncDrawable = ThumbnailsCacheManager.AsyncThumbnailDrawable(resources, thumbnail, task) + + // If drawable is not visible, do not update it. + if (asyncDrawable.minimumHeight > 0 && asyncDrawable.minimumWidth > 0) { + thumbnailBottomSheet.setImageDrawable(asyncDrawable) + } + task.execute(file) + } + } + + if (file.mimeType == "image/png") { + thumbnailBottomSheet.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.background_color)) + } + } + } + + val fileNameBottomSheet = fileOptionsBottomSheetSingleFile.findViewById(R.id.file_name_bottom_sheet) + fileNameBottomSheet.text = file.fileName + + val fileSizeBottomSheet = fileOptionsBottomSheetSingleFile.findViewById(R.id.file_size_bottom_sheet) + fileSizeBottomSheet.text = DisplayUtils.bytesToHumanReadable(file.length, requireContext()) + + val fileLastModBottomSheet = fileOptionsBottomSheetSingleFile.findViewById(R.id.file_last_mod_bottom_sheet) + fileLastModBottomSheet.text = DisplayUtils.getRelativeTimestamp(requireContext(), file.modificationTimestamp) + + fileOptionsBottomSheetSingleFileLayout = fileOptionsBottomSheetSingleFile.findViewById(R.id.file_options_bottom_sheet_layout) + menuOptions.forEach { menuOption -> + val fileOptionItemView = BottomSheetFragmentItemView(requireContext()) + fileOptionItemView.apply { + title = if (menuOption.toResId() == R.id.action_open_file_with && !file.hasWritePermission) { + getString(R.string.actionbar_open_with_read_only) + } else { + getString(menuOption.toStringResId()) + } + itemIcon = ResourcesCompat.getDrawable(resources, menuOption.toDrawableResId(), null) + setOnClickListener { + when (menuOption) { + FileMenuOption.SELECT_ALL -> { + // Not applicable here + } + + FileMenuOption.SELECT_INVERSE -> { + // Not applicable here + } + + FileMenuOption.DOWNLOAD, FileMenuOption.SYNC -> { + syncFiles(listOf(file)) + } + + FileMenuOption.RENAME -> { + val dialogRename = RenameFileDialogFragment.newInstance(file) + dialogRename.show(requireActivity().supportFragmentManager, FRAGMENT_TAG_RENAME_FILE) + } + + FileMenuOption.MOVE -> { + val action = Intent(activity, FolderPickerActivity::class.java) + action.putParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES, arrayListOf(file)) + action.putExtra(FolderPickerActivity.EXTRA_PICKER_MODE, FolderPickerActivity.PickerMode.MOVE) + requireActivity().startActivityForResult(action, FileDisplayActivity.REQUEST_CODE__MOVE_FILES) + } + + FileMenuOption.COPY -> { + val action = Intent(activity, FolderPickerActivity::class.java) + action.putParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES, arrayListOf(file)) + action.putExtra(FolderPickerActivity.EXTRA_PICKER_MODE, FolderPickerActivity.PickerMode.COPY) + requireActivity().startActivityForResult(action, FileDisplayActivity.REQUEST_CODE__COPY_FILES) + } + + FileMenuOption.REMOVE -> { + val dialogRemove = RemoveFilesDialogFragment.newInstance(file) + dialogRemove.show(requireActivity().supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) + } + + FileMenuOption.OPEN_WITH -> { + fileActions?.openFile(file) + } + + FileMenuOption.CANCEL_SYNC -> { + fileActions?.cancelFileTransference(arrayListOf(file)) + } + + FileMenuOption.SHARE -> { + fileActions?.onShareFileClicked(file) + } + + FileMenuOption.DETAILS -> { + fileActions?.showDetails(file) + } + + FileMenuOption.SEND -> { + if (!file.isAvailableLocally) { // Download the file + Timber.d("${file.remotePath} : File must be downloaded") + fileActions?.initDownloadForSending(file) + } else { + fileActions?.sendDownloadedFile(file) + } + } + + FileMenuOption.SET_AV_OFFLINE -> { + fileOperationsViewModel.performOperation(FileOperation.SetFilesAsAvailableOffline(listOf(file))) + if (file.isFolder) { + fileOperationsViewModel.performOperation(FileOperation.SynchronizeFolderOperation(file, file.owner)) + } else { + fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(file, file.owner)) + } + } + + FileMenuOption.UNSET_AV_OFFLINE -> { + fileOperationsViewModel.performOperation(FileOperation.UnsetFilesAsAvailableOffline(listOf(file))) + } + } + dialog.hide() + dialog.dismiss() + } + } + fileOptionsBottomSheetSingleFileLayout!!.addView(fileOptionItemView) + } + // Disable drag gesture + fileOptionsBottomSheetSingleFileBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_DRAGGING) { + fileOptionsBottomSheetSingleFileBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + }) + dialog.setOnShowListener { fileOptionsBottomSheetSingleFileBehavior.peekHeight = fileOptionsBottomSheetSingleFile.measuredHeight } + dialog.show() + mainFileListViewModel.getAppRegistryForMimeType(file.mimeType, isMultiselection = false) + } + } + + // Observe the app registry for a single file + collectLatestLifecycleFlow(mainFileListViewModel.appRegistryMimeTypeSingleFile) { appRegistryMimeType -> + fileSingleFile?.let { file -> + val appProviders = appRegistryMimeType?.appProviders + appProviders?.forEach { appRegistryProvider -> + val appProviderItemView = BottomSheetFragmentItemView(requireContext()) + appProviderItemView.apply { + title = getString(R.string.ic_action_open_with_web, appRegistryProvider.name) + itemIcon = ResourcesCompat.getDrawable(resources, R.drawable.ic_open_in_web, null) + setOnClickListener { + mainFileListViewModel.openInWeb(file.remoteId!!, appRegistryProvider.name) + } + } + fileOptionsBottomSheetSingleFileLayout!!.addView(appProviderItemView, 1) + } + } + fileSingleFile = null + } + // Observe the file list ui state collectLatestLifecycleFlow(mainFileListViewModel.fileListUiState) { fileListUiState -> if (fileListUiState !is MainFileListViewModel.FileListUiState.Success) return@collectLatestLifecycleFlow @@ -328,16 +555,12 @@ class MainFileListFragment : Fragment(), val spaceSpecialImage = it.getSpaceSpecialImage() if (spaceSpecialImage != null) { - binding.spaceHeader.spaceHeaderImage.tag = spaceSpecialImage.id - val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(spaceSpecialImage.id) - if (thumbnail != null) { - binding.spaceHeader.spaceHeaderImage.run { - setImageBitmap(thumbnail) - scaleType = ImageView.ScaleType.CENTER_CROP - } - if (spaceSpecialImage.file.mimeType == "image/png") { - binding.spaceHeader.spaceHeaderImage.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.background_color)) - } + binding.spaceHeader.spaceHeaderImage.load( + ThumbnailsRequester.getPreviewUriForSpaceSpecial(spaceSpecialImage), + ThumbnailsRequester.getCoilImageLoader() + ) { + placeholder(R.drawable.ic_spaces) + error(R.drawable.ic_spaces) } } binding.spaceHeader.spaceHeaderName.text = it.name @@ -347,11 +570,14 @@ class MainFileListFragment : Fragment(), actionMode?.invalidate() } + /* FileOperationsViewModel observables */ + // Observe the refresh folder operation fileOperationsViewModel.refreshFolderLiveData.observe(viewLifecycleOwner) { binding.syncProgressBar.isIndeterminate = it.peekContent().isLoading binding.swipeRefreshMainFileList.isRefreshing = it.peekContent().isLoading } + // Observe the create file with app provider operation collectLatestLifecycleFlow(fileOperationsViewModel.createFileWithAppProviderFlow) { val uiResult = it?.peekContent() if (uiResult is UIResult.Error) { @@ -570,6 +796,7 @@ class MainFileListFragment : Fragment(), dialogView.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(requireContext()) val input = dialogView.findViewById(R.id.inputFileName) + val inputLayout: TextInputLayout = dialogView.findViewById(R.id.inputTextLayout) input.requestFocus() val builder = AlertDialog.Builder(requireContext()).apply { @@ -580,13 +807,6 @@ class MainFileListFragment : Fragment(), val currentFolder = mainFileListViewModel.getFile() val filename = input.text.toString() var error: String? = null - if (filename.length > MAX_FILENAME_LENGTH) { - error = getString(R.string.uploader_upload_text_dialog_filename_error_length_max, MAX_FILENAME_LENGTH) - } else if (filename.isEmpty() || filename.isBlank()) { - error = getString(R.string.uploader_upload_text_dialog_filename_error_empty) - } else if (forbiddenChars.any { filename.contains(it) }) { - error = getString(R.string.filename_forbidden_characters_from_server) - } if (error != null) { showMessageInSnackbar(error) @@ -604,6 +824,33 @@ class MainFileListFragment : Fragment(), setNegativeButton(android.R.string.cancel, null) } val alertDialog = builder.create() + + input.doOnTextChanged { text, _, _, _ -> + val okButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) + var error: String? = null + if (text.isNullOrBlank()) { + okButton.isEnabled = false + error = getString(R.string.uploader_upload_text_dialog_filename_error_empty) + } else if (text.length > MAX_FILENAME_LENGTH) { + error = String.format( + getString(R.string.uploader_upload_text_dialog_filename_error_length_max), + MAX_FILENAME_LENGTH + ) + } else if (forbiddenChars.any { text.contains(it) }) { + error = getString(R.string.filename_forbidden_characters) + } else { + okButton.isEnabled = true + error = null + inputLayout.error = error + } + + if (error != null) { + okButton.isEnabled = false + inputLayout.error = error + } + } + + alertDialog.apply { window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) show() @@ -685,6 +932,14 @@ class MainFileListFragment : Fragment(), } else if (checkedFilesWithSyncInfo.size == 1) { /// action only possible on a single file val singleFile = checkedFilesWithSyncInfo.first().file + + openInWebProviders.forEach { (openInWebProviderName, menuItemId) -> + if (menuItemId == menuId) { + mainFileListViewModel.openInWeb(singleFile.remoteId!!, openInWebProviderName) + return true + } + } + when (menuId) { R.id.action_share_file -> { fileActions?.onShareFileClicked(singleFile) @@ -692,12 +947,14 @@ class MainFileListFragment : Fragment(), updateActionModeAfterTogglingSelected() return true } + R.id.action_open_file_with -> { fileActions?.openFile(singleFile) fileListAdapter.clearSelection() updateActionModeAfterTogglingSelected() return true } + R.id.action_rename_file -> { val dialog = RenameFileDialogFragment.newInstance(singleFile) dialog.show(requireActivity().supportFragmentManager, FRAGMENT_TAG_RENAME_FILE) @@ -705,16 +962,19 @@ class MainFileListFragment : Fragment(), updateActionModeAfterTogglingSelected() return true } + R.id.action_see_details -> { fileListAdapter.clearSelection() updateActionModeAfterTogglingSelected() fileActions?.showDetails(singleFile) return true } + R.id.action_sync_file -> { syncFiles(listOf(singleFile)) return true } + R.id.action_send_file -> { //Obtain the file if (!singleFile.isAvailableLocally) { // Download the file @@ -725,6 +985,7 @@ class MainFileListFragment : Fragment(), } return true } + R.id.action_set_available_offline -> { fileOperationsViewModel.performOperation(FileOperation.SetFilesAsAvailableOffline(listOf(singleFile))) if (singleFile.isFolder) { @@ -734,6 +995,7 @@ class MainFileListFragment : Fragment(), } return true } + R.id.action_unset_available_offline -> { fileOperationsViewModel.performOperation(FileOperation.UnsetFilesAsAvailableOffline(listOf(singleFile))) } @@ -748,11 +1010,13 @@ class MainFileListFragment : Fragment(), updateActionModeAfterTogglingSelected() return true } + R.id.action_select_inverse -> { fileListAdapter.selectInverse() updateActionModeAfterTogglingSelected() return true } + R.id.action_remove_file -> { val dialog = RemoveFilesDialogFragment.newInstance(checkedFiles) dialog.show(requireActivity().supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) @@ -760,15 +1024,18 @@ class MainFileListFragment : Fragment(), updateActionModeAfterTogglingSelected() return true } + R.id.action_download_file, R.id.action_sync_file -> { syncFiles(checkedFiles) return true } + R.id.action_cancel_sync -> { fileActions?.cancelFileTransference(checkedFiles) return true } + R.id.action_set_available_offline -> { fileOperationsViewModel.performOperation(FileOperation.SetFilesAsAvailableOffline(checkedFiles)) checkedFiles.forEach { ocFile -> @@ -780,13 +1047,16 @@ class MainFileListFragment : Fragment(), } return true } + R.id.action_unset_available_offline -> { fileOperationsViewModel.performOperation(FileOperation.UnsetFilesAsAvailableOffline(checkedFiles)) return true } + R.id.action_send_file -> { requireActivity().sendDownloadedFilesByShareSheet(checkedFiles) } + R.id.action_move -> { val action = Intent(activity, FolderPickerActivity::class.java) action.putParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES, checkedFiles) @@ -796,6 +1066,7 @@ class MainFileListFragment : Fragment(), updateActionModeAfterTogglingSelected() return true } + R.id.action_copy -> { val action = Intent(activity, FolderPickerActivity::class.java) action.putParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES, checkedFiles) @@ -855,6 +1126,29 @@ class MainFileListFragment : Fragment(), return true } + override fun onThreeDotButtonClick(fileWithSyncInfo: OCFileWithSyncInfo) { + val file = fileWithSyncInfo.file + fileSingleFile = file + val fileSync = OCFileSyncInfo( + fileId = fileWithSyncInfo.file.id!!, + uploadWorkerUuid = fileWithSyncInfo.uploadWorkerUuid, + downloadWorkerUuid = fileWithSyncInfo.downloadWorkerUuid, + isSynchronizing = fileWithSyncInfo.isSynchronizing + ) + + val secondFragment = requireActivity().supportFragmentManager.findFragmentByTag(TAG_SECOND_FRAGMENT) + val isAnyFileVideoPreviewing = if (secondFragment is PreviewVideoFragment) { + secondFragment.file == file + } else { + false + } + + mainFileListViewModel.filterMenuOptions( + listOf(file), listOf(fileSync), isAnyFileVideoPreviewing, + displaySelectAll = false, isMultiselection = false + ) + } + private val actionModeCallback: ActionMode.Callback = object : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { @@ -863,6 +1157,8 @@ class MainFileListFragment : Fragment(), val inflater = requireActivity().menuInflater inflater.inflate(R.menu.file_actions_menu, menu) + this@MainFileListFragment.menu = menu + mode?.invalidate() // Set gray color @@ -892,7 +1188,7 @@ class MainFileListFragment : Fragment(), ) mode?.title = title - val checkedFiles = checkedFilesWithSyncInfo.map { it.file } + checkedFiles = checkedFilesWithSyncInfo.map { it.file } val checkedFilesSync = checkedFilesWithSyncInfo.map { OCFileSyncInfo( @@ -903,21 +1199,28 @@ class MainFileListFragment : Fragment(), ) } - val fileMenuFilter = FileMenuFilter( - checkedFiles, - AccountUtils.getCurrentOwnCloudAccount(requireContext()), - requireActivity() as FileActivity, - activity, - checkedFilesSync + val secondFragment = requireActivity().supportFragmentManager.findFragmentByTag(TAG_SECOND_FRAGMENT) + val isAnyFileVideoPreviewing = if (secondFragment is PreviewVideoFragment) { + checkedFiles.any { secondFragment.file == it } + } else { + false + } + val displaySelectAll = checkedCount != fileListAdapter.itemCount - 1 // -1 because one of them is the footer :S + mainFileListViewModel.filterMenuOptions( + checkedFiles, checkedFilesSync, isAnyFileVideoPreviewing, + displaySelectAll, isMultiselection = true ) - fileMenuFilter.filter( - menu, - checkedCount != fileListAdapter.itemCount - 1, // -1 because one of them is the footer :S - true, - mainFileListViewModel.fileListOption.value.isAvailableOffline(), - mainFileListViewModel.fileListOption.value.isSharedByLink(), - ) + if (checkedFiles.size == 1) { + mainFileListViewModel.getAppRegistryForMimeType(checkedFiles.first().mimeType, isMultiselection = true) + } else { + menu?.let { + openInWebProviders.forEach { (_, menuItemId) -> + it.removeItem(menuItemId) + } + openInWebProviders = emptyMap() + } + } return true } @@ -992,6 +1295,8 @@ class MainFileListFragment : Fragment(), private const val DIALOG_CREATE_FOLDER = "DIALOG_CREATE_FOLDER" + private const val TAG_SECOND_FRAGMENT = "SECOND_FRAGMENT" + @JvmStatic fun newInstance( initialFolderToDisplay: OCFile, diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListViewModel.kt index b5bfc861eb2..a9dde92c8b8 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListViewModel.kt @@ -24,15 +24,19 @@ package com.owncloud.android.presentation.files.filelist import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.R +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.domain.appregistry.model.AppRegistryMimeType +import com.owncloud.android.domain.appregistry.usecases.GetAppRegistryForMimeTypeAsStreamUseCase import com.owncloud.android.domain.appregistry.usecases.GetAppRegistryWhichAllowCreationAsStreamUseCase import com.owncloud.android.domain.appregistry.usecases.GetUrlToOpenInWebUseCase import com.owncloud.android.domain.availableoffline.usecases.GetFilesAvailableOfflineFromAccountAsStreamUseCase import com.owncloud.android.domain.files.model.FileListOption +import com.owncloud.android.domain.files.model.FileMenuOption import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PARENT_ID import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PATH +import com.owncloud.android.domain.files.model.OCFileSyncInfo import com.owncloud.android.domain.files.model.OCFileWithSyncInfo import com.owncloud.android.domain.files.usecases.GetFileByIdUseCase import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase @@ -49,14 +53,19 @@ import com.owncloud.android.presentation.files.SortOrder.Companion.PREF_FILE_LIS import com.owncloud.android.presentation.files.SortType import com.owncloud.android.presentation.files.SortType.Companion.PREF_FILE_LIST_SORT_TYPE import com.owncloud.android.presentation.settings.advanced.SettingsAdvancedFragment.Companion.PREF_SHOW_HIDDEN_FILES +import com.owncloud.android.providers.ContextProvider import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.usecases.files.FilterFileMenuOptionsUseCase import com.owncloud.android.usecases.synchronization.SynchronizeFolderUseCase import com.owncloud.android.usecases.synchronization.SynchronizeFolderUseCase.SyncFolderMode.SYNC_CONTENTS import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -74,7 +83,10 @@ class MainFileListViewModel( private val sortFilesWithSyncInfoUseCase: SortFilesWithSyncInfoUseCase, private val synchronizeFolderUseCase: SynchronizeFolderUseCase, getAppRegistryWhichAllowCreationAsStreamUseCase: GetAppRegistryWhichAllowCreationAsStreamUseCase, + private val getAppRegistryForMimeTypeAsStreamUseCase: GetAppRegistryForMimeTypeAsStreamUseCase, private val getUrlToOpenInWebUseCase: GetUrlToOpenInWebUseCase, + private val filterFileMenuOptionsUseCase: FilterFileMenuOptionsUseCase, + private val contextProvider: ContextProvider, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, private val sharedPreferencesProvider: SharedPreferencesProvider, initialFolderToDisplay: OCFile, @@ -99,6 +111,12 @@ class MainFileListViewModel( initialValue = emptyList() ) + private val _appRegistryMimeType: MutableSharedFlow = MutableSharedFlow() + val appRegistryMimeType: SharedFlow = _appRegistryMimeType + + private val _appRegistryMimeTypeSingleFile: MutableSharedFlow = MutableSharedFlow() + val appRegistryMimeTypeSingleFile: SharedFlow = _appRegistryMimeTypeSingleFile + /** File list ui state combines the other fields and generate a new state whenever any of them changes */ val fileListUiState: StateFlow = combine( @@ -126,6 +144,12 @@ class MainFileListViewModel( private val _openInWebFlow = MutableStateFlow>?>(null) val openInWebFlow: StateFlow>?> = _openInWebFlow + private val _menuOptions: MutableSharedFlow> = MutableSharedFlow() + val menuOptions: SharedFlow> = _menuOptions + + private val _menuOptionsSingleFile: MutableSharedFlow> = MutableSharedFlow() + val menuOptionsSingleFile: SharedFlow> = _menuOptionsSingleFile + init { val sortTypeSelected = SortType.values()[sharedPreferencesProvider.getInt(PREF_FILE_LIST_SORT_TYPE, SortType.SORT_TYPE_BY_NAME.ordinal)] val sortOrderSelected = @@ -199,18 +223,21 @@ class MainFileListViewModel( FileListOption.ALL_FILES -> { parentDir = fileByIdResult.getDataOrNull() } + FileListOption.SHARED_BY_LINK -> { val fileById = fileByIdResult.getDataOrNull()!! parentDir = if ((!fileById.sharedByLink || fileById.sharedWithSharee != true) && fileById.spaceId == null) { getFileByRemotePathUseCase.execute(GetFileByRemotePathUseCase.Params(fileById.owner, ROOT_PATH)).getDataOrNull() } else fileById } + FileListOption.AV_OFFLINE -> { val fileById = fileByIdResult.getDataOrNull()!! parentDir = if (!fileById.isAvailableOffline) { getFileByRemotePathUseCase.execute(GetFileByRemotePathUseCase.Params(fileById.owner, ROOT_PATH)).getDataOrNull() } else fileById } + FileListOption.SPACES_LIST -> { parentDir = TODO("Move it to usecase if possible") } @@ -229,7 +256,7 @@ class MainFileListViewModel( TODO() } - updateFolderToDisplay(parentDir!!) + parentDir?.let { updateFolderToDisplay(it) } } } @@ -272,6 +299,50 @@ class MainFileListViewModel( _openInWebFlow.value = null } + fun filterMenuOptions( + files: List, filesSyncInfo: List, isAnyFileVideoPreviewing: Boolean, + displaySelectAll: Boolean, isMultiselection: Boolean + ) { + val shareViaLinkAllowed = contextProvider.getBoolean(R.bool.share_via_link_feature) + val shareWithUsersAllowed = contextProvider.getBoolean(R.bool.share_with_users_feature) + val sendAllowed = contextProvider.getString(R.string.send_files_to_other_apps).equals("on", ignoreCase = true) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val result = filterFileMenuOptionsUseCase.execute( + FilterFileMenuOptionsUseCase.Params( + files = files, + filesSyncInfo = filesSyncInfo, + accountName = currentFolderDisplayed.value.owner, + isAnyFileVideoPreviewing = isAnyFileVideoPreviewing, + displaySelectAll = displaySelectAll, + displaySelectInverse = isMultiselection, + onlyAvailableOfflineFiles = fileListOption.value.isAvailableOffline(), + onlySharedByLinkFiles = fileListOption.value.isSharedByLink(), + shareViaLinkAllowed = shareViaLinkAllowed, + shareWithUsersAllowed = shareWithUsersAllowed, + sendAllowed = sendAllowed, + ) + ) + if (isMultiselection) { + _menuOptions.emit(result) + } else { + _menuOptionsSingleFile.emit(result) + } + } + } + + fun getAppRegistryForMimeType(mimeType: String, isMultiselection: Boolean) { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val result = getAppRegistryForMimeTypeAsStreamUseCase.execute( + GetAppRegistryForMimeTypeAsStreamUseCase.Params(accountName = getFile().owner, mimeType) + ) + if (isMultiselection) { + _appRegistryMimeType.emit(result.firstOrNull()) + } else { + _appRegistryMimeTypeSingleFile.emit(result.firstOrNull()) + } + } + } + private fun updateSpace() { val folderToDisplay = currentFolderDisplayed.value viewModelScope.launch(coroutinesDispatcherProvider.io) { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperation.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperation.kt index 653a399a56a..04badcbef13 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperation.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperation.kt @@ -25,9 +25,22 @@ package com.owncloud.android.presentation.files.operations import com.owncloud.android.domain.files.model.OCFile sealed interface FileOperation { - data class CopyOperation(val listOfFilesToCopy: List, val targetFolder: OCFile) : FileOperation + data class CopyOperation( + val listOfFilesToCopy: List, + val targetFolder: OCFile?, + val replace: List = emptyList(), + val isUserLogged: Boolean, + ) : FileOperation + data class CreateFolder(val folderName: String, val parentFile: OCFile) : FileOperation - data class MoveOperation(val listOfFilesToMove: List, val targetFolder: OCFile) : FileOperation + data class MoveOperation( + val listOfFilesToMove: List, + val targetFolder: OCFile?, + val replace: List = emptyList(), + val isUserLogged: Boolean, + ) : + FileOperation + data class RemoveOperation(val listOfFilesToRemove: List, val removeOnlyLocalCopy: Boolean) : FileOperation data class RenameOperation(val ocFileToRename: OCFile, val newName: String) : FileOperation data class SynchronizeFileOperation(val fileToSync: OCFile, val accountName: String) : FileOperation diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt index bf0d1cc8c44..8483f028ad1 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt @@ -43,6 +43,7 @@ import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.presentation.common.UIResult import com.owncloud.android.providers.ContextProvider import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.ui.dialog.FileAlreadyExistsDialog import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase import com.owncloud.android.usecases.synchronization.SynchronizeFolderUseCase import kotlinx.coroutines.flow.MutableStateFlow @@ -68,11 +69,11 @@ class FileOperationsViewModel( private val _createFolder = MediatorLiveData>>() val createFolder: LiveData>> = _createFolder - private val _copyFileLiveData = MediatorLiveData>>() - val copyFileLiveData: LiveData>> = _copyFileLiveData + private val _copyFileLiveData = MediatorLiveData>>>() + val copyFileLiveData: LiveData>>> = _copyFileLiveData - private val _moveFileLiveData = MediatorLiveData>>() - val moveFileLiveData: LiveData>> = _moveFileLiveData + private val _moveFileLiveData = MediatorLiveData>>>() + val moveFileLiveData: LiveData>>> = _moveFileLiveData private val _removeFileLiveData = MediatorLiveData>>>() val removeFileLiveData: LiveData>>> = _removeFileLiveData @@ -92,6 +93,12 @@ class FileOperationsViewModel( private val _createFileWithAppProviderFlow = MutableStateFlow>?>(null) val createFileWithAppProviderFlow: StateFlow>?> = _createFileWithAppProviderFlow + val openDialogs = mutableListOf() + + + // Used to save the last operation folder + private var lastTargetFolder: OCFile? = null + fun performOperation(fileOperation: FileOperation) { when (fileOperation) { is FileOperation.MoveOperation -> moveOperation(fileOperation) @@ -118,21 +125,49 @@ class FileOperationsViewModel( } private fun copyOperation(fileOperation: FileOperation.CopyOperation) { - runOperation( - liveData = _copyFileLiveData, - useCase = copyFileUseCase, - useCaseParams = CopyFileUseCase.Params(fileOperation.listOfFilesToCopy, fileOperation.targetFolder), - postValue = fileOperation.targetFolder - ) + val targetFolder = if (fileOperation.targetFolder != null) { + lastTargetFolder = fileOperation.targetFolder + fileOperation.targetFolder + } else { + lastTargetFolder + } + targetFolder?.let { folder -> + runUseCaseWithResult( + coroutineDispatcher = coroutinesDispatcherProvider.io, + liveData = _copyFileLiveData, + useCase = copyFileUseCase, + useCaseParams = CopyFileUseCase.Params( + listOfFilesToCopy = fileOperation.listOfFilesToCopy, + targetFolder = folder, + replace = fileOperation.replace, + isUserLogged = fileOperation.isUserLogged, + ), + showLoading = true, + ) + } } private fun moveOperation(fileOperation: FileOperation.MoveOperation) { - runOperation( - liveData = _moveFileLiveData, - useCase = moveFileUseCase, - useCaseParams = MoveFileUseCase.Params(fileOperation.listOfFilesToMove, fileOperation.targetFolder), - postValue = fileOperation.targetFolder - ) + val targetFolder = if (fileOperation.targetFolder != null) { + lastTargetFolder = fileOperation.targetFolder + fileOperation.targetFolder + } else { + lastTargetFolder + } + targetFolder?.let { folder -> + runUseCaseWithResult( + coroutineDispatcher = coroutinesDispatcherProvider.io, + liveData = _moveFileLiveData, + useCase = moveFileUseCase, + useCaseParams = MoveFileUseCase.Params( + listOfFilesToMove = fileOperation.listOfFilesToMove, + targetFolder = folder, + replace = fileOperation.replace, + isUserLogged = fileOperation.isUserLogged, + ), + showLoading = true, + ) + } } private fun removeOperation(fileOperation: FileOperation.RemoveOperation) { @@ -245,6 +280,7 @@ class FileOperationsViewModel( is UseCaseResult.Success -> { liveData.postValue(Event(UIResult.Success(postValue))) } + is UseCaseResult.Error -> { liveData.postValue(Event(UIResult.Error(error = useCaseResult.throwable))) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/renamefile/RenameFileDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/renamefile/RenameFileDialogFragment.kt index f5fcc8babdb..1a284c06b94 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/renamefile/RenameFileDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/renamefile/RenameFileDialogFragment.kt @@ -28,7 +28,9 @@ import android.view.WindowManager import android.widget.EditText import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.DialogFragment +import com.google.android.material.textfield.TextInputLayout import com.owncloud.android.R import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.extensions.avoidScreenshotsIfNeeded @@ -43,12 +45,18 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel * * Triggers the rename operation when name is confirmed. */ + class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListener { private var targetFile: OCFile? = null private val filesViewModel: FileOperationsViewModel by sharedViewModel() - + private var isButtonEnabled = true + private val MAX_FILENAME_LENGTH = 223 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + if (savedInstanceState != null) { + isButtonEnabled = savedInstanceState.getBoolean(IS_BUTTON_ENABLED_FLAG_KEY) + } + targetFile = requireArguments().getParcelable(ARG_TARGET_FILE) // Inflate the layout for the dialog @@ -61,6 +69,8 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen // Setup layout val currentName = targetFile!!.fileName + var error: String? = null + val inputLayout: TextInputLayout = view.findViewById(R.id.edit_box_input_text_layout) val inputText = view.findViewById(R.id.user_input) inputText.setText(currentName) val selectionStart = 0 @@ -84,9 +94,45 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen }.create().apply { window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) avoidScreenshotsIfNeeded() + + setOnShowListener { + val okButton = getButton(AlertDialog.BUTTON_POSITIVE) + okButton.isEnabled = isButtonEnabled + + } + + inputText.doOnTextChanged { text, _, _, _ -> + val okButton = getButton(AlertDialog.BUTTON_POSITIVE) + if (text.isNullOrBlank()) { + okButton.isEnabled = false + error = getString(R.string.uploader_upload_text_dialog_filename_error_empty) + } else if (text.length > MAX_FILENAME_LENGTH) { + error = String.format( + getString(R.string.uploader_upload_text_dialog_filename_error_length_max), + MAX_FILENAME_LENGTH + ) + } else if (forbiddenChars.any { text.contains(it) }) { + error = getString(R.string.filename_forbidden_characters) + } else { + okButton.isEnabled = true + error = null + inputLayout.error = error + } + + if (error != null) { + okButton.isEnabled = false + inputLayout.error = error + } + + } } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(IS_BUTTON_ENABLED_FLAG_KEY, isButtonEnabled) + } + override fun onClick(dialog: DialogInterface, which: Int) { if (which == AlertDialog.BUTTON_POSITIVE) { // These checks are done in the RenameFileUseCase too, we could remove them too. @@ -109,6 +155,8 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen companion object { const val FRAGMENT_TAG_RENAME_FILE = "RENAME_FILE_FRAGMENT" private const val ARG_TARGET_FILE = "TARGET_FILE" + private const val IS_BUTTON_ENABLED_FLAG_KEY = "IS_BUTTON_ENABLED_FLAG_KEY" + private val forbiddenChars = listOf('/', '\\') /** * Public factory method to create new RenameFileDialogFragment instances. diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/logging/LogListViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/logging/LogListViewModel.kt index c342d6f67dd..b5c566b2da1 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/logging/LogListViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/logging/LogListViewModel.kt @@ -21,7 +21,7 @@ package com.owncloud.android.presentation.logging import androidx.lifecycle.ViewModel -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import java.io.File class LogListViewModel( diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/migration/MigrationViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/migration/MigrationViewModel.kt index 67485fde6b4..88834673cc7 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/migration/MigrationViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/migration/MigrationViewModel.kt @@ -28,9 +28,9 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider -import com.owncloud.android.data.storage.LegacyStorageProvider -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider +import com.owncloud.android.data.providers.LegacyStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.files.usecases.UpdateAlreadyDownloadedFilesPathUseCase import com.owncloud.android.domain.transfers.usecases.GetAllTransfersUseCase import com.owncloud.android.domain.transfers.usecases.UpdatePendingUploadsPathUseCase diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/migration/StorageMigrationActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/migration/StorageMigrationActivity.kt index ede1cecfed1..c5222493fc9 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/migration/StorageMigrationActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/migration/StorageMigrationActivity.kt @@ -26,8 +26,8 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.owncloud.android.MainApp import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider -import com.owncloud.android.data.storage.LegacyStorageProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.LegacyStorageProvider import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber import java.io.File diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/previews/PreviewAudioViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/previews/PreviewAudioViewModel.kt new file mode 100644 index 00000000000..70984b0b65b --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/previews/PreviewAudioViewModel.kt @@ -0,0 +1,73 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.previews + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.owncloud.android.R +import com.owncloud.android.domain.files.model.FileMenuOption +import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.providers.ContextProvider +import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.usecases.files.FilterFileMenuOptionsUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class PreviewAudioViewModel( + private val filterFileMenuOptionsUseCase: FilterFileMenuOptionsUseCase, + private val contextProvider: ContextProvider, + private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, +) : ViewModel() { + + private val _menuOptions: MutableStateFlow> = MutableStateFlow(emptyList()) + val menuOptions: StateFlow> = _menuOptions + + fun filterMenuOptions(file: OCFile, accountName: String) { + val shareViaLinkAllowed = contextProvider.getBoolean(R.bool.share_via_link_feature) + val shareWithUsersAllowed = contextProvider.getBoolean(R.bool.share_with_users_feature) + val sendAllowed = contextProvider.getString(R.string.send_files_to_other_apps).equals("on", ignoreCase = true) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val result = filterFileMenuOptionsUseCase.execute( + FilterFileMenuOptionsUseCase.Params( + files = listOf(file), + accountName = accountName, + isAnyFileVideoPreviewing = false, + displaySelectAll = false, + displaySelectInverse = false, + onlyAvailableOfflineFiles = false, + onlySharedByLinkFiles = false, + shareViaLinkAllowed = shareViaLinkAllowed, + shareWithUsersAllowed = shareWithUsersAllowed, + sendAllowed = sendAllowed, + ) + ) + result.apply { + remove(FileMenuOption.RENAME) + remove(FileMenuOption.MOVE) + remove(FileMenuOption.COPY) + remove(FileMenuOption.SYNC) + } + _menuOptions.update { result } + } + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/previews/PreviewTextViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/previews/PreviewTextViewModel.kt new file mode 100644 index 00000000000..0055633c7b4 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/previews/PreviewTextViewModel.kt @@ -0,0 +1,79 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * @author Parneet Singh + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.previews + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.owncloud.android.R +import com.owncloud.android.domain.files.model.FileMenuOption +import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.providers.ContextProvider +import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.usecases.files.FilterFileMenuOptionsUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class PreviewTextViewModel( + private val filterFileMenuOptionsUseCase: FilterFileMenuOptionsUseCase, + private val contextProvider: ContextProvider, + private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, +) : ViewModel() { + + private val _menuOptions: MutableStateFlow> = MutableStateFlow(emptyList()) + val menuOptions: StateFlow> = _menuOptions + + fun filterMenuOptions(file: OCFile, accountName: String) { + val shareViaLinkAllowed = contextProvider.getBoolean(R.bool.share_via_link_feature) + val shareWithUsersAllowed = contextProvider.getBoolean(R.bool.share_with_users_feature) + val sendAllowed = contextProvider.getString(R.string.send_files_to_other_apps).equals("on", ignoreCase = true) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val result = filterFileMenuOptionsUseCase.execute( + FilterFileMenuOptionsUseCase.Params( + files = listOf(file), + accountName = accountName, + isAnyFileVideoPreviewing = false, + displaySelectAll = false, + displaySelectInverse = false, + onlyAvailableOfflineFiles = false, + onlySharedByLinkFiles = false, + shareViaLinkAllowed = shareViaLinkAllowed, + shareWithUsersAllowed = shareWithUsersAllowed, + sendAllowed = sendAllowed, + ) + ) + result.apply { + remove(FileMenuOption.RENAME) + remove(FileMenuOption.MOVE) + remove(FileMenuOption.COPY) + remove(FileMenuOption.SYNC) + } + _menuOptions.update { result } + } + } + companion object { + const val NO_POSITION: Int = -1 + const val TAB_MARKDOWN_POSITION: Int = 0 + const val TAB_TEXT_POSITION: Int = 1 + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/previews/PreviewVideoViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/previews/PreviewVideoViewModel.kt new file mode 100644 index 00000000000..c1f209e360f --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/previews/PreviewVideoViewModel.kt @@ -0,0 +1,67 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.previews + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.owncloud.android.R +import com.owncloud.android.domain.files.model.FileMenuOption +import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.providers.ContextProvider +import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.usecases.files.FilterFileMenuOptionsUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class PreviewVideoViewModel( + private val filterFileMenuOptionsUseCase: FilterFileMenuOptionsUseCase, + private val contextProvider: ContextProvider, + private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, +) : ViewModel() { + + private val _menuOptions: MutableStateFlow> = MutableStateFlow(emptyList()) + val menuOptions: StateFlow> = _menuOptions + + fun filterMenuOptions(file: OCFile, accountName: String) { + val shareViaLinkAllowed = contextProvider.getBoolean(R.bool.share_via_link_feature) + val shareWithUsersAllowed = contextProvider.getBoolean(R.bool.share_with_users_feature) + val sendAllowed = contextProvider.getString(R.string.send_files_to_other_apps).equals("on", ignoreCase = true) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val result = filterFileMenuOptionsUseCase.execute( + FilterFileMenuOptionsUseCase.Params( + files = listOf(file), + accountName = accountName, + isAnyFileVideoPreviewing = true, + displaySelectAll = false, + displaySelectInverse = false, + onlyAvailableOfflineFiles = false, + onlySharedByLinkFiles = false, + shareViaLinkAllowed = shareViaLinkAllowed, + shareWithUsersAllowed = shareWithUsersAllowed, + sendAllowed = sendAllowed, + ) + ) + _menuOptions.update { result } + } + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt index 6ae863ae739..b32d25a4d1f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/releasenotes/ReleaseNotesViewModel.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.ViewModel import com.owncloud.android.MainApp import com.owncloud.android.MainApp.Companion.versionCode import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.providers.ContextProvider class ReleaseNotesViewModel( @@ -46,60 +46,45 @@ class ReleaseNotesViewModel( companion object { val releaseNotesList = listOf( ReleaseNote( - title = R.string.release_notes_4_0_title_1, - subtitle = R.string.release_notes_4_0_subtitle_1, - type = ReleaseNoteType.ENHANCEMENT + title = R.string.release_notes_4_1_title_1, + subtitle = R.string.release_notes_4_1_subtitle_1, + type = ReleaseNoteType.ENHANCEMENT, ), ReleaseNote( - title = R.string.release_notes_4_0_title_2, - subtitle = R.string.release_notes_4_0_subtitle_2, - type = ReleaseNoteType.ENHANCEMENT + title = R.string.release_notes_4_1_title_2, + subtitle = R.string.release_notes_4_1_subtitle_2, + type = ReleaseNoteType.ENHANCEMENT, ), ReleaseNote( - title = R.string.release_notes_4_0_title_3, - subtitle = R.string.release_notes_4_0_subtitle_3, - type = ReleaseNoteType.CHANGE + title = R.string.release_notes_4_1_title_3, + subtitle = R.string.release_notes_4_1_subtitle_3, + type = ReleaseNoteType.ENHANCEMENT, ), ReleaseNote( - title = R.string.release_notes_4_0_title_4, - subtitle = R.string.release_notes_4_0_subtitle_4, - type = ReleaseNoteType.ENHANCEMENT + title = R.string.release_notes_4_1_title_4, + subtitle = R.string.release_notes_4_1_subtitle_4, + type = ReleaseNoteType.BUGFIX, ), ReleaseNote( - title = R.string.release_notes_4_0_title_5, - subtitle = R.string.release_notes_4_0_subtitle_5, - type = ReleaseNoteType.ENHANCEMENT + title = R.string.release_notes_4_1_title_5, + subtitle = R.string.release_notes_4_1_subtitle_5, + type = ReleaseNoteType.ENHANCEMENT, ), ReleaseNote( - title = R.string.release_notes_4_0_title_6, - subtitle = R.string.release_notes_4_0_subtitle_6, - type = ReleaseNoteType.ENHANCEMENT + title = R.string.release_notes_4_1_title_6, + subtitle = R.string.release_notes_4_1_subtitle_6, + type = ReleaseNoteType.ENHANCEMENT, ), ReleaseNote( - title = R.string.release_notes_4_0_title_8, - subtitle = R.string.release_notes_4_0_subtitle_8, - type = ReleaseNoteType.ENHANCEMENT + title = R.string.release_notes_4_1_title_8, + subtitle = R.string.release_notes_4_1_subtitle_8, + type = ReleaseNoteType.CHANGE, ), ReleaseNote( - title = R.string.release_notes_4_0_title_9, - subtitle = R.string.release_notes_4_0_subtitle_9, - type = ReleaseNoteType.ENHANCEMENT + title = R.string.release_notes_4_1_title_7, + subtitle = R.string.release_notes_4_1_subtitle_7, + type = ReleaseNoteType.BUGFIX, ), - ReleaseNote( - title = R.string.release_notes_4_0_title_11, - subtitle = R.string.release_notes_4_0_subtitle_11, - type = ReleaseNoteType.ENHANCEMENT - ), - ReleaseNote( - title = R.string.release_notes_4_0_title_10, - subtitle = R.string.release_notes_4_0_subtitle_10, - type = ReleaseNoteType.ENHANCEMENT - ), - ReleaseNote( - title = R.string.release_notes_4_0_title_7, - subtitle = R.string.release_notes_4_0_subtitle_7, - type = ReleaseNoteType.BUGFIX - ) ) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/SecurityUtils.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/SecurityUtils.kt index 9bdc6b07a31..4610293e413 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/SecurityUtils.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/SecurityUtils.kt @@ -20,9 +20,11 @@ package com.owncloud.android.presentation.security +import android.app.KeyguardManager +import android.content.Context import android.os.SystemClock import com.owncloud.android.MainApp -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider const val PREFERENCE_LOCK_TIMEOUT = "lock_timeout" const val PREFERENCE_LAST_UNLOCK_TIMESTAMP = "last_unlock_timestamp" @@ -68,3 +70,5 @@ fun bayPassUnlockOnce() { preferencesProvider.putLong(PREFERENCE_LAST_UNLOCK_TIMESTAMP, newLastUnlockTimestamp) } } + +fun isDeviceSecure() = (MainApp.appContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager).isDeviceSecure diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricActivity.kt index 68afdf6748d..952c8962900 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricActivity.kt @@ -3,17 +3,18 @@ * * @author David González Verdugo * @author Juan Carlos Garrote Gascón - * Copyright (C) 2021 ownCloud GmbH. - *

+ * + * Copyright (C) 2023 ownCloud GmbH. + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. - *

+ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - *

+ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -21,10 +22,8 @@ package com.owncloud.android.presentation.security.biometric import android.content.Intent -import android.os.Build import android.os.Bundle import android.os.Handler -import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK @@ -56,7 +55,6 @@ class BiometricActivity : AppCompatActivity() { * * @param savedInstanceState Previously saved state - irrelevant in this case */ - @RequiresApi(api = Build.VERSION_CODES.M) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricManager.kt index ce8f9b88ca6..ae5283c4a70 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricManager.kt @@ -28,7 +28,7 @@ import android.os.SystemClock import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import com.owncloud.android.MainApp.Companion.appContext -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.presentation.security.LockTimeout import com.owncloud.android.presentation.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP import com.owncloud.android.presentation.security.PREFERENCE_LOCK_TIMEOUT diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricViewModel.kt index fe0a8980f25..e03bd83f206 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/biometric/BiometricViewModel.kt @@ -3,33 +3,31 @@ * * @author Juan Carlos Garrote Gascón * - * Copyright (C) 2021 ownCloud GmbH. - *

+ * Copyright (C) 2023 ownCloud GmbH. + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. - *

+ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - *

+ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.owncloud.android.presentation.security.biometric -import android.os.Build import android.os.SystemClock import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyProperties -import androidx.annotation.RequiresApi import androidx.biometric.BiometricPrompt import androidx.lifecycle.ViewModel import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP import com.owncloud.android.presentation.security.passcode.PassCodeActivity import com.owncloud.android.providers.ContextProvider @@ -55,7 +53,6 @@ class BiometricViewModel( * * @return the cipher if it is properly initialized, null otherwise */ - @RequiresApi(api = Build.VERSION_CODES.M) fun initCipher(): Cipher? { generateAndStoreKey() @@ -103,7 +100,6 @@ class BiometricViewModel( * Generate encryption key involved in biometric authentication process and store it securely on the device using * the Android Keystore system */ - @RequiresApi(api = Build.VERSION_CODES.M) private fun generateAndStoreKey() { try { // Access Android Keystore container, used to safely store cryptographic keys on Android devices @@ -149,9 +145,7 @@ class BiometricViewModel( } fun isBiometricLockAvailable(): Boolean { - return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - false - } else if (!BiometricManager.isHardwareDetected()) { // Biometric not supported + return if (!BiometricManager.isHardwareDetected()) { // Biometric not supported false } else BiometricManager.hasEnrolledBiometric() // Biometric not enrolled } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeManager.kt index aa0e7865e0b..76be126d416 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeManager.kt @@ -3,17 +3,17 @@ * * @author Juan Carlos Garrote Gascón * - * Copyright (C) 2021 ownCloud GmbH. - *

+ * Copyright (C) 2023 ownCloud GmbH. + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. - *

+ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - *

+ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -23,11 +23,10 @@ package com.owncloud.android.presentation.security.passcode import android.app.Activity import android.content.Context import android.content.Intent -import android.os.Build import android.os.PowerManager import android.os.SystemClock import com.owncloud.android.MainApp.Companion.appContext -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.presentation.security.LockTimeout import com.owncloud.android.presentation.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP import com.owncloud.android.presentation.security.PREFERENCE_LOCK_TIMEOUT @@ -45,7 +44,7 @@ object PassCodeManager { if (!exemptOfPasscodeActivities.contains(activity.javaClass) && passCodeShouldBeRequested()) { // Do not ask for passcode if biometric is enabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BiometricManager.isBiometricEnabled() && !visibleActivities.contains( + if (BiometricManager.isBiometricEnabled() && !visibleActivities.contains( PassCodeActivity::class.java ) ) { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeViewModel.kt index 36cbe52922d..221dd658285 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/passcode/PassCodeViewModel.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.domain.utils.Event import com.owncloud.android.presentation.security.PREFERENCE_LAST_UNLOCK_ATTEMPT_TIMESTAMP import com.owncloud.android.presentation.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternActivity.kt index 8c8851c969d..3f8bfd0db1f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternActivity.kt @@ -37,7 +37,7 @@ import com.andrognito.patternlockview.listener.PatternLockViewListener import com.andrognito.patternlockview.utils.PatternLockUtils import com.owncloud.android.BuildConfig import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.databinding.ActivityPatternLockBinding import com.owncloud.android.extensions.showBiometricDialog import com.owncloud.android.presentation.documentsprovider.DocumentsProviderUtils.Companion.notifyDocumentsProviderRoots diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternManager.kt index 98b55760924..18a7a0d820e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternManager.kt @@ -3,17 +3,17 @@ * * @author Juan Carlos Garrote Gascón * - * Copyright (C) 2021 ownCloud GmbH. - *

+ * Copyright (C) 2023 ownCloud GmbH. + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. - *

+ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - *

+ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -23,11 +23,10 @@ package com.owncloud.android.presentation.security.pattern import android.app.Activity import android.content.Context import android.content.Intent -import android.os.Build import android.os.PowerManager import android.os.SystemClock import com.owncloud.android.MainApp.Companion.appContext -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.presentation.security.LockTimeout import com.owncloud.android.presentation.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP import com.owncloud.android.presentation.security.PREFERENCE_LOCK_TIMEOUT @@ -45,7 +44,7 @@ object PatternManager { if (!exemptOfPatternActivities.contains(activity.javaClass) && patternShouldBeRequested()) { // Do not ask for pattern if biometric is enabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BiometricManager.isBiometricEnabled() && !visibleActivities.contains( + if (BiometricManager.isBiometricEnabled() && !visibleActivities.contains( PatternActivity::class.java ) ) { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternViewModel.kt index 1239e81071e..7d9ec77cdff 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/security/pattern/PatternViewModel.kt @@ -21,7 +21,7 @@ package com.owncloud.android.presentation.security.pattern import androidx.lifecycle.ViewModel -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.security.biometric.BiometricActivity class PatternViewModel( diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/advanced/SettingsAdvancedViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/advanced/SettingsAdvancedViewModel.kt index 166f341d497..1c81193407b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/advanced/SettingsAdvancedViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/advanced/SettingsAdvancedViewModel.kt @@ -20,7 +20,7 @@ package com.owncloud.android.presentation.settings.advanced import androidx.lifecycle.ViewModel -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.settings.advanced.SettingsAdvancedFragment.Companion.PREF_SHOW_HIDDEN_FILES class SettingsAdvancedViewModel( diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/logging/SettingsLogsViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/logging/SettingsLogsViewModel.kt index 813be57e8b2..635ab5905ee 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/logging/SettingsLogsViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/logging/SettingsLogsViewModel.kt @@ -21,7 +21,7 @@ package com.owncloud.android.presentation.settings.logging import androidx.lifecycle.ViewModel -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.providers.LogsProvider import com.owncloud.android.providers.WorkManagerProvider diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/security/SettingsSecurityFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/security/SettingsSecurityFragment.kt index 64975dd6217..0e3cbab8d17 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/security/SettingsSecurityFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/security/SettingsSecurityFragment.kt @@ -3,17 +3,17 @@ * * @author Juan Carlos Garrote Gascón * - * Copyright (C) 2022 ownCloud GmbH. - *

+ * Copyright (C) 2023 ownCloud GmbH. + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. - *

+ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - *

+ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -23,7 +23,6 @@ package com.owncloud.android.presentation.settings.security import android.app.Activity import android.content.DialogInterface import android.content.Intent -import android.os.Build import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog @@ -169,9 +168,7 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() { } // Biometric lock - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - screenSecurity?.removePreferenceFromScreen(prefBiometric) - } else if (prefBiometric != null) { + if (prefBiometric != null) { if (!BiometricManager.isHardwareDetected()) { // Biometric not supported screenSecurity?.removePreferenceFromScreen(prefBiometric) } else { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/security/SettingsSecurityViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/security/SettingsSecurityViewModel.kt index 77bd6345219..9b931d8a64c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/security/SettingsSecurityViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/settings/security/SettingsSecurityViewModel.kt @@ -22,14 +22,16 @@ package com.owncloud.android.presentation.settings.security import androidx.lifecycle.ViewModel import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.security.LockEnforcedType import com.owncloud.android.presentation.security.LockEnforcedType.Companion.parseFromInteger import com.owncloud.android.presentation.security.LockTimeout import com.owncloud.android.presentation.security.biometric.BiometricActivity +import com.owncloud.android.presentation.security.isDeviceSecure import com.owncloud.android.presentation.security.passcode.PassCodeActivity import com.owncloud.android.presentation.security.pattern.PatternActivity import com.owncloud.android.providers.MdmProvider +import com.owncloud.android.utils.CONFIGURATION_DEVICE_PROTECTION import com.owncloud.android.utils.CONFIGURATION_LOCK_DELAY_TIME import com.owncloud.android.utils.NO_MDM_RESTRICTION_YET @@ -50,8 +52,10 @@ class SettingsSecurityViewModel( fun getBiometricsState(): Boolean = preferencesProvider.getBoolean(BiometricActivity.PREFERENCE_SET_BIOMETRIC, false) + // If device protection is true and device is not secure, or lock_enforced is enabled. fun isSecurityEnforcedEnabled() = - parseFromInteger(mdmProvider.getBrandingInteger(NO_MDM_RESTRICTION_YET, R.integer.lock_enforced)) != LockEnforcedType.DISABLED + (mdmProvider.getBrandingBoolean(CONFIGURATION_DEVICE_PROTECTION, R.bool.device_protection) && !isDeviceSecure()) || + parseFromInteger(mdmProvider.getBrandingInteger(NO_MDM_RESTRICTION_YET, R.integer.lock_enforced)) != LockEnforcedType.DISABLED fun isLockDelayEnforcedEnabled() = LockTimeout.parseFromInteger( mdmProvider.getBrandingInteger( diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareViewModel.kt index 6a840bd284c..2fa41602f4c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/ShareViewModel.kt @@ -175,7 +175,6 @@ class ShareViewModel( name: String, password: String, expirationTimeInMillis: Long, - publicUpload: Boolean, accountName: String ) = runUseCaseWithResult( coroutineDispatcher = coroutineDispatcherProvider.io, @@ -188,7 +187,6 @@ class ShareViewModel( name, password, expirationTimeInMillis, - publicUpload, accountName ), postSuccessWithData = false @@ -203,7 +201,6 @@ class ShareViewModel( password: String?, expirationDateInMillis: Long, permissions: Int, - publicUpload: Boolean, accountName: String ) = runUseCaseWithResult( coroutineDispatcher = coroutineDispatcherProvider.io, @@ -216,7 +213,6 @@ class ShareViewModel( password, expirationDateInMillis, permissions, - publicUpload, accountName ), postSuccessWithData = false diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/shares/PublicShareDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/shares/PublicShareDialogFragment.kt index cb2314ff217..b97e3bc2035 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/shares/PublicShareDialogFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/sharing/shares/PublicShareDialogFragment.kt @@ -282,7 +282,6 @@ class PublicShareDialogFragment : DialogFragment() { publicLinkName, publicLinkPassword!!, publicLinkExpirationDateInMillis, - false, account?.name!! ) } else { // Updating an existing public share @@ -298,7 +297,6 @@ class PublicShareDialogFragment : DialogFragment() { publicLinkPassword, publicLinkExpirationDateInMillis, publicLinkPermissions, - publicUploadPermission, account?.name!! ) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListAdapter.kt index 0dfe3172898..976500fd6c2 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListAdapter.kt @@ -2,6 +2,7 @@ * ownCloud Android client application * * @author Juan Carlos Garrote Gascón + * @author Manuel Plazas Palacio * * Copyright (C) 2023 ownCloud GmbH. * @@ -22,15 +23,14 @@ package com.owncloud.android.presentation.spaces import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.ImageView import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import coil.load import com.owncloud.android.R import com.owncloud.android.databinding.SpacesListItemBinding -import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.domain.spaces.model.OCSpace -import com.owncloud.android.presentation.authentication.AccountUtils +import com.owncloud.android.presentation.thumbnails.ThumbnailsRequester import com.owncloud.android.utils.PreferenceUtils class SpacesListAdapter( @@ -65,34 +65,16 @@ class SpacesListAdapter( val spaceSpecialImage = space.getSpaceSpecialImage() if (spaceSpecialImage != null) { - val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(spaceSpecialImage.id) - if (thumbnail != null) { - spacesListItemImage.run { - setImageBitmap(thumbnail) - scaleType = ImageView.ScaleType.CENTER_CROP - } - } - if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(spaceSpecialImage, spacesListItemImage)) { - val account = AccountUtils.getOwnCloudAccountByName(spacesViewHolder.itemView.context, space.accountName) - val task = ThumbnailsCacheManager.ThumbnailGenerationTask(spacesListItemImage, account) - val asyncDrawable = ThumbnailsCacheManager.AsyncThumbnailDrawable(spacesViewHolder.itemView.resources, thumbnail, task) - - // If drawable is not visible, do not update it. - if (asyncDrawable.minimumHeight > 0 && asyncDrawable.minimumWidth > 0) { - spacesListItemImage.run { - spacesListItemImage.setImageDrawable(asyncDrawable) - scaleType = ImageView.ScaleType.CENTER_CROP - } - } - task.execute(spaceSpecialImage) - } - if (spaceSpecialImage.file.mimeType == "image/png") { - spacesListItemImage.setBackgroundColor(ContextCompat.getColor(spacesViewHolder.itemView.context, R.color.background_color)) + spacesListItemImage.load( + ThumbnailsRequester.getPreviewUriForSpaceSpecial(spaceSpecialImage), + ThumbnailsRequester.getCoilImageLoader() + ) { + placeholder(R.drawable.ic_spaces_placeholder) + error(R.drawable.ic_spaces_placeholder) } } else { spacesListItemImage.apply { - setImageResource(R.drawable.ic_spaces) - scaleType = ImageView.ScaleType.CENTER + setImageResource(R.drawable.ic_spaces_placeholder) setBackgroundColor(ContextCompat.getColor(spacesViewHolder.itemView.context, R.color.spaces_card_background_color)) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt index cc404921458..62430255c9d 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt @@ -120,7 +120,6 @@ class SpacesListFragment( override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) menu.findItem(R.id.action_share_current_folder)?.itemId?.let { menu.removeItem(it) } - menu.findItem(R.id.action_search)?.itemId?.let { menu.removeItem(it) } } companion object { diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/thumbnails/ThumbnailsRequester.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/thumbnails/ThumbnailsRequester.kt new file mode 100644 index 00000000000..e1441fbed39 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/thumbnails/ThumbnailsRequester.kt @@ -0,0 +1,138 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.thumbnails + +import android.accounts.Account +import android.net.Uri +import coil.ImageLoader +import coil.disk.DiskCache +import coil.memory.MemoryCache +import coil.util.DebugLogger +import com.owncloud.android.MainApp.Companion.appContext +import com.owncloud.android.R +import com.owncloud.android.data.ClientManager +import com.owncloud.android.domain.files.model.OCFileWithSyncInfo +import com.owncloud.android.domain.spaces.model.SpaceSpecial +import com.owncloud.android.lib.common.SingleSessionManager +import com.owncloud.android.lib.common.http.HttpConstants.ACCEPT_ENCODING_HEADER +import com.owncloud.android.lib.common.http.HttpConstants.ACCEPT_ENCODING_IDENTITY +import com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER +import com.owncloud.android.lib.common.http.HttpConstants.OC_X_REQUEST_ID +import com.owncloud.android.lib.common.http.HttpConstants.USER_AGENT_HEADER +import com.owncloud.android.lib.common.utils.RandomUtils +import com.owncloud.android.presentation.authentication.AccountUtils +import okhttp3.Headers.Companion.toHeaders +import okhttp3.Interceptor +import okhttp3.Response +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import timber.log.Timber +import java.util.Locale +import kotlin.math.roundToInt + +object ThumbnailsRequester : KoinComponent { + private val clientManager: ClientManager by inject() + + private const val SPACE_SPECIAL_PREVIEW_URI = "%s?scalingup=0&a=1&x=%d&y=%d&c=%s&preview=1" + private const val FILE_PREVIEW_URI = "%s%s?x=%d&y=%d&c=%s&preview=1&id=%s" + + private const val DISK_CACHE_SIZE: Long = 1024 * 1024 * 10 // 10MB + + fun getCoilImageLoader(): ImageLoader { + val ownCloudClient = getOwnCloudClient() + + val coilRequestHeaderInterceptor = CoilRequestHeaderInterceptor( + requestHeaders = hashMapOf( + AUTHORIZATION_HEADER to ownCloudClient.credentials.headerAuth, + ACCEPT_ENCODING_HEADER to ACCEPT_ENCODING_IDENTITY, + USER_AGENT_HEADER to SingleSessionManager.getUserAgent(), + OC_X_REQUEST_ID to RandomUtils.generateRandomUUID(), + ) + ) + + return ImageLoader(appContext).newBuilder().okHttpClient( + okHttpClient = ownCloudClient.okHttpClient.newBuilder().addNetworkInterceptor(coilRequestHeaderInterceptor).build() + ).logger(DebugLogger()) + .memoryCache { + MemoryCache.Builder(appContext) + .maxSizePercent(0.1) + .build() + } + .diskCache { + DiskCache.Builder() + .directory(appContext.cacheDir.resolve("thumbnails_coil_cache")) + .maxSizeBytes(DISK_CACHE_SIZE) + .build() + } + .build() + } + + fun getPreviewUriForSpaceSpecial(spaceSpecial: SpaceSpecial): String { + // Converts dp to pixel + val spacesThumbnailSize = appContext.resources.getDimension(R.dimen.spaces_thumbnail_height).roundToInt() + return String.format( + Locale.ROOT, + SPACE_SPECIAL_PREVIEW_URI, + spaceSpecial.webDavUrl, + spacesThumbnailSize, + spacesThumbnailSize, + spaceSpecial.eTag + ) + } + + fun getPreviewUriForFile(ocFile: OCFileWithSyncInfo, account: Account): String { + var baseUrl = getOwnCloudClient().baseUri.toString() + "/remote.php/dav/files/" + account.name.split("@".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray()[0] + ocFile.space?.getSpaceSpecialImage()?.let { + baseUrl = it.webDavUrl + } + + // Converts dp to pixel + val fileThumbnailSize = appContext.resources.getDimension(R.dimen.file_icon_size_grid).roundToInt() + return String.format( + Locale.ROOT, + FILE_PREVIEW_URI, + baseUrl, + Uri.encode(ocFile.file.remotePath, "/"), + fileThumbnailSize, + fileThumbnailSize, + ocFile.file.etag, + "${ocFile.file.remoteId}${ocFile.file.modificationTimestamp}", + ) + } + + private fun getOwnCloudClient() = clientManager.getClientForCoilThumbnails( + accountName = AccountUtils.getCurrentOwnCloudAccount(appContext).name + ) + + private class CoilRequestHeaderInterceptor( + private val requestHeaders: HashMap + ) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request().newBuilder() + requestHeaders.toHeaders().forEach { request.addHeader(it.first, it.second) } + return chain.proceed(request.build()).newBuilder().removeHeader("Cache-Control") + .addHeader("Cache-Control", "max-age=5000 , must-revalidate, value").build().also { Timber.d("Header :" + it.headers) } + } + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/providers/FileContentProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/providers/FileContentProvider.kt index 367a1ca9737..ad6f4018ba8 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/providers/FileContentProvider.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/providers/FileContentProvider.kt @@ -58,8 +58,8 @@ import com.owncloud.android.data.files.db.OCFileEntity import com.owncloud.android.data.folderbackup.datasources.FolderBackupLocalDataSource import com.owncloud.android.data.folderbackup.datasources.implementation.OCFolderBackupLocalDataSource import com.owncloud.android.data.migrations.CameraUploadsMigrationToRoom -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.data.transfers.db.OCTransferEntity import com.owncloud.android.db.ProviderMeta.ProviderTableMeta import com.owncloud.android.domain.camerauploads.model.UploadBehavior diff --git a/owncloudApp/src/main/java/com/owncloud/android/providers/LogsProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/providers/LogsProvider.kt index f72581e261e..151a0d1fc81 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/providers/LogsProvider.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/providers/LogsProvider.kt @@ -22,8 +22,8 @@ package com.owncloud.android.providers import android.content.Context import com.owncloud.android.BuildConfig import com.owncloud.android.MainApp -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider -import com.owncloud.android.data.storage.ScopedStorageProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.ScopedStorageProvider import com.owncloud.android.lib.common.http.LogInterceptor import com.owncloud.android.lib.common.utils.LoggingHelper import timber.log.Timber diff --git a/owncloudApp/src/main/java/com/owncloud/android/providers/MdmProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/providers/MdmProvider.kt index 36d951db398..d6c7a8f9871 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/providers/MdmProvider.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/providers/MdmProvider.kt @@ -29,7 +29,7 @@ import androidx.enterprise.feedback.KeyedAppState import androidx.enterprise.feedback.KeyedAppStatesReporter import com.owncloud.android.BuildConfig import com.owncloud.android.MainApp.Companion.MDM_FLAVOR -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.utils.MDMConfigurations import timber.log.Timber diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 2931cba2e7c..196ed8712d8 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -52,7 +52,7 @@ import com.owncloud.android.AppRater import com.owncloud.android.BuildConfig import com.owncloud.android.MainApp import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.databinding.ActivityMainBinding import com.owncloud.android.domain.camerauploads.model.UploadBehavior import com.owncloud.android.domain.capabilities.model.OCCapability @@ -96,6 +96,7 @@ import com.owncloud.android.presentation.spaces.SpacesListFragment.Companion.REQ import com.owncloud.android.presentation.transfers.TransfersViewModel import com.owncloud.android.providers.WorkManagerProvider import com.owncloud.android.syncadapter.FileSyncAdapter +import com.owncloud.android.ui.dialog.FileAlreadyExistsDialog import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.ui.fragment.TaskRetainerFragment import com.owncloud.android.ui.helpers.FilesUploadHelper @@ -438,6 +439,7 @@ class FileDisplayActivity : FileActivity(), autoplay ) } + PreviewVideoFragment.canBePreviewed(file) -> { val startPlaybackPosition = intent.getIntExtra(PreviewVideoActivity.EXTRA_START_POSITION, 0) val autoplay = intent.getBooleanExtra(PreviewVideoActivity.EXTRA_AUTOPLAY, true) @@ -448,12 +450,14 @@ class FileDisplayActivity : FileActivity(), autoplay ) } + PreviewTextFragment.canBePreviewed(file) -> { PreviewTextFragment.newInstance( file, account ) } + else -> { FileDetailsFragment.newInstance(file, account) } @@ -657,7 +661,11 @@ class FileDisplayActivity : FileActivity(), private fun requestMoveOperation(data: Intent) { val folderToMoveAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER) ?: return val files = data.getParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES) ?: return - val moveOperation = FileOperation.MoveOperation(listOfFilesToMove = files.toList(), targetFolder = folderToMoveAt) + val moveOperation = FileOperation.MoveOperation( + listOfFilesToMove = files.toList(), + targetFolder = folderToMoveAt, + isUserLogged = com.owncloud.android.presentation.authentication.AccountUtils.getCurrentOwnCloudAccount(this) != null, + ) fileOperationsViewModel.performOperation(moveOperation) } @@ -669,7 +677,11 @@ class FileDisplayActivity : FileActivity(), private fun requestCopyOperation(data: Intent) { val folderToCopyAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER) ?: return val files = data.getParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES) ?: return - val copyOperation = FileOperation.CopyOperation(listOfFilesToCopy = files.toList(), targetFolder = folderToCopyAt) + val copyOperation = FileOperation.CopyOperation( + listOfFilesToCopy = files.toList(), + targetFolder = folderToCopyAt, + isUserLogged = com.owncloud.android.presentation.authentication.AccountUtils.getCurrentOwnCloudAccount(this) != null, + ) fileOperationsViewModel.performOperation(copyOperation) } @@ -696,7 +708,8 @@ class FileDisplayActivity : FileActivity(), if (secondFragment != null) { // If secondFragment was shown, we need to navigate to the parent of the displayed file // Need a cleanup - val folderIdToDisplay = if (fileListOption == FileListOption.AV_OFFLINE) storageManager.getRootPersonalFolder()!!.id!! else secondFragment!!.file!!.parentId!! + val folderIdToDisplay = + if (fileListOption == FileListOption.AV_OFFLINE) storageManager.getRootPersonalFolder()!!.id!! else secondFragment!!.file!!.parentId!! mainFileListFragment?.navigateToFolderId(folderIdToDisplay) cleanSecondFragment() updateToolbar(mainFileListFragment?.getCurrentFile()) @@ -729,6 +742,7 @@ class FileDisplayActivity : FileActivity(), // responsibility of restore is preferred in onCreate() before than in // onRestoreInstanceState when there are Fragments involved Timber.v("onSaveInstanceState() start") + super.onSaveInstanceState(outState) outState.putParcelable(KEY_WAITING_TO_PREVIEW, fileWaitingToPreview) outState.putBoolean(KEY_SYNC_IN_PROGRESS, syncInProgress) @@ -764,6 +778,7 @@ class FileDisplayActivity : FileActivity(), syncBroadcastReceiver = SyncBroadcastReceiver() localBroadcastManager!!.registerReceiver(syncBroadcastReceiver!!, syncIntentFilter) + showDialogs() Timber.v("onResume() end") } @@ -775,6 +790,7 @@ class FileDisplayActivity : FileActivity(), } super.onPause() + dismissDialogs() Timber.v("onPause() end") } @@ -914,6 +930,7 @@ class FileDisplayActivity : FileActivity(), } else { getString(R.string.bottom_nav_links) } + FileListOption.ALL_FILES -> getString(R.string.default_display_name_for_root_folder) FileListOption.SPACES_LIST -> getString(R.string.bottom_nav_spaces) } @@ -937,6 +954,7 @@ class FileDisplayActivity : FileActivity(), is UIResult.Loading -> { // Not blocking the UI } + is UIResult.Success -> { val listOfFilesRemoved = uiResult.data ?: return val lastRemovedFile = listOfFilesRemoved.last() @@ -953,6 +971,7 @@ class FileDisplayActivity : FileActivity(), is PreviewAudioFragment -> { secondFragment.stopPreview() } + is PreviewVideoFragment -> { secondFragment.releasePlayer() } @@ -962,6 +981,7 @@ class FileDisplayActivity : FileActivity(), } invalidateOptionsMenu() } + is UIResult.Error -> { showErrorInSnackbar(R.string.remove_fail_msg, uiResult.getThrowableOrNull()) @@ -977,15 +997,23 @@ class FileDisplayActivity : FileActivity(), * file. */ private fun onMoveFileOperationFinish( - uiResult: UIResult + uiResult: UIResult> ) { when (uiResult) { is UIResult.Loading -> { showLoadingDialog(R.string.wait_a_moment) } + is UIResult.Success -> { dismissLoadingDialog() + val replace = mutableListOf() + uiResult.data?.let { + showConflictDecisionDialog(uiResult = uiResult, data = it, replace = replace) { data, replace -> + launchMoveFile(data, replace) + } + } } + is UIResult.Error -> { dismissLoadingDialog() @@ -1005,25 +1033,199 @@ class FileDisplayActivity : FileActivity(), * @param uiResult - UIResult wrapping the target folder where files were copied */ private fun onCopyFileOperationFinish( - uiResult: UIResult + uiResult: UIResult> ) { when (uiResult) { is UIResult.Loading -> { showLoadingDialog(R.string.wait_a_moment) } + is UIResult.Success -> { dismissLoadingDialog() + val replace = mutableListOf() + uiResult.data?.let { + showConflictDecisionDialog(uiResult = uiResult, data = it, replace = replace) { data, replace -> + launchCopyFile(data, replace) + } + } } + is UIResult.Error -> { dismissLoadingDialog() uiResult.error?.let { showMessageInSnackbar( - message = it.parseError(getString(R.string.copy_file_error), resources, true) + message = it.parseError( + genericErrorMessage = getString(R.string.copy_file_error), + resources = resources, + showJustReason = true, + ) + ) + } + } + } + } + + private fun showConflictDecisionDialog( + uiResult: UIResult.Success>, + data: List, + replace: MutableList, + launchAction: (List, List) -> Unit, + ) { + if (!uiResult.data.isNullOrEmpty()) { + val posArray = intArrayOf(0) + var posDialog = intArrayOf(data.lastIndex) + data.asReversed().forEachIndexed { index, file -> + val countDownValue = index + 1 + + val customDialog = FileAlreadyExistsDialog.newInstance( + titleText = this.getString( + if (file.isFolder) { + R.string.folder_already_exists + } else { + R.string.file_already_exists + } + ), + descriptionText = this.getString( + if (file.isFolder) { + R.string.folder_already_exists_description + } else { + R.string.file_already_exists_description + }, file.fileName + ), + checkboxText = this.getString(R.string.apply_to_all_conflicts, countDownValue.toString()), + checkboxVisible = countDownValue > 1 + ) + customDialog.isCancelable = false + customDialog.show(this.supportFragmentManager, CUSTOM_DIALOG_TAG) + + fileOperationsViewModel.openDialogs.add(customDialog) + + + customDialog.setDialogButtonClickListener(object : FileAlreadyExistsDialog.DialogButtonClickListener { + + override fun onKeepBothButtonClick() { + applyAction( + posDialog = posDialog, + data = data, + replace = replace, + pos = posArray, + launchAction = launchAction, + uiResult = uiResult, + action = false + ) + } + + override fun onSkipButtonClick() { + applyAction( + posDialog = posDialog, + data = data, + replace = replace, + pos = posArray, + launchAction = launchAction, + uiResult = uiResult, + action = null + ) + } + + override fun onReplaceButtonClick() { + applyAction( + posDialog = posDialog, + data = data, + replace = replace, + pos = posArray, + launchAction = launchAction, + uiResult = uiResult, + action = true + ) + } + } + ) + } + } + } + + private fun applyAction( + posDialog: IntArray, + data: List, + replace: MutableList, + pos: IntArray, + launchAction: (List, List) -> Unit, + uiResult: UIResult.Success>, + action: Boolean? + ) { + var posDialog1 = posDialog + var pos1 = pos + if (fileOperationsViewModel.openDialogs[posDialog1[0]].isCheckBoxChecked) { + repeat(data.asReversed().size) { + replace.add(action) + pos1[0]++ + if (pos1[0] == data.size) { + launchAction( + uiResult.data!!, + replace, ) } } + dismissAllOpenDialogs() + } else { + replace.add(action) + pos1[0]++ + if (pos1[0] == data.size) { + launchAction( + uiResult.data!!, + replace, + ) + } + fileOperationsViewModel.openDialogs[posDialog1[0]].dismiss() + fileOperationsViewModel.openDialogs.removeAt(posDialog1[0]) + if (posDialog1[0] == 0) { + fileOperationsViewModel.openDialogs.clear() + } else { + posDialog1[0]-- + } + } + } + + private fun dismissAllOpenDialogs() { + fileOperationsViewModel.openDialogs.forEach { dialog -> + dialog.dismiss() } + fileOperationsViewModel.openDialogs.clear() + } + + private fun showDialogs() { + fileOperationsViewModel.openDialogs.forEach { dialog -> + dialog.show(this.supportFragmentManager, CUSTOM_DIALOG_TAG) + } + } + + private fun dismissDialogs() { + fileOperationsViewModel.openDialogs.forEach { dialog -> + dialog.dismiss() + } + } + + private fun launchCopyFile(files: List, replace: List) { + fileOperationsViewModel.performOperation( + FileOperation.CopyOperation( + listOfFilesToCopy = files, + targetFolder = null, + replace = replace, + isUserLogged = com.owncloud.android.presentation.authentication.AccountUtils.getCurrentOwnCloudAccount(this) != null, + ) + ) + } + + private fun launchMoveFile(files: List, replace: List) { + fileOperationsViewModel.performOperation( + FileOperation.MoveOperation( + listOfFilesToMove = files, + targetFolder = null, + replace = replace, + isUserLogged = com.owncloud.android.presentation.authentication.AccountUtils.getCurrentOwnCloudAccount(this) != null, + ) + ) } /** @@ -1039,6 +1241,7 @@ class FileDisplayActivity : FileActivity(), is UIResult.Loading -> { showLoadingDialog(R.string.wait_a_moment) } + is UIResult.Success -> { dismissLoadingDialog() @@ -1052,6 +1255,7 @@ class FileDisplayActivity : FileActivity(), } } } + is UIResult.Error -> { dismissLoadingDialog() @@ -1078,11 +1282,13 @@ class FileDisplayActivity : FileActivity(), showSnackMessage(getString(R.string.sync_file_nothing_to_do_msg)) } } + is SynchronizeFileUseCase.SyncType.ConflictDetected -> { val showConflictActivityIntent = Intent(this, ConflictsResolveActivity::class.java) showConflictActivityIntent.putExtra(ConflictsResolveActivity.EXTRA_FILE, file) startActivity(showConflictActivityIntent) } + is SynchronizeFileUseCase.SyncType.DownloadEnqueued -> { fileWaitingToPreview?.let { showSnackMessage(getString(R.string.new_remote_version_found_msg)) @@ -1090,13 +1296,16 @@ class FileDisplayActivity : FileActivity(), fileWaitingToPreview = null } ?: showSnackMessage(getString(R.string.download_enqueued_msg)) } + SynchronizeFileUseCase.SyncType.FileNotFound -> { /** Nothing to do atm. If we are in details view, go back to file list */ } + is SynchronizeFileUseCase.SyncType.UploadEnqueued -> showSnackMessage(getString(R.string.upload_enqueued_msg)) null -> TODO() } } + is UIResult.Error -> { if (fileWaitingToPreview != null) { startPreview(fileWaitingToPreview) @@ -1116,14 +1325,17 @@ class FileDisplayActivity : FileActivity(), } } } + is CertificateCombinedException -> { showUntrustedCertDialogForThrowable(uiResult.error) } + else -> { showSnackMessage(getString(R.string.sync_fail_ticker)) } } } + is UIResult.Loading -> { /** Not needed at the moment, we may need it later */ } @@ -1137,6 +1349,7 @@ class FileDisplayActivity : FileActivity(), is UIResult.Success -> { // Nothing to handle when synchronizing a folder succeeds } + is UIResult.Error -> { when (uiResult.error) { is UnauthorizedException -> { @@ -1152,14 +1365,17 @@ class FileDisplayActivity : FileActivity(), } } } + is CertificateCombinedException -> { showUntrustedCertDialogForThrowable(uiResult.error) } + else -> { showSnackMessage(getString(R.string.sync_fail_ticker)) } } } + is UIResult.Loading -> { /** Not needed at the moment, we may need it later */ } @@ -1367,6 +1583,7 @@ class FileDisplayActivity : FileActivity(), PreviewTextFragment.canBePreviewed(file) -> { startTextPreview(file) } + PreviewAudioFragment.canBePreviewed(file) -> { startAudioPreview(file, 0) } @@ -1411,6 +1628,7 @@ class FileDisplayActivity : FileActivity(), browseToRoot() } } + FileListOption.SPACES_LIST -> { if (previousFileListOption != newFileListOption || initialState) { file = null @@ -1418,6 +1636,7 @@ class FileDisplayActivity : FileActivity(), updateToolbar(null) } } + FileListOption.SHARED_BY_LINK -> { if (previousFileListOption != newFileListOption || initialState) { val rootFolderForShares = storageManager.getRootSharesFolder() @@ -1434,6 +1653,7 @@ class FileDisplayActivity : FileActivity(), } } } + FileListOption.AV_OFFLINE -> { if (previousFileListOption != newFileListOption || initialState) { file = storageManager.getRootPersonalFolder() @@ -1495,16 +1715,19 @@ class FileDisplayActivity : FileActivity(), // preview image - it handles the sync, if needed startImagePreview(file) } + PreviewTextFragment.canBePreviewed(file) -> { setFile(file) fileWaitingToPreview = file fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(file, account.name)) } + PreviewAudioFragment.canBePreviewed(file) -> { setFile(file) fileWaitingToPreview = file fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(file, account.name)) } + PreviewVideoFragment.canBePreviewed(file) && !WorkManager.getInstance(this).isDownloadPending(account, file) -> { // Available offline but not downloaded yet, don't initialize streaming if (!file.isAvailableLocally && file.isAvailableOffline) { @@ -1521,6 +1744,7 @@ class FileDisplayActivity : FileActivity(), fileOperationsViewModel.performOperation(FileOperation.SynchronizeFileOperation(file, account.name)) } } + else -> { startSyncThenOpen(file) } @@ -1569,6 +1793,8 @@ class FileDisplayActivity : FileActivity(), private const val KEY_UPLOAD_HELPER = "FILE_UPLOAD_HELPER" private const val KEY_FILE_LIST_OPTION = "FILE_LIST_OPTION" + private const val CUSTOM_DIALOG_TAG = "CUSTOM_DIALOG" + private const val PREFERENCE_NOTIFICATION_PERMISSION_REQUESTED = "PREFERENCE_NOTIFICATION_PERMISSION_REQUESTED" const val ALL_FILES_SAF_REGEX = "*/*" diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index 4ecf11151ed..52d6a6bcccb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -106,6 +106,8 @@ import java.util.List; import java.util.Stack; import java.util.Vector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.koin.java.KoinJavaComponent.get; import static org.koin.java.KoinJavaComponent.inject; @@ -161,6 +163,8 @@ public class ReceiveExternalFilesActivity extends FileActivity private static final String DIALOG_WAIT_COPY_FILE = "DIALOG_WAIT_COPY_FILE"; + Pattern pattern = Pattern.compile("[/\\\\]"); + private ReceiveExternalFilesViewModel mReceiveExternalFilesViewModel; @Override @@ -865,6 +869,27 @@ private void showUploadTextDialog() { final TextInputEditText input = dialogView.findViewById(R.id.inputFileName); final TextInputLayout inputLayout = dialogView.findViewById(R.id.inputTextLayout); + final AlertDialog alertDialog = builder.create(); + setFileNameFromIntent(alertDialog, input); + alertDialog.setOnShowListener(dialog -> { + Button button = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(view -> { + String fileName = input.getText().toString(); + String error = null; + fileName += ".txt"; + String filePath = savePlainTextToFile(fileName); + ArrayList fileToUpload = new ArrayList<>(); + fileToUpload.add(filePath); + @NotNull Lazy transfersViewModelLazy = inject(TransfersViewModel.class); + TransfersViewModel transfersViewModel = transfersViewModelLazy.getValue(); + transfersViewModel.uploadFilesFromSystem(getAccount().name, fileToUpload, mUploadPath, mPersonalSpaceId); + finish(); + + inputLayout.setErrorEnabled(error != null); + inputLayout.setError(error); + }); + }); + input.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { @@ -873,44 +898,35 @@ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void afterTextChanged(Editable editable) { - inputLayout.setError(null); - inputLayout.setErrorEnabled(false); - } - }); - - final AlertDialog alertDialog = builder.create(); - setFileNameFromIntent(alertDialog, input); - alertDialog.setOnShowListener(dialog -> { - Button button = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - button.setOnClickListener(view -> { + Button okButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); String fileName = input.getText().toString(); String error = null; - if (fileName.length() > MAX_FILENAME_LENGTH) { + Matcher matcher = pattern.matcher(fileName); + if (charSequence == null || charSequence.toString().trim().isEmpty()) { + okButton.setEnabled(false); + error = getString(R.string.uploader_upload_text_dialog_filename_error_empty); + } else if (charSequence.length() > MAX_FILENAME_LENGTH) { error = String.format(getString(R.string.uploader_upload_text_dialog_filename_error_length_max), MAX_FILENAME_LENGTH); - } else if (fileName.length() == 0) { - error = getString(R.string.uploader_upload_text_dialog_filename_error_empty); - } else if (fileName.contains("/")) { + } else if (matcher.find()) { error = getString(R.string.filename_forbidden_characters); } else { - fileName += ".txt"; - String filePath = savePlainTextToFile(fileName); - ArrayList fileToUpload = new ArrayList<>(); - fileToUpload.add(filePath); - @NotNull Lazy transfersViewModelLazy = inject(TransfersViewModel.class); - TransfersViewModel transfersViewModel = transfersViewModelLazy.getValue(); - transfersViewModel.uploadFilesFromSystem(getAccount().name, fileToUpload, mUploadPath, mPersonalSpaceId); - finish(); + okButton.setEnabled(true); + error = null; + inputLayout.setError(error); } - inputLayout.setErrorEnabled(error != null); - inputLayout.setError(error); - }); + + if (error != null) { + okButton.setEnabled(false); + inputLayout.setError(error); + } + } + + @Override + public void afterTextChanged(Editable editable) { + } }); + alertDialog.show(); } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/SplashActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/SplashActivity.kt index 3a9b98c3480..46113a0798a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/SplashActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/SplashActivity.kt @@ -26,11 +26,12 @@ import androidx.appcompat.app.AppCompatActivity import com.owncloud.android.BuildConfig import com.owncloud.android.MainApp import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider import com.owncloud.android.presentation.security.LockTimeout import com.owncloud.android.presentation.security.PREFERENCE_LOCK_TIMEOUT import com.owncloud.android.providers.MdmProvider import com.owncloud.android.utils.CONFIGURATION_ALLOW_SCREENSHOTS +import com.owncloud.android.utils.CONFIGURATION_DEVICE_PROTECTION import com.owncloud.android.utils.CONFIGURATION_LOCK_DELAY_TIME import com.owncloud.android.utils.CONFIGURATION_OAUTH2_OPEN_ID_PROMPT import com.owncloud.android.utils.CONFIGURATION_OAUTH2_OPEN_ID_SCOPE @@ -52,6 +53,7 @@ class SplashActivity : AppCompatActivity() { cacheBooleanRestriction(CONFIGURATION_ALLOW_SCREENSHOTS, R.string.allow_screenshots_configuration_feedback_ok) cacheStringRestriction(CONFIGURATION_OAUTH2_OPEN_ID_SCOPE, R.string.oauth2_open_id_scope_configuration_feedback_ok) cacheStringRestriction(CONFIGURATION_OAUTH2_OPEN_ID_PROMPT, R.string.oauth2_open_id_prompt_configuration_feedback_ok) + cacheBooleanRestriction(CONFIGURATION_DEVICE_PROTECTION, R.string.device_protection_configuration_feedback_ok) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.kt index 438e7e9b9c2..67aa5c65fa4 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.kt @@ -83,6 +83,7 @@ abstract class ToolbarActivity : BaseActivity() { } toolbarTitle.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_search, 0) } else { + setOnClickListener(null) toolbarTitle.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/FileAlreadyExistsDialog.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/FileAlreadyExistsDialog.kt new file mode 100644 index 00000000000..123f74876bf --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/FileAlreadyExistsDialog.kt @@ -0,0 +1,82 @@ +package com.owncloud.android.ui.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.owncloud.android.databinding.DialogFileAlreadyExistsBinding + +class FileAlreadyExistsDialog : DialogFragment() { + + private lateinit var binding: DialogFileAlreadyExistsBinding + internal var isCheckBoxChecked: Boolean = false + + interface DialogButtonClickListener { + fun onKeepBothButtonClick() + fun onSkipButtonClick() + fun onReplaceButtonClick() + } + + companion object { + var mListener: DialogButtonClickListener? = null + + const val TITLE_TEXT = "titleText" + const val DESCRIPTION_TEXT = "descriptionText" + const val CHECKBOX_TEXT = "checkboxText" + private const val CHECKBOX_VISIBLE = "checkboxVisible" + + fun newInstance( + titleText: String?, + descriptionText: String?, + checkboxText: String?, + checkboxVisible: Boolean, + dialogClickListener: DialogButtonClickListener? = null, + ): FileAlreadyExistsDialog { + val fragment = FileAlreadyExistsDialog() + val args = Bundle() + args.putString(TITLE_TEXT, titleText) + args.putString(DESCRIPTION_TEXT, descriptionText) + args.putString(CHECKBOX_TEXT, checkboxText) + args.putBoolean(CHECKBOX_VISIBLE, checkboxVisible) + + mListener = dialogClickListener + fragment.arguments = args + return fragment + } + } + + fun setDialogButtonClickListener(listener: DialogButtonClickListener) = apply { mListener = listener } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + binding = DialogFileAlreadyExistsBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val titleText = arguments?.getString(TITLE_TEXT) + val descriptionText = arguments?.getString(DESCRIPTION_TEXT) + val checkboxText = arguments?.getString(CHECKBOX_TEXT) + val checkboxVisible = arguments?.getBoolean(CHECKBOX_VISIBLE) + + binding.dialogFileAlreadyExistsTitle.text = titleText + binding.dialogFileAlreadyExistsInformation.text = descriptionText + binding.dialogFileAlreadyExistsCheckbox.text = checkboxText + + binding.dialogFileAlreadyExistsKeepBoth.setOnClickListener { mListener?.onKeepBothButtonClick() } + binding.dialogFileAlreadyExistsCheckbox.setOnCheckedChangeListener { _, isChecked -> + isCheckBoxChecked = isChecked + } + binding.dialogFileAlreadyExistsReplace.setOnClickListener { mListener?.onReplaceButtonClick() } + binding.dialogFileAlreadyExistsSkip.setOnClickListener { mListener?.onSkipButtonClick() } + + binding.dialogFileAlreadyExistsCheckbox.visibility = if (checkboxVisible == true) { View.VISIBLE } else { View.GONE } + } + +} \ No newline at end of file diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewAudioFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewAudioFragment.kt index b66389b4a49..403d5b13d8f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewAudioFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewAudioFragment.kt @@ -6,23 +6,23 @@ * @author David González Verdugo * @author Abel García de Prada * @author Shashvat Kedia - * Copyright (C) 2020 ownCloud GmbH. + * @author Juan Carlos Garrote Gascón * + * Copyright (C) 2023 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. * - * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * - * * You should have received a copy of the GNU General Public License * along with this program. If not, see //www.gnu.org/licenses/>. */ + package com.owncloud.android.ui.preview import android.accounts.Account @@ -44,29 +44,30 @@ import android.widget.ImageView import android.widget.ProgressBar import com.owncloud.android.R import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.extensions.collectLatestLifecycleFlow +import com.owncloud.android.extensions.filterMenuOptions import com.owncloud.android.extensions.sendDownloadedFilesByShareSheet -import com.owncloud.android.files.FileMenuFilter import com.owncloud.android.media.MediaControlView import com.owncloud.android.media.MediaService import com.owncloud.android.media.MediaServiceBinder import com.owncloud.android.presentation.files.operations.FileOperation import com.owncloud.android.presentation.files.operations.FileOperationsViewModel import com.owncloud.android.presentation.files.removefile.RemoveFilesDialogFragment +import com.owncloud.android.presentation.previews.PreviewAudioViewModel import com.owncloud.android.ui.controller.TransferProgressController import com.owncloud.android.ui.dialog.ConfirmationDialogFragment import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.utils.PreferenceUtils import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber /** * This fragment shows a preview of a downloaded audio. * - * * Trying to get an instance with NULL [OCFile] or ownCloud [Account] values will * produce an [IllegalStateException]. * - * * If the [OCFile] passed is not downloaded, an [IllegalStateException] is * generated on instantiation too. * @@ -85,8 +86,9 @@ class PreviewAudioFragment : FileFragment() { private var mediaServiceConnection: MediaServiceConnection? = null private var autoplay = true private var progressBar: ProgressBar? = null - var progressController: TransferProgressController? = null + private var progressController: TransferProgressController? = null + private val previewAudioViewModel by viewModel() private val fileOperationsViewModel: FileOperationsViewModel by inject() /** @@ -234,47 +236,19 @@ class PreviewAudioFragment : FileFragment() { */ override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) - val fileMenuFilter = FileMenuFilter( - file, - account, - mContainerActivity, - activity - ) - fileMenuFilter.filter( - menu, - false, - false, - false, - false, - ) - - // additional restriction for this fragment - // TODO allow renaming in PreviewAudioFragment - menu.findItem(R.id.action_rename_file).apply { - isVisible = false - isEnabled = false - } + val safeFile = file + val accountName = account!!.name + previewAudioViewModel.filterMenuOptions(safeFile, accountName) - // additional restriction for this fragment - menu.findItem(R.id.action_move).apply { - isVisible = false - isEnabled = false - } - - // additional restriction for this fragment - menu.findItem(R.id.action_copy).apply { - isVisible = false - isEnabled = false + collectLatestLifecycleFlow(previewAudioViewModel.menuOptions) { menuOptions -> + val hasWritePermission = safeFile.hasWritePermission + menu.filterMenuOptions(menuOptions, hasWritePermission) } menu.findItem(R.id.action_search)?.apply { isVisible = false isEnabled = false } - menu.findItem(R.id.action_sync_file)?.apply { - isVisible = false - isEnabled = false - } } /** diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewFormatTextFragmentStateAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewFormatTextFragmentStateAdapter.kt new file mode 100644 index 00000000000..4c7f44b3fa7 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewFormatTextFragmentStateAdapter.kt @@ -0,0 +1,107 @@ +/** + * ownCloud Android client application + * + * @author Parneet Singh + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.ui.preview + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.owncloud.android.R +import io.noties.markwon.Markwon +import io.noties.markwon.ext.strikethrough.StrikethroughPlugin +import io.noties.markwon.ext.tables.TablePlugin +import io.noties.markwon.ext.tasklist.TaskListPlugin +import io.noties.markwon.html.HtmlPlugin + +class PreviewFormatTextFragmentStateAdapter( + fragment: Fragment, + private val text: String, + private val mimeType: String +) : FragmentStateAdapter(fragment) { + + val formatTypes = + mapOf(TYPE_MARKDOWN to fragment.getString(R.string.tab_label_markdown), TYPE_PLAIN to fragment.getString(R.string.tab_label_ascii)) + + override fun getItemCount(): Int = 2 + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> PreviewFormatTextFragment.newInstance(text, mimeType) + else -> PreviewFormatTextFragment.newInstance(text) + } + } + + class PreviewFormatTextFragment : Fragment() { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.preview_format_text_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val args = requireArguments() + val text = args.getString(TEXT_KEY) + val mimeType: String? = args.getString(MIME_TYPE_KEY) + + val textView: TextView = view.findViewById(R.id.text_preview) + + if (mimeType == TYPE_MARKDOWN) { + setMarkdown(textView, text!!) + } else { + textView.text = text + } + } + + private fun setMarkdown(textView: TextView, text: String) { + val context: Context = textView.context + val markwon = + Markwon.builder(context).usePlugin(TablePlugin.create(context)).usePlugin(StrikethroughPlugin.create()) + .usePlugin(TaskListPlugin.create(context)).usePlugin(HtmlPlugin.create()).build() + markwon.setMarkdown(textView, text) + } + + companion object { + private const val TEXT_KEY = "TEXT_KEY" + private const val MIME_TYPE_KEY = "MIME_TYPE_KEY" + + fun newInstance(text: String, mimeType: String? = null): PreviewFormatTextFragment { + val args = Bundle() + args.apply { + putString(TEXT_KEY, text) + putString(MIME_TYPE_KEY, mimeType) + } + val fragment = PreviewFormatTextFragment() + fragment.arguments = args + return fragment + } + + } + } + + companion object { + const val TYPE_PLAIN = "text/plain" + private const val TYPE_MARKDOWN = "text/markdown" + } +} + diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt index 39ee5d7ce12..3277a9b6b3c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt @@ -37,7 +37,7 @@ import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager.OnPageChangeListener import androidx.work.WorkInfo import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.domain.files.model.FileListOption import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PARENT_ID diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt index af6261fc507..5e6b3a7a262 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt @@ -6,23 +6,23 @@ * @author Christian Schabesberger * @author Abel García de Prada * @author Shashvat Kedia - * Copyright (C) 2020 ownCloud GmbH. + * @author Juan Carlos Garrote Gascón * + * Copyright (C) 2023 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. * - * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * - * * You should have received a copy of the GNU General Public License * along with this program. If not, see //www.gnu.org/licenses/>. */ + package com.owncloud.android.ui.preview import android.accounts.Account @@ -50,8 +50,9 @@ import com.owncloud.android.databinding.PreviewImageFragmentBinding import com.owncloud.android.databinding.TopProgressBarBinding import com.owncloud.android.domain.files.model.MIME_SVG import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.extensions.collectLatestLifecycleFlow +import com.owncloud.android.extensions.filterMenuOptions import com.owncloud.android.extensions.sendDownloadedFilesByShareSheet -import com.owncloud.android.files.FileMenuFilter import com.owncloud.android.presentation.files.operations.FileOperation import com.owncloud.android.presentation.files.operations.FileOperationsViewModel import com.owncloud.android.presentation.files.removefile.RemoveFilesDialogFragment @@ -60,6 +61,7 @@ import com.owncloud.android.ui.dialog.ConfirmationDialogFragment import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.utils.PreferenceUtils import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber import java.io.File @@ -74,7 +76,6 @@ import java.io.File * MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically * (for instance, when the device is turned a aside). * - * * DO NOT CALL IT: an [OCFile] and [Account] must be provided for a successful * construction */ @@ -90,6 +91,7 @@ class PreviewImageFragment : FileFragment() { private var _bindingTopProgress: TopProgressBarBinding? = null private val bindingTopProgress get() = _bindingTopProgress!! + private val previewImageViewModel by viewModel() private val fileOperationsViewModel: FileOperationsViewModel by inject() /** @@ -190,48 +192,15 @@ class PreviewImageFragment : FileFragment() { */ override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) - file?.let { - // Update the file - file = mContainerActivity.storageManager.getFileById(it.id ?: -1) - val fileMenuFilter = FileMenuFilter( - it, - mContainerActivity.storageManager.account, - mContainerActivity, - activity - ) - fileMenuFilter.filter( - menu, - false, - false, - false, - false, - ) - } - - // additional restriction for this fragment - // TODO allow renaming in PreviewImageFragment - menu.findItem(R.id.action_rename_file)?.apply { - isVisible = false - isEnabled = false - } - - // additional restriction for this fragment - // TODO allow refresh file in PreviewImageFragment - menu.findItem(R.id.action_sync_file)?.apply { - isVisible = false - isEnabled = false - } - - // additional restriction for this fragment - menu.findItem(R.id.action_move)?.apply { - isVisible = false - isEnabled = false - } - - // additional restriction for this fragment - menu.findItem(R.id.action_copy)?.apply { - isVisible = false - isEnabled = false + val safeFile = file + // Update the file + file = mContainerActivity.storageManager.getFileById(file.id ?: -1) + val accountName = mContainerActivity.storageManager.account.name + previewImageViewModel.filterMenuOptions(safeFile, accountName) + + collectLatestLifecycleFlow(previewImageViewModel.menuOptions) { menuOptions -> + val hasWritePermission = safeFile.hasWritePermission + menu.filterMenuOptions(menuOptions, hasWritePermission) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageViewModel.kt index 677fd11ff9b..c07aa3846b7 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewImageViewModel.kt @@ -2,7 +2,9 @@ * ownCloud Android client application * * @author Abel García de Prada - * Copyright (C) 2021 ownCloud GmbH. + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -16,6 +18,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package com.owncloud.android.ui.preview import android.accounts.Account @@ -24,21 +27,33 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo +import com.owncloud.android.R +import com.owncloud.android.domain.files.model.FileMenuOption import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.usecases.GetFileByIdUseCase +import com.owncloud.android.providers.ContextProvider import com.owncloud.android.providers.CoroutinesDispatcherProvider +import com.owncloud.android.usecases.files.FilterFileMenuOptionsUseCase import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForFinishedDownloadsFromAccountUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class PreviewImageViewModel( private val getFileByIdUseCase: GetFileByIdUseCase, private val getLiveDataForFinishedDownloadsFromAccountUseCase: GetLiveDataForFinishedDownloadsFromAccountUseCase, + private val filterFileMenuOptionsUseCase: FilterFileMenuOptionsUseCase, + private val contextProvider: ContextProvider, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider ) : ViewModel() { private val _downloads = MediatorLiveData>>() val downloads: LiveData>> = _downloads + private val _menuOptions: MutableStateFlow> = MutableStateFlow(emptyList()) + val menuOptions: StateFlow> = _menuOptions + fun startListeningToDownloadsFromAccount(account: Account) { _downloads.addSource( getLiveDataForFinishedDownloadsFromAccountUseCase.execute(GetLiveDataForFinishedDownloadsFromAccountUseCase.Params(account)) @@ -50,6 +65,35 @@ class PreviewImageViewModel( } } + fun filterMenuOptions(file: OCFile, accountName: String) { + val shareViaLinkAllowed = contextProvider.getBoolean(R.bool.share_via_link_feature) + val shareWithUsersAllowed = contextProvider.getBoolean(R.bool.share_with_users_feature) + val sendAllowed = contextProvider.getString(R.string.send_files_to_other_apps).equals("on", ignoreCase = true) + viewModelScope.launch(coroutinesDispatcherProvider.io) { + val result = filterFileMenuOptionsUseCase.execute( + FilterFileMenuOptionsUseCase.Params( + files = listOf(file), + accountName = accountName, + isAnyFileVideoPreviewing = false, + displaySelectAll = false, + displaySelectInverse = false, + onlyAvailableOfflineFiles = false, + onlySharedByLinkFiles = false, + shareViaLinkAllowed = shareViaLinkAllowed, + shareWithUsersAllowed = shareWithUsersAllowed, + sendAllowed = sendAllowed, + ) + ) + result.apply { + remove(FileMenuOption.RENAME) + remove(FileMenuOption.MOVE) + remove(FileMenuOption.COPY) + remove(FileMenuOption.SYNC) + } + _menuOptions.update { result } + } + } + /** * It receives a list of WorkInfo, and it returns a list of Pair(OCFile, WorkInfo) * This way, each OCFile is linked to its latest work info. diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java index d0a78c77e43..7da619736d6 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewTextFragment.java @@ -3,17 +3,20 @@ * * @author Christian Schabesberger * @author Shashvat Kedia - * Copyright (C) 2020 ownCloud GmbH. - *

+ * @author Juan Carlos Garrote Gascón + * @author Parneet Singh + * + * Copyright (C) 2023 ownCloud GmbH. + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. - *

+ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - *

+ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ @@ -21,7 +24,6 @@ package com.owncloud.android.ui.preview; import android.accounts.Account; -import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import android.view.LayoutInflater; @@ -31,29 +33,33 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; +import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.Lifecycle; +import androidx.viewpager2.widget.ViewPager2; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.domain.files.model.OCFile; import com.owncloud.android.extensions.ActivityExtKt; -import com.owncloud.android.files.FileMenuFilter; +import com.owncloud.android.extensions.FragmentExtKt; +import com.owncloud.android.extensions.MenuExtKt; import com.owncloud.android.presentation.files.operations.FileOperation; import com.owncloud.android.presentation.files.operations.FileOperationsViewModel; import com.owncloud.android.presentation.files.removefile.RemoveFilesDialogFragment; +import com.owncloud.android.presentation.previews.PreviewTextViewModel; import com.owncloud.android.ui.controller.TransferProgressController; import com.owncloud.android.ui.dialog.ConfirmationDialogFragment; import com.owncloud.android.ui.dialog.LoadingDialog; import com.owncloud.android.ui.fragment.FileFragment; import com.owncloud.android.utils.PreferenceUtils; -import io.noties.markwon.Markwon; -import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; -import io.noties.markwon.ext.tables.TablePlugin; -import io.noties.markwon.ext.tasklist.TaskListPlugin; -import io.noties.markwon.html.HtmlPlugin; import timber.log.Timber; import java.io.BufferedWriter; @@ -72,13 +78,18 @@ public class PreviewTextFragment extends FileFragment { private static final String EXTRA_FILE = "FILE"; private static final String EXTRA_ACCOUNT = "ACCOUNT"; + private static final String TAG_SECOND_FRAGMENT = "SECOND_FRAGMENT"; private Account mAccount; private ProgressBar mProgressBar; private TransferProgressController mProgressController; private TextView mTextPreview; + private View mTextLayout; + private TabLayout mTabLayout; + private ViewPager2 mViewPager2; + private RelativeLayout rootView; private TextLoadAsyncTask mTextLoadTask; - + PreviewTextViewModel previewTextViewModel = get(PreviewTextViewModel.class); FileOperationsViewModel fileOperationsViewModel = get(FileOperationsViewModel.class); /** @@ -128,9 +139,13 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(getContext()) ); + rootView = ret.findViewById(R.id.top); mProgressBar = ret.findViewById(R.id.syncProgressBar); + mTabLayout = ret.findViewById(R.id.tab_layout); + mViewPager2 = ret.findViewById(R.id.view_pager); mTextPreview = ret.findViewById(R.id.text_preview); + mTextLayout = ret.findViewById(R.id.text_layout); return ret; } @@ -166,6 +181,12 @@ public void onActivityCreated(Bundle savedState) { mProgressController.setProgressBar(mProgressBar); } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + loadAndShowTextPreview(); + } + /** * {@inheritDoc} */ @@ -176,16 +197,11 @@ public void onSaveInstanceState(Bundle outState) { outState.putParcelable(EXTRA_ACCOUNT, mAccount); } - @Override - public void onStart() { - super.onStart(); - Timber.v("onStart"); - - loadAndShowTextPreview(); - } - private void loadAndShowTextPreview() { - mTextLoadTask = new TextLoadAsyncTask(new WeakReference<>(mTextPreview)); + mTextLoadTask = new TextLoadAsyncTask(new WeakReference(mTextPreview), + new WeakReference(rootView), new WeakReference(mTextLayout), + new WeakReference(mTabLayout), + new WeakReference(mViewPager2)); mTextLoadTask.execute(getFile()); } @@ -195,10 +211,20 @@ private void loadAndShowTextPreview() { private class TextLoadAsyncTask extends AsyncTask { private final String DIALOG_WAIT_TAG = "DIALOG_WAIT"; private final WeakReference mTextViewReference; + private final WeakReference mRootView; + private final WeakReference mTextLayout; + private final WeakReference mTabLayout; + private final WeakReference mViewPager; private String mimeType; - private TextLoadAsyncTask(WeakReference textView) { + private TextLoadAsyncTask(WeakReference textView, WeakReference rootView, + WeakReference textLayout, + WeakReference tabLayout, WeakReference viewPager) { mTextViewReference = textView; + mRootView = rootView; + mTextLayout = textLayout; + mTabLayout = tabLayout; + mViewPager = viewPager; } @Override @@ -254,23 +280,14 @@ protected StringWriter doInBackground(java.lang.Object... params) { @Override protected void onPostExecute(final StringWriter stringWriter) { final TextView textView = mTextViewReference.get(); - - if (textView != null) { - String text = new String(stringWriter.getBuffer()); - if (mimeType.equals("text/markdown")) { - Context context = textView.getContext(); - Markwon markwon = Markwon - .builder(context) - .usePlugin(TablePlugin.create(context)) - .usePlugin(StrikethroughPlugin.create()) - .usePlugin(TaskListPlugin.create(context)) - .usePlugin(HtmlPlugin.create()) - .build(); - markwon.setMarkdown(textView, text); - } else { - textView.setText(text); - } - textView.setVisibility(View.VISIBLE); + final RelativeLayout rootView = mRootView.get(); + final View textLayout = mTextLayout.get(); + final TabLayout tabLayout = mTabLayout.get(); + final ViewPager2 viewPager = mViewPager.get(); + + String text = new String(stringWriter.getBuffer()); + if (textView != null && rootView != null && textLayout != null && tabLayout != null && viewPager != null) { + showPreviewText(text, mimeType, rootView, textView, textLayout, tabLayout, viewPager); } try { @@ -310,6 +327,36 @@ public void dismissLoadingDialog() { loading.dismiss(); } } + + private void showPreviewText(String text, String mimeType, RelativeLayout rootView, TextView textView, + View textLayout, + TabLayout tabLayout, ViewPager2 viewPager) { + if (mimeType.equals("text/markdown")) { + rootView.removeView(textLayout); + showFormatType(text, mimeType, tabLayout, viewPager); + tabLayout.setVisibility(View.VISIBLE); + viewPager.setVisibility(View.VISIBLE); + } else { + rootView.removeView(tabLayout); + rootView.removeView(viewPager); + textView.setText(text); + textLayout.setVisibility(View.VISIBLE); + } + } + + private void showFormatType(String text, String mimeType, TabLayout tabLayout, ViewPager2 viewPager) { + PreviewFormatTextFragmentStateAdapter adapter = + new PreviewFormatTextFragmentStateAdapter(PreviewTextFragment.this, text, mimeType); + viewPager.setAdapter(adapter); + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + if (position == 0) { + tab.setText(adapter.getFormatTypes().get(mimeType)); + } else { + tab.setText(adapter.getFormatTypes().get(PreviewFormatTextFragmentStateAdapter.TYPE_PLAIN)); + } + }).attach(); + } + } /** @@ -329,55 +376,21 @@ public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); if (mContainerActivity.getStorageManager() != null) { - FileMenuFilter mf = new FileMenuFilter( - getFile(), - mContainerActivity.getStorageManager().getAccount(), - mContainerActivity, - getActivity() - ); - mf.filter( - menu, - false, - false, - false, - false + OCFile safeFile = getFile(); + String accountName = mContainerActivity.getStorageManager().getAccount().name; + previewTextViewModel.filterMenuOptions(safeFile, accountName); + + FragmentExtKt.collectLatestLifecycleFlow(this, previewTextViewModel.getMenuOptions(), + Lifecycle.State.CREATED, + (menuOptions, continuation) -> { + boolean hasWritePermission = safeFile.getHasWritePermission(); + MenuExtKt.filterMenuOptions(menu, menuOptions, hasWritePermission); + return null; + } ); } - // additional restriction for this fragment - MenuItem item = menu.findItem(R.id.action_rename_file); - if (item != null) { - item.setVisible(false); - item.setEnabled(false); - } - - // additional restriction for this fragment - item = menu.findItem(R.id.action_move); - if (item != null) { - item.setVisible(false); - item.setEnabled(false); - } - - item = menu.findItem(R.id.action_copy); - if (item != null) { - item.setVisible(false); - item.setEnabled(false); - } - - // this one doesn't make sense since the file has to be down in order to be previewed - item = menu.findItem(R.id.action_download_file); - if (item != null) { - item.setVisible(false); - item.setEnabled(false); - } - - item = menu.findItem(R.id.action_sync_file); - if (item != null) { - item.setVisible(false); - item.setEnabled(false); - } - - item = menu.findItem(R.id.action_search); + MenuItem item = menu.findItem(R.id.action_search); if (item != null) { item.setVisible(false); item.setEnabled(false); @@ -412,7 +425,8 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } case R.id.action_sync_file: { - fileOperationsViewModel.performOperation(new FileOperation.SynchronizeFileOperation(getFile(), mAccount.name)); + fileOperationsViewModel.performOperation(new FileOperation.SynchronizeFileOperation(getFile(), + mAccount.name)); return true; } case R.id.action_set_available_offline: { @@ -437,9 +451,8 @@ private void seeDetails() { } @Override - public void onStop() { - super.onStop(); - Timber.v("onStop"); + public void onDestroyView() { + super.onDestroyView(); if (mTextLoadTask != null) { mTextLoadTask.cancel(Boolean.TRUE); mTextLoadTask.dismissLoadingDialog(); diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFragment.java b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFragment.java index 0c25c531017..6a4f32f3016 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFragment.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFragment.java @@ -5,20 +5,23 @@ * @author David González Verdugo * @author Christian Schabesberger * @author Shashvat Kedia - * Copyright (C) 2021 ownCloud GmbH. - *

+ * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. - *

+ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - *

+ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + package com.owncloud.android.ui.preview; import android.accounts.Account; @@ -37,6 +40,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.Lifecycle; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; @@ -52,10 +56,11 @@ import com.owncloud.android.domain.files.model.OCFile; import com.owncloud.android.extensions.ActivityExtKt; import com.owncloud.android.extensions.FragmentExtKt; -import com.owncloud.android.files.FileMenuFilter; +import com.owncloud.android.extensions.MenuExtKt; import com.owncloud.android.presentation.files.operations.FileOperation; import com.owncloud.android.presentation.files.operations.FileOperationsViewModel; import com.owncloud.android.presentation.files.removefile.RemoveFilesDialogFragment; +import com.owncloud.android.presentation.previews.PreviewVideoViewModel; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.controller.TransferProgressController; @@ -106,6 +111,7 @@ public class PreviewVideoFragment extends FileFragment implements View.OnClickLi private boolean mAutoplay; private long mPlaybackPosition; + PreviewVideoViewModel previewVideoViewModel = get(PreviewVideoViewModel.class); FileOperationsViewModel fileOperationsViewModel = get(FileOperationsViewModel.class); /** @@ -307,18 +313,16 @@ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflat public void onPrepareOptionsMenu(@NonNull Menu menu) { super.onPrepareOptionsMenu(menu); - FileMenuFilter mf = new FileMenuFilter( - getFile(), - mAccount, - mContainerActivity, - getActivity() - ); - mf.filter( - menu, - false, - false, - false, - false + OCFile safeFile = getFile(); + String accountName = mAccount.name; + previewVideoViewModel.filterMenuOptions(safeFile, accountName); + + FragmentExtKt.collectLatestLifecycleFlow(this, previewVideoViewModel.getMenuOptions(), Lifecycle.State.CREATED, + (menuOptions, continuation) -> { + boolean hasWritePermission = safeFile.getHasWritePermission(); + MenuExtKt.filterMenuOptions(menu, menuOptions, hasWritePermission); + return null; + } ); // additional restrictions for this fragment diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt new file mode 100644 index 00000000000..eaac49c418d --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt @@ -0,0 +1,205 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.usecases.files + +import androidx.work.WorkManager +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.availableoffline.model.AvailableOfflineStatus +import com.owncloud.android.domain.capabilities.CapabilityRepository +import com.owncloud.android.domain.files.model.FileMenuOption +import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.domain.files.model.OCFileSyncInfo +import com.owncloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAccountUseCase +import com.owncloud.android.extensions.getRunningWorkInfosByTags +import com.owncloud.android.usecases.transfers.TRANSFER_TAG_DOWNLOAD + +class FilterFileMenuOptionsUseCase( + private val workManager: WorkManager, + private val capabilityRepository: CapabilityRepository, + private val getSpaceWithSpecialsByIdForAccountUseCase: GetSpaceWithSpecialsByIdForAccountUseCase, +) : BaseUseCase, FilterFileMenuOptionsUseCase.Params>() { + override fun run(params: Params): MutableList { + val optionsToShow = mutableListOf() + val files = params.files + + if (files.isEmpty()) { + return mutableListOf() + } + + val filesSyncInfo = params.filesSyncInfo + val capability = capabilityRepository.getStoredCapabilities(params.accountName) + val space = getSpaceWithSpecialsByIdForAccountUseCase.execute(GetSpaceWithSpecialsByIdForAccountUseCase.Params( + spaceId = files.first().spaceId, + accountName = params.accountName, + )) + + val isAnyFileSynchronizing: Boolean = if (filesSyncInfo.isEmpty()) { + anyFileSynchronizingLookingIntoWorkers(files, params.accountName) + } else { + anyFileSynchronizingLookingIIntoFilesSyncInfo(filesSyncInfo) + } + val isAnyFileVideoPreviewing = params.isAnyFileVideoPreviewing + val isAnyFileVideoStreaming = isAnyFileVideoPreviewing && !anyFileDownloaded(files) + val hasRenamePermission: Boolean = if (isSingleSelection(files)) { + files.first().hasRenamePermission + } else { + false + } + val hasMovePermission = files.all { it.hasMovePermission } + val hasRemovePermission = files.all { it.hasDeletePermission } + val hasResharePermission: Boolean = if (isSingleSelection(files)) { + files.first().hasResharePermission + } else { + false + } + val isPersonalSpace = space?.isPersonal ?: true + val resharingAllowed = capability?.let { !anyFileSharedWithMe(files) || it.filesSharingResharing.isTrue } ?: false + val displaySelectAll = params.displaySelectAll + val displaySelectInverse = params.displaySelectInverse + val onlyAvailableOfflineFiles = params.onlyAvailableOfflineFiles + val onlySharedByLinkFiles = params.onlySharedByLinkFiles + val shareViaLinkAllowed = params.shareViaLinkAllowed + val shareWithUsersAllowed = params.shareWithUsersAllowed + val sendAllowed = params.sendAllowed + + // Select all + if (displaySelectAll) { + optionsToShow.add(FileMenuOption.SELECT_ALL) + } + // Select inverse + if (displaySelectInverse) { + optionsToShow.add(FileMenuOption.SELECT_INVERSE) + } + // Share + if (!onlyAvailableOfflineFiles && (shareViaLinkAllowed || shareWithUsersAllowed) && resharingAllowed && + isPersonalSpace && hasResharePermission) { + optionsToShow.add(FileMenuOption.SHARE) + } + // Open with (different to preview!) + if (!isAnyFileSynchronizing && isSingleFile(files)) { + optionsToShow.add(FileMenuOption.OPEN_WITH) + } + // Download + if (!isAnyFileSynchronizing && !isAnyFileVideoPreviewing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && + !anyFolder(files) && !anyFileDownloaded(files)) { + optionsToShow.add(FileMenuOption.DOWNLOAD) + } + // Synchronize + if (!isAnyFileSynchronizing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && + (anyFileDownloaded(files) || anyFolder(files))) { + optionsToShow.add(FileMenuOption.SYNC) + } + // Cancel sync + if (isAnyFileSynchronizing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && !anyAvailableOfflineFile(files)) { + optionsToShow.add(FileMenuOption.CANCEL_SYNC) + } + // Rename + if (!isAnyFileSynchronizing && !isAnyFileVideoPreviewing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && + hasRenamePermission) { + optionsToShow.add(FileMenuOption.RENAME) + } + // Move + if (!isAnyFileSynchronizing && !isAnyFileVideoPreviewing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && + hasMovePermission) { + optionsToShow.add(FileMenuOption.MOVE) + } + // Copy + if (!isAnyFileSynchronizing && !isAnyFileVideoPreviewing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles) { + optionsToShow.add(FileMenuOption.COPY) + } + // Send + if (!isAnyFileSynchronizing && !isAnyFileVideoStreaming && !onlyAvailableOfflineFiles && !anyFolder(files) && + (allFilesDownloaded(files) || isSingleFile(files)) && sendAllowed) { + optionsToShow.add(FileMenuOption.SEND) + } + // Set as available offline + if (!isAnyFileSynchronizing && anyNotAvailableOfflineFile(files) && !isAnyFileVideoStreaming) { + optionsToShow.add(FileMenuOption.SET_AV_OFFLINE) + } + // Unset as available offline + if (anyAvailableOfflineFile(files) && !isAnyFileVideoStreaming) { + optionsToShow.add(FileMenuOption.UNSET_AV_OFFLINE) + } + // Details + if (isSingleFile(files)) { + optionsToShow.add(FileMenuOption.DETAILS) + } + // Remove + if (!isAnyFileSynchronizing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && hasRemovePermission) { + optionsToShow.add(FileMenuOption.REMOVE) + } + + return optionsToShow + } + + private fun anyFileSynchronizingLookingIntoWorkers(files: List, accountName: String): Boolean { + val workInfos = workManager.getRunningWorkInfosByTags(listOf(TRANSFER_TAG_DOWNLOAD, accountName)) + val workInfosNotFinished = workInfos.filter { !it.state.isFinished } + workInfosNotFinished.forEach { workInfoNotFinished -> + if (files.any { workInfoNotFinished.tags.contains(it.id.toString()) }) { + return true + } + } + return false + } + + private fun anyFileSynchronizingLookingIIntoFilesSyncInfo(filesSyncInfo: List) = + filesSyncInfo.any { it.isSynchronizing } + + private fun anyFileDownloaded(files: List) = + files.any { it.isAvailableLocally } + + private fun allFilesDownloaded(files: List) = + files.all { it.isAvailableLocally } + + private fun anyFolder(files: List) = + files.any { it.isFolder } + + private fun anyAvailableOfflineFile(files: List) = + files.any { it.availableOfflineStatus == AvailableOfflineStatus.AVAILABLE_OFFLINE } + + private fun anyNotAvailableOfflineFile(files: List) = + files.any { it.availableOfflineStatus == AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE } + + private fun anyFileSharedWithMe(files: List) = + files.any { it.isSharedWithMe } + + private fun isSingleSelection(files: List) = + files.size == 1 + + private fun isSingleFile(files: List) = + isSingleSelection(files) && !files.first().isFolder + + + data class Params( + val files: List, + val filesSyncInfo: List = emptyList(), + val accountName: String, + val isAnyFileVideoPreviewing: Boolean, + val displaySelectAll: Boolean, + val displaySelectInverse: Boolean, + val onlyAvailableOfflineFiles: Boolean, + val onlySharedByLinkFiles: Boolean, + val shareViaLinkAllowed: Boolean, + val shareWithUsersAllowed: Boolean, + val sendAllowed: Boolean + ) +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadUseCase.kt index d363190cdbe..d927155e819 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadUseCase.kt @@ -22,7 +22,7 @@ package com.owncloud.android.usecases.transfers.uploads import androidx.work.WorkManager -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.BaseUseCase import com.owncloud.android.domain.transfers.TransferRepository import com.owncloud.android.domain.transfers.model.OCTransfer diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadsRecursivelyUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadsRecursivelyUseCase.kt index 531cd354ddf..26779c8c936 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadsRecursivelyUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/CancelUploadsRecursivelyUseCase.kt @@ -22,7 +22,7 @@ package com.owncloud.android.usecases.transfers.uploads import androidx.work.WorkInfo import androidx.work.WorkManager -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.BaseUseCase import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.usecases.GetFolderContentUseCase diff --git a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/ClearFailedTransfersUseCase.kt b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/ClearFailedTransfersUseCase.kt index 94611bca77b..108312a3e37 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/ClearFailedTransfersUseCase.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/usecases/transfers/uploads/ClearFailedTransfersUseCase.kt @@ -21,7 +21,7 @@ package com.owncloud.android.usecases.transfers.uploads import androidx.work.WorkManager -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.BaseUseCase import com.owncloud.android.domain.transfers.TransferRepository diff --git a/owncloudApp/src/main/java/com/owncloud/android/utils/FileStorageUtils.java b/owncloudApp/src/main/java/com/owncloud/android/utils/FileStorageUtils.java index 1e256844eb7..140c64bbe62 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/utils/FileStorageUtils.java +++ b/owncloudApp/src/main/java/com/owncloud/android/utils/FileStorageUtils.java @@ -27,7 +27,7 @@ import android.annotation.SuppressLint; import android.webkit.MimeTypeMap; -import com.owncloud.android.data.storage.LocalStorageProvider; +import com.owncloud.android.data.providers.LocalStorageProvider; import kotlin.Lazy; import org.jetbrains.annotations.NotNull; import timber.log.Timber; diff --git a/owncloudApp/src/main/java/com/owncloud/android/utils/MdmConfigurations.kt b/owncloudApp/src/main/java/com/owncloud/android/utils/MdmConfigurations.kt index caa4edb03a1..0bfc5db6b9c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/utils/MdmConfigurations.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/utils/MdmConfigurations.kt @@ -31,6 +31,7 @@ const val CONFIGURATION_SERVER_URL_INPUT_VISIBILITY = "server_url_input_visibili const val CONFIGURATION_ALLOW_SCREENSHOTS = "allow_screenshots_configuration" const val CONFIGURATION_OAUTH2_OPEN_ID_SCOPE = "oauth2_open_id_scope" const val CONFIGURATION_OAUTH2_OPEN_ID_PROMPT = "oauth2_open_id_prompt" +const val CONFIGURATION_DEVICE_PROTECTION = "device_protection" @StringDef( NO_MDM_RESTRICTION_YET, @@ -40,6 +41,7 @@ const val CONFIGURATION_OAUTH2_OPEN_ID_PROMPT = "oauth2_open_id_prompt" CONFIGURATION_ALLOW_SCREENSHOTS, CONFIGURATION_OAUTH2_OPEN_ID_SCOPE, CONFIGURATION_OAUTH2_OPEN_ID_PROMPT, + CONFIGURATION_DEVICE_PROTECTION, ) @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.VALUE_PARAMETER) diff --git a/owncloudApp/src/main/java/com/owncloud/android/utils/MimetypeIconUtil.java b/owncloudApp/src/main/java/com/owncloud/android/utils/MimetypeIconUtil.java index 7dbe2bdfc42..87884a799db 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/utils/MimetypeIconUtil.java +++ b/owncloudApp/src/main/java/com/owncloud/android/utils/MimetypeIconUtil.java @@ -85,23 +85,6 @@ public static int getFileTypeIconId(String mimetype, String filename) { return determineIconIdByMimeTypeList(possibleMimeTypes); } - /** - * Returns the resource identifier of an image to use as icon associated to a type of folder. - * - * @param isSharedViaUsers flag if the folder is shared via the users system - * @param isSharedViaLink flag if the folder is publicly shared via link - * @return Identifier of an image resource. - */ - public static int getFolderTypeIconId(boolean isSharedViaUsers, boolean isSharedViaLink) { - if (isSharedViaLink) { - return R.drawable.folder_public; - } else if (isSharedViaUsers) { - return R.drawable.shared_with_me_folder; - } - - return R.drawable.ic_menu_archive; - } - /** * Returns a single MIME type of all the possible, by inspection of the file extension, and taking * into account the MIME types known by ownCloud first. diff --git a/owncloudApp/src/main/java/com/owncloud/android/utils/NotificationUtils.kt b/owncloudApp/src/main/java/com/owncloud/android/utils/NotificationUtils.kt index 5c81f598b63..53254e35ad1 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/utils/NotificationUtils.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/utils/NotificationUtils.kt @@ -1,24 +1,23 @@ /** * ownCloud Android client application * + * @author Abel García de Prada * - * Copyright (C) 2020 ownCloud GmbH. - * + * Copyright (C) 2023 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. * - * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * - * * You should have received a copy of the GNU General Public License * along with this program. If not, see //www.gnu.org/licenses/>. */ + package com.owncloud.android.utils import android.accounts.Account @@ -47,11 +46,7 @@ import java.util.Random object NotificationUtils { - val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } + const val pendingIntentFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT @JvmStatic fun newNotificationBuilder(context: Context, channelId: String): NotificationCompat.Builder { @@ -115,11 +110,7 @@ object NotificationUtils { addFlags(Intent.FLAG_FROM_BACKGROUND) } - val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT - } else { - PendingIntent.FLAG_ONE_SHOT - } + val pendingIntent = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT return PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), updateCredentialsIntent, pendingIntent) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/utils/RemoteFileUtils.kt b/owncloudApp/src/main/java/com/owncloud/android/utils/RemoteFileUtils.kt index 001aca82b36..68dc34e351e 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/utils/RemoteFileUtils.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/utils/RemoteFileUtils.kt @@ -37,9 +37,15 @@ class RemoteFileUtils { fun getAvailableRemotePath( ownCloudClient: OwnCloudClient, remotePath: String, - spaceWebDavUrl: String? = null + spaceWebDavUrl: String? = null, + isUserLogged: Boolean, ): String { - var checkExistsFile = existsFile(ownCloudClient, remotePath, spaceWebDavUrl) + var checkExistsFile = existsFile( + ownCloudClient = ownCloudClient, + remotePath = remotePath, + spaceWebDavUrl = spaceWebDavUrl, + isUserLogged = isUserLogged, + ) if (!checkExistsFile) { return remotePath } @@ -52,13 +58,23 @@ class RemoteFileUtils { substring(0, pos) } } - var count = 2 + var count = 1 do { suffix = " ($count)" checkExistsFile = if (pos >= 0) { - existsFile(ownCloudClient, "${remotePath.substringBeforeLast('.', "")}$suffix.$extension", spaceWebDavUrl) + existsFile( + ownCloudClient = ownCloudClient, + remotePath = "${remotePath.substringBeforeLast('.', "")}$suffix.$extension", + spaceWebDavUrl = spaceWebDavUrl, + isUserLogged = isUserLogged, + ) } else { - existsFile(ownCloudClient, remotePath + suffix, spaceWebDavUrl) + existsFile( + ownCloudClient = ownCloudClient, + remotePath = remotePath + suffix, + spaceWebDavUrl = spaceWebDavUrl, + isUserLogged = isUserLogged, + ) } count++ } while (checkExistsFile) @@ -73,11 +89,12 @@ class RemoteFileUtils { ownCloudClient: OwnCloudClient, remotePath: String, spaceWebDavUrl: String?, + isUserLogged: Boolean, ): Boolean { val existsOperation = CheckPathExistenceRemoteOperation( remotePath = remotePath, - isUserLoggedIn = false, + isUserLoggedIn = isUserLogged, spaceWebDavUrl = spaceWebDavUrl, ) return existsOperation.execute(ownCloudClient).isSuccess diff --git a/owncloudApp/src/main/java/com/owncloud/android/workers/DownloadFileWorker.kt b/owncloudApp/src/main/java/com/owncloud/android/workers/DownloadFileWorker.kt index c002f7c3eea..5a7bcffbfad 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/workers/DownloadFileWorker.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/workers/DownloadFileWorker.kt @@ -31,7 +31,7 @@ import androidx.work.workDataOf import at.bitfire.dav4jvm.exception.UnauthorizedException import com.owncloud.android.R import com.owncloud.android.data.executeRemoteOperation -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.exceptions.CancelledException import com.owncloud.android.domain.exceptions.LocalStorageNotMovedException import com.owncloud.android.domain.exceptions.NoConnectionWithServerException diff --git a/owncloudApp/src/main/java/com/owncloud/android/workers/OldLogsCollectorWorker.kt b/owncloudApp/src/main/java/com/owncloud/android/workers/OldLogsCollectorWorker.kt index ef4b2c09895..e632c79e50c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/workers/OldLogsCollectorWorker.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/workers/OldLogsCollectorWorker.kt @@ -21,7 +21,7 @@ package com.owncloud.android.workers import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import org.koin.core.component.KoinComponent import org.koin.core.component.inject import timber.log.Timber diff --git a/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromContentUriWorker.kt b/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromContentUriWorker.kt index 5e07144cb59..ca26622ebcf 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromContentUriWorker.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromContentUriWorker.kt @@ -32,7 +32,7 @@ import androidx.work.workDataOf import com.owncloud.android.R import com.owncloud.android.presentation.authentication.AccountUtils import com.owncloud.android.data.executeRemoteOperation -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.camerauploads.model.UploadBehavior import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase import com.owncloud.android.domain.exceptions.LocalFileNotFoundException @@ -218,7 +218,12 @@ class UploadFileFromContentUriWorker( var pathToGrant: String = File(uploadPath).parent ?: "" pathToGrant = if (pathToGrant.endsWith(File.separator)) pathToGrant else pathToGrant + File.separator - val checkPathExistenceOperation = CheckPathExistenceRemoteOperation(pathToGrant, false, spaceWebDavUrl) + val checkPathExistenceOperation = + CheckPathExistenceRemoteOperation( + remotePath = pathToGrant, + isUserLoggedIn = AccountUtils.getCurrentOwnCloudAccount(appContext) != null, + spaceWebDavUrl = spaceWebDavUrl, + ) val checkPathExistenceResult = checkPathExistenceOperation.execute(client) if (checkPathExistenceResult.code == ResultCode.FILE_NOT_FOUND) { val createRemoteFolderOperation = CreateRemoteFolderOperation( @@ -232,7 +237,12 @@ class UploadFileFromContentUriWorker( private fun checkNameCollisionAndGetAnAvailableOneInCase(client: OwnCloudClient) { Timber.d("Checking name collision in server") - val remotePath = getAvailableRemotePath(client, uploadPath, spaceWebDavUrl) + val remotePath = getAvailableRemotePath( + ownCloudClient = client, + remotePath = uploadPath, + spaceWebDavUrl = spaceWebDavUrl, + isUserLogged = AccountUtils.getCurrentOwnCloudAccount(appContext) != null, + ) if (remotePath != uploadPath) { uploadPath = remotePath Timber.d("Name collision detected, let's rename it to %s", remotePath) diff --git a/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromFileSystemWorker.kt b/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromFileSystemWorker.kt index 392a24bb91c..a8837a79e41 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromFileSystemWorker.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/workers/UploadFileFromFileSystemWorker.kt @@ -192,7 +192,13 @@ class UploadFileFromFileSystemWorker( if (ocTransfer.forceOverwrite) { val getFileByRemotePathUseCase: GetFileByRemotePathUseCase by inject() - val useCaseResult = getFileByRemotePathUseCase.execute(GetFileByRemotePathUseCase.Params(ocTransfer.accountName, ocTransfer.remotePath, ocTransfer.spaceId)) + val useCaseResult = getFileByRemotePathUseCase.execute( + GetFileByRemotePathUseCase.Params( + ocTransfer.accountName, + ocTransfer.remotePath, + ocTransfer.spaceId + ) + ) eTagInConflict = useCaseResult.getDataOrNull()?.etagInConflict.orEmpty() @@ -200,7 +206,12 @@ class UploadFileFromFileSystemWorker( } else { Timber.d("Checking name collision in server") - val remotePath = getAvailableRemotePath(client, uploadPath, spaceWebDavUrl) + val remotePath = getAvailableRemotePath( + ownCloudClient = client, + remotePath = uploadPath, + spaceWebDavUrl = spaceWebDavUrl, + isUserLogged = AccountUtils.getCurrentOwnCloudAccount(appContext) != null, + ) if (remotePath != uploadPath) { uploadPath = remotePath Timber.d("Name collision detected, let's rename it to $remotePath") diff --git a/owncloudApp/src/main/res/drawable-hdpi/folder_public.png b/owncloudApp/src/main/res/drawable-hdpi/folder_public.png deleted file mode 100644 index 55dbdf9dd5a..00000000000 Binary files a/owncloudApp/src/main/res/drawable-hdpi/folder_public.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-hdpi/shared_via_users.png b/owncloudApp/src/main/res/drawable-hdpi/shared_via_users.png deleted file mode 100644 index 9ec18ced775..00000000000 Binary files a/owncloudApp/src/main/res/drawable-hdpi/shared_via_users.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-hdpi/shared_with_me_folder.png b/owncloudApp/src/main/res/drawable-hdpi/shared_with_me_folder.png deleted file mode 100644 index d312561b5cf..00000000000 Binary files a/owncloudApp/src/main/res/drawable-hdpi/shared_with_me_folder.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-mdpi/folder_public.png b/owncloudApp/src/main/res/drawable-mdpi/folder_public.png deleted file mode 100644 index 09945b62f8b..00000000000 Binary files a/owncloudApp/src/main/res/drawable-mdpi/folder_public.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-mdpi/shared_via_users.png b/owncloudApp/src/main/res/drawable-mdpi/shared_via_users.png deleted file mode 100644 index 0b17c765707..00000000000 Binary files a/owncloudApp/src/main/res/drawable-mdpi/shared_via_users.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-mdpi/shared_with_me_folder.png b/owncloudApp/src/main/res/drawable-mdpi/shared_with_me_folder.png deleted file mode 100644 index ed8f860b401..00000000000 Binary files a/owncloudApp/src/main/res/drawable-mdpi/shared_with_me_folder.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-xhdpi/folder_public.png b/owncloudApp/src/main/res/drawable-xhdpi/folder_public.png deleted file mode 100644 index 4e35b7a147a..00000000000 Binary files a/owncloudApp/src/main/res/drawable-xhdpi/folder_public.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-xhdpi/shared_via_users.png b/owncloudApp/src/main/res/drawable-xhdpi/shared_via_users.png deleted file mode 100644 index ef7779ca1df..00000000000 Binary files a/owncloudApp/src/main/res/drawable-xhdpi/shared_via_users.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-xhdpi/shared_with_me_folder.png b/owncloudApp/src/main/res/drawable-xhdpi/shared_with_me_folder.png deleted file mode 100644 index 1e6fb30122e..00000000000 Binary files a/owncloudApp/src/main/res/drawable-xhdpi/shared_with_me_folder.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-xxhdpi/folder_public.png b/owncloudApp/src/main/res/drawable-xxhdpi/folder_public.png deleted file mode 100644 index b6755f3e6e2..00000000000 Binary files a/owncloudApp/src/main/res/drawable-xxhdpi/folder_public.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable-xxhdpi/shared_with_me_folder.png b/owncloudApp/src/main/res/drawable-xxhdpi/shared_with_me_folder.png deleted file mode 100644 index d0d6ce80fb1..00000000000 Binary files a/owncloudApp/src/main/res/drawable-xxhdpi/shared_with_me_folder.png and /dev/null differ diff --git a/owncloudApp/src/main/res/drawable/ic_close.xml b/owncloudApp/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000000..844b6b62ef1 --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + diff --git a/owncloudApp/src/main/res/drawable/ic_open_in_web.xml b/owncloudApp/src/main/res/drawable/ic_open_in_web.xml new file mode 100644 index 00000000000..54576b82a38 --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_open_in_web.xml @@ -0,0 +1,9 @@ + + + diff --git a/owncloudApp/src/main/res/drawable/ic_select_all.xml b/owncloudApp/src/main/res/drawable/ic_select_all.xml new file mode 100644 index 00000000000..af331ce61c8 --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_select_all.xml @@ -0,0 +1,5 @@ + + + diff --git a/owncloudApp/src/main/res/drawable/ic_select_inverse.xml b/owncloudApp/src/main/res/drawable/ic_select_inverse.xml new file mode 100644 index 00000000000..a5b7b68ad7b --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_select_inverse.xml @@ -0,0 +1,5 @@ + + + diff --git a/owncloudApp/src/main/res/drawable/ic_share_generic.xml b/owncloudApp/src/main/res/drawable/ic_share_generic.xml new file mode 100644 index 00000000000..26707e6c7f3 --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_share_generic.xml @@ -0,0 +1,5 @@ + + + diff --git a/owncloudApp/src/main/res/drawable/ic_share_generic_black.xml b/owncloudApp/src/main/res/drawable/ic_share_generic_black.xml new file mode 100644 index 00000000000..06a14d80d06 --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_share_generic_black.xml @@ -0,0 +1,6 @@ + + + + diff --git a/owncloudApp/src/main/res/drawable/ic_share_generic_white.xml b/owncloudApp/src/main/res/drawable/ic_share_generic_white.xml new file mode 100644 index 00000000000..7b9fdaacda0 --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_share_generic_white.xml @@ -0,0 +1,5 @@ + + + diff --git a/owncloudApp/src/main/res/drawable/ic_shared_by_link.xml b/owncloudApp/src/main/res/drawable/ic_shared_by_link.xml index e8711dbde7e..ffb8c875be8 100644 --- a/owncloudApp/src/main/res/drawable/ic_shared_by_link.xml +++ b/owncloudApp/src/main/res/drawable/ic_shared_by_link.xml @@ -1,9 +1,7 @@ - - + + diff --git a/owncloudApp/src/main/res/drawable/ic_shared_via_users.xml b/owncloudApp/src/main/res/drawable/ic_shared_via_users.xml new file mode 100644 index 00000000000..04f6bf5b1b4 --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_shared_via_users.xml @@ -0,0 +1,5 @@ + + + diff --git a/owncloudApp/src/main/res/drawable/ic_spaces_placeholder.xml b/owncloudApp/src/main/res/drawable/ic_spaces_placeholder.xml new file mode 100644 index 00000000000..91975e389a1 --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_spaces_placeholder.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/owncloudApp/src/main/res/drawable/ic_three_dot_menu.xml b/owncloudApp/src/main/res/drawable/ic_three_dot_menu.xml new file mode 100644 index 00000000000..479403f499d --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_three_dot_menu.xml @@ -0,0 +1,5 @@ + + + diff --git a/owncloudApp/src/main/res/layout/bottom_sheet_fragment_item.xml b/owncloudApp/src/main/res/layout/bottom_sheet_fragment_item.xml index 5bf1edda985..be5cd41ca8f 100644 --- a/owncloudApp/src/main/res/layout/bottom_sheet_fragment_item.xml +++ b/owncloudApp/src/main/res/layout/bottom_sheet_fragment_item.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" android:padding="@dimen/standard_half_padding"> + + + + + diff --git a/owncloudApp/src/main/res/layout/dialog_file_already_exists.xml b/owncloudApp/src/main/res/layout/dialog_file_already_exists.xml new file mode 100644 index 00000000000..f88a5c71eeb --- /dev/null +++ b/owncloudApp/src/main/res/layout/dialog_file_already_exists.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/owncloudApp/src/main/res/layout/edit_box_dialog.xml b/owncloudApp/src/main/res/layout/edit_box_dialog.xml index 9795cec2222..59c3bbec4c8 100644 --- a/owncloudApp/src/main/res/layout/edit_box_dialog.xml +++ b/owncloudApp/src/main/res/layout/edit_box_dialog.xml @@ -17,8 +17,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . --> - - + diff --git a/owncloudApp/src/main/res/layout/file_details_fragment.xml b/owncloudApp/src/main/res/layout/file_details_fragment.xml index b08d08b4d65..ca656fb80a2 100644 --- a/owncloudApp/src/main/res/layout/file_details_fragment.xml +++ b/owncloudApp/src/main/res/layout/file_details_fragment.xml @@ -1,27 +1,9 @@ - - @@ -33,39 +15,112 @@ + + + + + - - + android:id="@+id/badgeDetailFile" + android:layout_width="@dimen/file_indicator_pin_size_grid" + android:layout_height="@dimen/file_indicator_pin_size_grid" + android:src="@drawable/sync_pin" + android:visibility="gone" + android:translationX="-8dp" + android:translationY="-8dp" + app:layout_constraintStart_toEndOf="@+id/cardView" + app:layout_constraintTop_toBottomOf="@+id/cardView" /> + + + + + + + + + + + + + app:layout_constraintTop_toTopOf="@id/fdModified" + app:layout_constraintVertical_bias="1.0" + tools:visibility="visible"/> + app:layout_constraintTop_toBottomOf="@id/divider2" + tools:text="@tools:sample/lorem[6]" + tools:visibility="visible"/> + + + tools:text="@tools:sample/lorem[6]" + tools:visibility="visible"/> - + + + + + + + app:layout_constraintTop_toTopOf="@id/fdPath" + tools:visibility="visible"/> - + + + + + + + app:layout_constraintStart_toEndOf="@id/fdSpace" + app:layout_constraintTop_toTopOf="@id/fdSpace" + tools:visibility="visible" + /> - + diff --git a/owncloudApp/src/main/res/layout/file_options_bottom_sheet_fragment.xml b/owncloudApp/src/main/res/layout/file_options_bottom_sheet_fragment.xml new file mode 100644 index 00000000000..9c05116af07 --- /dev/null +++ b/owncloudApp/src/main/res/layout/file_options_bottom_sheet_fragment.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/owncloudApp/src/main/res/layout/grid_item.xml b/owncloudApp/src/main/res/layout/grid_item.xml index 32e0280f9ee..d1b3e279d92 100644 --- a/owncloudApp/src/main/res/layout/grid_item.xml +++ b/owncloudApp/src/main/res/layout/grid_item.xml @@ -1,6 +1,6 @@ + + + + diff --git a/owncloudApp/src/main/res/layout/preview_text_fragment.xml b/owncloudApp/src/main/res/layout/preview_text_fragment.xml index 81fee4e059d..629c3b81ee5 100644 --- a/owncloudApp/src/main/res/layout/preview_text_fragment.xml +++ b/owncloudApp/src/main/res/layout/preview_text_fragment.xml @@ -16,34 +16,36 @@ along with this program. If not, see . --> - + android:gravity="center" + tools:context=".ui.preview.PreviewAudioFragment"> - - - - + android:visibility="gone" /> + + + + diff --git a/owncloudApp/src/main/res/layout/space_header.xml b/owncloudApp/src/main/res/layout/space_header.xml index b429f7a8bb9..bf961d42a36 100644 --- a/owncloudApp/src/main/res/layout/space_header.xml +++ b/owncloudApp/src/main/res/layout/space_header.xml @@ -43,7 +43,7 @@ android:id="@+id/space_header_image" android:layout_width="match_parent" android:layout_height="match_parent" - android:scaleType="center" + android:scaleType="centerCrop" android:src="@drawable/ic_spaces" /> diff --git a/owncloudApp/src/main/res/layout/spaces_list_item.xml b/owncloudApp/src/main/res/layout/spaces_list_item.xml index da2f565dbfc..65444feacda 100644 --- a/owncloudApp/src/main/res/layout/spaces_list_item.xml +++ b/owncloudApp/src/main/res/layout/spaces_list_item.xml @@ -45,7 +45,7 @@ android:id="@+id/spaces_list_item_image" android:layout_width="match_parent" android:layout_height="@dimen/spaces_thumbnail_height" - android:scaleType="center" + android:scaleType="centerCrop" android:src="@drawable/ic_spaces" /> - - + - diff --git a/owncloudApp/src/main/res/values-ar/strings.xml b/owncloudApp/src/main/res/values-ar/strings.xml index 727c77c0fc1..693b09854fa 100644 --- a/owncloudApp/src/main/res/values-ar/strings.xml +++ b/owncloudApp/src/main/res/values-ar/strings.xml @@ -10,6 +10,7 @@ محتوى من تطبيقات أخرى ملفات فتح باستخدام + فتح مع (للقراءة فقط) مجلد جديد الإعدادات التفاصيل @@ -34,6 +35,10 @@ جارٍ التسجيل المزيد الأمان + ما الجديد في أحدث إصدار؟ + قم بتعيين الأقفال للوصول إلى التطبيق والسماح باللمسات مع النوافذ المرئية الأخرى + تتبع السجلات الناتجة عن تنفيذ التطبيق + معلومات التطبيق والتعليقات وغيرها حسابات إدارة الحسابات قفل رمز المرور @@ -44,10 +49,14 @@ بعد دقيقة واحدة (1) بعد 5 دقائق بعد 30 دقيقة + تأمين الوصول من موفر المستندات + قفل الوصول من التطبيقات الأخرى إلى ملفات الحسابات في التطبيق عبر مستكشف الملفات الأصلي لنظام اندرويد. اللمس باستخدام نوافذ أخرى مرئية يتيح اللمس باستخدام نافذة أخرى مرئية عندما تكون نافذة العرض مخفية. فعِّله لاستخدام تطبيقات تصفية الإضاءة. هل أنت متأكد أنك ترغب في تفعيل هذه الميزة؟ استخدم هذه الميزة على مسؤوليتك فقد يحاول تطبيق ضار انتحال شخصيتك للقيام ببعض الإجراءات دون علمك باستخدام نوافذ عرض أخرى. + تحميل الصور تلقائيا + إدارة موقع وسلوك الصور التي يتم تحميلها تلقائيا تحميلات الصور حمِّل الصور الملتقطة بالكاميرا تلقائيًا مسار تحميل الصور @@ -175,7 +184,6 @@ شهادة الخادم غير موثوق بها أدخل اسمًا للملف الجديد يجب ألا يكون اسم الملف فارغًا - يجب ألا يزيد عدد أحرف اسم الملف عن %d اسم الملف تحميل ملفات تحميل الكاميرا تحميل الملفات المتوفرة دون اتصال بالإنترنت @@ -303,7 +311,6 @@ تعذّر فحص الملف البعيد تمت مزامنة محتويات الملف بالفعل تعذّر إنشاء المجلد - الرموز الممنوع استخدامها: / \\ < > : \" | ? * يحتوي اسم الملف على حرف واحد غير صالح على الأقل لا يمكن أن يكون اسم الملف فارغًا انتظر للحظة @@ -556,4 +563,7 @@ متابعة + الملف المطلوب غير متوفر بعد ، برجاء المحاولة مرة أخرى لاحقا + + النص diff --git a/owncloudApp/src/main/res/values-bg-rBG/strings.xml b/owncloudApp/src/main/res/values-bg-rBG/strings.xml index 87ac0913525..ee9ae8a9d97 100644 --- a/owncloudApp/src/main/res/values-bg-rBG/strings.xml +++ b/owncloudApp/src/main/res/values-bg-rBG/strings.xml @@ -10,6 +10,7 @@ Съдържание от други приложения Файлове Отваряне с + Отваряне с (само за четене) Нова папка Настройки Подробности @@ -174,6 +175,7 @@ Размер: Тип: Създаден на: + Последно синхронизиране: Променен на: Изтегляне Файл с изображение @@ -211,7 +213,6 @@ Сертификатът на сървъра не е надежден Въведете име на новия файл Името на файла не може да е празно - Името на файла не трябва да бъде повече от %d знака Име на файла Качване на файлове от камерата %d нови изображения бяха качени @@ -364,14 +365,16 @@ Папката, която съдържа този файл е налична офлайн Локалното копие не може да бъде преименувано; опитайте с друго име Преименуването не можe да бъде завършено - файлът вече съществува + Файлът вече съществува + Папката вече съществува + Файл с името %1$s вече съществува. + Папка с името %1$s вече съществува. Неуспешна проверка на отдалечения файл Съдържанието на файла вече е синхронизирано В сървъра е открита нова версия. Изтегляне... Изтеглянето е поставено в опашка Качването е поставено в опашка Папката не може да бъде създадена - Забранени символи: / \\ < > : \" | ? * Името на файла съдържа поне един невалиден символ Името на файла не може да бъде празно Името на файла не може да бъде толкова дълго @@ -428,6 +431,7 @@ Локална версия Сървърна версия Възникна грешка в целевата папка + Замяна Преглед на изображението Изображението не може да бъде показано %1$s не може да бъде копиран в локалната папка %2$s @@ -452,6 +456,7 @@ Отговорът на сървъра отне твърде много време Свързването със сървъра отне твърде много време Сървърът не може да бъде намерен + Възникна грешка в мрежата Нямате необходимите права %s за преименуване на този файл за изтриване на този файл @@ -478,6 +483,7 @@ Избор Преместване тук Копиране тук + Нямате необходимите права за добавяне на съдържание тук! Неуспешно преместване. Моля, проверете дали файлът съществува Не е възможно да преместите папка в нейна подпапка Файлът вече съществува в отдалечената папка. @@ -652,14 +658,28 @@ Продължаване Икона за бележка по изданието Поддръжка за пространства - Въвеждане на поддръжка за функцията за пространства (oCIS). Налична само за нови профили + Въвеждане на поддръжка за функцията за пространства (oCIS). Налична само за нови регистрирани профили Долна лента за навигация Пренареждане на някои раздели Край на поддръжката на Lollipop Това ще бъде последната версия с поддръжка на Android Lollipop (v5.0) + Актуализиране на потока от WebFinger + За сървърите на WebFinger първо ще бъде поисканo търсене в сървър и ако не е успешно, ще бъде последван редовния процес на удостоверяване + Боравене с разрешения + Сега, когато липсват разрешения за извършване на някои действия с дадена папка или файл, тези действия ще бъдат скрити. + Нова настройка \"Управление на известия\" + В настройките е добавен нов елемент, който пренасочва към настройките за известия на приложението на устройството. + Незначителни поправки на грешки и подобрения + Отстранени са някои дребни грешки и са въведени незначителни технически подобрения, за да се подобри работата с приложението. + Незначителни поправки на грешки и подобрения + Отстранени са някои дребни грешки и са въведени незначителни технически подобрения, за да се подобри работата с приложението. Отваряне в мрежата + Отваряне в %1$s (уеб) Не може да се отвори в мрежата няма приложения, които да поддържат този тип файл Поисканият файл все още не е наличен, моля опитайте отново по-късно + + Не показвай отново + Текст diff --git a/owncloudApp/src/main/res/values-ca/strings.xml b/owncloudApp/src/main/res/values-ca/strings.xml index e04a8b928ae..191a53b1f57 100644 --- a/owncloudApp/src/main/res/values-ca/strings.xml +++ b/owncloudApp/src/main/res/values-ca/strings.xml @@ -132,6 +132,7 @@ Mida: Tipus: Creat: + Última sincronització: Modificat: Baixa Sincronitza @@ -166,7 +167,6 @@ El certificat del servidor no és de confiança Introdueix un nom pel fitxer EL nom no es pot deixar en blanc - El nom no pot ser més llarg de %d caràcters Nom del fitxer Pujant arxius pujats desde càmera Pujant arxius disponibles offline @@ -294,7 +294,6 @@ L\'arxiu remot no ha pogut ser comprovat Contingut de l\'arxiu ja sincronitzat La carpeta no s\'ha pogut crear - Caràcters no permesos: / \\ < > : \" | ? * El nom del fitxer conté al menys un caràcter invàlid El Nom de l\'arxiu no pot estar buit Espereu @@ -542,4 +541,6 @@ Procedeix + + Text diff --git a/owncloudApp/src/main/res/values-cs-rCZ/strings.xml b/owncloudApp/src/main/res/values-cs-rCZ/strings.xml index a995c268d90..682906d91c7 100644 --- a/owncloudApp/src/main/res/values-cs-rCZ/strings.xml +++ b/owncloudApp/src/main/res/values-cs-rCZ/strings.xml @@ -69,7 +69,9 @@ Odesílat videa pouze přes wifi Nahrávat videa pouze při nabíjení Pokročilé + Spravovat oznámení Zobrazit skryté soubory + Soubory logu Vymazat koš! Povolit logování Použito k zaznamenávání problémů @@ -102,6 +104,7 @@ Gesto a bezpečnostní kód nemohou být povoleny současně, nejprve zakažte bezpečnostní kód Gesto již nebude vyžadováno Gesto bude vyžadováno při každém startu aplikace + Uzamčeno Soubory Osobní @@ -139,8 +142,10 @@ Došlo k chybě při kopírování souboru do dočasného adresáře. Zkuste prosím zopakovat odeslání. před pár sekundami Žádné soubory + Žádné dostupné soubory offline Žádné sdílené odkazy Nahrajte nějaký obsah nebo synchronizujte se svými přístroji! + Zde uvidíte offline soubory Zde budou zobrazeny soubory a adresáře, které sdílíte jako webové odkazy Načítání... Pro typ souboru nebyla nalezena aplikace @@ -148,6 +153,7 @@ V tomto adresáři nejsou žádné podadresáře. Nebyly nalezeny žádné výsledky odpovídající zadání Nahraná data nejsou dostupná. + Nahrané data se zobrazí zde adresář adresářů soubor @@ -158,6 +164,7 @@ Velikost: Typ: Vytvořen: + Poslední synchronizace: Upraven: Stáhnout Synchronizovat @@ -192,7 +199,6 @@ Certifikát serveru není důvěryhodný Vložte název nového souboru Název souboru nesmí být prázdný - Název souboru nesmí být delší než %d znaků Jméno souboru Nahrávám soubory z fotoparátu %d nových obrázků bude odesláno @@ -340,12 +346,10 @@ Složka obsahující tento soubor je dostupná offline Místní kopii nelze přejmenovat, zkuste jiný nový název Přejmenování nelze dokončit - soubor již existuje Vzdálený soubor nemohl být zkontrolován Obsah souboru je již synchronizován Novější verze byla nalezena na serveru. Stahuji... Adresář nemohl být vytvořen - Zakázané znaky: / \\ < > : \" | ? * Jméno souboru obsahuje alespoň jeden neplatný znak Název nemůže být prázdný Název souboru nemůže být tak dlouhý @@ -611,4 +615,7 @@ správce systému. Otevřít na webu Nepodařilo se otevřít na webu + + Znovu neukazovat + Text diff --git a/owncloudApp/src/main/res/values-de-rCH/strings.xml b/owncloudApp/src/main/res/values-de-rCH/strings.xml index 4a041082226..216ac8d74c0 100644 --- a/owncloudApp/src/main/res/values-de-rCH/strings.xml +++ b/owncloudApp/src/main/res/values-de-rCH/strings.xml @@ -141,6 +141,7 @@ Grösse: Art: Erstellt: + Letzte Synchronisation: Geändert: Herunterladen Synchronisation @@ -175,7 +176,6 @@ Das Zertifikat des Servers ist nicht vertrauenswürdig Einen Namen für die neue Datei eingeben Der Dateiname muss angegeben werden - Der Dateiname darf nicht mehr als %d Zeichen haben Dateiname Hochladen von Hochladedateien der Kamera Hochladen von verfügbaren Offline-Dateien @@ -304,7 +304,6 @@ Die entfernte Datei konnte nicht überprüft werden Dateiinhalte wurden bereits synchronisiert Das Verzeichnis konnte nicht erstellt werden - Verbotene Zeichen sind: / \\ < > : \" | ? * Der Dateiname enthält mindestens ein ungültiges Zeichen Der Dateiname darf nicht leer sein Bitte warte einen Moment. @@ -553,4 +552,7 @@ Fortfahren + + Nicht mehr anzeigen + Text diff --git a/owncloudApp/src/main/res/values-de-rDE/strings.xml b/owncloudApp/src/main/res/values-de-rDE/strings.xml index 562b775bf11..a6f58af6f7e 100644 --- a/owncloudApp/src/main/res/values-de-rDE/strings.xml +++ b/owncloudApp/src/main/res/values-de-rDE/strings.xml @@ -171,10 +171,12 @@ Dateien Klicken Sie auf eine Datei für weitere Informationen. Hochgeladen nach %1$s + Wähle den zu erzeugenden Dokumenten Typ: Dateien Größe: Art: Erstellt: + Letzte Synchronisation: Geändert: Herunterladen Datei-Abbild @@ -212,7 +214,6 @@ Das Zertifikat des Servers ist nicht vertrauenswürdig. Einen Namen für die neue Datei eingeben Der Dateiname muss angegeben werden. - Der Dateiname darf nicht mehr als %d Zeichen haben Dateiname Fotos werden übertragen. %d neue Bilder werden hochgeladen. @@ -365,14 +366,17 @@ Ein Ordner, der diese Datei beinhaltet, ist offline verfügbar. Die lokale Kopie konnte nicht umbenannt werden. Versuchen Sie es mit einem anderen Namen. Die Umbenennung konnte nicht abgeschlossen werden. - Datei existiert bereits. + Datei existiert bereits + Ordner existiert bereits + Datei mit dem Namen %1$s existiert bereits. + Ordner mit dem Namen %1$s existiert bereits. Die entfernte Datei konnte nicht überprüft werden. Dateiinhalte wurden bereits synchronisiert. Eine neue Version befindet sich auf dem Server. Lade herunter ... Herunterladen in Warteschlange eingereiht Hochladen in Warteschlange eingereiht Das Verzeichnis konnte nicht erstellt werden. - Verbotene Zeichen sind: / \\ < > : \" | ? * + Die Datei konnte nicht erstellt werden Der Dateiname enthält mindestens ein ungültiges Zeichen. Der Dateiname darf nicht leer sein. Der Dateiname darf nicht so lang sein. @@ -429,6 +433,7 @@ lokale Version Server Version Im Zielordner ist ein Fehler aufgetreten. + Ersetzen Bildvorschau Dieses Bild kann nicht angezeigt werden. %1$s konnte nicht in den lokalen %2$s Ordner kopiert werden. @@ -655,7 +660,7 @@ Fortfahren Symbol für Veröffentlichungsnotizen Unterstützung für Spaces - Unterstützung von Spaces (oCIS) ist für neue Konten verfügbar. + Unterstützung von Spaces (oCIS)Nur verfügbar für neu eingeloggte Konten Untere Navigationsleiste Einige Tabs neu angeordnet Unterstützung von Lollipop endet. @@ -668,10 +673,20 @@ In den Einstellungen wurde ein neuer Eintrag hinzugefügt, der zu den App-Benachrichtigungseinstellungen des Geräts weiterleitet. Kleine Fehlerberichtigungen und Verbesserungen Kleine Fehler wurden behoben und geringfügige technische Verbesserungen durchgeführt, um die Benutzung der App zu erleichtern. + Markdown-Unterstützung + Markdown-Dateien werden gerendert und im entsprechenden Format dargestellt. + Liste der Provider (Infinite Scale) + Wenn der Server zum Öffnen spezifischer Dateiarten App-Provider unterstützt, werden alle in der Detail-Ansicht angezeigt. + Kleine Fehlerberichtigungen und Verbesserungen + Kleine Fehler wurden behoben und geringfügige technische Verbesserungen durchgeführt, um die Benutzung der App zu erleichtern. Öffnen im Webbrowser Öffnen in %1$s (web) Kann im Webbrowser nicht geöffnet werden es gibt keine Apps die diesen Filetyp unterstützen Die Datei ist derzeit nicht verfügbar, bitte versuchen Sie es später noch einmal. + + Nicht mehr anzeigen + Verstanden + Text diff --git a/owncloudApp/src/main/res/values-de/strings.xml b/owncloudApp/src/main/res/values-de/strings.xml index 2a5bf1a5195..348411062e0 100644 --- a/owncloudApp/src/main/res/values-de/strings.xml +++ b/owncloudApp/src/main/res/values-de/strings.xml @@ -171,10 +171,12 @@ Dateien Klicken Sie auf eine Datei für weitere Informationen. Hochgeladen nach %1$s + Wähle den zu erzeugenden Dokumenten Typ: Dateien Größe: Art: Erstellt: + Letzte Synchronisation: Geändert: Herunterladen Bilddatei @@ -212,7 +214,6 @@ Das Zertifikat des Servers ist nicht vertrauenswürdig Einen Namen für die neue Datei eingeben Der Dateiname muss angegeben werden - Der Dateiname darf nicht mehr als %d Zeichen haben Dateiname Hochladen von Hochladedateien der Kamera %d neue Bilder werden hochgeladen @@ -365,14 +366,17 @@ Ein Ordner der diese Datei beinhaltet ist Offline verfügbar Die lokale Kopie konnte nicht umbenannt werden. Versuche es mit einem anderen neuen Namen. Die Umbenennung konnte nicht abgeschlossen werden. - Datei existiert bereits. + Datei existiert bereits + Ordner existiert bereits + Datei mit dem Namen %1$s existiert bereits. + Ordner mit dem Namen %1$s existiert bereits. Die entfernte Datei konnte nicht überprüft werden Dateiinhalte wurden bereits synchronisiert Eine neue Version befindet sich auf dem Server. Lade herunter ... Herunterladen in Warteschlange eingereiht Hochladen in Warteschlange eingereiht Das Verzeichnis konnte nicht erstellt werden - Verbotene Zeichen sind: / \\ < > : \" | ? * + Die Datei konnte nicht erstellt werden. Der Dateiname enthält mindestens ein ungültiges Zeichen Der Dateiname darf nicht leer sein Der Dateiname darf nicht so lang sein. @@ -429,6 +433,7 @@ Lokale Version Serverversion Im Zielordner ist ein Fehler aufgetreten. + Ersetzen Bildvorschau Dieses Bild kann nicht angezeigt werden %1$s konnte nicht in den lokalen %2$s Ordner kopiert werden @@ -655,7 +660,7 @@ Fortfahren Icon für Veröffentlichungsnotizen Unterstützung für Spaces - Unterstützung von Spaces (oCIS) ist für neue Konten verfügbar. + Unterstützung von Spaces (oCIS)Nur verfügbar für neu eingeloggte Konten Untere Navigationsleiste Einige Tabs neu angeordnet Unterstützung von Lollipop endet. @@ -672,10 +677,16 @@ Markdown-Dateien werden gerendert und im entsprechenden Format dargestellt. Liste der Provider (Infinite Scale) Wenn der Server zum Öffnen spezifischer Dateiarten App-Provider unterstützt, werden alle in der Detail-Ansicht angezeigt. + Kleine Fehlerberichtigungen und Verbesserungen + Kleine Fehler wurden behoben und geringfügige technische Verbesserungen durchgeführt, um die Benutzung der App zu erleichtern. Öffnen im Webbrowser Öffnen in %1$s (web) Kann im Webbrowser nicht geöffnet werden es gibt keine Apps die diesen Filetyp unterstützen Die Datei ist derzeit nicht verfügbar, bitte versuchen Sie es später noch einmal. + + Nicht mehr anzeigen + Verstanden + Text diff --git a/owncloudApp/src/main/res/values-el/strings.xml b/owncloudApp/src/main/res/values-el/strings.xml index b8950ea5816..7d36472198d 100644 --- a/owncloudApp/src/main/res/values-el/strings.xml +++ b/owncloudApp/src/main/res/values-el/strings.xml @@ -142,6 +142,7 @@ Μέγεθος: Τύπος: Δημιουργήθηκε: + Τελευταίος συγχρονισμός: Τροποποιήθηκε: Λήψη Συγχρονισμός @@ -176,7 +177,6 @@ Το πιστοποιητικό του διακομιστή είναι αναξιόπιστο Εισάγετε ένα όνομα για το καινούριο αρχείο Το όνομα αρχείου δεν πρέπει να είναι κενό - Το όνομα αρχείου δεν θα πρέπει να έχει περισσότερους από %d χαρακτήρες Όνομα αρχείου Γίνεται μεταφόρτωση αρχείων από την κάμερα Αποτυχία μεταφορτώσεων κάμερας @@ -303,7 +303,6 @@ Αδυναμία ελέγχου του απομακρυσμένου αρχείου Τα περιεχόμενα του αρχείου έχουν ήδη συγχρονιστεί Ο φάκελος δεν ήταν δυνατό να δημιουργηθεί - Μη-επιτρεπόμενοι χαρακτήρες: / \\ < > : \" | ? * Το όνομα αρχείου περιέχει τουλάχιστο ένα μη έγκυρο χαρακτήρα Το όνομα αρχείου δεν μπορεί να είναι κενό Παρακαλούμε περιμένετε @@ -357,6 +356,7 @@ Διατήρηση και των δύο τοπική έκδοση έκδοση διακομιστή + Αντικατάσταση Προεπισκόπηση εικόνας Αυτή η εικόνα δεν μπορεί να προβληθεί Το %1$s δεν μπόρεσε να αντιγραφεί στον τοπικό φάκελο %2$s @@ -553,4 +553,7 @@ Συνέχιση + + Μην εμφανιστεί ξανά + Κείμενο diff --git a/owncloudApp/src/main/res/values-en-rGB/strings.xml b/owncloudApp/src/main/res/values-en-rGB/strings.xml index 0ce58efee7d..d601eb35bb8 100644 --- a/owncloudApp/src/main/res/values-en-rGB/strings.xml +++ b/owncloudApp/src/main/res/values-en-rGB/strings.xml @@ -10,6 +10,7 @@ Content from other apps Files Open with + Open with (read only) New folder Settings Details @@ -34,6 +35,10 @@ Logging More Security + What´s new in the latest version? + Set locks to access the app and allow touches with other visible windows + Keep track of logs produced by the execution of the app + App info, feedback and others Accounts Manage accounts Passcode lock @@ -44,20 +49,35 @@ After 1 minute After 5 minutes After 30 minutes + Lock access from document provider + Lock access from other apps to the files of the accounts in the app via the Android native file explorer. Touches with other visible windows Allow touches when the view is obscured by another visible window. Enable it to use light filtering apps. Are you sure you want to enable this feature? Use this feature under your own responsibility, a malicious application could try to spoof you into performing some actions, unaware, using other views. + Auto upload pictures + Manage location and behaviour of the pictures uploaded automatically + Auto upload videos + Manage location and behaviour of the videos uploaded automatically Picture uploads Automatically upload pictures taken by camera Picture upload path + Conditions to execute uploads + Uploads will be triggered when all selected options are fulfilled Upload pictures via wifi only + Upload pictures only when charging + Enable wifi only uploads to activate this option Video uploads Automatically upload videos recorded by camera Video upload path Upload videos via wifi only + Upload videos only when charging Advanced + Manage notifications Show hidden files + Log files + Empty log folder! + Enable logging and log files will show up here Enable logging This is used to log problems Logging history @@ -66,6 +86,8 @@ Help Sync your contacts, calendars and tasks Install DAVx⁵ + Access document provider + Suggested app to browse the files of your device via the Android native file explorer Recommend to a friend Feedback Imprint @@ -73,17 +95,23 @@ Remember last share upload location Pattern lock Enable logging + When activated, logs may impact performance and include sensitive information. However the logs are not subject to automatic submission to ownCloud servers. Sharing logs with others is sole user responsibility. Send feedback + App version + %1$s %2$s version %3$s (%4$s) Please enter your pattern Enter your pattern Remove your pattern Incorrect pattern Please re-enter your pattern The patterns are not same + An error occurred while setting the pattern lock + An error occurred while removing the pattern lock Pattern and passcode locks cannot be enabled at the same time, please disable pattern first Pattern and passcode locks cannot be enabled at the same time, please disable passcode first The pattern will not be asked anymore The pattern will be requested every time the app is started + Locked A valid ACTION is needed in the Intent passed to Files @@ -122,31 +150,44 @@ An error occurred whilst copying the file to a temporary folder. Please try to send again. seconds ago No files in here + No spaces + No available offline files No shared links Upload some content or sync with your devices! + No shares + You are not collaborating on other people\'s resources + You don\'t have access to any space! + Files and folders you mark as available offline will show up here Files and folders you share by link will show up here Loading… No app found for file type + No app found for this action There are no files in this folder. There are no folders in this folder. No matches for this search No uploads available. + Upload something and it will show up here folder folders file file Tap on a file to display additional information. Upload to %1$s + Pick a document type to create: Files Size: Type: Created: + Last sync: Modified: Download + File Image + Cancel download button Synchronise File was renamed to %1$s during upload List layout Share + Preparing account for first launch Yes No Remove upload @@ -175,9 +216,13 @@ Server certificate is not trusted Insert a name for the new file Filename must not be empty - Filename must not be more than %d characters Filename Uploading camera upload files + %d new pictures will be uploaded + %d new videos will be uploaded + Camera Uploads failed + Picture uploads source path is not valid anymore + Video uploads source path is not valid anymore Uploading available offline files Uploading requested from WiFi files Server certificate is not trusted @@ -185,6 +230,7 @@ Current Failed (tap to retry) Uploaded + Enqueued %d files %d file Completed @@ -202,13 +248,19 @@ App was terminated Unknown error Waiting for WiFi connectivity + Enqueued Waiting to upload Unsupported media type + Upload of %1$s has been enqueued + Upload cancelled + Download cancelled + Download of %1$s has been enqueued Downloading… %1$d%% Downloading %2$s Download succeeded %1$s downloaded Download failed + Unable to display the file Download of %1$s could not be completed Not downloaded yet Download failed, you need to log in again @@ -225,17 +277,31 @@ Folder %1$s does not exist anymore Please insert your passcode Enter your passcode + Enter a new %1$d-digit passcode The passcode will be requested every time the app is started Please reenter your passcode Remove your passcode The passcodes are not the same Incorrect passcode + An error occurred while setting the passcode lock + An error occurred while removing the passcode lock + Backspace button + Biometric button + 0 + 1 + 2 3 4 5 + 6 + 7 + 8 + 9 + Please, try again in %1$s Register at least one biometric to use this feature Biometric log in Log in using your biometric credential + Do you want to additionally activate biometric security? %1$s music player %1$s (playing) %1$s (loading) @@ -292,6 +358,7 @@ Do you really want to remove %1$s and its contents? Local only Do you really want to remove the account %1$s? + This account has camera uploads attached, removing the account will disable the camera uploads. Do you really want to remove the account %1$s? Remove link Remove private share Do you really want to remove %1$s share? @@ -301,12 +368,20 @@ A folder that contains this file is available offline Local copy could not be renamed; try a different name Rename could not be completed + File already exists + Folder already exists + File with name %1$s already exists. + Folder with name %1$s already exists. Remote file could not be checked File contents already synchronised + A new version was found in server. Downloading… + Download enqueued + Upload enqueued Folder could not be created - Forbidden characters: / \\ < > : \" | ? * + File could not be created File name contains at least one invalid character File name cannot be empty + File name cannot be that long Wait a moment Checking stored credentials Unexpected problem; please select the file from a different app @@ -353,11 +428,14 @@ Are you sure you want to disable this feature? The pending videos will not be uploaded Please make sure the folder selected is where the camera you are using saves the pictures taken. Otherwise, the feature will not be able to detect your pictures. Keep in mind that pictures will be uploaded in at least 15 minutes after taking them. Please make sure the folder selected is where the camera you are using saves the videos recorded. Otherwise, the feature will not be able to detect your videos. Keep in mind that videos will be uploaded in at least 15 minutes after recording them. + File in conflict There is a conflict in file %1$s, tap to solve it Which files do you want to keep? If you select both versions, the local file will have a number added to its name. Keep both local version server version + An error occurred in the destination folder + Replace Image preview This image cannot be shown %1$s could not be copied to %2$s local folder @@ -382,6 +460,7 @@ The server took too long to respond The server took too long to connect Server could not be reached + An error occurred in the network You do not have permission %s to rename this file to delete this file @@ -406,6 +485,9 @@ Move Nothing in here. You can add a folder! Choose + Move Here + Copy Here + You have no permissions to add content here! Unable to move. Please check whether the file exists It is not possible to move a folder into a descendant The file exists already in the destination folder @@ -434,13 +516,18 @@ %1$d files %1$d files, 1 folder %1$d files, %2$d folders + Account to upload pictures + Account to upload videos Camera folder (%1$s) required Original file will be Original file will be + Last synchronisation + You can update your preferences in Settings Copy file Move file kept in original folder + removed from original folder Share Share %1$s Users and Groups @@ -518,6 +605,8 @@ See music player File sync See file sync result + File conflicts + See file conflicts when they occur You %1$d clicks away to enable developer menus Rate %1$s app! If you enjoy using this app, could you take a moment to rate it? Your feedback is very important for us. @@ -546,13 +635,68 @@ It was not possible to know the server base URL Log HTTP requests and responses Logs may contain sensitive information. Sharing logs with others is sole user responsibility + Synchronising account + Spaces could not be refreshed + Found %1$s of data on your external storage. It will be moved to the safe storage on your device. Remaining files will be cleaned from your external storage after the migration to avoid duplicates and vulnerability. + Your app has been updated. This app update migrates your files to a more secure location on your device. This one-time data migration is required and can take up to a minute or more. + Let\'s get started + ⚠️ Your free space is currently limited on your device. Some files may not be migrated, please check that all downloaded files keep downloaded after the process has been completed. + Migrate now + Migration completed successfully. Your files are now safer than ever before. + Access your files + Migrating your files. Please don’t turn your device off. + Working… Please wait + More security for your files + Please, select an option to lock the app: Passcode lock + Pattern lock Upload some content or sync with your devices! Proceed + Username empty + New in %1$s + Thank you for using %1$s.\n❤ Proceed + Release note icon + Support for Spaces + Introducing support for spaces feature (oCIS). Only available for newly logged-in accounts + Bottom navigation bar + Reordered some tabs + End of support Lollipop + This will be the last release with support for Android Lollipop (v5.0) + Updated WebFinger flow + For WebFinger servers, lookup server will be requested first, and if not successful, regular authentication flow will be followed + Permission handling + Now, when there is lack of permissions to perform some actions over a folder or a file, those actions will be hidden + New \"Manage notifications\" setting + A new item has been added in the settings, which redirects to the app notifications settings of the device + Minor bugfixes and improvements + Some minor bugs have been fixed, and minor technical improvements were introduced to improve the experience in the app + Markdown support + Markdown files rendered and displayed with proper format + List of providers (oCIS) + If your server supports app providers to open specific kind of files, all of them will be listed in Details view + Themed icons supported + Android 13 feature \"Themed icons\" will let you set app icon in monochrome + Create new documents via web (oCIS) + If oCIS server supports application providers, new documents can be created via browser and synced with oC + New setting with app suggested to browse files + A new setting was added in \"More\" section with a suggested app to access files via document provider + Minor bugfixes and improvements + Some minor bugs have been fixed, and minor technical improvements were introduced to improve the experience in the app + Open in web + Open in %1$s (web) + Couldn\'t open in web + there are no apps that support this file type + The requested file is not yet available, please try again later + + oCIS accounts warning + Please, remove the account and login again to get the spaces feature + Don\'t show again + Understood + Text diff --git a/owncloudApp/src/main/res/values-es-rAR/strings.xml b/owncloudApp/src/main/res/values-es-rAR/strings.xml index ba8bad94da8..c955a25aad5 100644 --- a/owncloudApp/src/main/res/values-es-rAR/strings.xml +++ b/owncloudApp/src/main/res/values-es-rAR/strings.xml @@ -143,6 +143,7 @@ Tamaño: Tipo: Creado: + Ultima sincronización: Modificado: Descargar Sincronizar @@ -177,7 +178,6 @@ El certificado del servidor no es confiable Inserte un nombre para el archivo nuevo El nombre de archivo no debe quedar vacío - El nombre de archivo no debe ser mayor a %d caracteres Nombre de archivo Subiendo archivos de la cámara marcados como subir Subiendo archivos disponibles sin conexión @@ -304,7 +304,6 @@ No pudo comprobarse el archivo remoto Ya está sincronizado La carpeta puede no haber sido creada - Caracteres prohibidos: / \\ < > : \" | ? * El nombre del archivo contiene algún caracter inválido El nombre de archivo no puede estar vacío Esperá un momento @@ -551,4 +550,6 @@ Prosiga + + Texto diff --git a/owncloudApp/src/main/res/values-es/strings.xml b/owncloudApp/src/main/res/values-es/strings.xml index fb416c43821..53c8b3f5e47 100644 --- a/owncloudApp/src/main/res/values-es/strings.xml +++ b/owncloudApp/src/main/res/values-es/strings.xml @@ -86,6 +86,8 @@ Ayuda Sincronice sus contactos, calendarios y tareas Instalar DAVx⁵ + Acceso al proveedor de documentos + Aplicación sugerida para navegar por los archivos de tu dispositivo a través del explorador de archivos nativo de Android. Recomendar a un amigo Sugerencias Imprimir @@ -148,9 +150,13 @@ Ha ocurrido un error al copiar a la carpeta temporal. Por favor intentelo de nuevo. hace segundos Aquí no hay archivos + Sin espacios No hay ficheros sin conexión No hay enlaces compartidos ¡Suba algún contenido o sincronice con sus dispositivos! + Sin acciones + No colaboras con recursos ajenos + ¡No tienes acceso a ningún espacio! Los ficheros y carpetas marcados como disponibles online aparecerán aquí Aquí aparecerán los archivos y carpetas compartidos mediante enlace Cargando... @@ -167,10 +173,12 @@ ficheros Pulsa sobre un archivo para mostrar información adicional. Subir a %1$s + Elige un tipo de documento para crear: Ficheros Tamaño: Tipo: Creado: + Última sincronización: Modificado: Descargar Imagen de archivo @@ -179,6 +187,7 @@ El fichero fue renombrado como %1$s durante la subida Listar fuente Compartir + Preparar la cuenta para el primer lanzamiento No Borrar subida @@ -207,7 +216,6 @@ El certificado del servidor no es de confianza Introduzca el nombre para el nuevo archivo El nombre del archivo no debe estar vacio - El nombre del archivo no debe tener mas de %d caracteres. Nombre Subiendo archivos de la cámara Se cargarán %d imágenes nuevas @@ -360,14 +368,17 @@ La carpeta que contiene este archivo está disponible offline No se pudo cambiar el nombre de la copia local, trata con un nombre differente No se pudo cambiar el nombre - fichero ya existe + El archivo ya existe + La carpeta ya existe + Ya existe un archivo con el nombre %1$s. + Ya existe una carpeta con el nombre %1$s. No se ha podido comprobar el fichero remoto Ya está sincronizado Se ha encontrado una nueva versión en el servidor. Descargando... Descarga encolada Subida encolada No se pudo crear la carpeta - Carácteres ilegales: / \\ < > : \" | ? * + No se ha podido crear el archivo El nombre del archivo contiene al menos un carácter no válido El nombre de archivo no puede estar vacío El nombre del fichero no puede ser tan largo @@ -423,6 +434,8 @@ Mantener ambos versión local versión del servidor + Se ha producido un error en la carpeta de destino + Reemplazar Previsualización de imagen No se puede mostrar la imagen %1$s se pudo copiar a la carpeta local %2$s @@ -447,6 +460,7 @@ El servidor ha tardado demasiado en responder El servidor ha tardado demasiado en conectar No puedo encontrar el servidor + Se ha producido un error en la red No tiene permiso %s para renombrar este archivo para eliminar este archivo @@ -473,6 +487,7 @@ Elegir Mover aquí Copiar aquí + ¡No tienes permisos para añadir contenido aquí! No se puede mover. Revise si el archivo existe No se puede mover una carpeta dentro de una de sus subcarpetas. El archivo ya existe en la carpeta de destino @@ -508,6 +523,7 @@ El archivo original será Archivo original será Última sincronización + Puedes actualizar tus preferencias en Ajustes Copiar archivo Mover archivo dejado en la carpeta original @@ -620,6 +636,7 @@ Registrar peticiones y respuestas HTTP Los registros pueden contener información sensible. Compartirlos o no es responsabilidad exclusiva del usuario Sincronizando cuenta + No se han podido actualizar los espacios Se encontraron %1$s de datos en su almacenamiento externo. Se moverá al almacenamiento seguro en su dispositivo. Los archivos restantes se limpiarán de su almacenamiento externo después de la migración para evitar duplicados y vulnerabilidades. Su aplicación ha sido actualizada. Esta actualización migra sus archivos a una ubicación más segura en su dispositivo. Esta migración de datos única es necesaria y puede tardar hasta un minuto o más. @@ -649,4 +666,7 @@ No se pudo abrir en web No hay aplicaciones que soporten este tipo de fichero El fichero solicitado no está disponible aún. Por favor, inténtelo más tarde + + No mostrar de nuevo + Texto diff --git a/owncloudApp/src/main/res/values-et-rEE/strings.xml b/owncloudApp/src/main/res/values-et-rEE/strings.xml index f894508ed62..3c31fc9ac73 100644 --- a/owncloudApp/src/main/res/values-et-rEE/strings.xml +++ b/owncloudApp/src/main/res/values-et-rEE/strings.xml @@ -151,6 +151,7 @@ sekundit tagasi Siin ei ole faile Ruume ei ole + Ruum: Võrguühenduseta saadavalolevaid faile ei ole Jagatud linke pole Laadi sisu üles või süngi oma seadmetega! @@ -178,6 +179,8 @@ Suurus: Tüüp: Loodud: + Viimane sünkroniseerimine: + Rada: Muudetud: Lae alla Faili tõmmis @@ -368,7 +371,10 @@ Kaust, mis sisaldab seda faili, on saadaval võrguühenduseta Kohalikku faili ei saa ümber nimetada; proovi teist uut nime Ümbernimetamine ebaõnnestus - fail on juba olemas + Fail on juba olemas + Kaust on juba olemas + Fail nimega %1$s on juba olemas. + Kaust nimega %1$s on juba olemas. Mujaloleva faili kontrollimine ebaõnnestus Faili sisu on juba sünkroniseeritud Serveris leidus uus versioon. Laen alla… @@ -376,7 +382,7 @@ Üleslaadimine lisati järjekorda Kataloogi ei saa tekitada Faili loomine ei õnnestunud - Keelatud sümbolid: / \\ < > : \" | ? * + Keelatud märgid: / \\ Faili nimi sisaldab vähemalt üht keelatud märki Faili nime lahter ei saa olla tühi Faili nimi ei saa olla nii pikk @@ -388,6 +394,7 @@ Faili kopeerimine privaatsest salvestusalast Logi sisse oAuth2-ga oAuth2 serveriga ühendumine... + Ühendus on ebaturvaline, http liiklus ei ole lubatud. Serveri identiteeti ei suudetud kinnitada - Serveri sertifikaat pole usaldusväärne - Serveri sertifikaat on aegunud @@ -433,6 +440,7 @@ kohalik versioon serveri versioon Sihtkaustas tekkis viga + Asenda Pildi eelvaade Seda pilti ei saa näidata %1$s ei suudetud kopeerida kohalikku kataloogi %2$s @@ -659,7 +667,7 @@ Jätka Väljalasketeavituse ikoon Ruumide tugi - Tutvustame tuge ruumide funktsioonile (oCIS). Saadaval ainult uutele kontodele + Spaces toe lisamine (oCIS). Saadaval vaid uutele kontodele Alumine navigatsiooniriba Mõnede kaartide ümberjärjestamine Lollipop toe lõpp @@ -680,10 +688,33 @@ Android 13 võimalus \"Temaatilised ikoonid\" lubab rakenduse ikoonid muuta mustvalgeks Loo uusi dokumente üle võrgu (oCIS) Kui oCIS server toetab rakenduste pakkujaid, siis saab uusi dokumente luua ja zoneCloudiga sünkroonida läbi brauseri + Uus seadistus failisirvija soovitusega + Lisatud on uus seadistus \"More\" alajaotuses, kus soovitatakse rakendust ligipääsuks failidele läbi dokumendi pakkuja + Lisatud on 3 punktiga nupp kõikide üksuste kuvamiseks + Lisatud on nupp näitamaks menüüd koos üksuse kõigi sätetega + Detailivaate parendused + Detailivaate uus kujundus + Tugi keelevahetuseks + Lisatud tugi uueks keelevahetuseks Android versioonil 13 ja üle + Parandatud viga ruumi piltide laadimisel + Ruumi piltide esmakordsel laadimisel neid näidatakse ja parandatakse üksuste piltide laadimist + Markdown failid avatakse ASCII-režiimis + Lisatud uus režiim markdown-failide avamiseks, mis näitab ASCII-koode + Näidatakse pop-up\'i kopeerimise/teisaldamise konflikti korral + Näidatakse 3 valikuga pop-up\'i kui kopeerimise või teisaldamise ajal tekib nimekonflikt + Väiksemad veaparandused ja parendused + Parandatud on mõned väiksemad tarkvaravead ning lisatud on väiksemad tehnilised parendused, et parandada rakenduse kasutajakogemust Ava veebis Ava %1$s (veebis) Ei saanud veebis avada seda failitüüpi toetavaid rakendusi ei ole Nõutud fail ei ole veel saadaval, palun proovige hiljem uuesti + + oCIS kontode hoiatus + Spaces võimaluse kasutamiseks eemalda, palun, konto ja logi uuesti sisse + Ära enam näita + Loetud + Tekst + Rakenda kõigile %1$s-le konfliktile diff --git a/owncloudApp/src/main/res/values-fr-rFR/strings.xml b/owncloudApp/src/main/res/values-fr-rFR/strings.xml index 568abe9408a..d36769ef4c5 100644 --- a/owncloudApp/src/main/res/values-fr-rFR/strings.xml +++ b/owncloudApp/src/main/res/values-fr-rFR/strings.xml @@ -191,7 +191,6 @@ Téléchargez-le ici : %2$s Le certificat du serveur n\'est pas sûr Saisissez le nom du nouveau fichier Le nom de fichier ne doit pas être vide - Le nom de fichier ne doit pas contenir plus de %d caractères Nom du fichier Téléversement des fichiers de l\'appareil photo %d nouvelles images seront téléversées @@ -327,7 +326,6 @@ Téléchargez-le ici : %2$s Le fichier distant ne peut pas être vérifié Le contenu du fichier est déjà synchronisé Echec de la création du dossier - Caractères non autorisés : / \\ < > : \" | ? * Le nom de fichier contient au moins un caractère invalide Le nom de fichier ne peut pas être vide Merci de patienter @@ -590,4 +588,5 @@ Téléchargez-le ici : %2$s + diff --git a/owncloudApp/src/main/res/values-fr/strings.xml b/owncloudApp/src/main/res/values-fr/strings.xml index 311dba7b9ee..0bf25543251 100644 --- a/owncloudApp/src/main/res/values-fr/strings.xml +++ b/owncloudApp/src/main/res/values-fr/strings.xml @@ -161,6 +161,7 @@ Téléchargez-le ici : %2$s Taille : Type : Créé le : + Dernière synchronisation : Modifié le : Télécharger Bouton d\'annulation de téléchargement @@ -196,7 +197,6 @@ Téléchargez-le ici : %2$s Le certificat du serveur n\'est pas sûr Saisissez le nom du nouveau fichier Le nom de fichier ne doit pas être vide - Le nom de fichier ne doit pas contenir plus de %d caractères Nom du fichier %d de nouvelles images vont être téléversées %d de nouvelles vidéos vont être téléversées @@ -332,7 +332,6 @@ Téléchargez-le ici : %2$s Le fichier distant n\'a pu être vérifié Le contenu du fichier est déjà synchronisé Le dossier n\'a pas pu être créé - Caractères interdits : / \\ < > : \" | ? * Le nom de fichier contient au moins un caractère non valide Le nom du fichier ne peut pas être vide Veuillez patienter @@ -386,6 +385,7 @@ Téléchargez-le ici : %2$s Conserver les deux versions version locale version serveur + Remplacer Prévisualisation de l\'image Cette image ne peut pas être affichée %1$s n\'a pas pu être copié dans le dossier local %2$s @@ -600,4 +600,7 @@ Téléchargez-le ici : %2$s Afficher dans le navigateur web Aucune application ne supporte ce type de fichier + + Ne plus montrer + Texte diff --git a/owncloudApp/src/main/res/values-gl/strings.xml b/owncloudApp/src/main/res/values-gl/strings.xml index b53d9825981..8350c3340aa 100644 --- a/owncloudApp/src/main/res/values-gl/strings.xml +++ b/owncloudApp/src/main/res/values-gl/strings.xml @@ -143,6 +143,7 @@ Descárgueo de aquí: %2$s Tamaño: Tipo: Creado: + Última sincronización: Modificado: Descargar Sincronizar @@ -177,7 +178,6 @@ Descárgueo de aquí: %2$s O certificado do servidor non é de confianza Introduza o nome do novo ficheiro O nome de ficheiro non debe estar baleiro - O nome do ficheiro non debe ter máis de %d caracteres Nome de ficheiro Enviando ficheiros pendentes de envío da cámara Enviando ficheiros dispoñíbeis sen conexión @@ -310,7 +310,6 @@ Descárgueo de aquí: %2$s Non foi posíbel comprobar o ficheiro remoto Os contidos do ficheiro xa están sincronizados Non foi posíbel crear o cartafol - Caracteres non permitidos: / \\ < > : \" | ? * O nome de ficheiro contén algún carácter incorrecto O nome de ficheiro non pode estar baleiro Agarde un chisco @@ -562,4 +561,7 @@ Descárgueo de aquí: %2$s Proceder + + Non volver amosar + Texto diff --git a/owncloudApp/src/main/res/values-he/strings.xml b/owncloudApp/src/main/res/values-he/strings.xml index 8ff7f7df97f..8402189bb5e 100644 --- a/owncloudApp/src/main/res/values-he/strings.xml +++ b/owncloudApp/src/main/res/values-he/strings.xml @@ -159,6 +159,7 @@ גודל: סוג: מועד היצירה: + סנכרון אחרון: מועד השינוי: הורדה קובץ תמונה @@ -196,7 +197,6 @@ תעודת שרת אינה מהימנה יש להכניס שם לקובץ החדש שדה שם קובץ אינו יכול להיות ריק - שדה שם קובץ אינו יכול להכיל יותר מ- %d תווים שם קובץ מעלה קובצי מצלמה %d תמונות חדשות יועלו @@ -330,7 +330,6 @@ לא ניתן לבדוק את הקובץ המרוחק תוכן הקובץ כבר מסונכרן לא ניתן ליצור תיקייה - תווים אסורים: / \\ < > : \" | ? * שם קובץ כולל לפחות תו אחד לא חוקי שם קובץ לא יכול להיות ריק נא להמתין רגע @@ -583,4 +582,7 @@ להמשך + + אל תציג שוב + טקסט diff --git a/owncloudApp/src/main/res/values-hu-rHU/strings.xml b/owncloudApp/src/main/res/values-hu-rHU/strings.xml index 8693314dca1..bd27fa8666f 100644 --- a/owncloudApp/src/main/res/values-hu-rHU/strings.xml +++ b/owncloudApp/src/main/res/values-hu-rHU/strings.xml @@ -10,6 +10,7 @@ Más alkalmazásokból származó tartalom Fájlok Megnyitás a következővel + Megnyitás (csak olvasható) Új mappa Beállítások Részletek @@ -34,12 +35,17 @@ Logolás Több Biztonság + Mi az újdonság a legújabb verzióban? + Zárak beállítása az alkalmazáshoz való hozzáféréshez és az érintések engedélyezése más látható ablakoknál + Az alkalmazás végrehajtása során keletkező naplófájlok nyomon követése Applikáció információk, visszajelzés, egyéb Fiókok Fiókok kezelése Jelszavas zár Biometrikus azonosítás Adjon meg zárolási jelszót vagy mintát a beállítás engedélyezéséhez + Alkalmazás lezárása + Azonnal Érintkezik más látható ablakkal Engedélyezze az érintéseket, amikor a kép takarásban van egy másik ablak által. Biztos, hogy engedélyezi ezt a funkciót? @@ -142,6 +148,7 @@ Méret: Típus: Létrehozva: + Utoljára szinkronizálva: Módosítva: Letöltés Szinkronizálás @@ -176,7 +183,6 @@ A kiszolgáló tanúsítványa nem megbízható Adja meg az új fájlt nevét A fájlnév nem lehet üres - A fájlnév legfeljebb %d karakter hosszú lehet Fájlnév Fényképezőgép feltöltési fájlok feltöltése %dúj kép kerül feltöltésre @@ -312,7 +318,6 @@ A távoli fájl nem volt ellenőrizhető Az állományok már szinkronizáltak A mappát nem lehet létrehozni - Tiltott karakterek: / \\ < > : \" | ? * A fájlnév legalább egy érvénytelen karaktert tartalmaz A fájlnév nem lehet üres Egy pillanat… @@ -566,4 +571,6 @@ Végrehajtás + + szöveg diff --git a/owncloudApp/src/main/res/values-id/strings.xml b/owncloudApp/src/main/res/values-id/strings.xml index 0f2b98b6979..d385334e395 100644 --- a/owncloudApp/src/main/res/values-id/strings.xml +++ b/owncloudApp/src/main/res/values-id/strings.xml @@ -139,6 +139,7 @@ Ukuran: Tipe: Dibuat: + Sinkronisasi terakhir: Diubah: Unduh Sinkronisasi @@ -299,7 +300,6 @@ Berkas jauh tidak dapat diperiksa Isi berkas sudah diselaraskan Folder tidak dapat dibuat - Karakter yang dilarang: / \\ < > : \" | ? * Nama berkas berisi setidaknya satu karakter yang tidak sah Nama berkas tidak boleh kosong Tunggu sebentar @@ -536,4 +536,6 @@ Melanjutkan + + Teks diff --git a/owncloudApp/src/main/res/values-it/strings.xml b/owncloudApp/src/main/res/values-it/strings.xml index 0fbe8786d2f..747ab506dba 100644 --- a/owncloudApp/src/main/res/values-it/strings.xml +++ b/owncloudApp/src/main/res/values-it/strings.xml @@ -176,6 +176,7 @@ Dimensione: Tipo: Creato: + Ultima sincronizzazione: Modificato: Scarica File immagine @@ -213,7 +214,6 @@ Il certificato del server non è affidabile Inserisci un nome per il nuovo file Il nome del file non può essere vuoto - Il nome del file non può contenere piu\' di %d caratteri Nome del file Caricamento dei file della fotocamera %d nuove foto saranno caricate @@ -366,7 +366,6 @@ Una cartella che contiene questo file è disponibile non in linea La copia locale non può essere rinominata; prova un nome diverso La rinomina non può essere completata - File già esistente Il file remoto non può essere controllato Contenuti del file già sincronizzati È stata trovata una nuova versione sul server. Scaricamento... @@ -374,7 +373,6 @@ Caricamento in coda La cartella non può essere creata Il file non può essere creato - Caratteri proibiti: / \\ < > : \" | ? * Il nome del file contiene almeno un carattere non valido Il nome del file non può essere vuoto Il nome del file non può essere così lungo @@ -648,4 +646,7 @@ Il Download del file inizierà automaticamente Grazie di usare %1$s.\n❤ Procedi + + Non mostrare di nuovo + Testo diff --git a/owncloudApp/src/main/res/values-ko/strings.xml b/owncloudApp/src/main/res/values-ko/strings.xml index 358a8622892..74c0c4507c2 100644 --- a/owncloudApp/src/main/res/values-ko/strings.xml +++ b/owncloudApp/src/main/res/values-ko/strings.xml @@ -174,6 +174,7 @@ 크기: 종류: 만든 날짜: + 마지막 동기화: 수정한 날짜: 다운로드 파일 이미지 @@ -211,7 +212,6 @@ 서버의 인증서를 신뢰할 수 없습니다. 새 파일 이름을 입력하십시오 파일 이름을 비워둘 수 없습니다. - 파일 이름은 %d글자를 초과하면 안 됩니다. 파일 이름 카메라 파일을 업로드 중 새 사진 %d개가 곧 업로드됨 @@ -364,14 +364,12 @@ 이 파일을 포함한 폴더를 오프라인에서 사용할 수 있음 로컬 파일의 이름을 변경할 수 없습니다. 다른 이름을 입력하십시오 이름을 변경할 수 없음 - 파일이 이미 존재함 원격 파일을 확인할 수 없음 파일 내용이 이미 동기화됨 새 버전을 서버 내에서 찾았습니다. 다운로드 중… 다운로드 대기 중 업로드 대기 중 폴더를 만들 수 없음 - 사용할 수 없는 문자: / \\ < > : \" | ? * 파일 이름에 하나 이상의 사용할 수 없는 문자가 포함되어 있음 파일 이름이 비어 있을 수 없음 파일 이름이 너무 긺 @@ -428,6 +426,7 @@ 로컬 버전 서버 버전 대상 폴더에서 오류 발생 + 바꾸기 사진 미리 보기 이 사진을 미리 볼 수 없음 %1$s을(를) 로컬 폴더 %2$s(으)로 복사할 수 없습니다 @@ -651,7 +650,6 @@ %1$s을(를) 사용해주셔서 감사합니다.\n❤ 진행 스페이스 관련 도움 - 스페이스 기능 관련 고객지원을 소개합니다 (oCIS). 새로 만들어진 계정에서만 이용할 수 있습니다. 하단 내비게이션 바 일부 탭을 재정렬함 Lollipop 지원 종료 @@ -661,4 +659,7 @@ 웹에서 열 수 없음 이 파일 형식을 지원하는 앱이 없음 요청한 파일을 아직 이용할 수 없습니다. 나중에 다시 시도해 주세요. + + 다시 보지 않기 + 텍스트 diff --git a/owncloudApp/src/main/res/values-land/dims.xml b/owncloudApp/src/main/res/values-land/dims.xml new file mode 100644 index 00000000000..5be04722457 --- /dev/null +++ b/owncloudApp/src/main/res/values-land/dims.xml @@ -0,0 +1,4 @@ + + + 300dp + diff --git a/owncloudApp/src/main/res/values-nb-rNO/strings.xml b/owncloudApp/src/main/res/values-nb-rNO/strings.xml index 9071e5f1910..b259a7500fc 100644 --- a/owncloudApp/src/main/res/values-nb-rNO/strings.xml +++ b/owncloudApp/src/main/res/values-nb-rNO/strings.xml @@ -134,6 +134,7 @@ Størrelse: Type: Opprettet: + Siste synkronisering: Endret: Last ned Synkroniser @@ -294,7 +295,6 @@ Eksterne filer kunne ikke sjekkes filinnhold er allerede synkronisert Mappe kunne ikke opprettes - Forbudte tegn: / \\ < > : \" | ? * Filnavnet inneholder minst ett ulovlig tegn Filnavn kan ikke være tomt Vent et øyeblikk @@ -531,4 +531,7 @@ Fortsett + + Ikke vis igjen + Tekst diff --git a/owncloudApp/src/main/res/values-nl/strings.xml b/owncloudApp/src/main/res/values-nl/strings.xml index 3d3f940972a..4f334b109fa 100644 --- a/owncloudApp/src/main/res/values-nl/strings.xml +++ b/owncloudApp/src/main/res/values-nl/strings.xml @@ -142,6 +142,7 @@ Download hier: %2$s Grootte: Type: Aangemaakt: + Laatste sync: Aangepast: Download Synchroniseren @@ -176,7 +177,6 @@ Download hier: %2$s Het servercertificaat is niet vertrouwd Voer de naam in van het nieuwe bestand Bestandsnaam mag niet leeg zijn - De bestandsnaam mag niet langer zijn dan %d tekens Bestandsnaam Camera bestanden worden geupload Uploaden van beschikbare offline bestanden @@ -309,7 +309,6 @@ Download hier: %2$s Extern bestand kon niet worden gecontroleerd Bestandsinhoud is al gesynchroniseerd Map kon niet worden aangemaakt - Verboden tekens: / \\ < > : \" | ? * De bestandsnaam bevat ten minste één ongeldig teken Bestandsnaam mag niet leeg zijn Even geduld @@ -558,4 +557,7 @@ Neem contact op met uw beheerder Ga verder + + Toon niet nogmaals + Tekst diff --git a/owncloudApp/src/main/res/values-pl/strings.xml b/owncloudApp/src/main/res/values-pl/strings.xml index 0688af6c69e..383de78686b 100644 --- a/owncloudApp/src/main/res/values-pl/strings.xml +++ b/owncloudApp/src/main/res/values-pl/strings.xml @@ -165,6 +165,7 @@ Rozmiar: Typ: Utworzono: + Ostatni sync: Zmodyfikowano: Pobierz Plik Obraz @@ -201,7 +202,6 @@ Certyfikat serwera jest niezaufany Wprowadź nazwę nowego pliku Nazwa pliku nie może być pusta - Nazwa pliku nie może być dłuższa niż %d znaków Nazwa pliku Wgrywanie plików zdjęć %d nowych plików zostanie wysłanych @@ -351,7 +351,6 @@ Nie można sprawdzić zdalnego pliku Zawartość pliku została już synchronizowana Folder nie może zostać utworzony - Znaki zabronione: / \\ < > : \" | ? * Nazwa pliku zawiera co najmniej jeden nieprawidłowy znak Nazwa pliku nie może być pusta. Poczekaj chwilę @@ -625,4 +624,5 @@ Wykonaj Ikona + diff --git a/owncloudApp/src/main/res/values-pt-rBR/strings.xml b/owncloudApp/src/main/res/values-pt-rBR/strings.xml index 997cc437de7..390e3c166c8 100644 --- a/owncloudApp/src/main/res/values-pt-rBR/strings.xml +++ b/owncloudApp/src/main/res/values-pt-rBR/strings.xml @@ -86,6 +86,8 @@ Ajuda Sincronize seus contatos, calendários e tarefas Instalar DAVx⁵ + Acessar provedor de documentos + Aplicativo sugerido para navegar pelos arquivos do seu dispositivo por meio do explorador de arquivos nativo do Android Recomendar a um amigo Feedback Imprint @@ -149,6 +151,7 @@ segundos atrás Nenhum arquivo aqui No spaces + Espaço: Nenhum arquivo off-line disponível Nenhum link compartilhado Carregue algum conteúdo ou sincronize com seus dispositivos! @@ -171,10 +174,13 @@ arquivos Toque em um arquivo para mostrar informações adicionais. Enviado para %1$s + Escolha um tipo de documento para criar: Arquivos Tamanho: Tipo: Criado: + Última sincronização: + Caminho: Modificado: Baixar Imagem do Arquivo @@ -212,7 +218,7 @@ O certificado de servidor não é confiável Insira um nome para o novo arquivo O nome do arquivo não pode estar em branco - O nome de arquivo deve ter mais de %d caracteres + O nome do arquivo não deve ser maior que %d caracteres Nome do Arquivo Enviando arquivos de envio da camera %d novas fotos serão carregadas @@ -366,13 +372,17 @@ Cópia local não pôde ser renomeada; tente outro nome Renomeação não pôde ser finalizada O arquivo já existe + Folder already exists + File with name %1$s already exists. + Folder with name %1$s already exists. Arquivo remoto não pode ser verificado Conteúdo do arquivo já foi sincronizado Uma nova versão foi encontrada no servidor. Baixando… Baixar em fila Enviar em fila A pasta não pode ser criada - Caracteres proibidos: / \\ < > : \" | ? * + O arquivo não pôde ser criado + Caracteres proibidos: / \\ O nome do arquivo contem pelo menos um caractere inválido O nome do arquivo não pode estar vazio O nome do arquivo não pode ser tão longo @@ -384,6 +394,7 @@ Copiando o arquivo de armazenagem privada Login com oAuth2 Conectando ao servidor OAuth2… + A conexão não é segura, o tráfego http não é permitido. A identidade do servidor não pôde ser verificada - O certificado do servidor não é confiável - O certificado do servidor expirou @@ -429,6 +440,7 @@ versão local versão do servidor An error occurred in the destination folder + Substituir Pré-visualização da imagem Esta imagem não pode ser mostrada %1$s não pôde ser copiado para pasta local %2$s @@ -656,7 +668,7 @@ Prosseguir Ícone de nota de lançamento Support for Spaces - Introducing support for spaces feature (oCIS). Only available for new accounts + Apresentando o suporte para o recurso de espaços (oCIS). Disponível apenas para contas recém-logadas Bottom navigation bar Reordered some tabs Fim do suporte Lollipop @@ -675,10 +687,35 @@ Se o seu servidor oferecer suporte a provedores de aplicativos para abrir tipos específicos de arquivos, todos eles serão listados na visualização Detalhes Ícones temáticos suportados O recurso \"Ícones temáticos\" do Android 13 permitirá que você defina o ícone do aplicativo em monocromático + Criar novos documentos via web (oCIS) + Se o servidor oCIS suportar provedores de aplicativos, novos documentos podem ser criados via navegador e sincronizados com + Nova configuração com aplicativo sugerido para procurar arquivos + Uma nova configuração foi adicionada na seção \"Mais\" com um aplicativo sugerido para acessar arquivos via provedor de documentos + Adicionado botão de 3 pontos a todos os itens da lista + Adicionado botão a todos os elementos da lista para mostrar um menu com todas as opções do item + Melhorar visualização de detalhes + Redesenho da visualização de detalhes + Suporte para nova mudança de idioma + Adicionado suporte para a nova alteração de idioma no Android 13+ + Bug corrigido sobre o carregamento de imagens do espaço + Carregando as imagens dos espaços na primeira vez que mostramos e melhorando o carregamento das imagens dos itens + Abrir arquivos markdown no modo ASCII + Adicionado um novo modo para abrir arquivos markdown para mostrar o código ASCII + Mostrando pop-up no conflito de copiar/mover + Mostrando um pop-up de 3 opções ao ter um conflito de nomenclatura na ação de copiar ou mover + Pequenas correções de bugs e melhorias + Alguns pequenos bugs foram corrigidos e pequenas melhorias técnicas foram introduzidas para melhorar a experiência no aplicativo Abrir na web Abrir em%1$s (web) Não foi possível abrir na web Não há aplicativos que suportam este tipo de arquivo O arquivo requisitado ainda não está disponível, por favor tente novamente mais tarde + + Aviso de contas oCIS + Por favor, remova a conta e faça o login novamente para obter o recurso de espaços + Não mostrar novamente + Entendido + Texto + Aplicar para todos os %1$s conflitos diff --git a/owncloudApp/src/main/res/values-ru/strings.xml b/owncloudApp/src/main/res/values-ru/strings.xml index b663c9ac3cc..099c70a4fc7 100644 --- a/owncloudApp/src/main/res/values-ru/strings.xml +++ b/owncloudApp/src/main/res/values-ru/strings.xml @@ -175,6 +175,7 @@ Размер: Тип: Создан: + Последняя синхронизация: Изменён: Скачать Файл изображения @@ -212,7 +213,6 @@ Нет доверия сертификату сервера Укажите имя для нового файла Имя файла не должно быть пустым - Имя файла должно быть не длиннее %d символов Имя файла Загрузка файлов, загруженных из камеры Будет загружено %d новых изображений @@ -365,14 +365,12 @@ Каталог, содержащий этот файл, доступен автономно Локальная копия не может быть переименована; попробуйте другое имя Переименование не может быть завершено - файл уже существует Удаленный файл не может быть проверен Содержимое файла уже синхронизировано Найдена новая версия на сервере. Загрузка... Скачивание в очереди Загрузка в очереди Не удалось создать каталог - Недопустимые символы: / \\ < > : \" | ? * Имя файла содержит по крайней мере один некорректный символ Имя файла не может быть пустым Неверная длина имени файла @@ -656,7 +654,6 @@ Продолжить Иконка к примечанию релиза Поддержка хранилищ - Введение поддержки функции хранилищ (oCIS). Доступно только для новых аккаунтов Нижняя панель навигации Переупорядочены некоторые вкладки Окончание поддержки Lollipop @@ -669,10 +666,15 @@ В настройках добавлен новый пункт, который перенаправляет в настройки уведомлений приложений устройства Мелкие исправления и улучшения Были исправлены некоторые незначительные ошибки, а также были внесены незначительные технические улучшения для улучшения работы приложения. + Мелкие исправления и улучшения + Были исправлены некоторые незначительные ошибки, а также были внесены незначительные технические улучшения для улучшения работы приложения. Открыть в браузере Открыть в %1$s (веб) Не удалось открыть в сети нет приложений, которые поддерживают этот тип файла Запрашиваемый файл в данный момент недоступен, пожалуйста, попробуйте позже + + Не показывать снова + Текст diff --git a/owncloudApp/src/main/res/values-sq/strings.xml b/owncloudApp/src/main/res/values-sq/strings.xml index 000180b2b69..dfd42d3192c 100644 --- a/owncloudApp/src/main/res/values-sq/strings.xml +++ b/owncloudApp/src/main/res/values-sq/strings.xml @@ -150,6 +150,7 @@ sekonda më parë S’ka kartela këtu Pa hapësira + Hapësirë: S’ka kartela “offline” të passhme Pa lidhje ndarjesh Ngarkoni ca lëndë ose bëni njëkohësim me pajisjet tuaja! @@ -177,6 +178,8 @@ Madhësi: Lloj: Krijuar më: + Njëkohësimi i fundit: + Shteg: Ndryshuar më: Shkarkoje Pamje Kartele @@ -214,7 +217,7 @@ Dëshmia e shërbyesit s\’është e besuar Jepni një emër për kartelën e re Emri i kartelës s\’duhet të jetë i zbrazët - Emri i kartelës s\’duhet të jetë më shumë se %d shenja + Emri i kartelës s’duhet të jetë më i gjatë se %d shenja Emër kartele Po ngarkohen kartela ngarkimi nga kamera Do të ngarkohen %d foto të reja @@ -366,7 +369,10 @@ Një dosje që përmban këtë kartelë është gati “offline” Kopja vendore s\’u riemërtua dot; provoni një emër tjetër Riemërtimi s\’u plotësua dot - ka tashmë një kartelë të tillë + Ka tashmë një kartelë të tillë + Ka tashmë një dosje të tillë + Ka tashmë një kartelë me emrin %1$s. + Ka tashmë një dosje me emrin %1$s. S\’u kap dot kartela e largët Lëndë kartele tashmë e njëkohësuar U gjet një version i ri në shërbyes. Po shkarkohet… @@ -374,7 +380,7 @@ Ngarkimi u vu në radhë S\’u krijua dot dosja Kartela s’u krijua dot - Shenja të ndaluara: / \\ < > : \" | ? * + Shenja të ndaluara: / \\ Emri i kartelës përmban të paktën një shenjë të pavlefshme Emri i kartelës s\’mund të jetë i zbrazët Emri i kartelës s’mund të jetë aq i gjatë @@ -386,6 +392,7 @@ Po kopjohet kartelë nga depo private Hyrje me oAuth2 Po lidhet te shërbyesi OAuth2… + Lidhja s’është e sigurt, trafiku http nuk lejohet. Identiteti i sajtit s\’u verifikua dot - Dëshmia e shërbyesit s\’është e besuar - Dëshmia e shërbyesit skadoi @@ -431,6 +438,7 @@ versionin vendor versionin e shërbyesit Ndodhi një gabim te dosja vendmbërritje + Zëvendësoje Paraparje figure Kjo figurë nuk mund të shfaqet %1$s s\’u kopjua dot te dosja vendore %2$s @@ -656,7 +664,7 @@ Bëje Ikonë shënimesh hedhjeje në qarkullim Mbulim për Hapësira - Prezantim i mbulimi për veçorinë hapësira (oCIS). E përdorshme vetëm për llogari të reja + Prezantim mbulimi për veçorinë hapësira (oCIS). E përdorshme vetëm për llogari me hyrje të re Shtyllë lëvizjesh në fund U rirenditën disa skeda Fund i mbulimit për Lollipop @@ -677,10 +685,33 @@ Veçoria “Ikona teme” e Android 13-s do t’ju lejojë të caktoni ikona aplikacioni njëngjyrëshe Krijoni dokumente të reja përmes web-it (oCIS) Nëse shërbyesi oCIS mbulon shërbime aplikacionesh, dokumentet e reja do të krijohen përmes shfletuesit dhe njëkohësohen me oC + Rregullime të reja me aplikacionin sugjeruan shfletim kartelash + Te ndarja “Më tepër” u shtua një rregullim i ri, me një aplikacion të sugjeruar për hyrje në kartela përmes furnizuesi dokumentesh + U shtua buton me 3 pika te krejt objektet e listës + U shtua buton te krejt elementët e listës, për të shfaqur një menu me krejt mundësitë për elementin + Përmirësim parjeje hollësish + Rihartim i parjes së hollësive + Mbulim për ndryshim gjuhe të re + U shtua mbulim për ndryshim gjuhe të re në Android 13+ + U ndreq e metë e lidhur me ngarkim figurash hapësire + Ngarkim i figurave të hapësirës herën e parë që e shfaqim dhe përmirësim i ngarkimit të figurave të elementëve + Hapje kartelash Markdown nën mënyrën ASCII + U shtua një mënyrë e re për hapje kartelash Markdown për të shfaqur kodin ASCII + Shfaqje dritare flluskë në rast përplasjeje veprimesh kopjimi/lëvizjeje + Shfaqje e një dritareje flluskë me 3 mundësi, kur kihet përplasje emri gjatë veprimi kopjimi, ose lëvizjeje + Ndreqje të metash të vockla dhe përmirësime + Janë ndrequr disa të meta të cokla dhe u sollën për herë të parë përmirësime të vockla teknike për të përmirësuar punën te aplikacioni Hape në web Hape në %1$s (web) S’u hap dot në web s’ka aplikacione që e mbulojnë këtë lloj kartele Kartela e kërkuar s’është ende e passhme, ju lutemi, riprovoni më vonë + + Sinjalizim për llogari oCIS + Ju lutemi, që të merret veçoria e hapësirave, hiqeni llogarinë dhe ribëni hyrjen + Mos e shfaq më + E kuptova + Tekst + Zbatoje për krejt %1$s përplasjet diff --git a/owncloudApp/src/main/res/values-sv/strings.xml b/owncloudApp/src/main/res/values-sv/strings.xml index ddfca9faf4f..7975ed0dae8 100644 --- a/owncloudApp/src/main/res/values-sv/strings.xml +++ b/owncloudApp/src/main/res/values-sv/strings.xml @@ -143,6 +143,7 @@ Storlek: Typ: Skapad: + Senaste synk: Ändrad: Ladda ner Fil bild @@ -179,7 +180,6 @@ Litar inte på servercertifikat Välj ett namn för den nya filen Filnamnet får inte vara tomt - Filnamnet får inte ha fler än %d tecken Filnamn %d nya bilder kommer att laddas upp %d nya videor kommer att laddas upp @@ -310,7 +310,6 @@ Fjärrfilen kunde inte kontrolleras Filinnehåll redan synkroniserat Mapp kunde inte skapas - Förbjudna tecken är: / \\ < > : \" | ? * Filnamnet innehåller minst ett ogiltigt tecken Filnamnet får inte lämnas blankt Var god vänta @@ -557,4 +556,7 @@ Fortsätt + + Visa inte igen + Text diff --git a/owncloudApp/src/main/res/values-th-rTH/strings.xml b/owncloudApp/src/main/res/values-th-rTH/strings.xml index 5abc0ce7eec..1b12b8233b8 100644 --- a/owncloudApp/src/main/res/values-th-rTH/strings.xml +++ b/owncloudApp/src/main/res/values-th-rTH/strings.xml @@ -160,6 +160,7 @@ ขนาด: ชนิด: สร้างเมื่อ: + การประสานข้อมูลล่าสุด: แก้ไขเมื่อ: ดาวน์โหลด ประสานข้อมูล @@ -194,7 +195,6 @@ ใบรับรองของเซิร์ฟเวอร์ไม่น่าเชื่อถือ กรอกชื่อไฟล์ใหม่ ชื่อไฟล์ต้องไม่ว่างเปล่า - ชื่อไฟล์ต้องมีอักขระไม่เกิน %d ตัว ชื่อไฟล์ กำลังอัปโหลดไฟล์รูปภาพ %d รูปภาพใหม่ถูกอัปโหลดแล้ว @@ -330,7 +330,6 @@ ไม่สามารถตรวจสอบไฟล์รีโมทได้ เนื้อหาของไฟล์ถูกประสานข้อมูลอยู่แล้ว ไม่สามารถสร้างโฟลเดอร์ - ห้ามใช้ตัวอักษรดังนี้: / \\ < > : \" | ? * มีชื่อแฟ้มอย่างน้อยหนึ่งตัวอักษรที่ไม่ถูกต้อง จำเป็นต้องตั้งชื่อไฟล์ กรุณารอสักครู่ @@ -384,6 +383,7 @@ เก็บไว้ทั้งสองอย่าง เวอร์ชันปัจจุบัน เวอร์ชันเซิร์ฟเวอร์ + แทนที่ แสดงรูปภาพตัวอย่าง ไม่สามารถแสดงรูปภาพนี้ได้ %1$s ไม่สามารถคัดลอกไปยังโฟลเดอร์ %2$s ในเครื่อง @@ -592,4 +592,7 @@ ดำเนินการ + + ไม่ต้องแสดงอีกครั้ง + ข้อความ diff --git a/owncloudApp/src/main/res/values-tr/strings.xml b/owncloudApp/src/main/res/values-tr/strings.xml index c1c49b00962..2c09c10e0c3 100644 --- a/owncloudApp/src/main/res/values-tr/strings.xml +++ b/owncloudApp/src/main/res/values-tr/strings.xml @@ -86,6 +86,8 @@ Yardım Kişilerinizi, takviminizi ve görevlerinizi eşitleyin DAVx⁵ yükle + Doküman sağlayıcıya eriş + Android yerel dosya gezgini aracılığıyla cihazınızın dosyalarına göz atmak için önerilen uygulama Bir arkadaşa öner Geribildirim İzlenim @@ -149,6 +151,7 @@ saniyeler önce Burada hiç dosya yok Alan yok + Alan: Uygun çevrimdışı dosya yok Paylaşılan bağlantı yok Bir şeyler yükleyin veya aygıtlarınızla eşitleyin! @@ -171,10 +174,13 @@ dosya Ek bilgileri görmek için dosyaya dokunun. %1$s konumuna yükle + Oluşturmak için bir doküman türü seçin: Dosyalar Boyut: Tür: Oluşturulma: + Son eşitleme: + Yol: Değiştirilme: İndir Dosya Resmi @@ -365,14 +371,18 @@ Bu dosyayı içeren bir klasör çevrimiçi olarak mevcut Yerel kopya adlandırılamadı; farklı bir ad deneyin Yeniden adlandırılma tamamlanamadı - dosya zaten mevcut + Dosya zaten mevcut + Klasör zaten mevcut + %1$s adlı dosya zaten mevcut. + %1$s adlı klasör zaten mevcut. Uzak dosya denetlenemedi Dosya içerikleri zaten eşitlenmiş Sunucuda yeni bir sürüm bulundu. İndiriliyor… İndirme kuyruğa alınmış Yükleme kuyruğa alınmış Klasör oluşturulamadı - Yasaklı karakterler: / \\ < > : \" | ? * + Dosya oluşturulamadı + Yasaklı Karakterler: / \\ Dosya adı en az bir geçersiz karakter içeriyor Dosya adı boş olamaz Dosya adı bu kadar uzun olamaz @@ -384,6 +394,7 @@ Dosya özel depolamadan kopyalanıyor oAuth2 ile oturum aç OAuth2 sunucusuna bağlanıyor... + Bağlantı güvenli değil, http trafiğine izin verilmedi. Sunucunun kimliği doğrulanamadı. - Sunucu sertifikasına güvenilmiyor - Sunucu sertifikasının süresi dolmuş @@ -429,6 +440,7 @@ yerel sürüm sunucu sürümü Hedef klasörde bir hata oluştu + Yer değiştir Resim önizleme Bu resim gösterilemiyor %1$s, %2$s yerel klasörüne kopyalanamadı @@ -599,6 +611,7 @@ Dosya eşitleme Dosya eşitleme sonucunu gör Dosya çakışmaları + Oluştuklarında dosya çakışmalarını görün Geliştirici menülerini etkinleştirmeye %1$d tık uzaktasınız %1$s uygulamasını oyla! Bu uygulamayı kullanmaktan memnunsanız oylamak için birkaç dakikanızı ayırır mısınız? Geri dönüşünüz bizim için çok önemli. @@ -654,8 +667,17 @@ Devam et Sürüm notu simgesi Alanlar için Destek - Alanlar için destek özelliği (oCIS) ile tanışın. Yalnızca yeni hesaplar için kullanılabilir + Alanlar için destek özelliği (oCIS) ile tanışın. Yalnızca yeni oturum açmış hesaplar için kullanılabilir + Alt gezinme çubuğu + Bazı sekmeleri yeniden sıraladı + Lollipop desteğinin sonu + Bu, Android Lollipop (v5.0) destekli son sürüm olacak + WebFinger akışı güncellendi + WebFinger sunucuları için önce arama sunucusu istenir ve başarılı olmazsa normal kimlik doğrulama akışı izlenir + İzin işleniyor + Artık, bir klasör veya dosya üzerinde bazı eylemleri gerçekleştirme izni olmadığında, bu işlemler gizlenecek Yeni \"Bildirimleri yönet\" ayarı + Ayarlara, cihazın uygulama bildirimleri ayarlarına yönlendiren yeni bir öge eklendi Küçük hata düzeltmeleri ve iyileştirmeler Bazı küçük hatalar giderildi ve uygulamadaki deneyimi iyileştirmek için küçük teknik iyileştirmeler yapıldı. Markdown desteği @@ -664,10 +686,35 @@ Sunucunuz, uygulama sağlayıcıların belirli türden dosyaları açmasını destekliyorsa, bunların tümü Ayrıntılar görünümünde listelenir Temalı ikonlar desteklendi Android 13 özelliği \"Temalı ikonlar\", uygulama simgesini tek renkli olarak ayarlamanıza izin verir + Web (oCIS) aracılığıyla yeni dokümanlar oluştur + oCIS sunucusu uygulama sağlayıcıları destekliyorsa, tarayıcı aracılığıyla yeni belgeler oluşturulabilir ve oC ile senkronize edilebilir + Dosyalara göz atmak için önerilen uygulamayla yeni ayar + Dosyalara belge sağlayıcı aracılığıyla erişmek için önerilen bir uygulama ile \"Diğer\" bölümüne yeni bir ayar eklendi + Tüm liste ögelerine 3 nokta düğmesi eklendi + Ögenin tüm seçeneklerini içeren bir menüyü göstermek için tüm liste elementlerine buton eklendi + Detaylar görünümünü iyileştir + Detaylar görünümünü yeniden tasarla + Yeni dil değişikliği desteği + Android 13+\'de yeni dil değişikliği için destek eklendi + Alan resimlerinin yüklenmesiyle ilgili hata düzeltildi + Alan resimlerini ilk gösterdiğimizde yüklemek ve ögelerin resimlerinin yüklenmesini iyileştirmek + Markdown dosyalarını ASCII modunda aç + Markdown dosyalarını ASCII kodunu göstererek açmak için yeni bir mod eklendi + Kopyalama/taşıma çakışmasında açılır pencere gösteriliyor + Kopyalama veya taşıma eyleminde bir adlandırma çakışması olduğunda 3 seçenek gösterilir + Küçük hata düzeltmeleri ve iyileştirmeler + Bazı küçük hatalar giderildi ve uygulamadaki deneyimi iyileştirmek için küçük teknik iyileştirmeler yapıldı. Web\'te aç %1$s\'da aç (web) Web\'te açılamadı bu dosya türünü destekleyen hiçbir uygulama yok İstenen dosya henüz mevcut değil, lütfen daha sonra tekrar deneyin. + + oCIS hesaplar uyarısı + Boşluk özelliğini almak için lütfen hesabı kaldırın ve tekrar giriş yapın + Tekrar gösterme + Anlaşıldı + Metin + Tüm %1$s çakışma için uygula diff --git a/owncloudApp/src/main/res/values-zh-rCN/strings.xml b/owncloudApp/src/main/res/values-zh-rCN/strings.xml index 9051d22fe73..0895983122c 100644 --- a/owncloudApp/src/main/res/values-zh-rCN/strings.xml +++ b/owncloudApp/src/main/res/values-zh-rCN/strings.xml @@ -166,6 +166,7 @@ 大小: 类型: 创建于: + 最后同步: 修改于: 下载 文件图片 @@ -202,7 +203,6 @@ 服务器证书不被信任 输入新文件的名称 文件名不能为空 - 文件名不能超过 %d 个字符 文件名 使用相机上传文件 %d将上传新图片 @@ -351,7 +351,6 @@ 无法核实远程文件 文件内容已同步 文件夹无法创建 - 禁用字符: / \\ < > : \" | ? * 文件名中存在至少一个非法字符 文件名不能为空 请稍候 @@ -621,4 +620,7 @@ 继续 发行说明图标 + + 不再显示 + 文本 diff --git a/owncloudApp/src/main/res/values-zh-rTW/strings.xml b/owncloudApp/src/main/res/values-zh-rTW/strings.xml index 3d15357ae15..1c808b60220 100644 --- a/owncloudApp/src/main/res/values-zh-rTW/strings.xml +++ b/owncloudApp/src/main/res/values-zh-rTW/strings.xml @@ -154,6 +154,7 @@ 容量: 類型: 建立: + 最後同步: 修改: 下載 同步 @@ -188,7 +189,6 @@ 伺服器憑證不是可信的 插入新檔案的名稱 檔名不能為空 - 檔名不能超過%d個字元 檔案名稱 上傳相機上傳文件 %d將上傳新圖片 @@ -325,7 +325,6 @@ 無法檢查遠端的檔案 檔案與同步 資料夾無法建立 - 禁止使用字符: / \\ < > : \" | ? * 檔案名稱含有不合法的字元 檔名不能為空的 請稍後 @@ -580,4 +579,7 @@ 感謝您使用 %1$s.\n❤ 已執行 + + 不再顯示 + 文字 diff --git a/owncloudApp/src/main/res/values/dims.xml b/owncloudApp/src/main/res/values/dims.xml index 49091c6b41a..4b7e71d48fc 100644 --- a/owncloudApp/src/main/res/values/dims.xml +++ b/owncloudApp/src/main/res/values/dims.xml @@ -49,8 +49,10 @@ 12dp 18dp 32dp + 48dp 128dp 128dp + 400dp 16sp 14sp @@ -80,4 +82,7 @@ 156dp 80dp + + + 48dp diff --git a/owncloudApp/src/main/res/values/setup.xml b/owncloudApp/src/main/res/values/setup.xml index 4f6d7cd33df..c6cb4488195 100644 --- a/owncloudApp/src/main/res/values/setup.xml +++ b/owncloudApp/src/main/res/values/setup.xml @@ -118,6 +118,7 @@ 4 + false 0 0 @@ -129,4 +130,8 @@ + + + false + diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 54dfcf63fc3..ac0dfde4e07 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -156,6 +156,7 @@ seconds ago No files in here No spaces + Space: No available offline files No shared links Upload some content or sync with your devices! @@ -183,6 +184,8 @@ Size: Type: Created: + Last sync: + Path: Modified: Download File Image @@ -220,7 +223,7 @@ Server certificate is not trusted Insert a name for the new file Filename must not be empty - Filename must not be more than %d characters + Filename must not be longer than %d characters Filename Uploading camera upload files %d new pictures will be uploaded @@ -380,7 +383,10 @@ A folder that containing this file is available offline "Local copy could not be renamed; try a different name" "Rename could not be completed" - file already exists + File already exists + Folder already exists + File with name %1$s already exists. + Folder with name %1$s already exists. Remote file could not be checked File contents already synchronized A new version was found in server. Downloading… @@ -388,7 +394,7 @@ Upload enqueued Folder could not be created File could not be created - Forbidden characters: / \\ < > : " | ? * + Forbidden characters: / \\ File name contains at least one invalid character File name cannot be empty File name cannot be that long @@ -402,6 +408,7 @@ Login with oAuth2 Connecting to OAuth2 server… + Connection is not secure, http traffic is not allowed. The identity of the server could not be verified - The server certificate is not trusted - The server certificate expired @@ -449,6 +456,7 @@ local version server version An error occurred in the destination folder + Replace Image preview This image cannot be shown @@ -718,7 +726,7 @@ Release note icon Support for Spaces - Introducing support for spaces feature (oCIS). Only available for new accounts + Introducing support for spaces feature (oCIS). Only available for newly logged-in accounts Bottom navigation bar Reordered some tabs End of support Lollipop @@ -739,6 +747,26 @@ Android 13 feature "Themed icons" will let you set app icon in monochrome Create new documents via web (oCIS) If oCIS server supports application providers, new documents can be created via browser and synced with oC + New setting with app suggested to browse files + A new setting was added in "More" section with a suggested app to access files via document provider + + Added 3 dot button to all list item + Added button to all list element to show a menu with all the options of the item + Improve details view + Redesign of the details view + Support for new language change + Added support for the new language change on Android 13+ + Bug fixed about loading space images + Loading the spaces images the first time we show it and improving the loading of the images of the items + Open markdown files in ASCII mode + Added a new mode to open markdown files to show the ASCII code + Showing pop up on copy/move conflict + Showing a 3 option pop up when having a naming conflict on copy or move action + Minor bugfixes and improvements + Some minor bugs have been fixed, and minor technical improvements were introduced to improve the experience in the app + Http traffic banned + New accounts over servers running on http will not be longer allowed. Only https allowed + Open in web @@ -747,4 +775,13 @@ there are no apps that support this file type The requested file is not yet available, please try again later + + oCIS accounts warning + Please, remove the account and login again to get the spaces feature + Don\'t show again + Understood + + Text + Apply to all %1$s conflicts + diff --git a/owncloudApp/src/main/res/values/strings__not_to_translate.xml b/owncloudApp/src/main/res/values/strings__not_to_translate.xml index e68ff226eff..5e3da56ddca 100644 --- a/owncloudApp/src/main/res/values/strings__not_to_translate.xml +++ b/owncloudApp/src/main/res/values/strings__not_to_translate.xml @@ -29,6 +29,8 @@ Enables or disables the possibility to allow screenshots or not OpenID Connect scope Indicates the scope for OpenID Connect + App security forced when device not protected + Indicates if the app security when device is not protected is forced The lock delay was set correctly @@ -37,8 +39,11 @@ The preference for allowing screenshots was set correctly The OpenID Connect scope was set correctly The OpenID Connect prompt was set correctly + The app security forced when device not protected was set correctly Spaces + Markdown + diff --git a/owncloudApp/src/main/res/xml/locales_config.xml b/owncloudApp/src/main/res/xml/locales_config.xml new file mode 100644 index 00000000000..5a58bfe3dff --- /dev/null +++ b/owncloudApp/src/main/res/xml/locales_config.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/owncloudApp/src/main/res/xml/managed_configurations.xml b/owncloudApp/src/main/res/xml/managed_configurations.xml index 4d01668d587..a8c6b8b0171 100644 --- a/owncloudApp/src/main/res/xml/managed_configurations.xml +++ b/owncloudApp/src/main/res/xml/managed_configurations.xml @@ -31,4 +31,10 @@ android:title="@string/oauth2_open_id_scope_configuration_title" android:description="@string/oauth2_open_id_scope_configuration_description" android:defaultValue="" /> + diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/DrawerViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/DrawerViewModelTest.kt index 561e58a90ee..bf9f9d68acc 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/DrawerViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/DrawerViewModelTest.kt @@ -18,7 +18,7 @@ */ package com.owncloud.android.presentation.viewmodels -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.UseCaseResult import com.owncloud.android.domain.user.model.UserQuota import com.owncloud.android.domain.user.usecases.GetStoredQuotaUseCase diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/authentication/AuthenticationViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/authentication/AuthenticationViewModelTest.kt index 60813efb16c..2ba4770cbe8 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/authentication/AuthenticationViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/authentication/AuthenticationViewModelTest.kt @@ -21,6 +21,7 @@ package com.owncloud.android.presentation.viewmodels.authentication +import com.owncloud.android.R import com.owncloud.android.domain.UseCaseResult import com.owncloud.android.domain.authentication.oauth.RegisterClientUseCase import com.owncloud.android.domain.authentication.oauth.RequestTokenUseCase @@ -125,6 +126,7 @@ class AuthenticationViewModelTest : ViewModelTest() { every { anyConstructed().generateRandomCodeVerifier() } returns "CODE VERIFIER" every { anyConstructed().generateCodeChallenge(any()) } returns "CODE CHALLENGE" every { anyConstructed().generateRandomState() } returns "STATE" + every { contextProvider.getBoolean(R.bool.enforce_secure_connection) } returns false testCoroutineDispatcher.pauseDispatcher() @@ -142,7 +144,8 @@ class AuthenticationViewModelTest : ViewModelTest() { requestTokenUseCase = requestTokenUseCase, registerClientUseCase = registerClientUseCase, workManagerProvider = workManagerProvider, - coroutinesDispatcherProvider = coroutineDispatcherProvider + coroutinesDispatcherProvider = coroutineDispatcherProvider, + contextProvider = contextProvider, ) } diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt index fe0ff1ad5e3..2c74a0b282c 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/BiometricViewModelTest.kt @@ -21,7 +21,7 @@ package com.owncloud.android.presentation.viewmodels.security import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.security.biometric.BiometricViewModel import com.owncloud.android.presentation.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP import com.owncloud.android.presentation.security.passcode.PassCodeActivity diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PassCodeViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PassCodeViewModelTest.kt index d2573e853f5..d418831799a 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PassCodeViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PassCodeViewModelTest.kt @@ -22,7 +22,7 @@ package com.owncloud.android.presentation.viewmodels.security import android.os.SystemClock import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.security.PREFERENCE_LAST_UNLOCK_ATTEMPT_TIMESTAMP import com.owncloud.android.presentation.security.PREFERENCE_LAST_UNLOCK_TIMESTAMP import com.owncloud.android.presentation.security.passcode.PassCodeViewModel diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModelTest.kt index ebd3c0db5e3..eb785b7f905 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/security/PatternViewModelTest.kt @@ -20,7 +20,7 @@ package com.owncloud.android.presentation.viewmodels.security -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.security.pattern.PatternActivity import com.owncloud.android.presentation.security.pattern.PatternViewModel import com.owncloud.android.presentation.viewmodels.ViewModelTest diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsAdvancedViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsAdvancedViewModelTest.kt index d1dfef1eaa2..52dd90d2665 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsAdvancedViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsAdvancedViewModelTest.kt @@ -19,7 +19,7 @@ package com.owncloud.android.presentation.viewmodels.settings -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.settings.advanced.SettingsAdvancedViewModel import com.owncloud.android.presentation.settings.advanced.SettingsAdvancedFragment.Companion.PREF_SHOW_HIDDEN_FILES import io.mockk.every diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsLogsViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsLogsViewModelTest.kt index da2d5684d6f..6c3c6af01f0 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsLogsViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsLogsViewModelTest.kt @@ -20,7 +20,7 @@ package com.owncloud.android.presentation.viewmodels.settings -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.settings.logging.SettingsLogsViewModel import com.owncloud.android.presentation.settings.logging.SettingsLogsFragment import com.owncloud.android.presentation.viewmodels.ViewModelTest diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt index a4340eae1f7..29dfadee311 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/settings/SettingsSecurityViewModelTest.kt @@ -21,8 +21,9 @@ package com.owncloud.android.presentation.viewmodels.settings import com.owncloud.android.R -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.presentation.security.LockEnforcedType +import com.owncloud.android.presentation.security.isDeviceSecure import com.owncloud.android.presentation.security.passcode.PassCodeActivity import com.owncloud.android.presentation.security.pattern.PatternActivity import com.owncloud.android.presentation.settings.security.SettingsSecurityViewModel @@ -31,6 +32,7 @@ import com.owncloud.android.presentation.viewmodels.ViewModelTest import com.owncloud.android.providers.MdmProvider import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert.assertFalse @@ -49,6 +51,7 @@ class SettingsSecurityViewModelTest : ViewModelTest() { preferencesProvider = mockk(relaxUnitFun = true) mdmProvider = mockk(relaxUnitFun = true) securityViewModel = SettingsSecurityViewModel(preferencesProvider, mdmProvider) + mockkStatic(::isDeviceSecure) } @Test @@ -140,7 +143,9 @@ class SettingsSecurityViewModelTest : ViewModelTest() { } @Test - fun `is security enforced enabled - ok - true`() { + fun `is security enforced enabled false - ok - device secure device protection lock enforced`() { + every { isDeviceSecure() } returns true + every { mdmProvider.getBrandingBoolean(any(), R.bool.device_protection) } returns true every { mdmProvider.getBrandingInteger(any(), R.integer.lock_enforced) } returns LockEnforcedType.EITHER_ENFORCED.ordinal val result = securityViewModel.isSecurityEnforcedEnabled() @@ -149,13 +154,75 @@ class SettingsSecurityViewModelTest : ViewModelTest() { } @Test - fun `is security enforced enabled - ok - false`() { + fun `is security enforced enabled false - ok - device not secure device protection no lock enforced`() { + every { isDeviceSecure() } returns false + every { mdmProvider.getBrandingBoolean(any(), R.bool.device_protection) } returns true + every { mdmProvider.getBrandingInteger(any(), R.integer.lock_enforced) } returns LockEnforcedType.DISABLED.ordinal + + val result = securityViewModel.isSecurityEnforcedEnabled() + assertTrue(result) + } + + @Test + fun `is security enforced enabled true - ok - device not secure no device protection lock enforced`() { + every { isDeviceSecure() } returns false + every { mdmProvider.getBrandingBoolean(any(), R.bool.device_protection) } returns false + every { mdmProvider.getBrandingInteger(any(), R.integer.lock_enforced) } returns LockEnforcedType.EITHER_ENFORCED.ordinal + + val result = securityViewModel.isSecurityEnforcedEnabled() + assertTrue(result) + } + + @Test + fun `is security enforced enabled true - ok - device secure no device protection lock enforced`() { + every { isDeviceSecure() } returns true + every { mdmProvider.getBrandingBoolean(any(), R.bool.device_protection) } returns false + every { mdmProvider.getBrandingInteger(any(), R.integer.lock_enforced) } returns LockEnforcedType.EITHER_ENFORCED.ordinal + + val result = securityViewModel.isSecurityEnforcedEnabled() + assertTrue(result) + } + + @Test + fun `is security enforced enabled false - ok - device not secure no device protection no lock enforced`() { + every { isDeviceSecure() } returns false + every { mdmProvider.getBrandingBoolean(any(), R.bool.device_protection) } returns false every { mdmProvider.getBrandingInteger(any(), R.integer.lock_enforced) } returns LockEnforcedType.DISABLED.ordinal val result = securityViewModel.isSecurityEnforcedEnabled() assertFalse(result) } + @Test + fun `is security enforced enabled false - ok - device secure no device protection no lock enforced`() { + every { isDeviceSecure() } returns true + every { mdmProvider.getBrandingBoolean(any(), R.bool.device_protection) } returns false + every { mdmProvider.getBrandingInteger(any(), R.integer.lock_enforced) } returns LockEnforcedType.DISABLED.ordinal + + val result = securityViewModel.isSecurityEnforcedEnabled() + assertFalse(result) + } + + @Test + fun `is security enforced enabled false - ok - device secure device protection no lock enforced`() { + every { isDeviceSecure() } returns true + every { mdmProvider.getBrandingBoolean(any(), R.bool.device_protection) } returns true + every { mdmProvider.getBrandingInteger(any(), R.integer.lock_enforced) } returns LockEnforcedType.DISABLED.ordinal + + val result = securityViewModel.isSecurityEnforcedEnabled() + assertFalse(result) + } + + @Test + fun `is security enforced enabled true - ok - device not secure device protection lock enforced`() { + every { isDeviceSecure() } returns false + every { mdmProvider.getBrandingBoolean(any(), R.bool.device_protection) } returns true + every { mdmProvider.getBrandingInteger(any(), R.integer.lock_enforced) } returns LockEnforcedType.EITHER_ENFORCED.ordinal + + val result = securityViewModel.isSecurityEnforcedEnabled() + assertTrue(result) + } + @Test fun `is lock delay enforced enabled - ok - true`() { every { mdmProvider.getBrandingInteger(any(), R.integer.lock_delay_enforced) } returns 1 diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/sharing/ShareViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/sharing/ShareViewModelTest.kt index fcd39e41653..faa1feeb321 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/sharing/ShareViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/sharing/ShareViewModelTest.kt @@ -286,7 +286,6 @@ class ShareViewModelTest { name = "Photos 2 link", password = "1234", expirationTimeInMillis = -1, - publicUpload = false, permissions = OC_SHARE.permissions, accountName = OC_SHARE.accountOwner ) @@ -330,7 +329,6 @@ class ShareViewModelTest { name = "Photos 2 link", password = "1234", expirationDateInMillis = -1, - publicUpload = false, permissions = -1, accountName = "Carlos" ) diff --git a/owncloudData/build.gradle b/owncloudData/build.gradle index cee72ebf277..7cc161d750d 100644 --- a/owncloudData/build.gradle +++ b/owncloudData/build.gradle @@ -56,31 +56,30 @@ dependencies { api project(":owncloud-android-library:owncloudComLibrary") // Kotlin - implementation "org.jetbrains.kotlin:kotlin-stdlib:$orgJetbrainsKotlin" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$orgJetbrainsKotlinx" + implementation libs.kotlin.stdlib + implementation libs.kotlinx.coroutines.core // Room - implementation "androidx.room:room-ktx:$androidxRoom" - ksp "androidx.room:room-compiler:$androidxRoom" + implementation libs.androidx.room.ktx + ksp libs.androidx.room.compiler - implementation("com.squareup.moshi:moshi-kotlin:$comSquareupMoshi") - ksp "com.squareup.moshi:moshi-kotlin-codegen:$comSquareupMoshi" + implementation libs.moshi.kotlin + ksp libs.moshi.kotlin.codegen // Dependencies for unit tests testImplementation project(":owncloudTestUtil") - testImplementation "junit:junit:$junitVersion" - testImplementation "androidx.arch.core:core-testing:$androidxArchCore" - testImplementation "io.mockk:mockk:$ioMockk" + testImplementation libs.androidx.arch.core.testing + testImplementation libs.junit4 + testImplementation libs.kotlinx.coroutines.test + testImplementation libs.mockk // Dependencies for instrumented tests androidTestImplementation project(":owncloudTestUtil") - androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$orgJetbrainsKotlinx" - androidTestImplementation "androidx.test:runner:$androidxTest" - androidTestImplementation "androidx.test.espresso:espresso-core:$androidxTestEspresso" - androidTestImplementation "androidx.test.ext:junit:$androidxTestExt" - androidTestImplementation "androidx.arch.core:core-testing:$androidxArchCore" - androidTestImplementation "androidx.room:room-testing:$androidxRoom" - androidTestImplementation("io.mockk:mockk-android:$ioMockk") { - exclude module: "objenesis" - } + androidTestImplementation libs.androidx.arch.core.testing + androidTestImplementation libs.androidx.room.testing + androidTestImplementation libs.androidx.test.espresso.core + androidTestImplementation libs.androidx.test.ext.junit + androidTestImplementation libs.androidx.test.runner + androidTestImplementation libs.kotlinx.coroutines.test + androidTestImplementation(libs.mockk.android) { exclude module: "objenesis" } } diff --git a/owncloudData/src/androidTest/java/com/owncloud/android/data/authentication/datasources/implementation/OCLocalAuthenticationDataSourceTest.kt b/owncloudData/src/androidTest/java/com/owncloud/android/data/authentication/datasources/implementation/OCLocalAuthenticationDataSourceTest.kt index 3420ef8f115..a401c1a0475 100644 --- a/owncloudData/src/androidTest/java/com/owncloud/android/data/authentication/datasources/implementation/OCLocalAuthenticationDataSourceTest.kt +++ b/owncloudData/src/androidTest/java/com/owncloud/android/data/authentication/datasources/implementation/OCLocalAuthenticationDataSourceTest.kt @@ -30,7 +30,7 @@ import com.owncloud.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_S import com.owncloud.android.data.authentication.KEY_OAUTH2_REFRESH_TOKEN import com.owncloud.android.data.authentication.KEY_OAUTH2_SCOPE import com.owncloud.android.data.authentication.SELECTED_ACCOUNT -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.domain.authentication.oauth.model.ClientRegistrationInfo import com.owncloud.android.domain.exceptions.AccountNotFoundException import com.owncloud.android.domain.exceptions.AccountNotNewException diff --git a/owncloudData/src/main/AndroidManifest.xml b/owncloudData/src/main/AndroidManifest.xml index a2ad22eff0e..cc947c56799 100644 --- a/owncloudData/src/main/AndroidManifest.xml +++ b/owncloudData/src/main/AndroidManifest.xml @@ -1,4 +1 @@ - - - - + diff --git a/owncloudData/src/main/java/com/owncloud/android/data/ClientManager.kt b/owncloudData/src/main/java/com/owncloud/android/data/ClientManager.kt index cf6cd8cea98..ad6da678d95 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/ClientManager.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/ClientManager.kt @@ -23,7 +23,7 @@ import android.accounts.AccountManager import android.content.Context import androidx.core.net.toUri import com.owncloud.android.data.authentication.SELECTED_ACCOUNT -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.lib.common.ConnectionValidator import com.owncloud.android.lib.common.OwnCloudAccount import com.owncloud.android.lib.common.OwnCloudClient @@ -124,6 +124,8 @@ class ClientManager( return ocAccounts.firstOrNull() } + fun getClientForCoilThumbnails(accountName: String) = getClientForAccount(accountName = accountName) + fun getUserService(accountName: String? = ""): UserService { val ownCloudClient = getClientForAccount(accountName) return OCUserService(client = ownCloudClient) diff --git a/owncloudData/src/main/java/com/owncloud/android/data/appregistry/OCAppRegistryRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/appregistry/OCAppRegistryRepository.kt index 733e5367b1b..0d71643e7e3 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/appregistry/OCAppRegistryRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/appregistry/OCAppRegistryRepository.kt @@ -23,6 +23,7 @@ package com.owncloud.android.data.appregistry import com.owncloud.android.data.appregistry.datasources.LocalAppRegistryDataSource import com.owncloud.android.data.appregistry.datasources.RemoteAppRegistryDataSource +import com.owncloud.android.data.capabilities.datasources.LocalCapabilitiesDataSource import com.owncloud.android.domain.appregistry.AppRegistryRepository import com.owncloud.android.domain.appregistry.model.AppRegistryMimeType import kotlinx.coroutines.flow.Flow @@ -30,9 +31,12 @@ import kotlinx.coroutines.flow.Flow class OCAppRegistryRepository( private val localAppRegistryDataSource: LocalAppRegistryDataSource, private val remoteAppRegistryDataSource: RemoteAppRegistryDataSource, + private val localCapabilitiesDataSource: LocalCapabilitiesDataSource, ) : AppRegistryRepository { override fun refreshAppRegistryForAccount(accountName: String) { - remoteAppRegistryDataSource.getAppRegistryForAccount(accountName).also { + val capabilities = localCapabilitiesDataSource.getCapabilityForAccount(accountName) + val appUrl = capabilities?.filesAppProviders?.appsUrl?.substring(1) + remoteAppRegistryDataSource.getAppRegistryForAccount(accountName, appUrl).also { localAppRegistryDataSource.saveAppRegistryForAccount(it) } } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/appregistry/datasources/RemoteAppRegistryDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/appregistry/datasources/RemoteAppRegistryDataSource.kt index 3ab7ae2879a..661caac6a7b 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/appregistry/datasources/RemoteAppRegistryDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/appregistry/datasources/RemoteAppRegistryDataSource.kt @@ -25,7 +25,8 @@ import com.owncloud.android.domain.appregistry.model.AppRegistry interface RemoteAppRegistryDataSource { fun getAppRegistryForAccount( - accountName: String + accountName: String, + appUrl: String?, ): AppRegistry fun getUrlToOpenInWeb( diff --git a/owncloudData/src/main/java/com/owncloud/android/data/appregistry/datasources/implementation/OCRemoteAppRegistryDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/appregistry/datasources/implementation/OCRemoteAppRegistryDataSource.kt index 171fffe8ef6..6a920a29058 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/appregistry/datasources/implementation/OCRemoteAppRegistryDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/appregistry/datasources/implementation/OCRemoteAppRegistryDataSource.kt @@ -32,9 +32,9 @@ import com.owncloud.android.lib.resources.appregistry.responses.AppRegistryRespo class OCRemoteAppRegistryDataSource( private val clientManager: ClientManager ) : RemoteAppRegistryDataSource { - override fun getAppRegistryForAccount(accountName: String): AppRegistry = + override fun getAppRegistryForAccount(accountName: String, appUrl: String?): AppRegistry = executeRemoteOperation { - clientManager.getAppRegistryService(accountName).getAppRegistry() + clientManager.getAppRegistryService(accountName).getAppRegistry(appUrl) }.toModel(accountName) override fun getUrlToOpenInWeb( diff --git a/owncloudData/src/main/java/com/owncloud/android/data/authentication/datasources/implementation/OCLocalAuthenticationDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/authentication/datasources/implementation/OCLocalAuthenticationDataSource.kt index 85055e77b7a..dbe898ce98b 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/authentication/datasources/implementation/OCLocalAuthenticationDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/authentication/datasources/implementation/OCLocalAuthenticationDataSource.kt @@ -32,7 +32,7 @@ import com.owncloud.android.data.authentication.KEY_OAUTH2_REFRESH_TOKEN import com.owncloud.android.data.authentication.KEY_OAUTH2_SCOPE import com.owncloud.android.data.authentication.SELECTED_ACCOUNT import com.owncloud.android.data.authentication.datasources.LocalAuthenticationDataSource -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.domain.authentication.oauth.model.ClientRegistrationInfo import com.owncloud.android.domain.exceptions.AccountNotFoundException import com.owncloud.android.domain.exceptions.AccountNotNewException diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt index fb594467853..c83d83c35ec 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.Flow import java.util.UUID interface LocalFileDataSource { - fun copyFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, remoteId: String) + fun copyFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, remoteId: String, replace: Boolean?) fun getFileById(fileId: Long): OCFile? fun getFileByIdAsFlow(fileId: Long): Flow fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String?): OCFile? @@ -44,6 +44,7 @@ interface LocalFileDataSource { fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner: String): Flow> fun getFilesAvailableOfflineFromAccount(owner: String): List fun getFilesAvailableOfflineFromEveryAccount(): List + fun getFileWithSyncInfoByIdAsFlow(id: Long): Flow fun moveFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, finalStoragePath: String) fun saveFilesInFolderAndReturnThem(listOfFiles: List, folder: OCFile): List fun saveFile(file: OCFile) diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt index 42115bf74b5..22cc669aa17 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt @@ -26,7 +26,7 @@ import com.owncloud.android.domain.files.model.OCFile interface RemoteFileDataSource { fun checkPathExistence( path: String, - checkUserCredentials: Boolean, + isUserLogged: Boolean, accountName: String, spaceWebDavUrl: String?, ): Boolean @@ -37,7 +37,8 @@ interface RemoteFileDataSource { accountName: String, sourceSpaceWebDavUrl: String?, targetSpaceWebDavUrl: String?, - ): String + replace: Boolean, + ): String? fun createFolder( remotePath: String, @@ -51,6 +52,7 @@ interface RemoteFileDataSource { remotePath: String, accountName: String, spaceWebDavUrl: String?, + isUserLogged: Boolean, ): String fun moveFile( @@ -58,6 +60,7 @@ interface RemoteFileDataSource { targetRemotePath: String, accountName: String, spaceWebDavUrl: String?, + replace: Boolean, ) fun readFile( diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt index 5c035f7bad3..648e69bdbe5 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt @@ -41,12 +41,13 @@ import java.util.UUID class OCLocalFileDataSource( private val fileDao: FileDao, ) : LocalFileDataSource { - override fun copyFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, remoteId: String) { + override fun copyFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, remoteId: String, replace: Boolean?) { fileDao.copy( sourceFile = sourceFile.toEntity(), targetFolder = targetFolder.toEntity(), finalRemotePath = finalRemotePath, - remoteId = remoteId + remoteId = remoteId, + replace = replace, ) } @@ -56,6 +57,12 @@ class OCLocalFileDataSource( override fun getFileByIdAsFlow(fileId: Long): Flow = fileDao.getFileByIdAsFlow(fileId).map { it?.toModel() } + override fun getFileWithSyncInfoByIdAsFlow(id: Long): Flow = + fileDao.getFileWithSyncInfoByIdAsFlow(id).map { + it?.toModel() + } + + override fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String?): OCFile? { fileDao.getFileByOwnerAndRemotePath(owner, remotePath, spaceId)?.let { return it.toModel() } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt index 7a87ffe4fee..fa69c27aad0 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt @@ -3,6 +3,7 @@ * * @author Abel García de Prada * @author Juan Carlos Garrote Gascón + * @author Manuel Plazas Palacio * * Copyright (C) 2023 ownCloud GmbH. * @@ -32,12 +33,12 @@ class OCRemoteFileDataSource( ) : RemoteFileDataSource { override fun checkPathExistence( path: String, - checkUserCredentials: Boolean, + isUserLogged: Boolean, accountName: String, spaceWebDavUrl: String?, ): Boolean = clientManager.getFileService(accountName).checkPathExistence( path = path, - isUserLogged = checkUserCredentials, + isUserLogged = isUserLogged, spaceWebDavUrl = spaceWebDavUrl, ).data @@ -47,12 +48,14 @@ class OCRemoteFileDataSource( accountName: String, sourceSpaceWebDavUrl: String?, targetSpaceWebDavUrl: String?, - ): String = executeRemoteOperation { + replace: Boolean, + ): String? = executeRemoteOperation { clientManager.getFileService(accountName).copyFile( sourceRemotePath = sourceRemotePath, targetRemotePath = targetRemotePath, sourceSpaceWebDavUrl = sourceSpaceWebDavUrl, targetSpaceWebDavUrl = targetSpaceWebDavUrl, + replace = replace, ) } @@ -82,8 +85,14 @@ class OCRemoteFileDataSource( remotePath: String, accountName: String, spaceWebDavUrl: String?, + isUserLogged: Boolean, ): String { - var checkExistsFile = checkPathExistence(remotePath, false, accountName, spaceWebDavUrl) + var checkExistsFile = checkPathExistence( + path = remotePath, + isUserLogged = isUserLogged, + accountName = accountName, + spaceWebDavUrl = spaceWebDavUrl, + ) if (!checkExistsFile) { return remotePath } @@ -97,13 +106,23 @@ class OCRemoteFileDataSource( substring(0, pos) } } - var count = 2 + var count = 1 do { suffix = " ($count)" checkExistsFile = if (pos >= 0) { - checkPathExistence("${remotePath.substringBeforeLast('.', "")}$suffix.$extension", false, accountName, spaceWebDavUrl) + checkPathExistence( + path = "${remotePath.substringBeforeLast('.', "")}$suffix.$extension", + isUserLogged = isUserLogged, + accountName = accountName, + spaceWebDavUrl = spaceWebDavUrl, + ) } else { - checkPathExistence(remotePath + suffix, false, accountName, spaceWebDavUrl) + checkPathExistence( + path = "$remotePath$suffix", + isUserLogged = isUserLogged, + accountName = accountName, + spaceWebDavUrl = spaceWebDavUrl, + ) } count++ } while (checkExistsFile) @@ -119,11 +138,13 @@ class OCRemoteFileDataSource( targetRemotePath: String, accountName: String, spaceWebDavUrl: String?, + replace: Boolean, ) = executeRemoteOperation { clientManager.getFileService(accountName).moveFile( sourceRemotePath = sourceRemotePath, targetRemotePath = targetRemotePath, spaceWebDavUrl = spaceWebDavUrl, + replace = replace, ) } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/db/FileDao.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/db/FileDao.kt index 08ac58a8af8..c1a80a87fd4 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/db/FileDao.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/db/FileDao.kt @@ -46,16 +46,22 @@ interface FileDao { id: Long ): OCFileEntity? + @Query(SELECT_FILE_WITH_ID) + fun getFileByIdAsFlow( + id: Long + ): Flow + @Transaction @Query(SELECT_FILE_WITH_ID) fun getFileWithSyncInfoById( id: Long ): OCFileAndFileSync? + @Transaction @Query(SELECT_FILE_WITH_ID) - fun getFileByIdAsFlow( + fun getFileWithSyncInfoByIdAsFlow( id: Long - ): Flow + ): Flow @Query(SELECT_FILE_FROM_OWNER_WITH_REMOTE_PATH) fun getFileByOwnerAndRemotePath( @@ -242,7 +248,8 @@ interface FileDao { sourceFile: OCFileEntity, targetFolder: OCFileEntity, finalRemotePath: String, - remoteId: String? + remoteId: String?, + replace: Boolean?, ) { // 1. Update target size upsert( @@ -251,6 +258,10 @@ interface FileDao { ).apply { id = targetFolder.id } ) + if (replace == true) { + remoteId?.let { deleteFileByRemoteId(it) } + } + // 2. Insert a new file with common attributes and retrieved remote id upsert( OCFileEntity( @@ -309,6 +320,9 @@ interface FileDao { @Query(DELETE_FILE_WITH_ID) fun deleteFileById(id: Long) + @Query(DELETE_FILE_WITH_REMOTE_ID) + fun deleteFileByRemoteId(remoteId: String) + @Query(UPDATE_FILES_STORAGE_DIRECTORY) fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String) @@ -479,6 +493,11 @@ interface FileDao { FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME} WHERE id = :id """ + private const val DELETE_FILE_WITH_REMOTE_ID = """ + DELETE + FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME} + WHERE remoteId = :remoteId + """ private const val SELECT_FOLDER_CONTENT = """ SELECT * diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt index cd56f99ef1d..fbb94a734cb 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt @@ -4,6 +4,7 @@ * @author Abel García de Prada * @author Christian Schabesberger * @author Juan Carlos Garrote Gascón + * @author Manuel Plazas Palacio * * Copyright (C) 2023 ownCloud GmbH. * @@ -25,7 +26,7 @@ package com.owncloud.android.data.files.repository import com.owncloud.android.data.files.datasources.LocalFileDataSource import com.owncloud.android.data.files.datasources.RemoteFileDataSource import com.owncloud.android.data.spaces.datasources.LocalSpacesDataSource -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.availableoffline.model.AvailableOfflineStatus import com.owncloud.android.domain.availableoffline.model.AvailableOfflineStatus.AVAILABLE_OFFLINE_PARENT import com.owncloud.android.domain.availableoffline.model.AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE @@ -47,7 +48,7 @@ class OCFileRepository( private val localFileDataSource: LocalFileDataSource, private val remoteFileDataSource: RemoteFileDataSource, private val localSpacesDataSource: LocalSpacesDataSource, - private val localStorageProvider: LocalStorageProvider + private val localStorageProvider: LocalStorageProvider, ) : FileRepository { override fun createFolder( remotePath: String, @@ -79,65 +80,84 @@ class OCFileRepository( } } - override fun copyFile(listOfFilesToCopy: List, targetFolder: OCFile) { + override fun copyFile(listOfFilesToCopy: List, targetFolder: OCFile, replace: List, isUserLogged: Boolean): List { val sourceSpaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(listOfFilesToCopy[0].spaceId, listOfFilesToCopy[0].owner) val targetSpaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(targetFolder.spaceId, targetFolder.owner) + val filesNeedAction = mutableListOf() - listOfFilesToCopy.forEach { ocFile -> + listOfFilesToCopy.forEachIndexed forEach@{ position, ocFile -> // 1. Get the final remote path for this file. val expectedRemotePath: String = targetFolder.remotePath + ocFile.fileName - val finalRemotePath: String = remoteFileDataSource.getAvailableRemotePath( - expectedRemotePath, - targetFolder.owner, - targetSpaceWebDavUrl, - ).let { - if (ocFile.isFolder) it.plus(File.separator) else it - } - // 2. Try to copy files in server - val remoteId = try { - remoteFileDataSource.copyFile( - sourceRemotePath = ocFile.remotePath, - targetRemotePath = finalRemotePath, - accountName = ocFile.owner, - sourceSpaceWebDavUrl = sourceSpaceWebDavUrl, + val finalRemotePath: String? = + getFinalRemotePath( + replace = replace, + expectedRemotePath = expectedRemotePath, + targetFolder = targetFolder, targetSpaceWebDavUrl = targetSpaceWebDavUrl, + filesNeedsAction = filesNeedAction, + ocFile = ocFile, + position = position, + isUserLogged = isUserLogged, ) - } catch (targetNodeDoesNotExist: ConflictException) { - // Target node does not exist anymore. Remove target folder from database and local storage and return - deleteLocalFolderRecursively(ocFile = targetFolder, onlyFromLocalStorage = false) - throw targetNodeDoesNotExist - } catch (sourceFileDoesNotExist: FileNotFoundException) { - // Source file does not exist anymore. Remove file from database and local storage and continue - if (ocFile.isFolder) { - deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false) - } else { - deleteLocalFile( - ocFile = ocFile, - onlyFromLocalStorage = false + if (finalRemotePath != null && (replace.isEmpty() || replace[position] != null)) { + // 2. Try to copy files in server + val remoteId = try { + remoteFileDataSource.copyFile( + sourceRemotePath = ocFile.remotePath, + targetRemotePath = finalRemotePath, + accountName = ocFile.owner, + sourceSpaceWebDavUrl = sourceSpaceWebDavUrl, + targetSpaceWebDavUrl = targetSpaceWebDavUrl, + replace = if (replace.isEmpty()) false else replace[position]!!, ) + } catch (targetNodeDoesNotExist: ConflictException) { + // Target node does not exist anymore. Remove target folder from database and local storage and return + deleteLocalFolderRecursively(ocFile = targetFolder, onlyFromLocalStorage = false) + throw targetNodeDoesNotExist + } catch (sourceFileDoesNotExist: FileNotFoundException) { + // Source file does not exist anymore. Remove file from database and local storage and continue + if (ocFile.isFolder) { + deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false) + } else { + deleteLocalFile( + ocFile = ocFile, + onlyFromLocalStorage = false + ) + } + if (listOfFilesToCopy.size == 1) { + throw sourceFileDoesNotExist + } else { + return@forEach + } } - if (listOfFilesToCopy.size == 1) { - throw sourceFileDoesNotExist - } else { - return@forEach + + // 3. Update database with latest changes + remoteId?.let { + localFileDataSource.copyFile( + sourceFile = ocFile, + targetFolder = targetFolder, + finalRemotePath = finalRemotePath, + remoteId = it, + replace = if (replace.isEmpty()) { + null + } else { + replace[position] + }, + ) } } - - // 3. Update database with latest changes - localFileDataSource.copyFile( - sourceFile = ocFile, - targetFolder = targetFolder, - finalRemotePath = finalRemotePath, - remoteId = remoteId - ) } + return filesNeedAction } override fun getFileById(fileId: Long): OCFile? = localFileDataSource.getFileById(fileId) + override fun getFileWithSyncInfoByIdAsFlow(fileId: Long): Flow = + localFileDataSource.getFileWithSyncInfoByIdAsFlow(fileId) + override fun getFileByIdAsFlow(fileId: Long): Flow = localFileDataSource.getFileByIdAsFlow(fileId) @@ -148,10 +168,14 @@ class OCFileRepository( val personalSpace = localSpacesDataSource.getPersonalSpaceForAccount(owner) if (personalSpace == null) { val legacyRootFolder = localFileDataSource.getFileByRemotePath(remotePath = ROOT_PATH, owner = owner, spaceId = null) - return legacyRootFolder!! + try { + return legacyRootFolder ?: throw IllegalStateException("LegacyRootFolder not found") + } catch (e: IllegalStateException) { + Timber.i("There was an error: $e") + } } // TODO: Retrieving the root folders should return a non nullable. If they don't exist yet, they are created and returned. Remove nullability - val personalRootFolder = localFileDataSource.getFileByRemotePath(remotePath = ROOT_PATH, owner = owner, spaceId = personalSpace.root.id) + val personalRootFolder = localFileDataSource.getFileByRemotePath(remotePath = ROOT_PATH, owner = owner, spaceId = personalSpace?.root?.id) return personalRootFolder!! } @@ -191,70 +215,125 @@ class OCFileRepository( override fun getFilesAvailableOfflineFromEveryAccount(): List = localFileDataSource.getFilesAvailableOfflineFromEveryAccount() - override fun moveFile(listOfFilesToMove: List, targetFile: OCFile) { - val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(targetFile.spaceId, targetFile.owner) + override fun moveFile(listOfFilesToMove: List, targetFolder: OCFile, replace: List, isUserLogged: Boolean): List { + val targetSpaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(targetFolder.spaceId, targetFolder.owner) + val filesNeedsAction = mutableListOf() - listOfFilesToMove.forEach { ocFile -> - // 1. Get the final remote path for this file. - val expectedRemotePath: String = targetFile.remotePath + ocFile.fileName - val finalRemotePath: String = remoteFileDataSource.getAvailableRemotePath(expectedRemotePath, targetFile.owner, spaceWebDavUrl).let { - if (ocFile.isFolder) it.plus(File.separator) else it - } - val finalStoragePath: String = localStorageProvider.getDefaultSavePathFor(targetFile.owner, finalRemotePath, targetFile.spaceId) + listOfFilesToMove.forEachIndexed forEach@{ position, ocFile -> - // 2. Try to move files in server - try { - remoteFileDataSource.moveFile( - sourceRemotePath = ocFile.remotePath, - targetRemotePath = finalRemotePath, - accountName = ocFile.owner, - spaceWebDavUrl = spaceWebDavUrl, + // 1. Get the final remote path for this file. + val expectedRemotePath: String = targetFolder.remotePath + ocFile.fileName + val finalRemotePath: String? = + getFinalRemotePath( + replace = replace, + expectedRemotePath = expectedRemotePath, + targetFolder = targetFolder, + targetSpaceWebDavUrl = targetSpaceWebDavUrl, + filesNeedsAction = filesNeedsAction, + ocFile = ocFile, + position = position, + isUserLogged = isUserLogged, ) - } catch (targetNodeDoesNotExist: ConflictException) { - // Target node does not exist anymore. Remove target folder from database and local storage and return - deleteLocalFolderRecursively(ocFile = targetFile, onlyFromLocalStorage = false) - throw targetNodeDoesNotExist - } catch (sourceFileDoesNotExist: FileNotFoundException) { - // Source file does not exist anymore. Remove file from database and local storage and continue - if (ocFile.isFolder) { - deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false) - } else { - deleteLocalFile( - ocFile = ocFile, - onlyFromLocalStorage = false + + if (finalRemotePath != null && (replace.isEmpty() || replace[position] != null)) { + val finalStoragePath: String = localStorageProvider.getDefaultSavePathFor(targetFolder.owner, finalRemotePath, targetFolder.spaceId) + + // 2. Try to move files in server + try { + remoteFileDataSource.moveFile( + sourceRemotePath = ocFile.remotePath, + targetRemotePath = finalRemotePath, + accountName = ocFile.owner, + spaceWebDavUrl = targetSpaceWebDavUrl, + replace = if (replace.isEmpty()) false else replace[position]!!, ) + } catch (targetNodeDoesNotExist: ConflictException) { + // Target node does not exist anymore. Remove target folder from database and local storage and return + deleteLocalFolderRecursively(ocFile = targetFolder, onlyFromLocalStorage = false) + throw targetNodeDoesNotExist + } catch (sourceFileDoesNotExist: FileNotFoundException) { + // Source file does not exist anymore. Remove file from database and local storage and continue + if (ocFile.isFolder) { + deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false) + } else { + deleteLocalFile( + ocFile = ocFile, + onlyFromLocalStorage = false + ) + } + if (listOfFilesToMove.size == 1) { + throw sourceFileDoesNotExist + } else { + return@forEach + } } - if (listOfFilesToMove.size == 1) { - throw sourceFileDoesNotExist - } else { - return@forEach + + // 3. Clean conflict in old location if there was a conflict + ocFile.etagInConflict?.let { + localFileDataSource.cleanConflict(ocFile.id!!) } - } - // 3. Clean conflict in old location if there was a conflict - ocFile.etagInConflict?.let { - localFileDataSource.cleanConflict(ocFile.id!!) - } + // 4. Update database with latest changes + localFileDataSource.moveFile( + sourceFile = ocFile, + targetFolder = targetFolder, + finalRemotePath = finalRemotePath, + finalStoragePath = finalStoragePath + ) - // 4. Update database with latest changes - localFileDataSource.moveFile( - sourceFile = ocFile, - targetFolder = targetFile, - finalRemotePath = finalRemotePath, - finalStoragePath = finalStoragePath - ) + // 5. Save conflict in new location if there was conflict + ocFile.etagInConflict?.let { + localFileDataSource.saveConflict(ocFile.id!!, it) + } - // 5. Save conflict in new location if there was conflict - ocFile.etagInConflict?.let { - localFileDataSource.saveConflict(ocFile.id!!, it) + // 6. Update local storage + localStorageProvider.moveLocalFile(ocFile, finalStoragePath) } - - // 6. Update local storage - localStorageProvider.moveLocalFile(ocFile, finalStoragePath) } + return filesNeedsAction } + private fun getFinalRemotePath( + replace: List, + expectedRemotePath: String, + targetFolder: OCFile, + targetSpaceWebDavUrl: String?, + filesNeedsAction: MutableList, + ocFile: OCFile, + position: Int, + isUserLogged: Boolean, + ) = + if (replace.isEmpty()) { + val pathExists = remoteFileDataSource.checkPathExistence( + path = expectedRemotePath, + isUserLogged = isUserLogged, + accountName = targetFolder.owner, + spaceWebDavUrl = targetSpaceWebDavUrl, + ) + if (pathExists) { + filesNeedsAction.add(ocFile) + null + } else { + if (ocFile.isFolder) expectedRemotePath.plus(File.separator) else expectedRemotePath + } + } else { + if (replace[position] == true) { + if (ocFile.isFolder) expectedRemotePath.plus(File.separator) else expectedRemotePath + } else if (replace[position] == false) { + remoteFileDataSource.getAvailableRemotePath( + remotePath = expectedRemotePath, + accountName = targetFolder.owner, + spaceWebDavUrl = targetSpaceWebDavUrl, + isUserLogged = isUserLogged, + ).let { + if (ocFile.isFolder) it.plus(File.separator) else it + } + } else { + null + } + } + override fun readFile(remotePath: String, accountName: String, spaceId: String?): OCFile { val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(spaceId, accountName) diff --git a/owncloudData/src/main/java/com/owncloud/android/data/migrations/Migration_34.kt b/owncloudData/src/main/java/com/owncloud/android/data/migrations/Migration_34.kt index 8a7135392b8..219681cf57b 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/migrations/Migration_34.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/migrations/Migration_34.kt @@ -22,7 +22,7 @@ package com.owncloud.android.data.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.owncloud.android.data.ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName import com.owncloud.android.domain.camerauploads.model.FolderBackUpConfiguration.Companion.videoUploadsName diff --git a/owncloudData/src/main/java/com/owncloud/android/data/storage/LegacyStorageProvider.kt b/owncloudData/src/main/java/com/owncloud/android/data/providers/LegacyStorageProvider.kt similarity index 95% rename from owncloudData/src/main/java/com/owncloud/android/data/storage/LegacyStorageProvider.kt rename to owncloudData/src/main/java/com/owncloud/android/data/providers/LegacyStorageProvider.kt index 11d9230c53f..48c15ea8ef0 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/storage/LegacyStorageProvider.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/providers/LegacyStorageProvider.kt @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.data.storage +package com.owncloud.android.data.providers import android.os.Environment import java.io.File diff --git a/owncloudData/src/main/java/com/owncloud/android/data/storage/LocalStorageProvider.kt b/owncloudData/src/main/java/com/owncloud/android/data/providers/LocalStorageProvider.kt similarity index 99% rename from owncloudData/src/main/java/com/owncloud/android/data/storage/LocalStorageProvider.kt rename to owncloudData/src/main/java/com/owncloud/android/data/providers/LocalStorageProvider.kt index cd29a6f90b1..77b9fa3396c 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/storage/LocalStorageProvider.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/providers/LocalStorageProvider.kt @@ -22,7 +22,7 @@ * along with this program. If not, see . */ -package com.owncloud.android.data.storage +package com.owncloud.android.data.providers import android.accounts.Account import android.annotation.SuppressLint diff --git a/owncloudData/src/main/java/com/owncloud/android/data/storage/ScopedStorageProvider.kt b/owncloudData/src/main/java/com/owncloud/android/data/providers/ScopedStorageProvider.kt similarity index 95% rename from owncloudData/src/main/java/com/owncloud/android/data/storage/ScopedStorageProvider.kt rename to owncloudData/src/main/java/com/owncloud/android/data/providers/ScopedStorageProvider.kt index e3089ff7135..9cd68168a7e 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/storage/ScopedStorageProvider.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/providers/ScopedStorageProvider.kt @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.data.storage +package com.owncloud.android.data.providers import android.content.Context import java.io.File diff --git a/owncloudData/src/main/java/com/owncloud/android/data/preferences/datasources/SharedPreferencesProvider.kt b/owncloudData/src/main/java/com/owncloud/android/data/providers/SharedPreferencesProvider.kt similarity index 95% rename from owncloudData/src/main/java/com/owncloud/android/data/preferences/datasources/SharedPreferencesProvider.kt rename to owncloudData/src/main/java/com/owncloud/android/data/providers/SharedPreferencesProvider.kt index d4c802a3774..3dc478482f1 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/preferences/datasources/SharedPreferencesProvider.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/providers/SharedPreferencesProvider.kt @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -package com.owncloud.android.data.preferences.datasources +package com.owncloud.android.data.providers interface SharedPreferencesProvider { diff --git a/owncloudData/src/main/java/com/owncloud/android/data/preferences/datasources/implementation/OCSharedPreferencesProvider.kt b/owncloudData/src/main/java/com/owncloud/android/data/providers/implementation/OCSharedPreferencesProvider.kt similarity index 93% rename from owncloudData/src/main/java/com/owncloud/android/data/preferences/datasources/implementation/OCSharedPreferencesProvider.kt rename to owncloudData/src/main/java/com/owncloud/android/data/providers/implementation/OCSharedPreferencesProvider.kt index ac88bc1b072..cf2097133c9 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/preferences/datasources/implementation/OCSharedPreferencesProvider.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/providers/implementation/OCSharedPreferencesProvider.kt @@ -17,12 +17,12 @@ * along with this program. If not, see . */ -package com.owncloud.android.data.preferences.datasources.implementation +package com.owncloud.android.data.providers.implementation import android.content.Context import android.content.SharedPreferences import android.preference.PreferenceManager -import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvider +import com.owncloud.android.data.providers.SharedPreferencesProvider class OCSharedPreferencesProvider( context: Context diff --git a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/RemoteShareDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/RemoteShareDataSource.kt index 9359dc0392d..58834dcd13a 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/RemoteShareDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/RemoteShareDataSource.kt @@ -39,7 +39,6 @@ interface RemoteShareDataSource { name: String = "", password: String = "", expirationDate: Long = INIT_EXPIRATION_DATE_IN_MILLIS, - publicUpload: Boolean = false, accountName: String ): OCShare @@ -49,7 +48,6 @@ interface RemoteShareDataSource { password: String? = "", expirationDateInMillis: Long = INIT_EXPIRATION_DATE_IN_MILLIS, permissions: Int, - publicUpload: Boolean = false, accountName: String ): OCShare diff --git a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSource.kt index a19ef6ecbfc..175cc46491f 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/datasources/implementation/OCRemoteShareDataSource.kt @@ -56,7 +56,6 @@ class OCRemoteShareDataSource( name: String, password: String, expirationDate: Long, - publicUpload: Boolean, accountName: String ): OCShare { executeRemoteOperation { @@ -68,7 +67,6 @@ class OCRemoteShareDataSource( name, password, expirationDate, - publicUpload ) }.let { return remoteShareMapper.toModel(it.shares.first())!!.apply { @@ -83,7 +81,6 @@ class OCRemoteShareDataSource( password: String?, expirationDateInMillis: Long, permissions: Int, - publicUpload: Boolean, accountName: String ): OCShare { executeRemoteOperation { @@ -93,7 +90,6 @@ class OCRemoteShareDataSource( password, expirationDateInMillis, permissions, - publicUpload ) }.let { return remoteShareMapper.toModel(it.shares.first())!!.apply { diff --git a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepository.kt index 8f016df7a85..3b0dbe6526e 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/sharing/shares/repository/OCShareRepository.kt @@ -72,7 +72,6 @@ class OCShareRepository( name: String, password: String, expirationTimeInMillis: Long, - publicUpload: Boolean, accountName: String ) { insertShare( @@ -82,7 +81,6 @@ class OCShareRepository( name = name, password = password, expirationTimeInMillis = expirationTimeInMillis, - publicUpload = publicUpload, accountName = accountName ) } @@ -93,7 +91,6 @@ class OCShareRepository( password: String?, expirationDateInMillis: Long, permissions: Int, - publicUpload: Boolean, accountName: String ) { return updateShare( @@ -102,7 +99,6 @@ class OCShareRepository( name, password, expirationDateInMillis, - publicUpload, accountName ) } @@ -156,7 +152,6 @@ class OCShareRepository( name: String = "", password: String = "", expirationTimeInMillis: Long = RemoteShare.INIT_EXPIRATION_DATE_IN_MILLIS, - publicUpload: Boolean = false, accountName: String ) { remoteShareDataSource.insert( @@ -167,7 +162,6 @@ class OCShareRepository( name, password, expirationTimeInMillis, - publicUpload, accountName ).also { remotelyInsertedShare -> localShareDataSource.insert(remotelyInsertedShare) @@ -180,7 +174,6 @@ class OCShareRepository( name: String = "", password: String? = "", expirationDateInMillis: Long = RemoteShare.INIT_EXPIRATION_DATE_IN_MILLIS, - publicUpload: Boolean = false, accountName: String ) { remoteShareDataSource.updateShare( @@ -189,7 +182,6 @@ class OCShareRepository( password, expirationDateInMillis, permissions, - publicUpload, accountName ).also { remotelyUpdatedShare -> localShareDataSource.update(remotelyUpdatedShare) diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesDao.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesDao.kt index dd8f796ffb3..bc7717da40c 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesDao.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesDao.kt @@ -26,6 +26,8 @@ import androidx.room.Query import androidx.room.Transaction import androidx.room.Upsert import com.owncloud.android.data.ProviderMeta +import com.owncloud.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_ACCOUNT_NAME +import com.owncloud.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_ID import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ACCOUNT_NAME import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_DRIVE_TYPE import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ID @@ -51,7 +53,18 @@ interface SpacesDao { deleteSpaceForAccountById(accountName = spaceToDelete.accountName, spaceId = spaceToDelete.id) } - // Upsert new spaces + // Delete specials that are not attached to the current spaces of the account anymore + val currentSpecials = getAllSpecialsForAccount(currentAccountName) + + val specialsToDelete = currentSpecials.filterNot { oldSpecial -> + listOfSpecialEntities.any { it.id == oldSpecial.id } + } + + specialsToDelete.forEach { specialToDelete -> + deleteSpecialForAccountById(accountName = specialToDelete.accountName, specialId = specialToDelete.id) + } + + // Upsert new spaces and specials upsertSpaces(listOfSpacesEntities) upsertSpecials(listOfSpecialEntities) } @@ -98,6 +111,11 @@ interface SpacesDao { accountName: String, ): SpacesEntity? + @Query(SELECT_ALL_SPECIALS_FOR_ACCOUNT) + fun getAllSpecialsForAccount( + accountName: String, + ): List + @Query(SELECT_WEB_DAV_URL_FOR_SPACE) fun getWebDavUrlForSpace( spaceId: String?, @@ -110,6 +128,9 @@ interface SpacesDao { @Query(DELETE_SPACE_FOR_ACCOUNT_BY_ID) fun deleteSpaceForAccountById(accountName: String, spaceId: String) + @Query(DELETE_SPECIAL_FOR_ACCOUNT_BY_ID) + fun deleteSpecialForAccountById(accountName: String, specialId: String) + companion object { private const val SELECT_SPACES_BY_DRIVE_TYPE = """ SELECT * @@ -136,6 +157,12 @@ interface SpacesDao { WHERE $SPACES_ID = :spaceId AND $SPACES_ACCOUNT_NAME = :accountName """ + private const val SELECT_ALL_SPECIALS_FOR_ACCOUNT = """ + SELECT * + FROM ${ProviderMeta.ProviderTableMeta.SPACES_SPECIAL_TABLE_NAME} + WHERE $SPACES_SPECIAL_ACCOUNT_NAME = :accountName + """ + private const val SELECT_WEB_DAV_URL_FOR_SPACE = """ SELECT $SPACES_ROOT_WEB_DAV_URL FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME} @@ -153,5 +180,11 @@ interface SpacesDao { FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME} WHERE $SPACES_ACCOUNT_NAME = :accountName AND $SPACES_ID LIKE :spaceId """ + + private const val DELETE_SPECIAL_FOR_ACCOUNT_BY_ID = """ + DELETE + FROM ${ProviderMeta.ProviderTableMeta.SPACES_SPECIAL_TABLE_NAME} + WHERE $SPACES_SPECIAL_ACCOUNT_NAME = :accountName AND $SPACES_SPECIAL_ID LIKE :specialId + """ } } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt index e5a2233f980..75dabf925a4 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt @@ -41,6 +41,9 @@ class OCSpacesRepository( override fun getSpacesByDriveTypeWithSpecialsForAccountAsFlow(accountName: String, filterDriveTypes: Set) = localSpacesDataSource.getSpacesByDriveTypeWithSpecialsForAccountAsFlow(accountName = accountName, filterDriveTypes = filterDriveTypes) + override fun getPersonalSpaceForAccount(accountName: String) = + localSpacesDataSource.getPersonalSpaceForAccount(accountName) + override fun getPersonalAndProjectSpacesForAccount(accountName: String) = localSpacesDataSource.getPersonalAndProjectSpacesForAccount(accountName) diff --git a/owncloudData/src/test/java/com/owncloud/android/data/appRegistry/datasources/implementation/OCLocalAppRegistryDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/appRegistry/datasources/implementation/OCLocalAppRegistryDataSourceTest.kt new file mode 100644 index 00000000000..bc28f2cbbe1 --- /dev/null +++ b/owncloudData/src/test/java/com/owncloud/android/data/appRegistry/datasources/implementation/OCLocalAppRegistryDataSourceTest.kt @@ -0,0 +1,173 @@ +package com.owncloud.android.data.appRegistry.datasources.implementation + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.owncloud.android.data.OwncloudDatabase +import com.owncloud.android.data.appregistry.datasources.implementation.OCLocalAppRegistryDataSource +import com.owncloud.android.data.appregistry.db.AppRegistryDao +import com.owncloud.android.data.appregistry.db.AppRegistryEntity +import com.owncloud.android.domain.appregistry.model.AppRegistry +import com.owncloud.android.domain.appregistry.model.AppRegistryMimeType +import com.owncloud.android.testutil.OC_ACCOUNT_NAME +import com.owncloud.android.testutil.OC_APP_REGISTRY_MIMETYPE +import io.mockk.every +import io.mockk.mockkClass +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class OCLocalAppRegistryDataSourceTest { + private lateinit var ocLocalAppRegistryDataSource: OCLocalAppRegistryDataSource + private val appRegistryDao = mockkClass(AppRegistryDao::class) + private val mimetype = "DIR" + private val ocAppRegistryEntity = AppRegistryEntity( + accountName = OC_ACCOUNT_NAME, + mimeType = mimetype, + ext = "appRegistryMimeTypes.ext", + appProviders = "null", + name = "appRegistryMimeTypes.name", + icon = "appRegistryMimeTypes.icon", + description = "appRegistryMimeTypes.description", + allowCreation = true, + defaultApplication = "appRegistryMimeTypes.defaultApplication", + ) + + @Rule + @JvmField + val instantExecutorRule = InstantTaskExecutorRule() + + @Before + fun init() { + + val db = mockkClass(OwncloudDatabase::class) + + every { + db.appRegistryDao() + } returns appRegistryDao + + ocLocalAppRegistryDataSource = + OCLocalAppRegistryDataSource( + appRegistryDao, + ) + } + + @Test + fun `getAppRegistryForMimeTypeAsStream returns a flow with AppRegistryMimeType object`() = runTest { + + every { appRegistryDao.getAppRegistryForMimeType(any(), any()) } returns flowOf(ocAppRegistryEntity) + + val appRegistry = ocLocalAppRegistryDataSource.getAppRegistryForMimeTypeAsStream(OC_ACCOUNT_NAME, mimetype) + + appRegistry.collect { appRegistryEmitted -> + Assert.assertEquals(OC_APP_REGISTRY_MIMETYPE, appRegistryEmitted) + } + + verify(exactly = 1) { appRegistryDao.getAppRegistryForMimeType(OC_ACCOUNT_NAME, mimetype) } + } + + @Test + fun `getAppRegistryForMimeTypeAsStream returns null when DAO no receive values from db`() = runTest { + + every { appRegistryDao.getAppRegistryForMimeType(any(), any()) } returns flowOf(null) + + val appRegistry = ocLocalAppRegistryDataSource.getAppRegistryForMimeTypeAsStream(OC_ACCOUNT_NAME, mimetype) + + appRegistry.collect { appRegistryEmitted -> + Assert.assertNull(appRegistryEmitted) + } + verify(exactly = 1) { appRegistryDao.getAppRegistryForMimeType(OC_ACCOUNT_NAME, mimetype) } + } + + @Test(expected = Exception::class) + fun `getAppRegistryForMimeTypeAsStream returns an Exception when DAO return an Exception`() = runTest { + + every { appRegistryDao.getAppRegistryForMimeType(any(), any()) } throws Exception() + + val appRegistry = ocLocalAppRegistryDataSource.getAppRegistryForMimeTypeAsStream(OC_ACCOUNT_NAME, mimetype) + + appRegistry.collect { appRegistryEmitted -> + Assert.assertNull(appRegistryEmitted) + } + verify(exactly = 1) { appRegistryDao.getAppRegistryForMimeType(OC_ACCOUNT_NAME, mimetype) } + } + + @Test + fun `getAppRegistryWhichAllowCreation returns a flow with a list of AppRegistryMimeType object`() = runTest { + + every { appRegistryDao.getAppRegistryWhichAllowCreation(any()) } returns flowOf(listOf(ocAppRegistryEntity)) + + val appRegistry = ocLocalAppRegistryDataSource.getAppRegistryWhichAllowCreation(OC_ACCOUNT_NAME) + + appRegistry.collect { appRegistryEmitted -> + Assert.assertEquals(listOf(OC_APP_REGISTRY_MIMETYPE), appRegistryEmitted) + } + + verify(exactly = 1) { appRegistryDao.getAppRegistryWhichAllowCreation(OC_ACCOUNT_NAME) } + } + + @Test + fun `getAppRegistryWhichAllowCreation returns empty list when DAO return empty list`() = runTest { + + every { appRegistryDao.getAppRegistryWhichAllowCreation(any()) } returns flowOf(emptyList()) + + val appRegistry = ocLocalAppRegistryDataSource.getAppRegistryWhichAllowCreation(OC_ACCOUNT_NAME) + + appRegistry.collect { listEmitted -> + Assert.assertEquals(emptyList(), listEmitted) + } + + verify(exactly = 1) { appRegistryDao.getAppRegistryWhichAllowCreation(OC_ACCOUNT_NAME) } + } + + @Test + fun `saveAppRegistryForAccount should save the AppRegistry entities`() = runTest { + val appRegistry = AppRegistry( + OC_ACCOUNT_NAME, mutableListOf( + AppRegistryMimeType("mime_type_1", "ext_1", emptyList(), "name_1", "icon_1", "description_1", true, "default_app_1"), + AppRegistryMimeType("mime_type_2", "ext_2", emptyList(), "name_2", "icon_2", "description_2", true, "default_app_2") + ) + ) + + every { appRegistryDao.deleteAppRegistryForAccount(OC_ACCOUNT_NAME) } returns Unit + every { appRegistryDao.upsertAppRegistries(any()) } returns Unit + + ocLocalAppRegistryDataSource.saveAppRegistryForAccount(appRegistry) + + verify(exactly = 1) { appRegistryDao.deleteAppRegistryForAccount(appRegistry.accountName) } + verify(exactly = 1) { appRegistryDao.upsertAppRegistries(any()) } + } + + @Test(expected = Exception::class) + fun `saveAppRegistryForAccount should returns an Exception`() = runTest { + val appRegistry = AppRegistry( + OC_ACCOUNT_NAME, mutableListOf( + AppRegistryMimeType("mime_type_1", "ext_1", emptyList(), "name_1", "icon_1", "description_1", true, "default_app_1"), + AppRegistryMimeType("mime_type_2", "ext_2", emptyList(), "name_2", "icon_2", "description_2", true, "default_app_2") + ) + ) + + every { appRegistryDao.deleteAppRegistryForAccount(OC_ACCOUNT_NAME) } throws Exception() + every { appRegistryDao.upsertAppRegistries(any()) } throws Exception() + + ocLocalAppRegistryDataSource.saveAppRegistryForAccount(appRegistry) + + verify(exactly = 1) { appRegistryDao.deleteAppRegistryForAccount(appRegistry.accountName) } + verify(exactly = 1) { appRegistryDao.upsertAppRegistries(any()) } + } + + @Test + fun `deleteAppRegistryForAccount should delete appRegistry`() = runTest { + + every { appRegistryDao.deleteAppRegistryForAccount(OC_ACCOUNT_NAME) } returns Unit + + ocLocalAppRegistryDataSource.deleteAppRegistryForAccount(OC_ACCOUNT_NAME) + + verify(exactly = 1) { appRegistryDao.deleteAppRegistryForAccount(any()) } + } + +} \ No newline at end of file diff --git a/owncloudData/src/test/java/com/owncloud/android/data/appRegistry/datasources/implementation/OCRemoteAppRegistryDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/appRegistry/datasources/implementation/OCRemoteAppRegistryDataSourceTest.kt new file mode 100644 index 00000000000..c01ab18e280 --- /dev/null +++ b/owncloudData/src/test/java/com/owncloud/android/data/appRegistry/datasources/implementation/OCRemoteAppRegistryDataSourceTest.kt @@ -0,0 +1,166 @@ +package com.owncloud.android.data.appRegistry.datasources.implementation + +import com.owncloud.android.data.ClientManager +import com.owncloud.android.data.appregistry.datasources.implementation.OCRemoteAppRegistryDataSource +import com.owncloud.android.domain.appregistry.model.AppRegistry +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.appregistry.responses.AppRegistryResponse +import com.owncloud.android.lib.resources.appregistry.services.OCAppRegistryService +import com.owncloud.android.testutil.OC_ACCOUNT_NAME +import com.owncloud.android.testutil.OC_FILE +import com.owncloud.android.utils.createRemoteOperationResultMock +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class OCRemoteAppRegistryDataSourceTest { + + private lateinit var ocRemoteAppRegistryDataSource: OCRemoteAppRegistryDataSource + private val clientManager: ClientManager = mockk(relaxed = true) + private val ocAppRegistryService: OCAppRegistryService = mockk() + private val appResgitryResponse = AppRegistryResponse(value = mockk(relaxed = true)) + private val openWebEndpoint = "https://example.com" + private val expectedFileUrl = "https://example.com/files/testFile.txt" + private val expectedUrl = "https://example.com/file123/TestApp" + private val appUrl = "storage/file123/TestApp" + private val appName = "TestApp" + + @Before + fun init() { + every { clientManager.getAppRegistryService(any()) } returns ocAppRegistryService + + ocRemoteAppRegistryDataSource = OCRemoteAppRegistryDataSource(clientManager) + } + + @Test + fun `getAppRegistryForAccount returns the appRegistry object`() { + val getAppRegistryResult: RemoteOperationResult = + createRemoteOperationResultMock(data = appResgitryResponse, isSuccess = true) + + val appResgitryMock = AppRegistry(accountName = OC_ACCOUNT_NAME, mimetypes = emptyList()) + + every { ocAppRegistryService.getAppRegistry(any()) } returns getAppRegistryResult + + val appRegistry = ocRemoteAppRegistryDataSource.getAppRegistryForAccount(OC_ACCOUNT_NAME, appUrl) + + assertEquals(appResgitryMock, appRegistry) + + verify(exactly = 1) { ocAppRegistryService.getAppRegistry(appUrl) } + } + + @Test(expected = Exception::class) + fun `getAppRegistryForAccount returns an Exception when the operation is not successful`() { + val getAppRegistryResult: RemoteOperationResult = + createRemoteOperationResultMock(data = appResgitryResponse, isSuccess = false) + + every { ocAppRegistryService.getAppRegistry(any()) } returns getAppRegistryResult + + ocRemoteAppRegistryDataSource.getAppRegistryForAccount(OC_ACCOUNT_NAME, appUrl) + + } + + @Test(expected = Exception::class) + fun `getAppRegistryForAccount returns an Exception when getAppRegistry() has an error controlled by an Exception`() { + every { ocAppRegistryService.getAppRegistry(any()) } throws Exception() + + ocRemoteAppRegistryDataSource.getAppRegistryForAccount(OC_ACCOUNT_NAME, appUrl) + } + + @Test + fun `getUrlToOpenInWeb returns an url to open in website`() { + val getUrlToOpenInWebResult: RemoteOperationResult = createRemoteOperationResultMock(data = expectedUrl, isSuccess = true) + + every { + ocAppRegistryService.getUrlToOpenInWeb( + openWebEndpoint = openWebEndpoint, + fileId = OC_FILE.remoteId.toString(), + appName = appName, + ) + } returns getUrlToOpenInWebResult + + val result = ocAppRegistryService.getUrlToOpenInWeb( + openWebEndpoint = openWebEndpoint, + fileId = OC_FILE.remoteId.toString(), + appName = appName, + ) + + assertEquals(expectedUrl, result.data) + + verify { + ocAppRegistryService.getUrlToOpenInWeb( + openWebEndpoint = openWebEndpoint, + fileId = OC_FILE.remoteId.toString(), + appName = appName, + ) + } + } + + @Test(expected = Exception::class) + fun `getUrlToOpenInWeb returns an exception when something there is an error in the method`() { + + every { + ocAppRegistryService.getUrlToOpenInWeb( + openWebEndpoint = openWebEndpoint, + fileId = OC_FILE.remoteId.toString(), + appName = appName, + ) + } throws Exception() + + ocAppRegistryService.getUrlToOpenInWeb( + openWebEndpoint = openWebEndpoint, + fileId = OC_FILE.remoteId.toString(), + appName = appName, + ) + } + + @Test + fun `createFileWithAppProvider returns the url to open in web`() { + + val createFileWithAppProviderResult: RemoteOperationResult = createRemoteOperationResultMock(data = expectedFileUrl, isSuccess = true) + + every { + ocAppRegistryService.createFileWithAppProvider( + createFileWithAppProviderEndpoint = openWebEndpoint, + parentContainerId = OC_FILE.remoteId.toString(), + filename = OC_FILE.fileName, + ) + } returns createFileWithAppProviderResult + + val result = ocAppRegistryService.createFileWithAppProvider( + createFileWithAppProviderEndpoint = openWebEndpoint, + parentContainerId = OC_FILE.remoteId.toString(), + filename = OC_FILE.fileName, + ) + + assertEquals(expectedFileUrl, result.data) + + verify(exactly = 1) { + ocAppRegistryService.createFileWithAppProvider( + createFileWithAppProviderEndpoint = openWebEndpoint, + parentContainerId = OC_FILE.remoteId.toString(), + filename = OC_FILE.fileName, + ) + } + } + + @Test(expected = Exception::class) + fun `createFileWithAppProvider returns an Exception in method`() { + + every { + ocAppRegistryService.createFileWithAppProvider( + createFileWithAppProviderEndpoint = openWebEndpoint, + parentContainerId = OC_FILE.remoteId.toString(), + filename = OC_FILE.fileName, + ) + } throws Exception() + + ocAppRegistryService.createFileWithAppProvider( + createFileWithAppProviderEndpoint = openWebEndpoint, + parentContainerId = OC_FILE.remoteId.toString(), + filename = OC_FILE.fileName, + ) + } +} \ No newline at end of file diff --git a/owncloudData/src/test/java/com/owncloud/android/data/capabilities/datasources/OCLocalCapabilitiesDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/capabilities/datasources/OCLocalCapabilitiesDataSourceTest.kt index be924bf8f6c..e6c2608bd93 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/capabilities/datasources/OCLocalCapabilitiesDataSourceTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/capabilities/datasources/OCLocalCapabilitiesDataSourceTest.kt @@ -26,6 +26,7 @@ import com.owncloud.android.data.capabilities.datasources.implementation.OCLocal import com.owncloud.android.data.capabilities.datasources.implementation.OCLocalCapabilitiesDataSource.Companion.toEntity import com.owncloud.android.data.capabilities.db.OCCapabilityDao import com.owncloud.android.data.capabilities.db.OCCapabilityEntity +import com.owncloud.android.testutil.OC_ACCOUNT_NAME import com.owncloud.android.testutil.OC_CAPABILITY import com.owncloud.android.testutil.livedata.getLastEmittedValue import io.mockk.every @@ -58,7 +59,7 @@ class OCLocalCapabilitiesDataSourceTest { } @Test - fun getCapabilitiesForAccountAsLiveData() { + fun `getCapabilitiesForAccountAsLiveData returns a livedata of OCCapability`() { val capabilitiesLiveData = MutableLiveData() every { ocCapabilityDao.getCapabilitiesForAccountAsLiveData(any()) } returns capabilitiesLiveData @@ -69,10 +70,14 @@ class OCLocalCapabilitiesDataSourceTest { .getLastEmittedValue() assertEquals(ocCapability, capabilityEmitted) + + verify(exactly = 1) { + ocCapabilityDao.getCapabilitiesForAccountAsLiveData(ocCapability.accountName!!) + } } @Test - fun getCapabilitiesForAccountAsLiveDataNull() { + fun `getCapabilitiesForAccountAsLiveData returns null when dao is null`() { val capabilitiesLiveData = MutableLiveData() every { ocCapabilityDao.getCapabilitiesForAccountAsLiveData(any()) } returns capabilitiesLiveData @@ -81,34 +86,54 @@ class OCLocalCapabilitiesDataSourceTest { .getLastEmittedValue() assertNull(capabilityEmitted) + + verify(exactly = 1) { + ocCapabilityDao.getCapabilitiesForAccountAsLiveData(ocCapability.accountName!!) + } } @Test - fun getCapabilitiesForAccount() { + fun `getCapabilitiesForAccount returns a ocCapabilities`() { every { ocCapabilityDao.getCapabilitiesForAccount(any()) } returns ocCapabilityEntity val capabilityEmitted = ocLocalCapabilitiesDataSource.getCapabilityForAccount(ocCapability.accountName!!) assertEquals(ocCapability, capabilityEmitted) + + verify(exactly = 1) { + ocCapabilityDao.getCapabilitiesForAccount(ocCapability.accountName!!) + } } @Test - fun getCapabilitiesForAccountNull() { - every { ocCapabilityDao.getCapabilitiesForAccountAsLiveData(any()) } returns MutableLiveData() + fun `getCapabilityForAccount returns null when dao is null`() { + every { ocCapabilityDao.getCapabilitiesForAccount(any()) } returns null val capabilityEmitted = - ocLocalCapabilitiesDataSource.getCapabilitiesForAccountAsLiveData(ocCapability.accountName!!) - .getLastEmittedValue() + ocLocalCapabilitiesDataSource.getCapabilityForAccount(ocCapability.accountName!!) assertNull(capabilityEmitted) + + verify(exactly = 1) { + ocCapabilityDao.getCapabilitiesForAccount(ocCapability.accountName!!) + } } @Test - fun insertCapabilities() { + fun `insertCapabilities returns a list of OCCapability`() { every { ocCapabilityDao.replace(any()) } returns Unit ocLocalCapabilitiesDataSource.insert(listOf(ocCapability)) verify(exactly = 1) { ocCapabilityDao.replace(listOf(ocCapabilityEntity)) } } + + @Test + fun `deleteCapabilitiesForAccount return unit dao is ok`() { + every { ocCapabilityDao.deleteByAccountName(OC_ACCOUNT_NAME) } returns Unit + + ocLocalCapabilitiesDataSource.deleteCapabilitiesForAccount(OC_ACCOUNT_NAME) + + verify(exactly = 1) { ocCapabilityDao.deleteByAccountName(OC_ACCOUNT_NAME) } + } } diff --git a/owncloudData/src/test/java/com/owncloud/android/data/capabilities/datasources/OCRemoteCapabilitiesDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/capabilities/datasources/OCRemoteCapabilitiesDataSourceTest.kt index e23dc447c2e..732a81f90a5 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/capabilities/datasources/OCRemoteCapabilitiesDataSourceTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/capabilities/datasources/OCRemoteCapabilitiesDataSourceTest.kt @@ -29,6 +29,7 @@ import com.owncloud.android.testutil.OC_CAPABILITY import com.owncloud.android.utils.createRemoteOperationResultMock import io.mockk.every import io.mockk.mockk +import io.mockk.verify import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before @@ -71,5 +72,9 @@ class OCRemoteCapabilitiesDataSourceTest { assertEquals(OC_CAPABILITY.versionMajor, capabilities.versionMajor) assertEquals(OC_CAPABILITY.versionMinor, capabilities.versionMinor) assertEquals(OC_CAPABILITY.versionMicro, capabilities.versionMicro) + + verify(exactly = 1) { + ocCapabilityService.getCapabilities() + } } } diff --git a/owncloudData/src/test/java/com/owncloud/android/data/file/datasources/OCLocalFileDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/file/datasources/OCLocalFileDataSourceTest.kt index 0a5e4ab9153..e7167c66cb1 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/file/datasources/OCLocalFileDataSourceTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/file/datasources/OCLocalFileDataSourceTest.kt @@ -22,24 +22,47 @@ package com.owncloud.android.data.file.datasources import com.owncloud.android.data.files.datasources.implementation.OCLocalFileDataSource import com.owncloud.android.data.files.datasources.implementation.OCLocalFileDataSource.Companion.toEntity import com.owncloud.android.data.files.db.FileDao +import com.owncloud.android.data.files.db.OCFileAndFileSync import com.owncloud.android.data.files.db.OCFileEntity +import com.owncloud.android.data.files.db.OCFileSyncEntity +import com.owncloud.android.data.spaces.datasources.implementation.OCLocalSpacesDataSource.Companion.toEntity +import com.owncloud.android.domain.availableoffline.model.AvailableOfflineStatus import com.owncloud.android.domain.files.model.MIME_DIR import com.owncloud.android.domain.files.model.MIME_PREFIX_IMAGE import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PARENT_ID import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PATH import com.owncloud.android.testutil.OC_FILE +import com.owncloud.android.testutil.OC_FILE_AVAILABLE_OFFLINE +import com.owncloud.android.testutil.OC_FILE_WITH_SYNC_INFO_AND_SPACE +import com.owncloud.android.testutil.OC_SPACE_PERSONAL import io.mockk.every +import io.mockk.mockk import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test +import java.util.UUID +@ExperimentalCoroutinesApi class OCLocalFileDataSourceTest { private lateinit var localDataSource: OCLocalFileDataSource private lateinit var dao: FileDao + private val ocFileAndFileSync = OCFileAndFileSync( + DUMMY_FILE_ENTITY, + OCFileSyncEntity( + fileId = OC_FILE.id!!, + uploadWorkerUuid = null, + downloadWorkerUuid = null, + isSynchronizing = false, + ), + OC_SPACE_PERSONAL.toEntity(), + ) @Before fun init() { @@ -48,58 +71,133 @@ class OCLocalFileDataSourceTest { } @Test - fun `get file by id - ok`() { + fun `getFileById returns the same result as the localDataSource getFileById called in this method`() { every { dao.getFileById(any()) } returns DUMMY_FILE_ENTITY val result = localDataSource.getFileById(OC_FILE.id!!) assertEquals(OC_FILE, result) - verify { dao.getFileById(OC_FILE.id!!) } + verify(exactly = 1) { dao.getFileById(OC_FILE.id!!) } } @Test - fun `get file by id - ok - null`() { + fun `getFileById returns null when dao is null`() { every { dao.getFileById(any()) } returns null val result = localDataSource.getFileById(DUMMY_FILE_ENTITY.id) assertNull(result) - verify { dao.getFileById(DUMMY_FILE_ENTITY.id) } + verify(exactly = 1) { dao.getFileById(DUMMY_FILE_ENTITY.id) } } @Test(expected = Exception::class) - fun `get file by id - ko`() { + fun `getFileById returns an Exception when getFileById receive an exception`() { every { dao.getFileById(any()) } throws Exception() localDataSource.getFileById(DUMMY_FILE_ENTITY.id) } @Test - fun `get file by remote path - ok`() { + fun `getFileByIdAsFlow returns a flow of OCFile`() = runTest { + every { dao.getFileByIdAsFlow(any()) } returns flowOf(DUMMY_FILE_ENTITY) + + val result = localDataSource.getFileByIdAsFlow(OC_FILE.id!!) + + result.collect { result -> + assertEquals(OC_FILE, result) + } + + verify(exactly = 1) { dao.getFileByIdAsFlow(OC_FILE.id!!) } + } + + @Test + fun `getFileByIdAsFlow returns null`() = runTest { + every { dao.getFileByIdAsFlow(any()) } returns flowOf(null) + + val result = localDataSource.getFileByIdAsFlow(DUMMY_FILE_ENTITY.id) + + result.collect { result -> + assertNull(result) + } + + verify(exactly = 1) { dao.getFileByIdAsFlow(DUMMY_FILE_ENTITY.id) } + } + + @Test(expected = Exception::class) + fun `getFileByIdAsFlow returns an Exception when getFileByIdAsFlow receive an exception`() { + every { dao.getFileByIdAsFlow(any()) } throws Exception() + + localDataSource.getFileByIdAsFlow(DUMMY_FILE_ENTITY.id) + } + + @Test + fun `getFileWithSyncInfoByIdAsFlow returns a flow of OCFileWithSyncInfo object`() = runTest { + + every { dao.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) } returns flowOf(ocFileAndFileSync) + + val result = localDataSource.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) + + result.collect { emittedFileWithSyncInfo -> + assertEquals(OC_FILE_WITH_SYNC_INFO_AND_SPACE, emittedFileWithSyncInfo) + } + + verify(exactly = 1) { dao.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) } + } + + @Test + fun `getFileWithSyncInfoByIdAsFlow returns null when DAO is null`() = runTest { + + every { dao.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) } returns flowOf(null) + + val result = localDataSource.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) + + result.collect { emittedFileWithSyncInfo -> + assertNull(emittedFileWithSyncInfo) + } + + verify(exactly = 1) { dao.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) } + } + + @Test(expected = Exception::class) + fun `getFileWithSyncInfoByIdAsFlow returns an exception when DAO receive a Exception`() = runTest { + + every { dao.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) } throws Exception() + + val result = localDataSource.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) + + result.collect { emittedFileWithSyncInfo -> + assertEquals(OC_FILE_WITH_SYNC_INFO_AND_SPACE, emittedFileWithSyncInfo) + } + + verify(exactly = 1) { dao.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) } + } + + @Test + fun `getFileByRemotePath returns the OCFIle`() { every { dao.getFileByOwnerAndRemotePath(any(), any(), any()) } returns DUMMY_FILE_ENTITY val result = localDataSource.getFileByRemotePath(OC_FILE.remotePath, OC_FILE.owner, OC_FILE.spaceId) assertEquals(OC_FILE, result) - verify { dao.getFileByOwnerAndRemotePath(OC_FILE.owner, OC_FILE.remotePath, OC_FILE.spaceId) } + verify(exactly = 1) { dao.getFileByOwnerAndRemotePath(OC_FILE.owner, OC_FILE.remotePath, OC_FILE.spaceId) } } @Test - fun `get file by remote path - ok - null`() { + fun `getFileByRemotePath returns null when DAO is null`() { every { dao.getFileByOwnerAndRemotePath(any(), any(), any()) } returns null val result = localDataSource.getFileByRemotePath(OC_FILE.remotePath, OC_FILE.owner, OC_FILE.spaceId) assertNull(result) - verify { dao.getFileByOwnerAndRemotePath(OC_FILE.owner, OC_FILE.remotePath, OC_FILE.spaceId) } + verify(exactly = 1) { dao.getFileByOwnerAndRemotePath(OC_FILE.owner, OC_FILE.remotePath, OC_FILE.spaceId) } } @Test - fun `get file by remote path - ok - null - create root folder`() { + fun `getFileByRemotePath returns null when create root folder`() { every { dao.getFileByOwnerAndRemotePath(any(), any(), any()) } returns null every { dao.mergeRemoteAndLocalFile(any()) } returns 1234 every { dao.getFileById(1234) } returns DUMMY_FILE_ENTITY.copy( @@ -116,7 +214,7 @@ class OCLocalFileDataSourceTest { assertEquals(MIME_DIR, result.mimeType) assertEquals(ROOT_PATH, result.remotePath) - verify { + verify(exactly = 1) { dao.getFileByOwnerAndRemotePath(OC_FILE.owner, ROOT_PATH, null) dao.mergeRemoteAndLocalFile(any()) dao.getFileById(1234) @@ -124,16 +222,46 @@ class OCLocalFileDataSourceTest { } @Test(expected = Exception::class) - fun `get file by remote path - ko`() { + fun `getFileByRemotePath returns an exception when getFileByRemotePath receive an exception`() { every { dao.getFileByOwnerAndRemotePath(any(), any(), any()) } throws Exception() localDataSource.getFileByRemotePath(OC_FILE.remotePath, OC_FILE.owner, null) - verify { dao.getFileByOwnerAndRemotePath(OC_FILE.owner, OC_FILE.remotePath, null) } + verify(exactly = 1) { dao.getFileByOwnerAndRemotePath(OC_FILE.owner, OC_FILE.remotePath, null) } + } + + @Test + fun `getFileByRemoteId returns OCFile`() { + every { dao.getFileByRemoteId(any()) } returns DUMMY_FILE_ENTITY + + val result = localDataSource.getFileByRemoteId(DUMMY_FILE_ENTITY.remoteId.toString()) + + assertEquals(OC_FILE, result) + + verify(exactly = 1) { dao.getFileByRemoteId(DUMMY_FILE_ENTITY.remoteId.toString()) } + } + + @Test + fun `getFileByRemoteId returns null when DAO is null`() { + every { dao.getFileByRemoteId(any()) } returns null + + val result = localDataSource.getFileByRemoteId(DUMMY_FILE_ENTITY.remoteId.toString()) + + assertEquals(null, result) + + verify(exactly = 1) { dao.getFileByRemoteId(DUMMY_FILE_ENTITY.remoteId.toString()) } + } + + @Test(expected = Exception::class) + fun `getFileByRemoteId returns an exception when getFileByRemoteId receive an exception`() { + every { dao.getFileByRemoteId(any()) } throws Exception() + + localDataSource.getFileByRemoteId(DUMMY_FILE_ENTITY.remoteId.toString()) + } @Test - fun `get folder content - ok`() { + fun `getFolderContent returns a list of OCFile`() { every { dao.getFolderContent(any()) } returns listOf(DUMMY_FILE_ENTITY) val result = localDataSource.getFolderContent(DUMMY_FILE_ENTITY.id) @@ -144,50 +272,428 @@ class OCLocalFileDataSourceTest { } @Test(expected = Exception::class) - fun `get folder content - ko`() { + fun `getFolderContent returns an exception when getFolderContent receive an exception`() { every { dao.getFolderContent(any()) } throws Exception() localDataSource.getFolderContent(DUMMY_FILE_ENTITY.id) - verify { dao.getFolderContent(DUMMY_FILE_ENTITY.id) } } @Test - fun `get folder images - ok`() { + fun `getSearchFolderContent returns a list of OCFile`() { + every { dao.getSearchFolderContent(any(), any()) } returns listOf(DUMMY_FILE_ENTITY) + + val result = localDataSource.getSearchFolderContent(OC_FILE.id!!, "test") + + assertEquals(listOf(OC_FILE), result) + + verify(exactly = 1) { + dao.getSearchFolderContent(DUMMY_FILE_ENTITY.id, "test") + } + } + + @Test(expected = Exception::class) + fun `getSearchFolderContent returns an exception when getSearchFolderContent receive an exception`() { + every { dao.getSearchFolderContent(any(), any()) } throws Exception() + + localDataSource.getSearchFolderContent(OC_FILE.id!!, "test") + + } + + @Test + fun `getSearchAvailableOfflineFolderContent returns a list of OCFile`() { + every { dao.getSearchAvailableOfflineFolderContent(any(), any()) } returns listOf(DUMMY_FILE_ENTITY) + + val result = localDataSource.getSearchAvailableOfflineFolderContent(OC_FILE.id!!, "test") + + assertEquals(listOf(OC_FILE), result) + + verify(exactly = 1) { + dao.getSearchAvailableOfflineFolderContent(DUMMY_FILE_ENTITY.id, "test") + } + } + + @Test(expected = Exception::class) + fun `getSearchAvailableOfflineFolderContent returns an exception when getSearchAvailableOfflineFolderContent receive an exception`() { + every { dao.getSearchAvailableOfflineFolderContent(any(), any()) } throws Exception() + + localDataSource.getSearchAvailableOfflineFolderContent(OC_FILE.id!!, "test") + + } + + @Test + fun `getSearchSharedByLinkFolderContent returns a list of OCFile`() { + + every { dao.getSearchSharedByLinkFolderContent(any(), any()) } returns listOf(DUMMY_FILE_ENTITY) + + val result = localDataSource.getSearchSharedByLinkFolderContent(OC_FILE.id!!, "test") + + assertEquals(listOf(OC_FILE), result) + + verify(exactly = 1) { + dao.getSearchSharedByLinkFolderContent(DUMMY_FILE_ENTITY.id, "test") + } + } + + @Test(expected = Exception::class) + fun `getSearchSharedByLinkFolderContent returns an exception when getSearchSharedByLinkFolderContent receive an exception`() { + + every { dao.getSearchSharedByLinkFolderContent(any(), any()) } throws Exception() + + localDataSource.getSearchSharedByLinkFolderContent(OC_FILE.id!!, "test") + + } + + @Test + fun `getFolderContentWithSyncInfoAsFlow returns a flow of list OcFileAndFileSync`() = runTest { + + every { dao.getFolderContentWithSyncInfoAsFlow(any()) } returns flowOf(listOf(ocFileAndFileSync)) + + val result = localDataSource.getFolderContentWithSyncInfoAsFlow(OC_FILE.id!!) + + result.collect { result -> + assertEquals(listOf(OC_FILE_WITH_SYNC_INFO_AND_SPACE), result) + } + + verify(exactly = 1) { + dao.getFolderContentWithSyncInfoAsFlow(OC_FILE.id!!) + } + } + + @Test(expected = Exception::class) + fun `getFolderContentWithSyncInfoAsFlow returns an exception when getFolderContentWithSyncInfoAsFlow receive an exception`() { + + every { dao.getFolderContentWithSyncInfoAsFlow(any()) } throws Exception() + + localDataSource.getFolderContentWithSyncInfoAsFlow(OC_FILE.id!!) + + } + + @Test + fun `getFolderImages returns a list of OCFile`() { every { dao.getFolderByMimeType(any(), any()) } returns listOf(DUMMY_FILE_ENTITY) val result = localDataSource.getFolderImages(DUMMY_FILE_ENTITY.id) assertEquals(listOf(OC_FILE), result) - verify { dao.getFolderByMimeType(DUMMY_FILE_ENTITY.id, MIME_PREFIX_IMAGE) } + verify(exactly = 1) { dao.getFolderByMimeType(DUMMY_FILE_ENTITY.id, MIME_PREFIX_IMAGE) } } @Test(expected = Exception::class) - fun `get folder images - ko`() { + fun `getFolderImages returns a exception when getFolderImages receive an exception`() { every { dao.getFolderByMimeType(any(), any()) } throws Exception() localDataSource.getFolderImages(DUMMY_FILE_ENTITY.id) - verify { dao.getFolderByMimeType(DUMMY_FILE_ENTITY.id, MIME_PREFIX_IMAGE) } + verify(exactly = 1) { dao.getFolderByMimeType(DUMMY_FILE_ENTITY.id, MIME_PREFIX_IMAGE) } + } + + @Test + fun `getSharedByLinkWithSyncInfoForAccountAsFlow returns a flow of list of OCFileWithSyncInfo`() = runTest { + every { dao.getFilesWithSyncInfoSharedByLinkAsFlow(any()) } returns flowOf(listOf(ocFileAndFileSync)) + + val result = localDataSource.getSharedByLinkWithSyncInfoForAccountAsFlow(DUMMY_FILE_ENTITY.owner) + + result.collect { result -> + assertEquals(listOf(OC_FILE_WITH_SYNC_INFO_AND_SPACE), result) + } + + verify(exactly = 1) { + dao.getFilesWithSyncInfoSharedByLinkAsFlow(DUMMY_FILE_ENTITY.owner) + } + } + + @Test(expected = Exception::class) + fun `getSharedByLinkWithSyncInfoForAccountAsFlow returns a exception when getSharedByLinkWithSyncInfoForAccountAsFlow receive an exception`() { + + every { dao.getFilesWithSyncInfoSharedByLinkAsFlow(any()) } throws Exception() + + localDataSource.getSharedByLinkWithSyncInfoForAccountAsFlow(DUMMY_FILE_ENTITY.owner) + + } + + @Test + fun `getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow returns a flow of list of OCFileWithSyncInfo`() = runTest { + every { dao.getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(any()) } returns flowOf(listOf(ocFileAndFileSync)) + + val result = localDataSource.getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(DUMMY_FILE_ENTITY.owner) + + result.collect { result -> + assertEquals(listOf(OC_FILE_WITH_SYNC_INFO_AND_SPACE), result) + } + + verify(exactly = 1) { + dao.getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(DUMMY_FILE_ENTITY.owner) + } + } + + @Test(expected = Exception::class) + fun `getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow returns an Exception when getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow receive an exception`() { + every { dao.getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(any()) } throws Exception() + + localDataSource.getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(DUMMY_FILE_ENTITY.owner) + } @Test - fun `remove file - ok`() { + fun `getFilesAvailableOfflineFromAccount returns a flow of list of OCFile`() { + every { dao.getFilesAvailableOfflineFromAccount(any()) } returns listOf(DUMMY_FILE_ENTITY) + + val result = localDataSource.getFilesAvailableOfflineFromAccount(DUMMY_FILE_ENTITY.owner) + + assertEquals(listOf(OC_FILE), result) + + verify(exactly = 1) { + dao.getFilesAvailableOfflineFromAccount(DUMMY_FILE_ENTITY.owner) + } + } + + @Test(expected = Exception::class) + fun `getFilesAvailableOfflineFromAccount returns an exception when getFilesAvailableOfflineFromAccount receive an exception`() { + every { dao.getFilesAvailableOfflineFromAccount(any()) } throws Exception() + + localDataSource.getFilesAvailableOfflineFromAccount(DUMMY_FILE_ENTITY.owner) + + } + + @Test + fun `getFilesAvailableOfflineFromEveryAccount returns list of OCFile`() { + every { dao.getFilesAvailableOfflineFromEveryAccount() } returns listOf(DUMMY_FILE_ENTITY) + + val result = localDataSource.getFilesAvailableOfflineFromEveryAccount() + + assertEquals(listOf(OC_FILE), result) + + verify(exactly = 1) { + dao.getFilesAvailableOfflineFromEveryAccount() + } + } + + @Test(expected = Exception::class) + fun `getFilesAvailableOfflineFromEveryAccount returns an exception when getFilesAvailableOfflineFromEveryAccount receive an exception`() { + every { dao.getFilesAvailableOfflineFromEveryAccount() } throws Exception() + + localDataSource.getFilesAvailableOfflineFromEveryAccount() + + } + + @Test + fun `moveFile should move a file from source to target folder when filedao movefile returns ok`() { + val finalRemotePath = "/final/path" + val finalStoragePath = "final_storage" + every { dao.moveFile(any(), any(), finalRemotePath, finalStoragePath) } returns Unit + + localDataSource.moveFile(OC_FILE, OC_FILE_AVAILABLE_OFFLINE, finalRemotePath, finalStoragePath) + + verify(exactly = 1) { + dao.moveFile(DUMMY_FILE_ENTITY, OC_FILE_AVAILABLE_OFFLINE.toEntity(), finalRemotePath, finalStoragePath) + } + } + + @Test(expected = Exception::class) + fun `moveFile returns an exception when moveFile receive an exception`() { + val finalRemotePath = "/final/path" + val finalStoragePath = "final_storage" + + every { dao.moveFile(any(), any(), finalRemotePath, finalStoragePath) } throws Exception() + + localDataSource.moveFile(OC_FILE, OC_FILE_AVAILABLE_OFFLINE, finalRemotePath, finalStoragePath) + } + + @Test + fun `saveFilesInFolderAndReturnThem should save a list of OCFile in a folder and return them`() { + + every { dao.insertFilesInFolderAndReturnThem(any(), any()) } returns listOf(DUMMY_FILE_ENTITY) + + val result = localDataSource.saveFilesInFolderAndReturnThem(listOf(OC_FILE), OC_FILE) + + assertEquals(listOf(OC_FILE), result) + + verify(exactly = 1) { + dao.insertFilesInFolderAndReturnThem(DUMMY_FILE_ENTITY, listOf(DUMMY_FILE_ENTITY)) + } + } + + @Test(expected = Exception::class) + fun `saveFilesInFolderAndReturnThem returns exception when saveFilesInFolderAndReturnThem receive an exception`() { + + every { dao.insertFilesInFolderAndReturnThem(any(), any()) } throws Exception() + + localDataSource.saveFilesInFolderAndReturnThem(listOf(OC_FILE), OC_FILE) + + } + + @Test + fun `saveFile should save a single file and returns unit`() { + + every { dao.upsert(any()) } returns Unit + + localDataSource.saveFile(OC_FILE) + + verify(exactly = 1) { + dao.upsert(DUMMY_FILE_ENTITY) + } + } + + @Test(expected = Exception::class) + fun `saveFile returns an exception when saveFile receive an exception`() { + + every { dao.upsert(any()) } throws Exception() + + localDataSource.saveFile(OC_FILE) + + } + + @Test + fun `saveConflict should save conflict status for a file and returns unit`() { + + val etagInConflict = "error" + + every { dao.updateConflictStatusForFile(any(), any()) } returns Unit + + localDataSource.saveConflict(OC_FILE.id!!, etagInConflict) + + verify(exactly = 1) { + dao.updateConflictStatusForFile(OC_FILE.id!!, etagInConflict) + } + } + + @Test(expected = Exception::class) + fun `saveConflict returns exception when dao receive an exception`() { + + every { dao.updateConflictStatusForFile(any(), any()) } throws Exception() + + localDataSource.saveConflict(OC_FILE.id!!, OC_FILE.etagInConflict!!) + + } + + @Test + fun `cleanConflict should remove conflict status for a file and returns unit`() { + + every { dao.updateConflictStatusForFile(any(), null) } returns Unit + + localDataSource.cleanConflict(OC_FILE.id!!) + + verify(exactly = 1) { + dao.updateConflictStatusForFile(OC_FILE.id!!, null) + } + } + + @Test(expected = Exception::class) + fun `cleanConflict returns exception when dao receive an exception`() { + + every { dao.updateConflictStatusForFile(any(), null) } throws Exception() + + localDataSource.cleanConflict(OC_FILE.id!!) + + } + + @Test + fun `deleteFile should delete a file by its ID and returns unit`() { every { dao.deleteFileById(any()) } returns Unit localDataSource.deleteFile(DUMMY_FILE_ENTITY.id) - verify { dao.deleteFileById(DUMMY_FILE_ENTITY.id) } + verify(exactly = 1) { dao.deleteFileById(DUMMY_FILE_ENTITY.id) } } @Test(expected = Exception::class) - fun `remove file - ko`() { + fun `deleteFile returns exception when dao receive an exception`() { every { dao.deleteFileById(any()) } throws Exception() localDataSource.deleteFile(DUMMY_FILE_ENTITY.id) + } + + @Test + fun `deleteFilesForAccount should delete files for a specific account and returns unit`() { + every { dao.deleteFilesForAccount(any()) } returns Unit + + localDataSource.deleteFilesForAccount(DUMMY_FILE_ENTITY.name!!) + + verify(exactly = 1) { dao.deleteFilesForAccount(DUMMY_FILE_ENTITY.name!!) } + } + + @Test(expected = Exception::class) + fun `deleteFilesForAccount returns exception when dao receive an exception`() { + every { dao.deleteFilesForAccount(any()) } throws Exception() + + localDataSource.deleteFilesForAccount(DUMMY_FILE_ENTITY.name!!) + } + + @Test + fun `disableThumbnailsForFile should disable thumbnails for a specific file and returns unit`() { + every { dao.disableThumbnailsForFile(any()) } returns Unit + + localDataSource.disableThumbnailsForFile(DUMMY_FILE_ENTITY.id) + + verify(exactly = 1) { dao.disableThumbnailsForFile(DUMMY_FILE_ENTITY.id) } + } + + @Test(expected = Exception::class) + fun `disableThumbnailsForFile returns exception when dao receive an exception`() { + every { dao.disableThumbnailsForFile(any()) } throws Exception() + + localDataSource.disableThumbnailsForFile(DUMMY_FILE_ENTITY.id) + } + + @Test + fun `updateAvailableOfflineStatusForFile should update available offline status for a file and returns unit`() { + val newAvailableOfflineStatus: AvailableOfflineStatus = mockk(relaxed = true) + + every { dao.updateAvailableOfflineStatusForFile(any(), any()) } returns Unit + + localDataSource.updateAvailableOfflineStatusForFile(OC_FILE, newAvailableOfflineStatus) + + verify(exactly = 1) { dao.updateAvailableOfflineStatusForFile(OC_FILE, newAvailableOfflineStatus.ordinal) } + } + + @Test(expected = Exception::class) + fun `updateAvailableOfflineStatusForFile returns exception when dao receive an exception`() { + val newAvailableOfflineStatus: AvailableOfflineStatus = mockk(relaxed = true) + + every { dao.updateAvailableOfflineStatusForFile(any(), any()) } throws Exception() + + localDataSource.updateAvailableOfflineStatusForFile(OC_FILE, newAvailableOfflineStatus) + } + + @Test + fun `saveDownloadWorkerUuid should save the worker UUID for a file and returns unit`() { + val workerUuid: UUID = mockk(relaxed = true) + + every { dao.updateSyncStatusForFile(any(), any()) } returns Unit + + localDataSource.saveDownloadWorkerUuid(OC_FILE.id!!, workerUuid) + + verify(exactly = 1) { dao.updateSyncStatusForFile(OC_FILE.id!!, workerUuid) } + } + + @Test(expected = Exception::class) + fun `saveDownloadWorkerUuid returns exception when dao receive an exception`() { + val workerUuid: UUID = mockk(relaxed = true) + + every { dao.updateSyncStatusForFile(any(), any()) } throws Exception() + + localDataSource.saveDownloadWorkerUuid(OC_FILE.id!!, workerUuid) + } + + @Test + fun `cleanWorkersUuid should clean the worker UUID for a file and returns unit`() { + + every { dao.updateSyncStatusForFile(any(), null) } returns Unit + + localDataSource.cleanWorkersUuid(OC_FILE.id!!) + + verify(exactly = 1) { dao.updateSyncStatusForFile(OC_FILE.id!!, null) } + } + + @Test(expected = Exception::class) + fun `cleanWorkersUuid returns an exception whe dao receive an exception`() { + + every { dao.updateSyncStatusForFile(any(), null) } throws Exception() + + localDataSource.cleanWorkersUuid(OC_FILE.id!!) - verify { dao.deleteFileById(DUMMY_FILE_ENTITY.id) } } companion object { diff --git a/owncloudData/src/test/java/com/owncloud/android/data/file/datasources/OCRemoteFileDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/file/datasources/OCRemoteFileDataSourceTest.kt index 477ef690d0f..11e6c8a7f31 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/file/datasources/OCRemoteFileDataSourceTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/file/datasources/OCRemoteFileDataSourceTest.kt @@ -22,10 +22,14 @@ package com.owncloud.android.data.file.datasources import com.owncloud.android.data.ClientManager import com.owncloud.android.data.files.datasources.implementation.OCRemoteFileDataSource import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.files.RemoteFile +import com.owncloud.android.lib.resources.files.services.FileService import com.owncloud.android.lib.resources.files.services.implementation.OCFileService import com.owncloud.android.testutil.OC_ACCOUNT_NAME +import com.owncloud.android.testutil.OC_FILE import com.owncloud.android.testutil.OC_FOLDER import com.owncloud.android.testutil.OC_SECURE_SERVER_INFO_BASIC_AUTH +import com.owncloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE import com.owncloud.android.utils.createRemoteOperationResultMock import io.mockk.every import io.mockk.mockk @@ -40,6 +44,42 @@ class OCRemoteFileDataSourceTest { private val clientManager: ClientManager = mockk(relaxed = true) private val ocFileService: OCFileService = mockk() + private val sourceRemotePath = "source" + private val remoteResult: RemoteOperationResult = + createRemoteOperationResultMock(data = Unit, isSuccess = true) + + private val remoteFileList = arrayListOf( + RemoteFile( + remotePath = OC_FILE.remotePath, + mimeType = OC_FILE.mimeType, + length = OC_FILE.length, + creationTimestamp = OC_FILE.creationTimestamp!!, + modifiedTimestamp = OC_FILE.modificationTimestamp, + etag = OC_FILE.etag, + permissions = OC_FILE.permissions, + remoteId = OC_FILE.remoteId, + privateLink = OC_FILE.privateLink, + owner = OC_FILE.owner, + sharedByLink = OC_FILE.sharedByLink, + sharedWithSharee = OC_FILE.sharedWithSharee!!, + ) + ) + + private val remoteFile = + RemoteFile( + remotePath = OC_FILE.remotePath, + mimeType = OC_FILE.mimeType, + length = OC_FILE.length, + creationTimestamp = OC_FILE.creationTimestamp!!, + modifiedTimestamp = OC_FILE.modificationTimestamp, + etag = OC_FILE.etag, + permissions = OC_FILE.permissions, + remoteId = OC_FILE.remoteId, + privateLink = OC_FILE.privateLink, + owner = OC_FILE.owner, + sharedByLink = OC_FILE.sharedByLink, + sharedWithSharee = OC_FILE.sharedWithSharee!!, + ) @Before fun init() { @@ -49,7 +89,7 @@ class OCRemoteFileDataSourceTest { } @Test - fun checkPathExistenceTrue() { + fun `checkPathExistence returns true when there is date`() { val checkPathExistenceRemoteResult: RemoteOperationResult = createRemoteOperationResultMock(data = true, isSuccess = true) @@ -62,11 +102,11 @@ class OCRemoteFileDataSourceTest { assertNotNull(checkPathExistence) assertEquals(checkPathExistenceRemoteResult.data, checkPathExistence) - verify { ocFileService.checkPathExistence(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) } + verify(exactly = 1) { ocFileService.checkPathExistence(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) } } @Test - fun checkPathExistenceFalse() { + fun `checkPathExistence returns false when there is not date`() { val checkPathExistenceRemoteResult: RemoteOperationResult = createRemoteOperationResultMock(data = false, isSuccess = true) @@ -79,11 +119,11 @@ class OCRemoteFileDataSourceTest { assertNotNull(checkPathExistence) assertEquals(checkPathExistenceRemoteResult.data, checkPathExistence) - verify { ocFileService.checkPathExistence(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) } + verify(exactly = 1) { ocFileService.checkPathExistence(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) } } @Test(expected = Exception::class) - fun checkPathExistenceException() { + fun `checkPathExistence returns an exception when checkPathExistence receive an exception`() { every { ocFileService.checkPathExistence(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) } throws Exception() @@ -92,28 +132,307 @@ class OCRemoteFileDataSourceTest { } @Test - fun createFolderSuccess() { - val createFolderRemoteResult: RemoteOperationResult = - createRemoteOperationResultMock(data = Unit, isSuccess = true) + fun `createFolder returns unit when createFolder is ok`() { every { ocFileService.createFolder(remotePath = OC_FOLDER.remotePath, createFullPath = false, isChunkFolder = false) - } returns createFolderRemoteResult + } returns remoteResult val createFolderResult = ocRemoteFileDataSource.createFolder(OC_FOLDER.remotePath, false, false, OC_ACCOUNT_NAME, null) assertNotNull(createFolderResult) - assertEquals(createFolderRemoteResult.data, createFolderResult) + assertEquals(remoteResult.data, createFolderResult) - verify { ocFileService.createFolder(any(), any(), any()) } + verify(exactly = 1) { ocFileService.createFolder(any(), any(), any()) } } @Test(expected = Exception::class) - fun createFolderException() { + fun `createFolder returns an exception when createFolder receive an exception`() { every { ocFileService.createFolder(OC_FOLDER.remotePath, false, false) } throws Exception() ocRemoteFileDataSource.createFolder(OC_FOLDER.remotePath, false, false, OC_ACCOUNT_NAME, null) } + + @Test + fun `getAvailableRemotePath returns same path if file does not exist`() { + every { + clientManager.getFileService(OC_ACCOUNT_NAME).checkPathExistence( + path = OC_FILE.remotePath, + isUserLogged = false, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + ).data + } returns false + + val firstCopyName = ocRemoteFileDataSource.getAvailableRemotePath( + remotePath = OC_FILE.remotePath, + accountName = OC_ACCOUNT_NAME, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + isUserLogged = false, + ) + assertEquals(OC_FILE.remotePath, firstCopyName) + + verify(exactly = 1) { + clientManager.getFileService(OC_ACCOUNT_NAME).checkPathExistence( + path = OC_FILE.remotePath, + isUserLogged = false, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + ) + } + } + + @Test + fun `getAvailableRemotePath returns path with one if file exists`() { + val suffix = "(1)" + val extension = "jpt" + + every { + clientManager.getFileService(OC_ACCOUNT_NAME).checkPathExistence( + path = any(), + isUserLogged = true, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + ).data + } returnsMany listOf(true, false) + + val firstCopyName = ocRemoteFileDataSource.getAvailableRemotePath( + remotePath = OC_FILE.remotePath, + accountName = OC_ACCOUNT_NAME, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + isUserLogged = true, + ) + assertEquals("${OC_FILE.remotePath.substringBeforeLast('.', "")} $suffix.$extension", firstCopyName) + } + + @Test + fun `getAvailableRemotePath returns path with two if file exists and with one`() { + val suffix = "(2)" + val extension = "jpt" + + every { + clientManager.getFileService(OC_ACCOUNT_NAME).checkPathExistence( + path = any(), + isUserLogged = true, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + ).data + } returnsMany listOf(true, true, false) + + val firstCopyName = ocRemoteFileDataSource.getAvailableRemotePath( + remotePath = OC_FILE.remotePath, + accountName = OC_ACCOUNT_NAME, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + isUserLogged = true, + ) + assertEquals("${OC_FILE.remotePath.substringBeforeLast('.', "")} $suffix.$extension", firstCopyName) + } + + @Test + fun `getAvailableRemotePath returns path with two ones if copying file with one`() { + val suffix = "(1)" + val extension = "jpt" + + every { + clientManager.getFileService(OC_ACCOUNT_NAME).checkPathExistence( + path = any(), + isUserLogged = false, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + ).data + } returnsMany listOf(true, false) + + val firstCopyName = ocRemoteFileDataSource.getAvailableRemotePath( + remotePath = "${OC_FILE.remotePath.substringBeforeLast('.', "")} $suffix.$extension", + accountName = OC_ACCOUNT_NAME, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + isUserLogged = false, + ) + assertEquals("${OC_FILE.remotePath.substringBeforeLast('.', "")} $suffix $suffix.$extension", firstCopyName) + + verify(exactly = 2) { + clientManager.getFileService(OC_ACCOUNT_NAME).checkPathExistence( + path = any(), + isUserLogged = false, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + ) + } + } + + @Test + fun `moveFile returns unit when replace is true`() { + + every { + ocFileService.moveFile( + sourceRemotePath = any(), + targetRemotePath = OC_FILE.remotePath, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + replace = true, + ) + } returns remoteResult + + ocRemoteFileDataSource.moveFile( + sourceRemotePath, + OC_FILE.remotePath, + OC_ACCOUNT_NAME, + OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + true, + ) + + verify(exactly = 1) { + ocFileService.moveFile( + sourceRemotePath = any(), + targetRemotePath = OC_FILE.remotePath, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + replace = true, + ) + } + + } + + @Test + fun `moveFile returns unit when replace is false`() { + + every { + ocFileService.moveFile(any(), OC_FILE.remotePath, OC_SPACE_PROJECT_WITH_IMAGE.webUrl, false) + } returns remoteResult + + ocRemoteFileDataSource.moveFile( + sourceRemotePath, OC_FILE.remotePath, OC_ACCOUNT_NAME, + OC_SPACE_PROJECT_WITH_IMAGE.webUrl, false + ) + + verify(exactly = 1) { + ocFileService.moveFile( + any(), OC_FILE.remotePath, OC_SPACE_PROJECT_WITH_IMAGE.webUrl, false + ) + } + } + + @Test(expected = Exception::class) + fun `moveFile returns an exception when moveFile receive an exception`() { + + every { + ocFileService.moveFile( + sourceRemotePath = any(), + targetRemotePath = OC_FILE.remotePath, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + replace = false + ) + } throws Exception() + + ocRemoteFileDataSource.moveFile( + sourceRemotePath, + OC_FILE.remotePath, + OC_ACCOUNT_NAME, + OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + false, + ) + + } + + @Test + fun `readFile should call readFile and convert to model and returns OCFile object`() { + val expectedOCFile = OC_FILE.copy(id = null, parentId = null, availableOfflineStatus = null) // Eliminar id y parentId + + val fileServiceMock = mockk(relaxed = true) + + val clientManagerMock = mockk() + + val remoteResult: RemoteOperationResult = + createRemoteOperationResultMock(data = remoteFile, isSuccess = true) + + every { + fileServiceMock.readFile( + remotePath = OC_FILE.remotePath, + spaceWebDavUrl = OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + ) + } returns remoteResult + + every { clientManagerMock.getFileService(OC_ACCOUNT_NAME) } returns fileServiceMock + + val ocRemoteFileDataSource = OCRemoteFileDataSource(clientManagerMock) + val result = ocRemoteFileDataSource.readFile(OC_FILE.remotePath, OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + + assertEquals(expectedOCFile, result) + + verify(exactly = 1) { + fileServiceMock.readFile( + OC_FILE.remotePath, OC_SPACE_PROJECT_WITH_IMAGE.webUrl + ) + } + } + + @Test + fun `refreshFolder call refreshFolder, convert to model and returns a list of OCFile`() { + val expectedFile = arrayListOf(OC_FILE.copy(id = null, parentId = null, availableOfflineStatus = null)) // Eliminar id y parentId + + val remoteResult: RemoteOperationResult> = + createRemoteOperationResultMock(data = remoteFileList, isSuccess = true) + + every { + ocFileService.refreshFolder(OC_FILE.remotePath, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + } returns remoteResult + + val result = ocRemoteFileDataSource.refreshFolder( + OC_FILE.remotePath, + OC_ACCOUNT_NAME, + OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + ) + assertEquals(expectedFile, result) + + verify(exactly = 1) { + ocFileService.refreshFolder(OC_FILE.remotePath, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + } + } + + @Test + fun `deleteFile returns Unit when deleteFile is ok`() { + every { + ocFileService.removeFile(OC_FILE.remotePath, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + } returns remoteResult + + ocRemoteFileDataSource.deleteFile( + OC_FILE.remotePath, OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.webUrl, + ) + + verify(exactly = 1) { + ocFileService.removeFile(OC_FILE.remotePath, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + } + } + + @Test(expected = Exception::class) + fun `deleteFile returns an exception when deleteFile receive an exception`() { + every { + ocFileService.removeFile(OC_FILE.remotePath, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + } throws Exception() + + ocRemoteFileDataSource.deleteFile(OC_FILE.remotePath, OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + } + + @Test + fun `renameFile returns Unit when renameFile is ok`() { + val oldName = "oldName" + val oldRemotePath = "oldRemotePath" + val newName = "newName" + every { + ocFileService.renameFile(oldName, oldRemotePath, newName, true, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + } returns remoteResult + + ocRemoteFileDataSource.renameFile(oldName, oldRemotePath, newName, true, OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + + verify(exactly = 1) { + ocFileService.renameFile(oldName, oldRemotePath, newName, true, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + } + } + + @Test(expected = Exception::class) + fun `renameFile returns an exception when renameFile receive an exception`() { + val oldName = "oldName" + val oldRemotePath = "oldRemotePath" + val newName = "newName" + every { + ocFileService.renameFile(oldName, oldRemotePath, newName, true, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + } throws Exception() + + ocRemoteFileDataSource.renameFile(oldName, oldRemotePath, newName, true, OC_ACCOUNT_NAME, OC_SPACE_PROJECT_WITH_IMAGE.webUrl) + + } } diff --git a/owncloudData/src/test/java/com/owncloud/android/data/file/repository/OCFileRepositoryTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/file/repository/OCFileRepositoryTest.kt index c644ab5026a..9e360c4e087 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/file/repository/OCFileRepositoryTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/file/repository/OCFileRepositoryTest.kt @@ -23,21 +23,26 @@ import com.owncloud.android.data.files.datasources.LocalFileDataSource import com.owncloud.android.data.files.datasources.RemoteFileDataSource import com.owncloud.android.data.files.repository.OCFileRepository import com.owncloud.android.data.spaces.datasources.LocalSpacesDataSource -import com.owncloud.android.data.storage.LocalStorageProvider +import com.owncloud.android.data.providers.LocalStorageProvider import com.owncloud.android.domain.exceptions.FileNotFoundException import com.owncloud.android.domain.exceptions.NoConnectionWithServerException import com.owncloud.android.testutil.OC_ACCOUNT_NAME import com.owncloud.android.testutil.OC_FILE +import com.owncloud.android.testutil.OC_FILE_WITH_SYNC_INFO_AND_SPACE import com.owncloud.android.testutil.OC_FOLDER import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Ignore import org.junit.Test @Ignore("Ignore temporary, pretty dependant on implementation... Will be reworked when finished") +@ExperimentalCoroutinesApi class OCFileRepositoryTest { private val remoteFileDataSource = mockk(relaxed = true) @@ -100,6 +105,47 @@ class OCFileRepositoryTest { } } + @Test + fun `getFileWithSyncInfoByIdAsFlow returns OCFileWithSyncInfo`() = runTest { + every { localFileDataSource.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) } returns flowOf(OC_FILE_WITH_SYNC_INFO_AND_SPACE) + + val ocFile = ocFileRepository.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) + + ocFile.collect { result -> + assertEquals(OC_FILE_WITH_SYNC_INFO_AND_SPACE, result) + } + + verify(exactly = 1) { + localFileDataSource.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) + } + } + + @Test + fun `getFileWithSyncInfoByIdAsFlow returns null`() = runTest { + every { localFileDataSource.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) } returns flowOf(null) + + val ocFile = ocFileRepository.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) + + ocFile.collect { result -> + assertNull(result) + } + + verify(exactly = 1) { + localFileDataSource.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) + } + } + + + @Test(expected = Exception::class) + fun `getFileWithSyncInfoByIdAsFlow returns an exception`() = runTest { + every { localFileDataSource.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) } throws Exception() + + ocFileRepository.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) + + verify(exactly = 1) { + localFileDataSource.getFileWithSyncInfoByIdAsFlow(OC_FILE.id!!) + } + } @Test fun `get file by id - ok - null`() { every { localFileDataSource.getFileById(OC_FOLDER.id!!) } returns null diff --git a/owncloudData/src/test/java/com/owncloud/android/data/preferences/datasource/implementation/OCSharedPreferencesProviderTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/preferences/datasource/implementation/OCSharedPreferencesProviderTest.kt new file mode 100644 index 00000000000..09b19cbdb9c --- /dev/null +++ b/owncloudData/src/test/java/com/owncloud/android/data/preferences/datasource/implementation/OCSharedPreferencesProviderTest.kt @@ -0,0 +1,156 @@ +package com.owncloud.android.data.preferences.datasource.implementation + +import android.content.Context +import android.content.SharedPreferences +import android.preference.PreferenceManager +import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class OCSharedPreferencesProviderTest { + + private lateinit var ocSharedPreferencesProvider: OCSharedPreferencesProvider + private lateinit var context: Context + private lateinit var sharedPreferences: SharedPreferences + private lateinit var editor: SharedPreferences.Editor + private val key = "test_key" + + @Before + fun setUp() { + context = mockk(relaxed = true) + sharedPreferences = mockk(relaxed = true) + editor = mockk(relaxed = true) + mockkStatic(PreferenceManager::class) + every { PreferenceManager.getDefaultSharedPreferences(any()) } returns sharedPreferences + every { sharedPreferences.edit() } returns editor + + ocSharedPreferencesProvider = OCSharedPreferencesProvider(context) + } + + @Test + fun `putString put a String in sharedPreferences`() { + val value = "test_value" + ocSharedPreferencesProvider.putString(key, value) + + verify(exactly = 1) { + editor.putString(key, value).apply() + } + } + + @Test + fun `getString returns a String from sharedPreferences`() { + val defaultValue = "default_value" + val savedValue = "saved_value" + + every { sharedPreferences.getString(key, defaultValue) } returns savedValue + + val result = ocSharedPreferencesProvider.getString(key, defaultValue) + assertEquals(savedValue, result) + + verify(exactly = 1) { + sharedPreferences.getString(key, defaultValue) + } + } + + @Test + fun `putInt put a Int in sharedPreferences`() { + val value = 12 + ocSharedPreferencesProvider.putInt(key, value) + + verify(exactly = 1) { + editor.putInt(key, value).apply() + } + } + + @Test + fun `getInt returns a Int from sharedPreferences`() { + val defaultValue = 111 + val savedValue = 233 + + every { sharedPreferences.getInt(key, defaultValue) } returns savedValue + + val result = ocSharedPreferencesProvider.getInt(key, defaultValue) + assertEquals(savedValue, result) + + verify(exactly = 1) { + sharedPreferences.getInt(key, defaultValue) + } + } + + @Test + fun `putLong put a Long in sharedPreferences`() { + val value = 12L + ocSharedPreferencesProvider.putLong(key, value) + + verify(exactly = 1) { + editor.putLong(key, value).apply() + } + } + + @Test + fun `getLong returns a Long from sharedPreferences`() { + val defaultValue = 1411L + val savedValue = 73L + + every { sharedPreferences.getLong(key, defaultValue) } returns savedValue + + val result = ocSharedPreferencesProvider.getLong(key, defaultValue) + assertEquals(savedValue, result) + + verify(exactly = 1) { + sharedPreferences.getLong(key, defaultValue) + } + } + + @Test + fun `putBoolean put a Boolean in sharedPreferences`() { + val value = true + ocSharedPreferencesProvider.putBoolean(key, value) + + verify(exactly = 1) { + editor.putBoolean(key, value).apply() + } + } + + @Test + fun `getBoolean returns a Boolean from sharedPreferences`() { + val defaultValue = false + val savedValue = true + + every { sharedPreferences.getBoolean(key, defaultValue) } returns savedValue + + val result = ocSharedPreferencesProvider.getBoolean(key, defaultValue) + assertTrue(result) + + verify(exactly = 1) { + sharedPreferences.getBoolean(key, defaultValue) + } + } + + @Test + fun `containsPreference verify if the value with the key is contained in sharedPreferences`() { + every { sharedPreferences.contains(key) } returns true + + val result = ocSharedPreferencesProvider.containsPreference(key) + assertTrue(result) + + verify(exactly = 1) { + sharedPreferences.contains(key) + } + } + + @Test + fun `removePreferences remove a preference by key`() { + ocSharedPreferencesProvider.removePreference(key) + + verify(exactly = 1) { + editor.remove(key).apply() + } + } +} diff --git a/owncloudData/src/test/java/com/owncloud/android/data/shares/datasources/OCRemoteShareDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/shares/datasources/OCRemoteShareDataSourceTest.kt index 1fd01c8a40b..613efe8cc5c 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/shares/datasources/OCRemoteShareDataSourceTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/shares/datasources/OCRemoteShareDataSourceTest.kt @@ -77,7 +77,7 @@ class OCRemoteShareDataSourceTest { ) every { - ocShareService.insertShare(any(), any(), any(), any(), any(), any(), any(), any()) + ocShareService.insertShare(any(), any(), any(), any(), any(), any(), any()) } returns createRemoteShareOperationResult // Insert share on remote datasource @@ -119,7 +119,7 @@ class OCRemoteShareDataSourceTest { ) every { - ocShareService.updateShare(any(), any(), any(), any(), any(), any()) + ocShareService.updateShare(any(), any(), any(), any(), any()) } returns updateRemoteShareOperationResult // Update share on remote datasource @@ -161,7 +161,7 @@ class OCRemoteShareDataSourceTest { ) every { - ocShareService.insertShare(any(), any(), any(), any(), any(), any(), any(), any()) + ocShareService.insertShare(any(), any(), any(), any(), any(), any(), any()) } returns createRemoteShareOperationResult // Insert share on remote datasource @@ -204,7 +204,7 @@ class OCRemoteShareDataSourceTest { ) every { - ocShareService.updateShare(any(), any(), any(), any(), any(), any()) + ocShareService.updateShare(any(), any(), any(), any(), any()) } returns updateRemoteShareOperationResult // Update share on remote datasource @@ -332,7 +332,7 @@ class OCRemoteShareDataSourceTest { ) every { - ocShareService.insertShare(any(), any(), any(), any(), any(), any(), any(), any()) + ocShareService.insertShare(any(), any(), any(), any(), any(), any(), any()) } returns createRemoteSharesOperationResult ocRemoteShareDataSource.insert( @@ -363,7 +363,7 @@ class OCRemoteShareDataSourceTest { ) every { - ocShareService.updateShare(any(), any(), any(), any(), any(), any()) + ocShareService.updateShare(any(), any(), any(), any(), any()) } returns updateRemoteShareOperationResult ocRemoteShareDataSource.updateShare( diff --git a/owncloudData/src/test/java/com/owncloud/android/data/shares/repository/OCShareRepositoryTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/shares/repository/OCShareRepositoryTest.kt index 4a2c2ebc62a..ef7c68122c2 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/shares/repository/OCShareRepositoryTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/shares/repository/OCShareRepositoryTest.kt @@ -212,7 +212,6 @@ class OCShareRepositoryTest { any(), any(), any(), - any(), any() ) } returns shares.first() @@ -223,7 +222,6 @@ class OCShareRepositoryTest { "Docs link", "password", 2000, - false, accountName ) @@ -236,7 +234,6 @@ class OCShareRepositoryTest { "Docs link", "password", 2000, - false, accountName ) } @@ -255,7 +252,6 @@ class OCShareRepositoryTest { any(), any(), any(), - any(), any() ) } throws FileNotFoundException() @@ -266,7 +262,6 @@ class OCShareRepositoryTest { "Docs link", "password", 2000, - false, accountName ) @@ -279,7 +274,6 @@ class OCShareRepositoryTest { "Docs link", "password", 2000, - false, accountName ) } @@ -298,7 +292,6 @@ class OCShareRepositoryTest { any(), any(), any(), - any(), any() ) } returns shares.first() @@ -309,7 +302,6 @@ class OCShareRepositoryTest { "password", 2000, -1, - false, accountName ) @@ -320,7 +312,6 @@ class OCShareRepositoryTest { "password", 2000, -1, - false, accountName ) } @@ -337,7 +328,6 @@ class OCShareRepositoryTest { any(), any(), any(), - any(), any() ) } throws FileNotFoundException() @@ -348,7 +338,6 @@ class OCShareRepositoryTest { "password", 2000, -1, - false, accountName ) @@ -359,7 +348,6 @@ class OCShareRepositoryTest { "password", 2000, -1, - false, accountName ) } @@ -380,7 +368,6 @@ class OCShareRepositoryTest { any(), any(), any(), - any(), any() ) } returns shares[2] @@ -417,7 +404,6 @@ class OCShareRepositoryTest { any(), any(), any(), - any(), any() ) } throws FileNotFoundException() @@ -454,7 +440,6 @@ class OCShareRepositoryTest { any(), any(), any(), - any(), any() ) } returns shares[2] @@ -485,7 +470,6 @@ class OCShareRepositoryTest { any(), any(), any(), - any(), any() ) } throws FileNotFoundException() diff --git a/owncloudData/src/test/java/com/owncloud/android/data/storage/ScopedStorageProviderTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/storage/ScopedStorageProviderTest.kt new file mode 100644 index 00000000000..b58febaa7ce --- /dev/null +++ b/owncloudData/src/test/java/com/owncloud/android/data/storage/ScopedStorageProviderTest.kt @@ -0,0 +1,329 @@ +package com.owncloud.android.data.storage + +import android.content.Context +import android.net.Uri +import com.owncloud.android.data.providers.ScopedStorageProvider +import com.owncloud.android.domain.transfers.model.OCTransfer +import com.owncloud.android.testutil.OC_FILE +import com.owncloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify +import java.io.File +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class ScopedStorageProviderTest { + private lateinit var scopedStorageProvider: ScopedStorageProvider + + private lateinit var context: Context + private lateinit var file: File + private lateinit var directory: File + private lateinit var filesDir: File + + private val absolutePath = "/storage/emulated/0/owncloud" + private val remotePath = "/storage/emulated/0/owncloud/remotepath" + private val spaceId = OC_SPACE_PROJECT_WITH_IMAGE.id + private val accountName = "owncloud" + private val newName = "owncloudNewName.txt" + private val uriEncoded = "/path/to/remote/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" + private val rootFolderName = "root_folder" + private val rootFolderPath = absolutePath + File.separator + rootFolderName + private val expectedSizeOfDirectoryValue: Long = 100 + private val separator = File.separator + private val accountDirectoryPath = absolutePath + File.separator + rootFolderName + File.separator + uriEncoded + + @Before + fun setUp() { + context = mockk() + filesDir = mockk() + scopedStorageProvider = ScopedStorageProvider(rootFolderName, context) + + file = mockk().apply { + every { exists() } returns true + every { isDirectory } returns false + every { length() } returns expectedSizeOfDirectoryValue + } + + directory = mockk().apply { + every { exists() } returns true + every { isDirectory } returns true + every { listFiles() } returns arrayOf(file) + } + + every { context.filesDir } returns filesDir + every { filesDir.absolutePath } returns absolutePath + } + + @Test + fun `getPrimaryStorageDirectory returns filesDir`() { + val result = scopedStorageProvider.getPrimaryStorageDirectory() + assertEquals(filesDir, result) + + verify(exactly = 1) { + context.filesDir + } + } + + @Test + fun `getRootFolderPath returns the root folder path String`() { + val actualPath = scopedStorageProvider.getRootFolderPath() + assertEquals(rootFolderPath, actualPath) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + } + + } + + @Test + fun `getDefaultSavePathFor returns the path with spaces when there is a space`() { + mockkStatic(Uri::class) + every { Uri.encode(accountName, "@") } returns uriEncoded + + val expectedPath = accountDirectoryPath + File.separator + spaceId + File.separator + remotePath + val actualPath = scopedStorageProvider.getDefaultSavePathFor(accountName, remotePath, spaceId) + + assertEquals(expectedPath, actualPath) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + } + } + + @Test + fun `getDefaultSavePathFor returns the path without spaces when there is not space`() { + val spaceId = null + + mockkStatic(Uri::class) + every { Uri.encode(accountName, "@") } returns uriEncoded + + val expectedPath = accountDirectoryPath + remotePath + val actualPath = scopedStorageProvider.getDefaultSavePathFor(accountName, remotePath, spaceId) + + assertEquals(expectedPath, actualPath) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + } + } + + @Test + fun `getExpectedRemotePath returns expected remote path with separator in the end when there is separator and is folder true`() { + + val isFolder = true + val parent = separator + "storage" + separator + "emulated" + separator + "0" + separator + "owncloud" + separator + val expectedPath = conditionsExpectedRemotePath(parent, newName, isFolder) + val actualPath = scopedStorageProvider.getExpectedRemotePath(remotePath, newName, isFolder) + + assertEquals(expectedPath, actualPath) + } + + @Test + fun `getExpectedRemotePath returns expected remote path with separator in the end when is separator and is folder false`() { + + val isFolder = false + val parent = separator + "storage" + separator + "emulated" + separator + "0" + separator + "owncloud" + separator + + val expectedPath = conditionsExpectedRemotePath(parent, newName, isFolder) + val actualPath = scopedStorageProvider.getExpectedRemotePath(remotePath, newName, isFolder) + + assertEquals(expectedPath, actualPath) + } + + @Test + fun `getExpectedRemotePath returns expected remote path with separator in the end when is not separator and is folder true`() { + + val isFolder = true + val parent = separator + "storage" + separator + "emulated" + separator + "0" + separator + "owncloud" + + val expectedPath = conditionsExpectedRemotePath(parent, newName, isFolder) + val actualPath = scopedStorageProvider.getExpectedRemotePath(remotePath, newName, isFolder) + + assertEquals(expectedPath, actualPath) + } + + @Test + fun `getExpectedRemotePath returns expected remote path with separator in the end when is not separator and is folder false`() { + val isFolder = false + val parent = separator + "storage" + separator + "emulated" + separator + "0" + separator + "owncloud" + + val expectedPath = conditionsExpectedRemotePath(parent, newName, isFolder) + val actualPath = scopedStorageProvider.getExpectedRemotePath(remotePath, newName, isFolder) + + assertEquals(expectedPath, actualPath) + } + + @Test(expected = IllegalArgumentException::class) + fun `getExpectedRemotePath returns a IllegalArgumentException when there is not file`() { + val isFolder = false + val remotePath = "" + + scopedStorageProvider.getExpectedRemotePath(remotePath, newName, isFolder) + } + + @Test + fun `getTemporalPath returns expected temporal path with separator and space when there is a space`() { + mockkStatic(Uri::class) + every { Uri.encode(accountName, "@") } returns uriEncoded + + val temporalPathWithoutSpace = rootFolderPath + File.separator + "tmp" + File.separator + uriEncoded + + val expectedValue = temporalPathWithoutSpace + File.separator + spaceId + val actualValue = scopedStorageProvider.getTemporalPath(accountName, spaceId) + assertEquals(expectedValue, actualValue) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + } + } + + @Test + fun `getTemporalPath returns expected temporal path neither with separator not space when there is not a space`() { + val spaceId = null + + mockkStatic(Uri::class) + every { Uri.encode(accountName, "@") } returns uriEncoded + + val expectedValue = rootFolderPath + File.separator + TEMPORAL_FOLDER_NAME + File.separator + uriEncoded + val actualValue = scopedStorageProvider.getTemporalPath(accountName, spaceId) + assertEquals(expectedValue, actualValue) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + } + } + + @Test + fun `getLogsPath returns logs path`() { + val expectedValue = rootFolderPath + File.separator + LOGS_FOLDER_NAME + File.separator + val actualValue = scopedStorageProvider.getLogsPath() + + assertEquals(expectedValue, actualValue) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + } + } + + @Test + fun `getUsableSpace returns usable space from the storage directory`() { + val expectedUsableSpace: Long = 1000000 + + every { filesDir.usableSpace } returns expectedUsableSpace + + val actualUsableSpace = scopedStorageProvider.getUsableSpace() + + assertEquals(expectedUsableSpace, actualUsableSpace) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + filesDir.usableSpace + } + } + + @Test + fun `sizeOfDirectory returns the sum the file size in bytes (Long) when isDirectory is true doing a recursive call if it's a directory`() { + every { filesDir.exists() } returns true + every { filesDir.listFiles() } returns arrayOf(directory) + + val actualValue = scopedStorageProvider.sizeOfDirectory(filesDir) + + assertEquals(expectedSizeOfDirectoryValue, actualValue) + + verify(exactly = 1) { + filesDir.exists() + filesDir.listFiles() + } + } + + @Test + fun `sizeOfDirectory returns the sum the file size in bytes (Long) when isDirectory is false without doing a recursive call`() { + val fileSizeDirectory: File = mockk() + every { fileSizeDirectory.exists() } returns true + every { fileSizeDirectory.listFiles() } returns arrayOf(file) + + val actualValue = scopedStorageProvider.sizeOfDirectory(fileSizeDirectory) + assertEquals(expectedSizeOfDirectoryValue, actualValue) + + verify(exactly = 1) { + fileSizeDirectory.exists() + fileSizeDirectory.listFiles() + } + } + + @Test + fun `sizeOfDirectory returns zero value when directory not exists`() { + val expectedSizeOfDirectoryValue: Long = 0 + + every { filesDir.exists() } returns false + + val actualValue = scopedStorageProvider.sizeOfDirectory(filesDir) + + assertEquals(expectedSizeOfDirectoryValue, actualValue) + + verify(exactly = 1) { + filesDir.exists() + } + } + + @Test + fun `deleteLocalFile calls getPrimaryStorageDirectory()`() { + mockkStatic(Uri::class) + every { Uri.encode(any(), any()) } returns uriEncoded + scopedStorageProvider.deleteLocalFile(OC_FILE) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + } + } + + @Test + fun `moveLocalFile calls getPrimaryStorageDirectory()`() { + val finalStoragePath: String = "file.txt" + mockkStatic(Uri::class) + + every { Uri.encode(any(), any()) } returns uriEncoded + scopedStorageProvider.moveLocalFile(OC_FILE, finalStoragePath) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + } + } + + @Test + fun `deleteCacheIfNeeded delete cache file when transfer local path start with cacheDir`() { + val transfer: OCTransfer = mockk() + val accountName = "testAccount" + val localPath = "/file.txt" + + mockkStatic(Uri::class) + every { Uri.encode(any(), any()) } returns uriEncoded + every { transfer.accountName } returns accountName + every { transfer.localPath } returns localPath + + scopedStorageProvider.deleteCacheIfNeeded(transfer) + + verify(exactly = 1) { + scopedStorageProvider.getPrimaryStorageDirectory() + } + } + + private fun conditionsExpectedRemotePath(parent: String, newName: String, isFolder: Boolean): String { + every { filesDir.parent } returns parent + val parent = if (parent.endsWith(File.separator)) parent else parent + File.separator + var newRemotePath = parent + newName + if (isFolder) { + newRemotePath += File.separator + } + return newRemotePath + } + + companion object { + private const val LOGS_FOLDER_NAME = "logs" + private const val TEMPORAL_FOLDER_NAME = "tmp" + } + +} diff --git a/owncloudDomain/build.gradle b/owncloudDomain/build.gradle index 81f13e40783..4d6b3547408 100644 --- a/owncloudDomain/build.gradle +++ b/owncloudDomain/build.gradle @@ -33,16 +33,16 @@ android { } dependencies { - implementation "androidx.appcompat:appcompat:$androidxAppcompat" + implementation libs.androidx.appcompat // Kotlin - implementation "org.jetbrains.kotlin:kotlin-stdlib:$orgJetbrainsKotlin" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$orgJetbrainsKotlinx" + implementation libs.kotlin.stdlib + implementation libs.kotlinx.coroutines.core // Dependencies for unit tests testImplementation project(":owncloudTestUtil") - testImplementation "junit:junit:$junitVersion" - testImplementation "androidx.arch.core:core-testing:$androidxArchCore" - testImplementation "io.mockk:mockk:$ioMockk" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$orgJetbrainsKotlinx" + testImplementation libs.androidx.arch.core.testing + testImplementation libs.junit4 + testImplementation libs.kotlinx.coroutines.test + testImplementation libs.mockk } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/SSLErrorException.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/SSLErrorException.kt index b2d94d34bed..9b9e04229d4 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/SSLErrorException.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/SSLErrorException.kt @@ -21,4 +21,8 @@ package com.owncloud.android.domain.exceptions import java.lang.Exception -class SSLErrorException : Exception() +class SSLErrorException(override val message: String? = null, val code: SSLErrorCode = SSLErrorCode.GENERIC) : Exception(message) + +enum class SSLErrorCode { GENERIC, NOT_HTTP_ALLOWED } + +const val NOT_HTTP_ALLOWED_MESSAGE = "Connection is not secure, http traffic is not allowed." \ No newline at end of file diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt index 4b118221f95..16c6dc9b8ea 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt @@ -4,6 +4,7 @@ * @author Abel García de Prada * @author Christian Schabesberger * @author Juan Carlos Garrote Gascón + * @author Manuel Plazas Palacio * * Copyright (C) 2022 ownCloud GmbH. * @@ -31,9 +32,12 @@ import java.util.UUID interface FileRepository { fun createFolder(remotePath: String, parentFolder: OCFile) - fun copyFile(listOfFilesToCopy: List, targetFolder: OCFile) + + // Returns files in conflict + fun copyFile(listOfFilesToCopy: List, targetFolder: OCFile, replace: List = emptyList(), isUserLogged: Boolean): List fun getFileById(fileId: Long): OCFile? fun getFileByIdAsFlow(fileId: Long): Flow + fun getFileWithSyncInfoByIdAsFlow(fileId: Long): Flow fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String? = null): OCFile? fun getPersonalRootFolderForAccount(owner: String): OCFile fun getSharesRootFolderForAccount(owner: String): OCFile? @@ -45,7 +49,9 @@ interface FileRepository { fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner: String): Flow> fun getFilesAvailableOfflineFromAccount(owner: String): List fun getFilesAvailableOfflineFromEveryAccount(): List - fun moveFile(listOfFilesToMove: List, targetFile: OCFile) + + // Returns files in conflict + fun moveFile(listOfFilesToMove: List, targetFolder: OCFile, replace: List = emptyList(), isUserLogged: Boolean): List fun readFile(remotePath: String, accountName: String, spaceId: String? = null): OCFile fun refreshFolder(remotePath: String, accountName: String, spaceId: String? = null): List fun deleteFiles(listOfFilesToDelete: List, removeOnlyLocalCopy: Boolean) @@ -60,4 +66,5 @@ interface FileRepository { fun disableThumbnailsForFile(fileId: Long) fun updateFileWithNewAvailableOfflineStatus(ocFile: OCFile, newAvailableOfflineStatus: AvailableOfflineStatus) fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String) + } diff --git a/owncloudApp/src/main/java/com/owncloud/android/utils/PowerUtils.java b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/model/FileMenuOption.kt similarity index 55% rename from owncloudApp/src/main/java/com/owncloud/android/utils/PowerUtils.java rename to owncloudDomain/src/main/java/com/owncloud/android/domain/files/model/FileMenuOption.kt index 27b50f3442b..6f04bedc056 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/utils/PowerUtils.java +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/model/FileMenuOption.kt @@ -1,34 +1,26 @@ /** * ownCloud Android client application * - * @author David A. Velasco - * Copyright (C) 2017 ownCloud GmbH. - *

+ * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. - *

+ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - *

+ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.owncloud.android.utils; - -import android.content.Context; -import android.os.Build; -import android.os.PowerManager; - -public class PowerUtils { +package com.owncloud.android.domain.files.model - public static boolean isDeviceIdle(Context context) { - return ( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - ((PowerManager) context.getSystemService(Context.POWER_SERVICE)).isDeviceIdleMode() - ); - } +enum class FileMenuOption { + SELECT_ALL, SELECT_INVERSE, DOWNLOAD, RENAME, MOVE, COPY, REMOVE, OPEN_WITH, + SYNC, CANCEL_SYNC, SHARE, DETAILS, SEND, SET_AV_OFFLINE, UNSET_AV_OFFLINE; } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/CopyFileUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/CopyFileUseCase.kt index 2466f2f89fd..ceaf2a5a97e 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/CopyFileUseCase.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/CopyFileUseCase.kt @@ -32,15 +32,16 @@ import com.owncloud.android.domain.files.model.OCFile * Copying files to a descendant or copying files to the same directory will throw an exception. */ class CopyFileUseCase( - private val fileRepository: FileRepository -) : BaseUseCaseWithResult() { + private val fileRepository: FileRepository, +) : BaseUseCaseWithResult, CopyFileUseCase.Params>() { - override fun run(params: Params) { + override fun run(params: Params): List { validateOrThrowException(params.listOfFilesToCopy, params.targetFolder) - return fileRepository.copyFile( listOfFilesToCopy = params.listOfFilesToCopy, - targetFolder = params.targetFolder + targetFolder = params.targetFolder, + replace = params.replace, + isUserLogged = params.isUserLogged, ) } @@ -55,6 +56,8 @@ class CopyFileUseCase( data class Params( val listOfFilesToCopy: List, - val targetFolder: OCFile + val targetFolder: OCFile, + val replace: List = emptyList(), + val isUserLogged: Boolean, ) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/GetFileWithSyncInfoByIdUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/GetFileWithSyncInfoByIdUseCase.kt new file mode 100644 index 00000000000..76c5e88905d --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/GetFileWithSyncInfoByIdUseCase.kt @@ -0,0 +1,17 @@ +package com.owncloud.android.domain.files.usecases + +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.files.FileRepository +import com.owncloud.android.domain.files.model.OCFileWithSyncInfo +import kotlinx.coroutines.flow.Flow + +class GetFileWithSyncInfoByIdUseCase( + private val fileRepository: FileRepository +) : BaseUseCase, GetFileWithSyncInfoByIdUseCase.Params>() { + + override fun run(params: Params): Flow = + fileRepository.getFileWithSyncInfoByIdAsFlow(params.fileId) + + data class Params(val fileId: Long) + +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/MoveFileUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/MoveFileUseCase.kt index 524e1f51dde..bd1f26569d2 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/MoveFileUseCase.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/MoveFileUseCase.kt @@ -36,18 +36,25 @@ import com.owncloud.android.domain.files.model.OCFile */ class MoveFileUseCase( private val fileRepository: FileRepository -) : BaseUseCaseWithResult() { +) : BaseUseCaseWithResult, MoveFileUseCase.Params>() { - override fun run(params: Params) { + override fun run(params: Params): List { validateOrThrowException(params.listOfFilesToMove, params.targetFolder) return fileRepository.moveFile( listOfFilesToMove = params.listOfFilesToMove, - targetFile = params.targetFolder + targetFolder = params.targetFolder, + replace = params.replace, + isUserLogged = params.isUserLogged, ) } - @Throws(IllegalArgumentException::class, MoveIntoSameFolderException::class, MoveIntoDescendantException::class, MoveIntoAnotherSpaceException::class) + @Throws( + IllegalArgumentException::class, + MoveIntoSameFolderException::class, + MoveIntoDescendantException::class, + MoveIntoAnotherSpaceException::class + ) fun validateOrThrowException(listOfFilesToMove: List, targetFolder: OCFile) { require(listOfFilesToMove.isNotEmpty()) if (listOfFilesToMove[0].spaceId != targetFolder.spaceId) { @@ -61,6 +68,8 @@ class MoveFileUseCase( data class Params( val listOfFilesToMove: List, - val targetFolder: OCFile + val targetFolder: OCFile, + val replace: List = emptyList(), + val isUserLogged: Boolean, ) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCase.kt index 6e1f7f9dca0..8cf042ae82a 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCase.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCase.kt @@ -20,6 +20,9 @@ package com.owncloud.android.domain.server.usecases import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.exceptions.NOT_HTTP_ALLOWED_MESSAGE +import com.owncloud.android.domain.exceptions.SSLErrorCode +import com.owncloud.android.domain.exceptions.SSLErrorException import com.owncloud.android.domain.server.ServerInfoRepository import com.owncloud.android.domain.server.model.ServerInfo import com.owncloud.android.domain.server.model.ServerInfo.Companion.HTTPS_PREFIX @@ -27,16 +30,21 @@ import com.owncloud.android.domain.server.model.ServerInfo.Companion.HTTP_PREFIX import java.util.Locale class GetServerInfoAsyncUseCase( - private val serverInfoRepository: ServerInfoRepository + private val serverInfoRepository: ServerInfoRepository, ) : BaseUseCaseWithResult() { override fun run(params: Params): ServerInfo { val normalizedServerUrl = normalizeProtocolPrefix(params.serverPath).trimEnd(TRAILING_SLASH) - return serverInfoRepository.getServerInfo(normalizedServerUrl, params.creatingAccount) + val serverInfo = serverInfoRepository.getServerInfo(normalizedServerUrl, params.creatingAccount) + if (!serverInfo.isSecureConnection && params.secureConnectionEnforced) { + throw SSLErrorException(NOT_HTTP_ALLOWED_MESSAGE, SSLErrorCode.NOT_HTTP_ALLOWED) + } + return serverInfo } data class Params( val serverPath: String, val creatingAccount: Boolean, + val secureConnectionEnforced: Boolean, ) /** diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/ShareRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/ShareRepository.kt index 9d278d7f1d6..cbe4be07a07 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/ShareRepository.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/ShareRepository.kt @@ -55,7 +55,6 @@ interface ShareRepository { name: String, password: String, expirationTimeInMillis: Long, - publicUpload: Boolean, accountName: String ) @@ -65,7 +64,6 @@ interface ShareRepository { password: String?, expirationDateInMillis: Long, permissions: Int, - publicUpload: Boolean, accountName: String ) diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/CreatePublicShareAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/CreatePublicShareAsyncUseCase.kt index 64ddb74cedf..297c179c1f4 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/CreatePublicShareAsyncUseCase.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/CreatePublicShareAsyncUseCase.kt @@ -33,7 +33,6 @@ class CreatePublicShareAsyncUseCase( params.name, params.password, params.expirationTimeInMillis, - params.publicUpload, params.accountName ) @@ -43,7 +42,6 @@ class CreatePublicShareAsyncUseCase( val name: String, val password: String, val expirationTimeInMillis: Long, - val publicUpload: Boolean, val accountName: String ) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/EditPublicShareAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/EditPublicShareAsyncUseCase.kt index 3b51bc514df..472a68550fc 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/EditPublicShareAsyncUseCase.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/sharing/shares/usecases/EditPublicShareAsyncUseCase.kt @@ -33,7 +33,6 @@ class EditPublicShareAsyncUseCase( params.password, params.expirationDateInMillis, params.permissions, - params.publicUpload, params.accountName ) } @@ -44,7 +43,6 @@ class EditPublicShareAsyncUseCase( val password: String?, val expirationDateInMillis: Long, val permissions: Int, - val publicUpload: Boolean, val accountName: String ) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt index 14ed75cf7b1..0bcef75f4cf 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt @@ -28,6 +28,7 @@ interface SpacesRepository { fun refreshSpacesForAccount(accountName: String) fun getSpacesFromEveryAccountAsStream(): Flow> fun getSpacesByDriveTypeWithSpecialsForAccountAsFlow(accountName: String, filterDriveTypes: Set): Flow> + fun getPersonalSpaceForAccount(accountName: String): OCSpace? fun getPersonalAndProjectSpacesForAccount(accountName: String): List fun getSpaceWithSpecialsByIdForAccount(spaceId: String?, accountName: String): OCSpace fun getWebDavUrlForSpace(accountName: String, spaceId: String?): String? diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetPersonalSpaceForAccountUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetPersonalSpaceForAccountUseCase.kt new file mode 100644 index 00000000000..0a7aba41bae --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetPersonalSpaceForAccountUseCase.kt @@ -0,0 +1,36 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.spaces.usecases + +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.spaces.SpacesRepository +import com.owncloud.android.domain.spaces.model.OCSpace + +class GetPersonalSpaceForAccountUseCase( + private val spacesRepository: SpacesRepository +) : BaseUseCase() { + + override fun run(params: Params) = spacesRepository.getPersonalSpaceForAccount(params.accountName) + + data class Params( + val accountName: String + ) +} diff --git a/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/CopyFileUseCaseTest.kt b/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/CopyFileUseCaseTest.kt index 80cef224afe..594e8acc98a 100644 --- a/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/CopyFileUseCaseTest.kt +++ b/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/CopyFileUseCaseTest.kt @@ -21,11 +21,13 @@ package com.owncloud.android.domain.files.usecases import com.owncloud.android.domain.exceptions.CopyIntoDescendantException import com.owncloud.android.domain.exceptions.UnauthorizedException import com.owncloud.android.domain.files.FileRepository +import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.testutil.OC_FILE import com.owncloud.android.testutil.OC_FOLDER import io.mockk.every import io.mockk.spyk import io.mockk.verify +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -34,31 +36,34 @@ class CopyFileUseCaseTest { private val useCase = CopyFileUseCase(repository) private val useCaseParams = CopyFileUseCase.Params( listOfFilesToCopy = listOf(OC_FILE.copy(remotePath = "/video.mp4", parentId = 101)), - targetFolder = OC_FOLDER.copy(id = 100) + targetFolder = OC_FOLDER.copy(id = 100), + isUserLogged = true, ) @Test fun `copy file - ok`() { - every { repository.copyFile(any(), any()) } returns Unit + every { repository.copyFile(any(), any(), any(), any()) } returns emptyList() val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isSuccess) + assertEquals(useCaseResult.getDataOrNull(), emptyList()) - verify(exactly = 1) { repository.copyFile(any(), any()) } + verify(exactly = 1) { repository.copyFile(any(), any(), any(), any()) } } @Test fun `copy file - ok - single copy into same folder`() { val useCaseParams = CopyFileUseCase.Params( listOfFilesToCopy = listOf(element = OC_FOLDER.copy(remotePath = "/Photos/", parentId = 100)), - targetFolder = OC_FOLDER.copy(remotePath = "/Directory/Descendant/", id = 100) + targetFolder = OC_FOLDER.copy(remotePath = "/Directory/Descendant/", id = 100), + isUserLogged = true, ) val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isSuccess) - verify(exactly = 1) { repository.copyFile(any(), any()) } + verify(exactly = 1) { repository.copyFile(any(), any(), any(), any()) } } @Test @@ -68,45 +73,73 @@ class CopyFileUseCaseTest { assertTrue(useCaseResult.isError) assertTrue(useCaseResult.getThrowableOrNull() is IllegalArgumentException) - verify(exactly = 0) { repository.copyFile(any(), any()) } + verify(exactly = 0) { repository.copyFile(any(), any(), any(), any()) } } @Test fun `copy file - ko - single copy into descendant`() { val useCaseParams = CopyFileUseCase.Params( - listOf(OC_FOLDER.copy(remotePath = "/Directory")), - OC_FOLDER.copy(remotePath = "/Directory/Descendant/") + listOfFilesToCopy = listOf(OC_FOLDER.copy(remotePath = "/Directory")), + targetFolder = OC_FOLDER.copy(remotePath = "/Directory/Descendant/"), + isUserLogged = true ) val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isError) assertTrue(useCaseResult.getThrowableOrNull() is CopyIntoDescendantException) - verify(exactly = 0) { repository.copyFile(any(), any()) } + verify(exactly = 0) { repository.copyFile(any(), any(), any(), any()) } } @Test fun `copy file - ko - multiple copy into descendant`() { val useCaseParams = CopyFileUseCase.Params( - listOf(OC_FOLDER.copy(remotePath = "/Directory"), OC_FILE.copy(remotePath = "/Document.pdf")), - OC_FOLDER.copy(remotePath = "/Directory/Descendant/") + listOfFilesToCopy = listOf(OC_FOLDER.copy(remotePath = "/Directory"), OC_FILE.copy(remotePath = "/Document.pdf")), + targetFolder = OC_FOLDER.copy(remotePath = "/Directory/Descendant/"), + isUserLogged = true ) val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isError) - verify(exactly = 0) { repository.copyFile(any(), any()) } + verify(exactly = 0) { repository.copyFile(any(), any(), any(), any()) } } @Test fun `copy file - ko - other exception`() { - every { repository.copyFile(any(), any()) } throws UnauthorizedException() + every { repository.copyFile(any(), any(), any(), any()) } throws UnauthorizedException() val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isError) assertTrue(useCaseResult.getThrowableOrNull() is UnauthorizedException) - verify(exactly = 1) { repository.copyFile(any(), any()) } + verify(exactly = 1) { repository.copyFile(any(), any(), any(), any()) } } + + @Test + fun `copy file - ok - return list files`() { + val filesList = listOf(OC_FILE, OC_FILE) + every { repository.copyFile(any(), any(), any(), any()) } returns filesList + + val useCaseResult = useCase.execute(useCaseParams) + + assertTrue(useCaseResult.isSuccess) + assertEquals(filesList, useCaseResult.getDataOrNull()) + + verify(exactly = 1) { repository.copyFile(any(), any(), any(), any()) } + } + + @Test + fun `copy file - ok - passing replace`() { + val replace = listOf(true, false) + every { repository.copyFile(any(), any(), replace, any()) } returns emptyList() + + val useCaseResult = useCase.execute(useCaseParams.copy(replace = replace)) + + assertTrue(useCaseResult.isSuccess) + + verify(exactly = 1) { repository.copyFile(any(), any(), replace, any()) } + } + } diff --git a/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/GetFileWithSyncInfoByIdUseCaseTest.kt b/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/GetFileWithSyncInfoByIdUseCaseTest.kt new file mode 100644 index 00000000000..ed1b23026f0 --- /dev/null +++ b/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/GetFileWithSyncInfoByIdUseCaseTest.kt @@ -0,0 +1,56 @@ +package com.owncloud.android.domain.files.usecases + +import com.owncloud.android.domain.files.FileRepository +import com.owncloud.android.testutil.OC_FILE +import com.owncloud.android.testutil.OC_FILE_WITH_SYNC_INFO_AND_SPACE +import io.mockk.every +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + +@ExperimentalCoroutinesApi +class GetFileWithSyncInfoByIdUseCaseTest { + + private val repository: FileRepository = spyk() + private val useCase = GetFileWithSyncInfoByIdUseCase(repository) + private val useCaseParams = GetFileWithSyncInfoByIdUseCase.Params(OC_FILE.id!!) + + @Test + fun `get file with sync by id returns OCFileWithSyncInfo when no error`() = runTest { + every { repository.getFileWithSyncInfoByIdAsFlow(useCaseParams.fileId) } returns flowOf(OC_FILE_WITH_SYNC_INFO_AND_SPACE) + + val useCaseResult = useCase.execute(useCaseParams) + + useCaseResult.collect { result -> + Assert.assertEquals(OC_FILE_WITH_SYNC_INFO_AND_SPACE, result) + } + + verify(exactly = 1) { repository.getFileWithSyncInfoByIdAsFlow(useCaseParams.fileId) } + } + + @Test + fun `get file with sync by id returns true when repository is null`() = runTest { + val useCaseResult = useCase.execute(useCaseParams) + + every { repository.getFileWithSyncInfoByIdAsFlow(useCaseParams.fileId) } returns flowOf(null) + Assert.assertEquals(null, useCaseResult) + + verify(exactly = 1) { repository.getFileWithSyncInfoByIdAsFlow(useCaseParams.fileId) } + } + + @Test(expected = Exception::class) + fun `get file with sync by id returns an exception`() = runTest { + every { repository.getFileWithSyncInfoByIdAsFlow(useCaseParams.fileId) } throws Exception() + + useCase.execute(useCaseParams) + + verify(exactly = 1) { + repository.getFileWithSyncInfoByIdAsFlow(useCaseParams.fileId) + } + } + +} \ No newline at end of file diff --git a/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/MoveFileUseCaseTest.kt b/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/MoveFileUseCaseTest.kt index 835b3ae45ff..309e49a199a 100644 --- a/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/MoveFileUseCaseTest.kt +++ b/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/MoveFileUseCaseTest.kt @@ -27,6 +27,7 @@ import com.owncloud.android.testutil.OC_FOLDER import io.mockk.every import io.mockk.spyk import io.mockk.verify +import org.junit.Assert import org.junit.Assert.assertTrue import org.junit.Test @@ -35,18 +36,19 @@ class MoveFileUseCaseTest { private val useCase = MoveFileUseCase(repository) private val useCaseParams = MoveFileUseCase.Params( listOfFilesToMove = listOf(OC_FILE.copy(remotePath = "/video.mp4", parentId = 123)), - targetFolder = OC_FOLDER.copy(id = 100) + targetFolder = OC_FOLDER.copy(id = 100), + isUserLogged = true, ) @Test fun `move file - ok`() { - every { repository.moveFile(any(), any()) } returns Unit + every { repository.moveFile(any(), any(), any(), any()) } returns emptyList() val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isSuccess) - verify(exactly = 1) { repository.moveFile(any(), any()) } + verify(exactly = 1) { repository.moveFile(any(), any(), any(), any()) } } @Test @@ -56,59 +58,91 @@ class MoveFileUseCaseTest { assertTrue(useCaseResult.isError) assertTrue(useCaseResult.getThrowableOrNull() is IllegalArgumentException) - verify(exactly = 0) { repository.moveFile(any(), any()) } + verify(exactly = 0) { repository.moveFile(any(), any(), any(), any()) } } @Test fun `move file - ko - single move into descendant`() { val useCaseParams = MoveFileUseCase.Params( - listOf(OC_FOLDER.copy(remotePath = "/Directory")), - OC_FOLDER.copy(remotePath = "/Directory/Descendant/") + listOfFilesToMove = listOf(OC_FOLDER.copy(remotePath = "/Directory")), + targetFolder = OC_FOLDER.copy(remotePath = "/Directory/Descendant/"), + isUserLogged = true, ) val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isError) assertTrue(useCaseResult.getThrowableOrNull() is MoveIntoDescendantException) - verify(exactly = 0) { repository.moveFile(any(), any()) } + verify(exactly = 0) { repository.moveFile(any(), any(), any(), any()) } } @Test fun `move file - ko - multiple move into descendant`() { val useCaseParams = MoveFileUseCase.Params( - listOfFilesToMove = listOf(OC_FOLDER.copy(remotePath = "/Directory", parentId = 1), OC_FILE.copy(remotePath = "/Document.pdf", parentId = 1)), - targetFolder = OC_FOLDER.copy(remotePath = "/Directory/Descendant/", id = 100) + listOfFilesToMove = listOf( + OC_FOLDER.copy(remotePath = "/Directory", parentId = 1), + OC_FILE.copy(remotePath = "/Document.pdf", parentId = 1), + ), + targetFolder = OC_FOLDER.copy(remotePath = "/Directory/Descendant/", id = 100), + isUserLogged = true, ) val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isError) - verify(exactly = 0) { repository.moveFile(any(), any()) } + verify(exactly = 0) { repository.moveFile(any(), any(), any(), any()) } } @Test fun `move file - ko - single move into same folder`() { val useCaseParams = MoveFileUseCase.Params( listOfFilesToMove = listOf(element = OC_FOLDER.copy(remotePath = "/Photos/", parentId = 100)), - targetFolder = OC_FOLDER.copy(remotePath = "/Directory/Descendant/", id = 100) + targetFolder = OC_FOLDER.copy(remotePath = "/Directory/Descendant/", id = 100), + isUserLogged = true, ) val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isError) assertTrue(useCaseResult.getThrowableOrNull() is MoveIntoSameFolderException) - verify(exactly = 0) { repository.moveFile(any(), any()) } + verify(exactly = 0) { repository.moveFile(any(), any(), any(), any()) } } @Test fun `move file - ko - other exception`() { - every { repository.moveFile(any(), any()) } throws UnauthorizedException() + every { repository.moveFile(any(), any(), any(), any()) } throws UnauthorizedException() val useCaseResult = useCase.execute(useCaseParams) assertTrue(useCaseResult.isError) assertTrue(useCaseResult.getThrowableOrNull() is UnauthorizedException) - verify(exactly = 1) { repository.moveFile(any(), any()) } + verify(exactly = 1) { repository.moveFile(any(), any(), any(), any()) } } + + @Test + fun ` move file - ok - return list files`() { + val filesList = listOf(OC_FILE, OC_FILE) + every { repository.moveFile(any(), any(), any(), any()) } returns filesList + + val useCaseResult = useCase.execute(useCaseParams) + + assertTrue(useCaseResult.isSuccess) + Assert.assertEquals(filesList, useCaseResult.getDataOrNull()) + + verify(exactly = 1) { repository.moveFile(any(), any(), any(), any()) } + } + + @Test + fun `mov file - ok - passing replace`() { + val replace = listOf(true, false) + every { repository.moveFile(any(), any(), replace, any()) } returns emptyList() + + val useCaseResult = useCase.execute(useCaseParams.copy(replace = replace)) + + assertTrue(useCaseResult.isSuccess) + + verify(exactly = 1) { repository.moveFile(any(), any(), replace, any()) } + } + } diff --git a/owncloudDomain/src/test/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCaseTest.kt b/owncloudDomain/src/test/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCaseTest.kt index 52fdc5b9a76..c5727975bfd 100644 --- a/owncloudDomain/src/test/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCaseTest.kt +++ b/owncloudDomain/src/test/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCaseTest.kt @@ -18,8 +18,10 @@ */ package com.owncloud.android.domain.server.usecases +import com.owncloud.android.domain.exceptions.SSLErrorException import com.owncloud.android.domain.server.ServerInfoRepository import com.owncloud.android.domain.server.usecases.GetServerInfoAsyncUseCase.Companion.TRAILING_SLASH +import com.owncloud.android.testutil.OC_INSECURE_SERVER_INFO_BASIC_AUTH import com.owncloud.android.testutil.OC_SECURE_SERVER_INFO_BASIC_AUTH import io.mockk.every import io.mockk.spyk @@ -32,7 +34,11 @@ class GetServerInfoAsyncUseCaseTest { private val repository: ServerInfoRepository = spyk() private val useCase = GetServerInfoAsyncUseCase((repository)) - private val useCaseParams = GetServerInfoAsyncUseCase.Params(serverPath = "http://demo.owncloud.com", false) + private val useCaseParams = GetServerInfoAsyncUseCase.Params( + serverPath = "http://demo.owncloud.com", + creatingAccount = false, + secureConnectionEnforced = false, + ) private val useCaseParamsWithSlash = useCaseParams.copy(serverPath = useCaseParams.serverPath.plus(TRAILING_SLASH)) @Test @@ -70,4 +76,29 @@ class GetServerInfoAsyncUseCaseTest { verify(exactly = 1) { repository.getServerInfo(useCaseParams.serverPath, false) } } + + @Test + fun `Should throw SSLErrorException when secureConnectionEnforced is true and ServerInfoRepository returns ServerInfo with isSecureConnection returning false`() { + every { repository.getServerInfo(useCaseParams.serverPath, false) } returns OC_INSECURE_SERVER_INFO_BASIC_AUTH + + val useCaseResult = useCase.execute(useCaseParams.copy(secureConnectionEnforced = true)) + + assertTrue(useCaseResult.isError) + assertTrue(useCaseResult.getThrowableOrNull() is SSLErrorException) + + verify(exactly = 1) { repository.getServerInfo(useCaseParams.serverPath, false) } + } + + @Test + fun `Should work correctly when secureConnectionEnforced is true and ServerInfoRepository returns ServerInfo with isSecureConnection returning true`() { + every { repository.getServerInfo(useCaseParams.serverPath, false) } returns OC_SECURE_SERVER_INFO_BASIC_AUTH + + val useCaseResult = useCase.execute(useCaseParams.copy(secureConnectionEnforced = true)) + + assertTrue(useCaseResult.isSuccess) + assertEquals(OC_SECURE_SERVER_INFO_BASIC_AUTH, useCaseResult.getDataOrNull()) + + verify(exactly = 1) { repository.getServerInfo(useCaseParams.serverPath, false) } + } + } diff --git a/owncloudDomain/src/test/java/com/owncloud/android/domain/shares/usecases/CreatePublicShareAsyncUseCaseTest.kt b/owncloudDomain/src/test/java/com/owncloud/android/domain/shares/usecases/CreatePublicShareAsyncUseCaseTest.kt index cf075ca26eb..13dfe418212 100644 --- a/owncloudDomain/src/test/java/com/owncloud/android/domain/shares/usecases/CreatePublicShareAsyncUseCaseTest.kt +++ b/owncloudDomain/src/test/java/com/owncloud/android/domain/shares/usecases/CreatePublicShareAsyncUseCaseTest.kt @@ -33,12 +33,12 @@ class CreatePublicShareAsyncUseCaseTest { private val repository: ShareRepository = spyk() private val useCase = CreatePublicShareAsyncUseCase(repository) - private val useCaseParams = CreatePublicShareAsyncUseCase.Params("", 1, "", "", 100, false, "") + private val useCaseParams = CreatePublicShareAsyncUseCase.Params("", 1, "", "", 100, "") @Test fun `create public share - ok`() { every { - repository.insertPublicShare(any(), any(), any(), any(), any(), any(), any()) + repository.insertPublicShare(any(), any(), any(), any(), any(), any()) } returns Unit val useCaseResult = useCase.execute(useCaseParams) @@ -46,13 +46,13 @@ class CreatePublicShareAsyncUseCaseTest { assertTrue(useCaseResult.isSuccess) assertEquals(Unit, useCaseResult.getDataOrNull()) - verify(exactly = 1) { repository.insertPublicShare("", 1, "", "", 100, false, "") } + verify(exactly = 1) { repository.insertPublicShare("", 1, "", "", 100, "") } } @Test fun `create public share - ko`() { every { - repository.insertPublicShare(any(), any(), any(), any(), any(), any(), any()) + repository.insertPublicShare(any(), any(), any(), any(), any(), any()) } throws UnauthorizedException() val useCaseResult = useCase.execute(useCaseParams) @@ -60,13 +60,13 @@ class CreatePublicShareAsyncUseCaseTest { assertTrue(useCaseResult.isError) assertTrue(useCaseResult.getThrowableOrNull() is UnauthorizedException) - verify(exactly = 1) { repository.insertPublicShare("", 1, "", "", 100, false, "") } + verify(exactly = 1) { repository.insertPublicShare("", 1, "", "", 100, "") } } @Test fun `create public share - ko - illegal argument exception`() { every { - repository.insertPublicShare(any(), any(), any(), any(), any(), any(), any()) + repository.insertPublicShare(any(), any(), any(), any(), any(), any()) } throws IllegalArgumentException() val useCaseResult = useCase.execute(useCaseParams) @@ -74,6 +74,6 @@ class CreatePublicShareAsyncUseCaseTest { assertTrue(useCaseResult.isError) assertTrue(useCaseResult.getThrowableOrNull() is IllegalArgumentException) - verify(exactly = 1) { repository.insertPublicShare("", 1, "", "", 100, false, "") } + verify(exactly = 1) { repository.insertPublicShare("", 1, "", "", 100, "") } } } diff --git a/owncloudDomain/src/test/java/com/owncloud/android/domain/shares/usecases/EditPublicShareAsyncUseCaseTest.kt b/owncloudDomain/src/test/java/com/owncloud/android/domain/shares/usecases/EditPublicShareAsyncUseCaseTest.kt index afdef3e66af..97edeb17ba3 100644 --- a/owncloudDomain/src/test/java/com/owncloud/android/domain/shares/usecases/EditPublicShareAsyncUseCaseTest.kt +++ b/owncloudDomain/src/test/java/com/owncloud/android/domain/shares/usecases/EditPublicShareAsyncUseCaseTest.kt @@ -39,14 +39,13 @@ class EditPublicShareAsyncUseCaseTest { "", OC_SHARE.expirationDate, OC_SHARE.permissions, - false, OC_SHARE.accountOwner ) @Test fun `edit public share - ok`() { every { - repository.updatePublicShare(any(), any(), any(), any(), any(), any(), any()) + repository.updatePublicShare(any(), any(), any(), any(), any(), any()) } returns Unit val useCaseResult = useCase.execute(useCaseParams) @@ -61,7 +60,6 @@ class EditPublicShareAsyncUseCaseTest { password = "", expirationDateInMillis = OC_SHARE.expirationDate, permissions = OC_SHARE.permissions, - publicUpload = false, accountName = OC_SHARE.accountOwner ) } @@ -70,7 +68,7 @@ class EditPublicShareAsyncUseCaseTest { @Test fun `edit public share - ko`() { every { - repository.updatePublicShare(any(), any(), any(), any(), any(), any(), any()) + repository.updatePublicShare(any(), any(), any(), any(), any(), any()) } throws UnauthorizedException() val useCaseResult = useCase.execute(useCaseParams) @@ -85,7 +83,6 @@ class EditPublicShareAsyncUseCaseTest { password = "", expirationDateInMillis = OC_SHARE.expirationDate, permissions = OC_SHARE.permissions, - publicUpload = false, accountName = OC_SHARE.accountOwner ) } diff --git a/owncloudTestUtil/build.gradle b/owncloudTestUtil/build.gradle index 9f3b6b02630..2b03162bd3f 100644 --- a/owncloudTestUtil/build.gradle +++ b/owncloudTestUtil/build.gradle @@ -22,6 +22,6 @@ dependencies { implementation project(':owncloudDomain') implementation project(':owncloud-android-library:owncloudComLibrary') - implementation "org.jetbrains.kotlin:kotlin-stdlib:$orgJetbrainsKotlin" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycle" + implementation libs.kotlin.stdlib + implementation libs.androidx.lifecycle.livedata.ktx } diff --git a/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCAppRegistryMimeType.kt b/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCAppRegistryMimeType.kt new file mode 100644 index 00000000000..d0287f3734e --- /dev/null +++ b/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCAppRegistryMimeType.kt @@ -0,0 +1,14 @@ +package com.owncloud.android.testutil + +import com.owncloud.android.domain.appregistry.model.AppRegistryMimeType + +val OC_APP_REGISTRY_MIMETYPE = AppRegistryMimeType( + mimeType = "DIR", + ext = "appRegistryMimeTypes.ext", + appProviders = emptyList(), + name = "appRegistryMimeTypes.name", + icon = "appRegistryMimeTypes.icon", + description = "appRegistryMimeTypes.description", + allowCreation = true, + defaultApplication = "appRegistryMimeTypes.defaultApplication", +) diff --git a/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCFile.kt b/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCFile.kt index 47fe7ebfe07..115ac1e229a 100644 --- a/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCFile.kt +++ b/owncloudTestUtil/src/main/java/com/owncloud/android/testutil/OCFile.kt @@ -48,7 +48,7 @@ val OC_FILE = OCFile( permissions = "RDNVCK", remoteId = "00000003oci9p7er2how", privateLink = "http://server.url/f/4", - creationTimestamp = 0, + creationTimestamp = 1593510589000, modificationTimestamp = 1593510589000, etag = "5efb0c13c688f", mimeType = "image/jpeg", @@ -56,6 +56,22 @@ val OC_FILE = OCFile( availableOfflineStatus = AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE, ) +val OC_FILE_AVAILABLE_OFFLINE = OCFile( + id = 124, + parentId = 122, + remotePath = "/Photos/image.jpt", + owner = OC_ACCOUNT_NAME, + permissions = "RDNVCK", + remoteId = "00000003oci9p7er2how", + privateLink = "http://server.url/f/4", + creationTimestamp = 1593510589000, + modificationTimestamp = 1593510589000, + etag = "5efb0c13c688f", + mimeType = "image/jpeg", + length = 3000000, + availableOfflineStatus = AvailableOfflineStatus.AVAILABLE_OFFLINE_PARENT +) + val OC_FILE_WITH_SYNC_INFO = OCFileWithSyncInfo( file = OC_FILE, uploadWorkerUuid = null, @@ -63,6 +79,30 @@ val OC_FILE_WITH_SYNC_INFO = OCFileWithSyncInfo( isSynchronizing = false, ) +val OC_FILE_WITH_SYNC_INFO_AND_SPACE = OCFileWithSyncInfo( + file = OC_FILE, + uploadWorkerUuid = null, + downloadWorkerUuid = null, + isSynchronizing = false, + space = OC_SPACE_PERSONAL +) + +val OC_FILE_WITH_SYNC_INFO_AND_WITHOUT_PERSONAL_SPACE = OCFileWithSyncInfo( + file = OC_FILE, + uploadWorkerUuid = null, + downloadWorkerUuid = null, + isSynchronizing = false, + space = OC_SPACE_PROJECT_WITH_IMAGE +) + +val OC_FILE_OC_AVAILABLE_OFFLINE_FILE = OCFileWithSyncInfo( + file = OC_FILE_AVAILABLE_OFFLINE, + uploadWorkerUuid = null, + downloadWorkerUuid = null, + isSynchronizing = false, + space = OC_SPACE_PROJECT_WITH_IMAGE +) + val OC_AVAILABLE_OFFLINE_FILE = OCFile( id = 125, parentId = 122, diff --git a/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 00000000000..16c0a4136fe --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1,2 @@ +/.gradle +/.build \ No newline at end of file diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts new file mode 100644 index 00000000000..9cb33dd98c7 --- /dev/null +++ b/plugins/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + google() +} \ No newline at end of file diff --git a/plugins/settings.gradle.kts b/plugins/settings.gradle.kts new file mode 100644 index 00000000000..c3b0279a155 --- /dev/null +++ b/plugins/settings.gradle.kts @@ -0,0 +1,10 @@ +dependencyResolutionManagement { + repositories { + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +}