` or `BaseUseCaseWithResult`. ViewModels call them via coroutines and expose `Flow` (preferred) or `LiveData` to the UI.
+
+**DI:** Koin with module-per-layer structure under `owncloudApp/.../dependecyinjection/`:
+`CommonModule`, `RemoteDataSourceModule`, `LocalDataSourceModule`, `RepositoryModule`, `UseCaseModule`, `ViewModelModule`.
+
+## Product Flavors
+
+Dimension: `management`
+
+| Flavor | Purpose |
+|--------|---------|
+| `original` | Standard ownCloud app |
+| `mdm` | Mobile Device Management — custom branding/policies |
+| `qa` | QA variant for automated testing |
+
+Build types: `debug` / `release`. Combined targets follow the pattern `assembleOriginalDebug`, `testMdmDebugUnitTest`, etc.
+
+## SDK & Toolchain
+
+- Min SDK 28 (Android 9), Target/Compile SDK 36
+- Kotlin 2.3.10, JDK 17, AGP 9.2.1, Gradle 9.7.1
+- Version catalog: `gradle/libs.versions.toml`
+
+## Build & Test Commands
+
+```bash
+# Build
+./gradlew assembleDebug
+./gradlew assembleRelease # requires signing env vars
+
+# Unit tests (all flavors)
+./gradlew testDebugUnitTest testMdmDebugUnitTest --continue
+
+# Single module unit tests
+./gradlew :owncloudApp:testOriginalDebugUnitTest
+./gradlew :owncloudData:testDebugUnitTest
+
+# Instrumented tests (requires running emulator/device)
+./gradlew :owncloudData:connectedAndroidTest
+
+# Lint & static analysis
+./gradlew detekt # maxIssues: 0 — any issue fails CI
+./gradlew lintDebug
+./gradlew ktlintCheck
+./gradlew ktlintFormat # auto-fix formatting
+```
+
+## Development Conventions
+
+- **Branching (OneFlow):** `feature/feature_name`, `fix/fix_name`, `improvement/improvement_name`, `technical/technical_description`, `chore/chore_description`
+- **Commit messages:** Conventional Commits format enforced by CI (`feat:`, `fix:`, `refactor:`, `test:`, `build:`, `chore:`)
+ - `feat:` introduces a new capability visible to the user or other modules (new screen, new API, new setting)
+ - `fix:` corrects a defect — something that was broken and is now working as intended
+ - `refactor:` restructuring existing code (rename, extract, move, simplify) with no change to observable behavior
+ - `test:` adds or updates tests only — no production code changes
+ - `build:` changes to dependencies (adding, removing, or upgrading libraries in `libs.versions.toml` or any `build.gradle`)
+ - `chore:` maintenance that has no user-facing effect (changelog entries, build config, tooling) — do not use for dependency changes
+- **Signed commits:** All commits **must** be GPG-signed with DCO sign-off: `git commit -s -S -m "type: description"`
+- **Rebase policy:** Always rebase; never create merge commits. Use `git pull --rebase` before pushing.
+- **Code style:** Detekt (zero-tolerance, `maxIssues: 0`), ktlint, EditorConfig (max line length: 150)
+- **PR process:** Rebase on the target branch before opening a PR. All CI checks must pass.
+
+## Changelog
+
+Every PR must include a file in `changelog/unreleased/` named after the PR number (e.g., `4936`):
+
+```
+Enhancement: Short title under 80 chars
+
+Description in present perfect passive tense.
+
+https://github.com/owncloud/android/issues/ ← optional, omit if there is no linked issue
+https://github.com/owncloud/android/pull/
+```
+
+Types: `Bugfix`, `Change`, `Enhancement`, `Security`. Calens reads these files to auto-generate `CHANGELOG.md` after merge.
+
+## Important Constraints
+
+- **No translation PRs** — submit translations to Transifex only.
+- **Detekt is zero-tolerance** (`maxIssues: 0`). Run `./gradlew detekt` locally before pushing.
+- **License migration in progress** (GPL-2.0 → Apache 2.0): do not introduce new copyleft dependencies without discussion.
+- **Security issues** go to security.owncloud.com, not GitHub issues.
+- Do not introduce new dependencies without discussion in an issue first.
+
+## OSPO Policy Constraints
+
+### GitHub Actions
+- **Only** use actions owned by `owncloud`, created by GitHub (`actions/*`), verified on the GitHub Marketplace, or verified by the ownCloud Maintainers.
+- Pin all actions to their full commit SHA (not tags): `uses: actions/checkout@ # vX.Y.Z`
+- Never introduce actions from unverified third parties.
+
+### Dependency Management
+- Dependabot is configured for automated dependency updates.
+- Review and merge Dependabot PRs as part of regular maintenance.
+- Do not introduce new dependencies without discussion in an issue first.
+
+## Context for AI Agents
+- Match existing code style
+- Do not refactor unrelated code in the same PR
+- Write tests for new functionality
+- Keep PRs focused and atomic
+- Follow the existing multi-module architecture (app/domain/data separation)
+- Use Kotlin idioms consistent with the existing codebase
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
deleted file mode 100644
index eb352ad22c5b..000000000000
--- a/AndroidManifest.xml
+++ /dev/null
@@ -1,236 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae3b330d4564..e902ae21ecbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,3849 @@
+# Table of Contents
+
+
+* [Changelog for 4.8.4](#changelog-for-owncloud-android-client-484-2026-08-31)
+* [Changelog for 4.8.3](#changelog-for-owncloud-android-client-483-2026-07-21)
+* [Changelog for 4.8.2](#changelog-for-owncloud-android-client-482-2026-07-01)
+* [Changelog for 4.8.1](#changelog-for-owncloud-android-client-481-2026-06-09)
+* [Changelog for 4.8.0](#changelog-for-owncloud-android-client-480-2026-05-18)
+* [Changelog for 4.7.0](#changelog-for-owncloud-android-client-470-2025-11-17)
+* [Changelog for 4.6.2](#changelog-for-owncloud-android-client-462-2025-08-13)
+* [Changelog for 4.6.1](#changelog-for-owncloud-android-client-461-2025-08-01)
+* [Changelog for 4.6.0](#changelog-for-owncloud-android-client-460-2025-07-22)
+* [Changelog for 4.5.1](#changelog-for-owncloud-android-client-451-2025-04-03)
+* [Changelog for 4.5.0](#changelog-for-owncloud-android-client-450-2025-03-24)
+* [Changelog for 4.4.1](#changelog-for-owncloud-android-client-441-2024-10-30)
+* [Changelog for 4.4.0](#changelog-for-owncloud-android-client-440-2024-09-30)
+* [Changelog for 4.3.1](#changelog-for-owncloud-android-client-431-2024-07-22)
+* [Changelog for 4.3.0](#changelog-for-owncloud-android-client-430-2024-07-01)
+* [Changelog for 4.2.2](#changelog-for-owncloud-android-client-422-2024-05-30)
+* [Changelog for 4.2.1](#changelog-for-owncloud-android-client-421-2024-02-22)
+* [Changelog for 4.2.0](#changelog-for-owncloud-android-client-420-2024-02-12)
+* [Changelog for 4.1.1](#changelog-for-owncloud-android-client-411-2023-10-18)
+* [Changelog for 4.1.0](#changelog-for-owncloud-android-client-410-2023-08-23)
+* [Changelog for 4.0.0](#changelog-for-owncloud-android-client-400-2023-05-29)
+* [Changelog for 3.0.4](#changelog-for-owncloud-android-client-304-2023-03-07)
+* [Changelog for 3.0.3](#changelog-for-owncloud-android-client-303-2023-02-13)
+* [Changelog for 3.0.2](#changelog-for-owncloud-android-client-302-2023-01-26)
+* [Changelog for 3.0.1](#changelog-for-owncloud-android-client-301-2022-12-21)
+* [Changelog for 3.0.0](#changelog-for-owncloud-android-client-300-2022-12-12)
+* [Changelog for 2.21.2](#changelog-for-owncloud-android-client-2212-2022-09-07)
+* [Changelog for 2.21.1](#changelog-for-owncloud-android-client-2211-2022-06-15)
+* [Changelog for 2.21.0](#changelog-for-owncloud-android-client-2210-2022-06-07)
+* [Changelog for 2.20.0](#changelog-for-owncloud-android-client-2200-2022-02-16)
+* [Changelog for 2.19.0](#changelog-for-owncloud-android-client-2190-2021-11-15)
+* [Changelog for 2.18.3](#changelog-for-owncloud-android-client-2183-2021-10-27)
+* [Changelog for 2.18.1](#changelog-for-owncloud-android-client-2181-2021-07-20)
+* [Changelog for 2.18.0](#changelog-for-owncloud-android-client-2180-2021-05-24)
+* [Changelog for 2.17 versions and below](#changelog-for-217-versions-and-below)
+
+# Changelog for ownCloud Android Client [4.8.4] (2026-08-31)
+
+The following sections list the changes in ownCloud Android Client 4.8.4 relevant to
+ownCloud admins and users.
+
+[4.8.4]: https://github.com/owncloud/android/compare/v4.8.3...v4.8.4
+
+## Summary
+
+* Security - Prevent bypassing passcode, pattern and biometric lock: [#4958](https://github.com/owncloud/android/issues/4958)
+
+## Details
+
+* Security - Prevent bypassing passcode, pattern and biometric lock: [#4958](https://github.com/owncloud/android/issues/4958)
+
+ The back gesture has been prevented from dismissing the lock screen without
+ validating the passcode, pattern or biometric lock.
+
+ https://github.com/owncloud/android/issues/4958
+ https://github.com/owncloud/android/pull/4963
+
+# Changelog for ownCloud Android Client [4.8.3] (2026-07-21)
+
+The following sections list the changes in ownCloud Android Client 4.8.3 relevant to
+ownCloud admins and users.
+
+[4.8.3]: https://github.com/owncloud/android/compare/v4.8.2...v4.8.3
+
+## Summary
+
+* Bugfix - Help URL incorrect: [#4931](https://github.com/owncloud/android/pull/4931)
+
+## Details
+
+* Bugfix - Help URL incorrect: [#4931](https://github.com/owncloud/android/pull/4931)
+
+ Help URL has been fixed because it returned "Page not found"
+
+ https://github.com/owncloud/android/pull/4931
+
+# Changelog for ownCloud Android Client [4.8.2] (2026-07-01)
+
+The following sections list the changes in ownCloud Android Client 4.8.2 relevant to
+ownCloud admins and users.
+
+[4.8.2]: https://github.com/owncloud/android/compare/v4.8.1...v4.8.2
+
+## Summary
+
+* Security - Redact cookie headers in logs: [#4897](https://github.com/owncloud/android/pull/4897)
+* Bugfix - Infinite edges: [#4576](https://github.com/owncloud/android/issues/4576)
+* Bugfix - Remove useless debug manifest file: [#4901](https://github.com/owncloud/android/pull/4901)
+
+## Details
+
+* Security - Redact cookie headers in logs: [#4897](https://github.com/owncloud/android/pull/4897)
+
+ Cookie headers have been redacted in the logs depending on the app
+ configuration, in order to prevent sensitive information leaks.
+
+ https://github.com/owncloud/android/pull/4897
+
+* Bugfix - Infinite edges: [#4576](https://github.com/owncloud/android/issues/4576)
+
+ All screens have been adapted to the edge-to-edge feature (starting from Android
+ 15) and are no longer cut off by the system and navigation bars
+
+ https://github.com/owncloud/android/issues/4576
+ https://github.com/owncloud/android/pull/4887
+
+* Bugfix - Remove useless debug manifest file: [#4901](https://github.com/owncloud/android/pull/4901)
+
+ Manifest file for debug builds has been removed because it was not used, in
+ order to fix a lint warning
+
+ https://github.com/owncloud/android/pull/4901
+
+# Changelog for ownCloud Android Client [4.8.1] (2026-06-09)
+
+The following sections list the changes in ownCloud Android Client 4.8.1 relevant to
+ownCloud admins and users.
+
+[4.8.1]: https://github.com/owncloud/android/compare/v4.8.0...v4.8.1
+
+## Summary
+
+* Security - SSL certificate verification and trusted host handling: [#4861](https://github.com/owncloud/android/pull/4861)
+* Bugfix - Crash from Google Play Console in FileDetailsFragment: [#4737](https://github.com/owncloud/android/issues/4737)
+* Bugfix - Crash from Google Play Console in ReceiveExternalFilesActivity: [#4738](https://github.com/owncloud/android/issues/4738)
+* Change - Bump target SDK to 36: [#4743](https://github.com/owncloud/android/issues/4743)
+* Change - Remove unicode characters from strings file: [#4872](https://github.com/owncloud/android/pull/4872)
+* Enhancement - Spaces icon color brandable: [#4860](https://github.com/owncloud/android/issues/4860)
+* Enhancement - Add gradle ecosystem to dependabot: [#4863](https://github.com/owncloud/android/pull/4863)
+* Enhancement - Remove unused resources: [#4869](https://github.com/owncloud/android/pull/4869)
+* Enhancement - Passcode and pattern screens in landscape mode: [#4871](https://github.com/owncloud/android/pull/4871)
+
+## Details
+
+* Security - SSL certificate verification and trusted host handling: [#4861](https://github.com/owncloud/android/pull/4861)
+
+ SSL certificate verification flow has been improved by enhancing host validation
+ and trusted certificate handling.
+
+ https://github.com/owncloud/android/pull/4861
+
+* Bugfix - Crash from Google Play Console in FileDetailsFragment: [#4737](https://github.com/owncloud/android/issues/4737)
+
+ To prevent crashes, the navigation logic when navigating back has been improved,
+ especially in cases where the observed file is null.
+
+ https://github.com/owncloud/android/issues/4737
+ https://github.com/owncloud/android/pull/4879
+
+* Bugfix - Crash from Google Play Console in ReceiveExternalFilesActivity: [#4738](https://github.com/owncloud/android/issues/4738)
+
+ In order to prevent crashes, a nullability check has been added before
+ refreshing a not found folder while receiving external files.
+
+ https://github.com/owncloud/android/issues/4738
+ https://github.com/owncloud/android/pull/4878
+
+* Change - Bump target SDK to 36: [#4743](https://github.com/owncloud/android/issues/4743)
+
+ Target SDK has been upgraded to 36 in order to fulfill Android platform
+ requirements.
+
+ https://github.com/owncloud/android/issues/4743
+ https://github.com/owncloud/android/pull/4862
+
+* Change - Remove unicode characters from strings file: [#4872](https://github.com/owncloud/android/pull/4872)
+
+ Unicode characters in strings.xml file have been replaced by the character
+ itself to prevent problems in translations
+
+ https://github.com/owncloud/android/pull/4872
+
+* Enhancement - Spaces icon color brandable: [#4860](https://github.com/owncloud/android/issues/4860)
+
+ The color of the generic space icon has been replaced with a brandable color to
+ provide a consistent design across branded variants.
+
+ https://github.com/owncloud/android/issues/4860
+ https://github.com/owncloud/android/pull/4877
+
+* Enhancement - Add gradle ecosystem to dependabot: [#4863](https://github.com/owncloud/android/pull/4863)
+
+ Dependabot workflow in GitHub Actions has been modified to update gradle
+ dependencies weekly
+
+ https://github.com/owncloud/android/pull/4863
+
+* Enhancement - Remove unused resources: [#4869](https://github.com/owncloud/android/pull/4869)
+
+ Useless strings and resources in the project have been removed in order to keep
+ the codebase cleaner
+
+ https://github.com/owncloud/android/pull/4869
+
+* Enhancement - Passcode and pattern screens in landscape mode: [#4871](https://github.com/owncloud/android/pull/4871)
+
+ Orientation restrictions defined in the manifest have been removed, and support
+ for both orientations (portrait and landscape) has been added to the passcode
+ and pattern screens.
+
+ https://github.com/owncloud/android/pull/4871
+
+# Changelog for ownCloud Android Client [4.8.0] (2026-05-18)
+
+The following sections list the changes in ownCloud Android Client 4.8.0 relevant to
+ownCloud admins and users.
+
+[4.8.0]: https://github.com/owncloud/android/compare/v4.7.0...v4.8.0
+
+## Summary
+
+* Bugfix - Option "Copy here" in spaces list after rotating device: [#4178](https://github.com/owncloud/android/issues/4178)
+* Bugfix - Reduce space header size after rotating device: [#4625](https://github.com/owncloud/android/issues/4625)
+* Bugfix - Edit space option available when there is no connection: [#4742](https://github.com/owncloud/android/issues/4742)
+* Bugfix - Sort is not alphabetically correct: [#4760](https://github.com/owncloud/android/issues/4760)
+* Bugfix - Conventional commits' action ignores SBOM automatic commits: [#4808](https://github.com/owncloud/android/pull/4808)
+* Change - Migrate tests to the new kotlinx-coroutines-test API: [#4710](https://github.com/owncloud/android/issues/4710)
+* Change - Increase rating dialog delay: [#4744](https://github.com/owncloud/android/pull/4744)
+* Change - Resource leak in CopyAndUploadContentUrisTask: [#4797](https://github.com/owncloud/android/issues/4797)
+* Change - Stream handling and double-close in UploadFileFromContentUriWorker: [#4798](https://github.com/owncloud/android/issues/4798)
+* Change - Update actions dependencies to be compliant with Node24 requirements: [#4804](https://github.com/owncloud/android/pull/4804)
+* Change - Actions with SHA commits instead of versions: [#4815](https://github.com/owncloud/android/pull/4815)
+* Change - Replace PATs for tokens generated by a GitHub app: [#4824](https://github.com/owncloud/android/pull/4824)
+* Change - Use reusable workflow for Calens: [#4825](https://github.com/owncloud/android/pull/4825)
+* Change - Use reusable workflow for Conventional commits: [#4832](https://github.com/owncloud/android/pull/4832)
+* Change - SBOM to be updated in a separate branch with signed commits: [#4837](https://github.com/owncloud/android/pull/4837)
+* Change - Weekly dependabot updates: [#4849](https://github.com/owncloud/android/pull/4849)
+* Enhancement - Show destination folder snackbar for copy/move operations: [#4379](https://github.com/owncloud/android/issues/4379)
+* Enhancement - Show members of a space: [#4612](https://github.com/owncloud/android/issues/4612)
+* Enhancement - Add a member to a space: [#4613](https://github.com/owncloud/android/issues/4613)
+* Enhancement - Set emoji as space image: [#4707](https://github.com/owncloud/android/issues/4707)
+* Enhancement - Edit a space member: [#4724](https://github.com/owncloud/android/issues/4724)
+* Enhancement - Remove a space member: [#4725](https://github.com/owncloud/android/issues/4725)
+* Enhancement - Workflow to build APK: [#4751](https://github.com/owncloud/android/pull/4751)
+* Enhancement - List links over a space: [#4752](https://github.com/owncloud/android/issues/4752)
+* Enhancement - Add a public link over a space: [#4753](https://github.com/owncloud/android/issues/4753)
+* Enhancement - Edit space link: [#4756](https://github.com/owncloud/android/issues/4756)
+* Enhancement - Remove space link: [#4757](https://github.com/owncloud/android/issues/4757)
+* Enhancement - Copy permanent link of a space: [#4758](https://github.com/owncloud/android/issues/4758)
+* Enhancement - Workflow to check Conventional Commits: [#4759](https://github.com/owncloud/android/pull/4759)
+* Enhancement - Pull to refresh members and links: [#4773](https://github.com/owncloud/android/issues/4773)
+* Enhancement - QA Content Provider: [#4776](https://github.com/owncloud/android/pull/4776)
+* Enhancement - Workflow to validate source strings: [#4807](https://github.com/owncloud/android/pull/4807)
+* Enhancement - Resource leaks in fragment view bindings: [#4813](https://github.com/owncloud/android/issues/4813)
+* Enhancement - Workflow to use the translation-sync reusable workflow: [#4818](https://github.com/owncloud/android/pull/4818)
+
+## Details
+
+* Bugfix - Option "Copy here" in spaces list after rotating device: [#4178](https://github.com/owncloud/android/issues/4178)
+
+ The "Copy here" button has been prevented from being displayed after device
+ rotation in the spaces list.
+
+ https://github.com/owncloud/android/issues/4178
+ https://github.com/owncloud/android/pull/4786
+
+* Bugfix - Reduce space header size after rotating device: [#4625](https://github.com/owncloud/android/issues/4625)
+
+ The space header height has been reduced in landscape orientation.
+
+ https://github.com/owncloud/android/issues/4625
+ https://github.com/owncloud/android/pull/4790
+
+* Bugfix - Edit space option available when there is no connection: [#4742](https://github.com/owncloud/android/issues/4742)
+
+ A new condition has been added to hide the edit space option when no network
+ connection is available.
+
+ https://github.com/owncloud/android/issues/4742
+ https://github.com/owncloud/android/pull/4750
+
+* Bugfix - Sort is not alphabetically correct: [#4760](https://github.com/owncloud/android/issues/4760)
+
+ Locale-aware collation has been applied to file sorting by name to handle
+ accented characters correctly.
+
+ https://github.com/owncloud/android/issues/4760
+ https://github.com/owncloud/android/pull/4785
+
+* Bugfix - Conventional commits' action ignores SBOM automatic commits: [#4808](https://github.com/owncloud/android/pull/4808)
+
+ Conventional commits' GitHub Action has been modified to ignore commits
+ generated by SBOM action because the leading capital.
+
+ https://github.com/owncloud/android/pull/4808
+
+* Change - Migrate tests to the new kotlinx-coroutines-test API: [#4710](https://github.com/owncloud/android/issues/4710)
+
+ Some tests from view model classes have been refactored in order to use the new
+ kotlinx-coroutines-test API.
+
+ https://github.com/owncloud/android/issues/4710
+ https://github.com/owncloud/android/pull/4722
+
+* Change - Increase rating dialog delay: [#4744](https://github.com/owncloud/android/pull/4744)
+
+ The time before the rating dialog appears has been increased to 7 days, in order
+ to make it less intrusive for the user.
+
+ https://github.com/owncloud/android/pull/4744
+
+* Change - Resource leak in CopyAndUploadContentUrisTask: [#4797](https://github.com/owncloud/android/issues/4797)
+
+ Input and output streams have been closed per URI iteration using
+ try-with-resources to prevent file descriptor and memory leaks when copying
+ multiple content URIs.
+
+ https://github.com/owncloud/android/issues/4797
+ https://github.com/owncloud/android/pull/4806
+
+* Change - Stream handling and double-close in UploadFileFromContentUriWorker: [#4798](https://github.com/owncloud/android/issues/4798)
+
+ Both the content resolver input stream and the file output stream from
+ UploadFileFromContentUriWorker have been closed via use blocks, and the
+ redundant double-close of the output stream has been removed.
+
+ https://github.com/owncloud/android/issues/4798
+ https://github.com/owncloud/android/pull/4799
+
+* Change - Update actions dependencies to be compliant with Node24 requirements: [#4804](https://github.com/owncloud/android/pull/4804)
+
+ Some dependencies and "uses" clauses in GitHub Actions workflows have been
+ updated to newer versions that support Node24
+
+ https://github.com/owncloud/android/pull/4804
+
+* Change - Actions with SHA commits instead of versions: [#4815](https://github.com/owncloud/android/pull/4815)
+
+ Commit SHA references have been added to GitHub Actions workflows to be
+ compliant with new security policies
+
+ https://github.com/owncloud/android/pull/4815
+
+* Change - Replace PATs for tokens generated by a GitHub app: [#4824](https://github.com/owncloud/android/pull/4824)
+
+ PATs have been replaced by tokens generated by a GitHub app in order to not
+ depend on personal setup
+
+ https://github.com/owncloud/android/pull/4824
+ https://github.com/owncloud/android/pull/4830
+
+* Change - Use reusable workflow for Calens: [#4825](https://github.com/owncloud/android/pull/4825)
+
+ A new reusable workflow for calens changelog changes has been added to the
+ current workflow in order to be used by all clients
+
+ https://github.com/owncloud/android/pull/4825
+ https://github.com/owncloud/android/pull/4826
+ https://github.com/owncloud/android/pull/4829
+ https://github.com/owncloud/android/pull/4853
+
+* Change - Use reusable workflow for Conventional commits: [#4832](https://github.com/owncloud/android/pull/4832)
+
+ A new reusable workflow to validate Conventional commits has been added to the
+ current workflow in order to be used by all clients
+
+ https://github.com/owncloud/android/pull/4832
+
+* Change - SBOM to be updated in a separate branch with signed commits: [#4837](https://github.com/owncloud/android/pull/4837)
+
+ SBOM workflow in GitHub Actions has been modified to accomplish security
+ policies, assuring that commits are verified and pushing them to a specific
+ branch
+
+ https://github.com/owncloud/android/pull/4837
+
+* Change - Weekly dependabot updates: [#4849](https://github.com/owncloud/android/pull/4849)
+
+ Dependabot workflow in GitHub Actions has been modified to be triggered weekly
+ with maximum 5 PRs to be created
+
+ https://github.com/owncloud/android/pull/4849
+
+* Enhancement - Show destination folder snackbar for copy/move operations: [#4379](https://github.com/owncloud/android/issues/4379)
+
+ A snackbar message has been displayed after copy or move operations with an
+ action button that allows users to quickly navigate to the destination folder.
+
+ https://github.com/owncloud/android/issues/4379
+ https://github.com/owncloud/android/pull/4802
+
+* Enhancement - Show members of a space: [#4612](https://github.com/owncloud/android/issues/4612)
+
+ A new option to view all members of a space has been added to the bottom sheet,
+ available when the three-dot menu button is tapped. This new option opens a new
+ view that shows information like member names, roles, and expiration dates (if
+ available).
+
+ https://github.com/owncloud/android/issues/4612
+ https://github.com/owncloud/android/issues/4763
+ https://github.com/owncloud/android/issues/4772
+ https://github.com/owncloud/android/issues/4782
+ https://github.com/owncloud/android/pull/4728
+ https://github.com/owncloud/android/pull/4765
+ https://github.com/owncloud/android/pull/4779
+ https://github.com/owncloud/android/pull/4784
+ https://github.com/owncloud/android/pull/4809
+ https://github.com/owncloud/android/pull/4841
+
+* Enhancement - Add a member to a space: [#4613](https://github.com/owncloud/android/issues/4613)
+
+ A new option to add members to a space has been added. It will be only visible
+ for users with proper permissions.
+
+ https://github.com/owncloud/android/issues/4613
+ https://github.com/owncloud/android/issues/4740
+ https://github.com/owncloud/android/issues/4770
+ https://github.com/owncloud/android/pull/4754
+ https://github.com/owncloud/android/pull/4764
+ https://github.com/owncloud/android/pull/4777
+ https://github.com/owncloud/android/pull/4847
+
+* Enhancement - Set emoji as space image: [#4707](https://github.com/owncloud/android/issues/4707)
+
+ A new option to set an emoji as space image has been added to the bottom sheet,
+ available only to users with the required permissions when the three-dot menu
+ button is tapped.
+
+ https://github.com/owncloud/android/issues/4707
+ https://github.com/owncloud/android/pull/4708
+
+* Enhancement - Edit a space member: [#4724](https://github.com/owncloud/android/issues/4724)
+
+ A new option to edit members from a space has been added. It will be only
+ visible for users with proper permissions.
+
+ https://github.com/owncloud/android/issues/4724
+ https://github.com/owncloud/android/pull/4774
+
+* Enhancement - Remove a space member: [#4725](https://github.com/owncloud/android/issues/4725)
+
+ A new option to remove members from a space has been added. It will be only
+ visible for users with proper permissions.
+
+ https://github.com/owncloud/android/issues/4725
+ https://github.com/owncloud/android/pull/4768
+
+* Enhancement - Workflow to build APK: [#4751](https://github.com/owncloud/android/pull/4751)
+
+ A new workflow to build unsigned APKs has been added, using variant and version
+ as inputs and generating the artifact and commit hash as outputs.
+
+ https://github.com/owncloud/android/pull/4751
+
+* Enhancement - List links over a space: [#4752](https://github.com/owncloud/android/issues/4752)
+
+ The list of all available space public links has been added below the list of
+ space members. This new section shows information like public link names, types
+ and expiration date (if available).
+
+ https://github.com/owncloud/android/issues/4752
+ https://github.com/owncloud/android/pull/4783
+ https://github.com/owncloud/android/pull/4834
+ https://github.com/owncloud/android/pull/4845
+
+* Enhancement - Add a public link over a space: [#4753](https://github.com/owncloud/android/issues/4753)
+
+ A new option to add public links over a space has been added. It will be only
+ visible for users with proper permissions.
+
+ https://github.com/owncloud/android/issues/4753
+ https://github.com/owncloud/android/pull/4794
+
+* Enhancement - Edit space link: [#4756](https://github.com/owncloud/android/issues/4756)
+
+ A new option to edit space public links has been added. It will be only visible
+ for users with proper permissions.
+
+ https://github.com/owncloud/android/issues/4756
+ https://github.com/owncloud/android/pull/4817
+
+* Enhancement - Remove space link: [#4757](https://github.com/owncloud/android/issues/4757)
+
+ A new option to remove space public links from a space has been added. It will
+ be only visible for users with proper permissions.
+
+ https://github.com/owncloud/android/issues/4757
+ https://github.com/owncloud/android/pull/4816
+
+* Enhancement - Copy permanent link of a space: [#4758](https://github.com/owncloud/android/issues/4758)
+
+ A new option to copy and share the permanent link of a space has been added next
+ to the space header in the members section.
+
+ https://github.com/owncloud/android/issues/4758
+ https://github.com/owncloud/android/pull/4778
+
+* Enhancement - Workflow to check Conventional Commits: [#4759](https://github.com/owncloud/android/pull/4759)
+
+ A new workflow has been added to check that commit names in a PR fits the
+ Conventional Commits convention.
+
+ https://github.com/owncloud/android/pull/4759
+
+* Enhancement - Pull to refresh members and links: [#4773](https://github.com/owncloud/android/issues/4773)
+
+ A pull to refresh has been added to the space members and links lists in order
+ to refresh the data and permissions with a simple gesture.
+
+ https://github.com/owncloud/android/issues/4773
+ https://github.com/owncloud/android/pull/4836
+
+* Enhancement - QA Content Provider: [#4776](https://github.com/owncloud/android/pull/4776)
+
+ A new content provider has been added to the QA variant in order to work with
+ the sharing preferences with testing purposes.
+
+ https://github.com/owncloud/android/pull/4776
+
+* Enhancement - Workflow to validate source strings: [#4807](https://github.com/owncloud/android/pull/4807)
+
+ A new workflow has been added to validate the source strings and prevent errors
+ in translation CI system from the first step
+
+ https://github.com/owncloud/android/pull/4807
+
+* Enhancement - Resource leaks in fragment view bindings: [#4813](https://github.com/owncloud/android/issues/4813)
+
+ View binding references have been cleared in onDestroyView() across fragments to
+ prevent memory leaks when fragment instances outlive their views.
+
+ https://github.com/owncloud/android/issues/4813
+ https://github.com/owncloud/android/pull/4814
+
+* Enhancement - Workflow to use the translation-sync reusable workflow: [#4818](https://github.com/owncloud/android/pull/4818)
+
+ A new workflow that uses the translation-sync reusable workflow has been added
+ in order to update translations from transifex
+
+ https://github.com/owncloud/android/pull/4818
+ https://github.com/owncloud/android/pull/4831
+ https://github.com/owncloud/android/pull/4846
+
+# Changelog for ownCloud Android Client [4.7.0] (2025-11-17)
+
+The following sections list the changes in ownCloud Android Client 4.7.0 relevant to
+ownCloud admins and users.
+
+[4.7.0]: https://github.com/owncloud/android/compare/v4.6.2...v4.7.0
+
+## Summary
+
+* Bugfix - Handling of HTTP code response 413: [#4094](https://github.com/owncloud/android/issues/4094)
+* Bugfix - Set authorization header for token request only when necessary: [#4575](https://github.com/owncloud/android/issues/4575)
+* Bugfix - Add explicit permission to Detekt workflow: [#4675](https://github.com/owncloud/android/pull/4675)
+* Change - SBOM workflow runs on dependabot PRs: [#4664](https://github.com/owncloud/android/pull/4664)
+* Enhancement - New layout for spaces list: [#4604](https://github.com/owncloud/android/issues/4604)
+* Enhancement - Add account ID to the user information: [#4605](https://github.com/owncloud/android/issues/4605)
+* Enhancement - Create a new space: [#4606](https://github.com/owncloud/android/issues/4606)
+* Enhancement - Edit a space: [#4607](https://github.com/owncloud/android/issues/4607)
+* Enhancement - Disable/Remove a space: [#4611](https://github.com/owncloud/android/issues/4611)
+* Enhancement - Update space image: [#4691](https://github.com/owncloud/android/issues/4691)
+* Enhancement - Show space quota: [#4693](https://github.com/owncloud/android/issues/4693)
+* Enhancement - Add user role to spaces: [#4698](https://github.com/owncloud/android/pull/4698)
+* Enhancement - New setting to show disabled spaces: [#4711](https://github.com/owncloud/android/issues/4711)
+* Enhancement - Cancellation policy for CI workflows: [#4712](https://github.com/owncloud/android/pull/4712)
+
+## Details
+
+* Bugfix - Handling of HTTP code response 413: [#4094](https://github.com/owncloud/android/issues/4094)
+
+ A new exception has been added to handle cases where the payload exceeds the
+ server limit (HTTP code 413)
+
+ https://github.com/owncloud/android/issues/4094
+ https://github.com/owncloud/android/pull/4685
+
+* Bugfix - Set authorization header for token request only when necessary: [#4575](https://github.com/owncloud/android/issues/4575)
+
+ A new field has been added to the token request to indicate whether the
+ authorization header should be applied in case `client_secret_post` method is
+ supported or not.
+
+ https://github.com/owncloud/android/issues/4575
+ https://github.com/owncloud/android/pull/4671
+
+* Bugfix - Add explicit permission to Detekt workflow: [#4675](https://github.com/owncloud/android/pull/4675)
+
+ Read permission has been added to the Detekt workflow. Without this explicit
+ setting, the default permissions would apply, which unnecessarily include write
+ access.
+
+ https://github.com/owncloud/android/pull/4675
+
+* Change - SBOM workflow runs on dependabot PRs: [#4664](https://github.com/owncloud/android/pull/4664)
+
+ Changes to dependency versions will be managed within Dependabot PRs, so they
+ can be updated together.
+
+ https://github.com/owncloud/android/pull/4664
+ https://github.com/owncloud/android/pull/4666
+
+* Enhancement - New layout for spaces list: [#4604](https://github.com/owncloud/android/issues/4604)
+
+ A three-dot button has been added to each space card, allowing a bottom sheet
+ dialog to be opened when clicked. In addition, the spaces layout has been
+ changed to horizontal.
+
+ https://github.com/owncloud/android/issues/4604
+ https://github.com/owncloud/android/pull/4665
+
+* Enhancement - Add account ID to the user information: [#4605](https://github.com/owncloud/android/issues/4605)
+
+ The account ID has been added to the Account Manager only for Infinite Scale
+ users. This information will be used to fetch all permissions related to space
+ management.
+
+ https://github.com/owncloud/android/issues/4605
+ https://github.com/owncloud/android/pull/4661
+
+* Enhancement - Create a new space: [#4606](https://github.com/owncloud/android/issues/4606)
+
+ A new floating action button has been added to the spaces list, visible only to
+ users with right permissions. In addition, a new dialog for space creation has
+ been implemented and, it is shown when the floating action button is tapped.
+
+ https://github.com/owncloud/android/issues/4606
+ https://github.com/owncloud/android/issues/4688
+ https://github.com/owncloud/android/pull/4683
+ https://github.com/owncloud/android/pull/4694
+
+* Enhancement - Edit a space: [#4607](https://github.com/owncloud/android/issues/4607)
+
+ A new edit space option has been added to the bottom sheet, available only to
+ users with the required permissions when the three-dot menu button is tapped.
+
+ https://github.com/owncloud/android/issues/4607
+ https://github.com/owncloud/android/issues/4688
+ https://github.com/owncloud/android/issues/4695
+ https://github.com/owncloud/android/pull/4687
+ https://github.com/owncloud/android/pull/4694
+ https://github.com/owncloud/android/pull/4713
+
+* Enhancement - Disable/Remove a space: [#4611](https://github.com/owncloud/android/issues/4611)
+
+ New options to disable, enable and remove spaces have been added to the bottom
+ sheet, available only to users with the required permissions when the three-dot
+ menu button is tapped.
+
+ https://github.com/owncloud/android/issues/4611
+ https://github.com/owncloud/android/pull/4696
+
+* Enhancement - Update space image: [#4691](https://github.com/owncloud/android/issues/4691)
+
+ A new option to update the space image has been added to the bottom sheet,
+ available only to users with the required permissions when the three-dot menu
+ button is tapped.
+
+ https://github.com/owncloud/android/issues/4691
+ https://github.com/owncloud/android/pull/4705
+
+* Enhancement - Show space quota: [#4693](https://github.com/owncloud/android/issues/4693)
+
+ The used and total values of the space quota have been added to the bottom sheet
+ dialog, displayed when the three-dot menu button is tapped.
+
+ https://github.com/owncloud/android/issues/4693
+ https://github.com/owncloud/android/pull/4704
+
+* Enhancement - Add user role to spaces: [#4698](https://github.com/owncloud/android/pull/4698)
+
+ A new field representing the user role has been added to the space model and to
+ the spaces table in the database.
+
+ https://github.com/owncloud/android/pull/4698
+
+* Enhancement - New setting to show disabled spaces: [#4711](https://github.com/owncloud/android/issues/4711)
+
+ A new setting has been added to the advanced section that allows space managers
+ to show or hide disabled spaces in the spaces list.
+
+ https://github.com/owncloud/android/issues/4711
+ https://github.com/owncloud/android/pull/4714
+
+* Enhancement - Cancellation policy for CI workflows: [#4712](https://github.com/owncloud/android/pull/4712)
+
+ A new cancellation policy has been set to CI workflows. Only one concurrent
+ execution of every workflow in every branch.
+
+ https://github.com/owncloud/android/pull/4712
+
+# Changelog for ownCloud Android Client [4.6.2] (2025-08-13)
+
+The following sections list the changes in ownCloud Android Client 4.6.2 relevant to
+ownCloud admins and users.
+
+[4.6.2]: https://github.com/owncloud/android/compare/v4.6.1...v4.6.2
+
+## Summary
+
+* Bugfix - Removed files in failed uploads: [#4653](https://github.com/owncloud/android/issues/4653)
+
+## Details
+
+* Bugfix - Removed files in failed uploads: [#4653](https://github.com/owncloud/android/issues/4653)
+
+ Files have been prevented from being deleted from the source folder on upload
+ retries by using the createdBy property.
+
+ https://github.com/owncloud/android/issues/4653
+ https://github.com/owncloud/android/pull/4657
+
+# Changelog for ownCloud Android Client [4.6.1] (2025-08-01)
+
+The following sections list the changes in ownCloud Android Client 4.6.1 relevant to
+ownCloud admins and users.
+
+[4.6.1]: https://github.com/owncloud/android/compare/v4.6.0...v4.6.1
+
+## Summary
+
+* Bugfix - Crash from Google Play Console in TransferListFragment: [#4650](https://github.com/owncloud/android/issues/4650)
+* Bugfix - Mini-fab for creating new document not always available: [#7277](https://github.com/owncloud/enterprise/issues/7277)
+* Bugfix - Re-login not working properly when credentials expire: [#7277](https://github.com/owncloud/enterprise/issues/7277)
+
+## Details
+
+* Bugfix - Crash from Google Play Console in TransferListFragment: [#4650](https://github.com/owncloud/android/issues/4650)
+
+ In order to prevent app crashes, the account parameter from TransferListFragment
+ has been removed and the newInstance method has been created to pass parameters
+ safely.
+
+ https://github.com/owncloud/android/issues/4650
+ https://github.com/owncloud/android/pull/4652
+
+* Bugfix - Mini-fab for creating new document not always available: [#7277](https://github.com/owncloud/enterprise/issues/7277)
+
+ The productName property has been made nullable to handle cases where it is not
+ present in the JSON response, allowing the mini-fab for creating new documents
+ to be properly shown.
+
+ https://github.com/owncloud/enterprise/issues/7277
+ https://github.com/owncloud/android/pull/4649
+
+* Bugfix - Re-login not working properly when credentials expire: [#7277](https://github.com/owncloud/enterprise/issues/7277)
+
+ The startActivityForResult method has been replaced with startActivity to make
+ re-login work properly when credentials expire.
+
+ https://github.com/owncloud/enterprise/issues/7277
+ https://github.com/owncloud/android/pull/4654
+
+# Changelog for ownCloud Android Client [4.6.0] (2025-07-22)
+
+The following sections list the changes in ownCloud Android Client 4.6.0 relevant to
+ownCloud admins and users.
+
+[4.6.0]: https://github.com/owncloud/android/compare/v4.5.1...v4.6.0
+
+## Summary
+
+* Bugfix - Changes in the automatic uploads algorithm to prevent duplications: [#3983](https://github.com/owncloud/android/issues/3983)
+* Bugfix - Token request with Bearer returns error: [#4080](https://github.com/owncloud/android/issues/4080)
+* Bugfix - Side menu collapses info in landscape: [#4513](https://github.com/owncloud/android/issues/4513)
+* Bugfix - Content in Spaces not shown from third-party apps: [#4522](https://github.com/owncloud/android/issues/4522)
+* Bugfix - Add bottom margin for used quota in account dialog: [#4566](https://github.com/owncloud/android/issues/4566)
+* Bugfix - Infinite edges in Android 15: [#4576](https://github.com/owncloud/android/issues/4576)
+* Bugfix - Crash from Google Play Console in PreviewImageFragment: [#4577](https://github.com/owncloud/android/issues/4577)
+* Bugfix - No message when uploading a file with no quota: [#4582](https://github.com/owncloud/android/issues/4582)
+* Bugfix - Crash from Google Play Console in PreviewImagePagerAdapter: [#4596](https://github.com/owncloud/android/issues/4596)
+* Bugfix - Change space icon in file details view for personal space: [#4624](https://github.com/owncloud/android/issues/4624)
+* Change - Bump target SDK to 35: [#4529](https://github.com/owncloud/android/issues/4529)
+* Change - Replace dav4android location: [#4536](https://github.com/owncloud/android/issues/4536)
+* Change - Modify biometrics fail source string: [#4572](https://github.com/owncloud/android/issues/4572)
+* Change - Update CI badges in README file: [#4623](https://github.com/owncloud/android/issues/4623)
+* Enhancement - QA variant: [#3791](https://github.com/owncloud/android/issues/3791)
+* Enhancement - Shares space in Android native file explorer: [#4515](https://github.com/owncloud/android/issues/4515)
+* Enhancement - Accessibility reports in 4.5.1: [#4568](https://github.com/owncloud/android/issues/4568)
+* Enhancement - Support for Kiteworks servers without client secret: [#4588](https://github.com/owncloud/android/issues/4588)
+* Enhancement - Polish UI and sync operations over Kiteworks servers: [#4591](https://github.com/owncloud/android/issues/4591)
+* Enhancement - Integration of instrumented tests in GitHub Actions: [#4595](https://github.com/owncloud/android/issues/4595)
+* Enhancement - SBOM (Software Bill of Materials): [#4598](https://github.com/owncloud/android/issues/4598)
+* Enhancement - New set of configurations for Kiteworks servers: [#4622](https://github.com/owncloud/android/issues/4622)
+
+## Details
+
+* Bugfix - Changes in the automatic uploads algorithm to prevent duplications: [#3983](https://github.com/owncloud/android/issues/3983)
+
+ The timestamp for automatic uploads is now updated at the beginning of the
+ upload process instead of at the end. Additionally, the filter used in
+ AutomaticUploadsWorker to select the files to upload has been modified in order
+ to reduce time and charge when evaluating all files.
+
+ https://github.com/owncloud/android/issues/3983
+ https://github.com/owncloud/android/pull/4571
+
+* Bugfix - Token request with Bearer returns error: [#4080](https://github.com/owncloud/android/issues/4080)
+
+ A new condition has been added into the network client to check if the network
+ request comes from TokenRequestRemoteOperation before setting the authorization
+ header. This allows users to have more than one logged-in account on the same
+ server.
+
+ https://github.com/owncloud/android/issues/4080
+ https://github.com/owncloud/android/pull/4586
+
+* Bugfix - Side menu collapses info in landscape: [#4513](https://github.com/owncloud/android/issues/4513)
+
+ Two empty and visual items have been added to prevent the drawer from collapsing
+ in landscape mode.
+
+ https://github.com/owncloud/android/issues/4513
+ https://github.com/owncloud/android/pull/4580
+
+* Bugfix - Content in Spaces not shown from third-party apps: [#4522](https://github.com/owncloud/android/issues/4522)
+
+ The root of the spaces has been synchronized before displaying the file list
+ when a file is shared from a third-party app.
+
+ https://github.com/owncloud/android/issues/4522
+ https://github.com/owncloud/android/pull/4574
+
+* Bugfix - Add bottom margin for used quota in account dialog: [#4566](https://github.com/owncloud/android/issues/4566)
+
+ Added bottom margin to the container holding used quota view when multi account
+ is disabled
+
+ https://github.com/owncloud/android/issues/4566
+ https://github.com/owncloud/android/pull/4567
+
+* Bugfix - Infinite edges in Android 15: [#4576](https://github.com/owncloud/android/issues/4576)
+
+ Infinite edges feature, enabled by default starting from Android 15, has been
+ disabled in the app.
+
+ https://github.com/owncloud/android/issues/4576
+ https://github.com/owncloud/android/pull/4581
+
+* Bugfix - Crash from Google Play Console in PreviewImageFragment: [#4577](https://github.com/owncloud/android/issues/4577)
+
+ In order to prevent app crashes when file variable is null, a nullability check
+ has been added in onPrepareOptionsMenu method from PreviewImageFragment
+
+ https://github.com/owncloud/android/issues/4577
+ https://github.com/owncloud/android/pull/4594
+
+* Bugfix - No message when uploading a file with no quota: [#4582](https://github.com/owncloud/android/issues/4582)
+
+ A message has been added in the file list when uploading a file (from file
+ system, camera or shortcut) without available quota
+
+ https://github.com/owncloud/android/issues/4582
+ https://github.com/owncloud/android/pull/4587
+
+* Bugfix - Crash from Google Play Console in PreviewImagePagerAdapter: [#4596](https://github.com/owncloud/android/issues/4596)
+
+ In order to prevent app crashes, a validation has been added in onPageSelected
+ method from PreviewImageActivity to ensure the image list contains items before
+ using it.
+
+ https://github.com/owncloud/android/issues/4596
+ https://github.com/owncloud/android/pull/4600
+
+* Bugfix - Change space icon in file details view for personal space: [#4624](https://github.com/owncloud/android/issues/4624)
+
+ The space icon in the file details view has been replaced by the folder icon
+ when the space that contains the file is personal.
+
+ https://github.com/owncloud/android/issues/4624
+ https://github.com/owncloud/android/pull/4640
+
+* Change - Bump target SDK to 35: [#4529](https://github.com/owncloud/android/issues/4529)
+
+ Target SDK has been upgraded to 35 in order to fulfill Android platform
+ requirements.
+
+ https://github.com/owncloud/android/issues/4529
+ https://github.com/owncloud/android/pull/4556
+
+* Change - Replace dav4android location: [#4536](https://github.com/owncloud/android/issues/4536)
+
+ Dav4android location has been moved from GitLab to GitHub.
+
+ https://github.com/owncloud/android/issues/4536
+ https://github.com/owncloud/android/pull/4558
+
+* Change - Modify biometrics fail source string: [#4572](https://github.com/owncloud/android/issues/4572)
+
+ The string that appears when biometric unlocking is not available has been
+ changed in order to make it clearer.
+
+ https://github.com/owncloud/android/issues/4572
+ https://github.com/owncloud/android/pull/4578
+
+* Change - Update CI badges in README file: [#4623](https://github.com/owncloud/android/issues/4623)
+
+ Bitrise badges in README file have been replaced by GitHub Actions badges.
+
+ https://github.com/owncloud/android/issues/4623
+ https://github.com/owncloud/android/pull/4637
+
+* Enhancement - QA variant: [#3791](https://github.com/owncloud/android/issues/3791)
+
+ A new flavor for QA has been created in order to make automatic tests easier.
+
+ https://github.com/owncloud/android/issues/3791
+ https://github.com/owncloud/android/pull/4569
+
+* Enhancement - Shares space in Android native file explorer: [#4515](https://github.com/owncloud/android/issues/4515)
+
+ The Shares space has been added to the spaces list shown in the Documents
+ Provider, the Android native file explorer.
+
+ https://github.com/owncloud/android/issues/4515
+ https://github.com/owncloud/android/pull/4579
+
+* Enhancement - Accessibility reports in 4.5.1: [#4568](https://github.com/owncloud/android/issues/4568)
+
+ Some content descriptions that were missing have been added to provide a better
+ accessibility experience.
+
+ https://github.com/owncloud/android/issues/4568
+ https://github.com/owncloud/android/pull/4573
+
+* Enhancement - Support for Kiteworks servers without client secret: [#4588](https://github.com/owncloud/android/issues/4588)
+
+ Support for connecting to Kiteworks servers without requiring client secret has
+ been added to the app.
+
+ https://github.com/owncloud/android/issues/4588
+ https://github.com/owncloud/android/pull/4589
+
+* Enhancement - Polish UI and sync operations over Kiteworks servers: [#4591](https://github.com/owncloud/android/issues/4591)
+
+ The UI and navigation behaviour after performing a sync operation have been
+ refined for accounts associated with Kiteworks servers.
+
+ https://github.com/owncloud/android/issues/4591
+ https://github.com/owncloud/android/pull/4608
+
+* Enhancement - Integration of instrumented tests in GitHub Actions: [#4595](https://github.com/owncloud/android/issues/4595)
+
+ A new workflow has been added to run instrumented tests in GitHub Actions in
+ order to have a more consistent CI pipeline in the project.
+
+ https://github.com/owncloud/android/issues/4595
+ https://github.com/owncloud/android/pull/4602
+
+* Enhancement - SBOM (Software Bill of Materials): [#4598](https://github.com/owncloud/android/issues/4598)
+
+ SBOM to be generated via GitHub Actions with the list of all dependencies used
+ in the code, powered by cyclonedx. Finally, it is pushed to the repo's root
+ folder if changes are detected.
+
+ https://github.com/owncloud/android/issues/4598
+ https://github.com/owncloud/android/pull/4641
+
+* Enhancement - New set of configurations for Kiteworks servers: [#4622](https://github.com/owncloud/android/issues/4622)
+
+ A new set of configurations in setup file has been added to support the
+ connection to Kiteworks servers.
+
+ https://github.com/owncloud/android/issues/4622
+ https://github.com/owncloud/android/pull/4632
+
+# Changelog for ownCloud Android Client [4.5.1] (2025-04-03)
+
+The following sections list the changes in ownCloud Android Client 4.5.1 relevant to
+ownCloud admins and users.
+
+[4.5.1]: https://github.com/owncloud/android/compare/v4.5.0...v4.5.1
+
+## Summary
+
+* Bugfix - Confusing behaviour when creating new files using apps provider: [#4560](https://github.com/owncloud/android/issues/4560)
+* Bugfix - App crashes at start when biometrics fail: [#7134](https://github.com/owncloud/enterprise/issues/7134)
+
+## Details
+
+* Bugfix - Confusing behaviour when creating new files using apps provider: [#4560](https://github.com/owncloud/android/issues/4560)
+
+ The error that appeared when creating a new file using the apps provider has
+ been fixed. Now, the custom tab is opened correctly with the file content.
+
+ https://github.com/owncloud/android/issues/4560
+ https://github.com/owncloud/android/pull/4562
+
+* Bugfix - App crashes at start when biometrics fail: [#7134](https://github.com/owncloud/enterprise/issues/7134)
+
+ The crash that happened when biometrics failed due to a system error has been
+ handled. In this case, an error is shown and pattern or passcode unlock are used
+ instead of biometrics.
+
+ https://github.com/owncloud/enterprise/issues/7134
+ https://github.com/owncloud/android/pull/4564
+
+# Changelog for ownCloud Android Client [4.5.0] (2025-03-24)
+
+The following sections list the changes in ownCloud Android Client 4.5.0 relevant to
+ownCloud admins and users.
+
+[4.5.0]: https://github.com/owncloud/android/compare/v4.4.1...v4.5.0
+
+## Summary
+
+* Bugfix - Crash from Google Play Store: [#4333](https://github.com/owncloud/android/issues/4333)
+* Bugfix - Navigation in automatic uploads folder picker: [#4340](https://github.com/owncloud/android/issues/4340)
+* Bugfix - Downloading non-previewable files in details view leads to empty list: [#4428](https://github.com/owncloud/android/issues/4428)
+* Bugfix - Ensure folder size updates automatically after file replacement: [#4505](https://github.com/owncloud/android/issues/4505)
+* Change - Replace auto-uploads with automatic uploads: [#4252](https://github.com/owncloud/android/issues/4252)
+* Change - Removed survey and chat from feedback: [#4540](https://github.com/owncloud/android/issues/4540)
+* Enhancement - Unit tests for repository classes - Part 2: [#4233](https://github.com/owncloud/android/issues/4233)
+* Enhancement - Unit tests for repository classes - Part 3: [#4234](https://github.com/owncloud/android/issues/4234)
+* Enhancement - Unit tests for repository classes - Part 4: [#4235](https://github.com/owncloud/android/issues/4235)
+* Enhancement - Add status message when (un)setting av. offline from preview: [#4382](https://github.com/owncloud/android/issues/4382)
+* Enhancement - Quota improvements from GraphAPI: [#4411](https://github.com/owncloud/android/issues/4411)
+* Enhancement - Upgraded AGP version to 8.7.2: [#4478](https://github.com/owncloud/android/issues/4478)
+* Enhancement - Added text labels for BottomNavigationView: [#4484](https://github.com/owncloud/android/issues/4484)
+* Enhancement - OCIS Light Users: [#4490](https://github.com/owncloud/android/issues/4490)
+* Enhancement - Enforce OIDC auth flow via branding: [#4500](https://github.com/owncloud/android/issues/4500)
+* Enhancement - Detekt: static code analyzer: [#4506](https://github.com/owncloud/android/issues/4506)
+* Enhancement - Multi-Personal (1st round): [#4514](https://github.com/owncloud/android/issues/4514)
+* Enhancement - Technical improvements for user quota: [#4521](https://github.com/owncloud/android/issues/4521)
+
+## Details
+
+* Bugfix - Crash from Google Play Store: [#4333](https://github.com/owncloud/android/issues/4333)
+
+ The androidx-appcompat version has been upgraded from 1.5.1 to 1.6.1 in order to
+ fix one crash reported by Play Console which is related to the
+ FileDataStorageManager constructor
+
+ https://github.com/owncloud/android/issues/4333
+ https://github.com/owncloud/android/pull/4542
+
+* Bugfix - Navigation in automatic uploads folder picker: [#4340](https://github.com/owncloud/android/issues/4340)
+
+ The button in the toolbar for going up when choosing an upload path has been
+ added when needed, since there were some cases in which it didn't appear.
+
+ https://github.com/owncloud/android/issues/4340
+ https://github.com/owncloud/android/pull/4535
+
+* Bugfix - Downloading non-previewable files in details view leads to empty list: [#4428](https://github.com/owncloud/android/issues/4428)
+
+ The error that led to an empty file list after downloading a file in details
+ view, due to the bottom sheet "Open with", has been fixed.
+
+ https://github.com/owncloud/android/issues/4428
+ https://github.com/owncloud/android/pull/4548
+
+* Bugfix - Ensure folder size updates automatically after file replacement: [#4505](https://github.com/owncloud/android/issues/4505)
+
+ The folder size has been updated automatically after replacing a file during a
+ move operation, eliminating the need for a manual refresh.
+
+ https://github.com/owncloud/android/issues/4505
+ https://github.com/owncloud/android/pull/4553
+
+* Change - Replace auto-uploads with automatic uploads: [#4252](https://github.com/owncloud/android/issues/4252)
+
+ Wording change in the feature name, in order to make it clearer in translations
+ and documentation
+
+ https://github.com/owncloud/android/issues/4252
+ https://github.com/owncloud/android/pull/4492
+
+* Change - Removed survey and chat from feedback: [#4540](https://github.com/owncloud/android/issues/4540)
+
+ Survey and chat have been removed from the feedback dialog due to they are not
+ maintained anymore or they have low traffic.
+
+ https://github.com/owncloud/android/issues/4540
+ https://github.com/owncloud/android/pull/4549
+
+* Enhancement - Unit tests for repository classes - Part 2: [#4233](https://github.com/owncloud/android/issues/4233)
+
+ Unit tests for OCFileRepository class have been completed.
+
+ https://github.com/owncloud/android/issues/4233
+ https://github.com/owncloud/android/pull/4389
+
+* Enhancement - Unit tests for repository classes - Part 3: [#4234](https://github.com/owncloud/android/issues/4234)
+
+ Unit tests for OCFolderBackupRepository, OCOAuthRepository,
+ OCServerInfoRepository, OCShareeRepository, OCShareRepository classes have been
+ completed.
+
+ https://github.com/owncloud/android/issues/4234
+ https://github.com/owncloud/android/pull/4523
+
+* Enhancement - Unit tests for repository classes - Part 4: [#4235](https://github.com/owncloud/android/issues/4235)
+
+ Unit tests for OCSpacesRepository, OCTransferRepository, OCUserRepository and
+ OCWebFingerRepository classes have been completed.
+
+ https://github.com/owncloud/android/issues/4235
+ https://github.com/owncloud/android/pull/4537
+
+* Enhancement - Add status message when (un)setting av. offline from preview: [#4382](https://github.com/owncloud/android/issues/4382)
+
+ A message has been added in all previews when the (un)setting av. offline
+ buttons are clicked. The options menu has been updated in all previews depending
+ on the file status.
+
+ https://github.com/owncloud/android/issues/4382
+ https://github.com/owncloud/android/pull/4482
+
+* Enhancement - Quota improvements from GraphAPI: [#4411](https://github.com/owncloud/android/issues/4411)
+
+ The quota in the drawer has been updated depending on its status and also when a
+ file is removed, copied, moved and after a refresh operation. In addition, the
+ quota value for each account has been added in the manage accounts dialog.
+
+ https://github.com/owncloud/android/issues/4411
+ https://github.com/owncloud/android/pull/4496
+
+* Enhancement - Upgraded AGP version to 8.7.2: [#4478](https://github.com/owncloud/android/issues/4478)
+
+ The Android Gradle Plugin version has been upgraded to 8.7.2, together with
+ Gradle version (updated to 8.9) and JDK version (updated to JBR 17).
+
+ https://github.com/owncloud/android/issues/4478
+ https://github.com/owncloud/android/pull/4507
+
+* Enhancement - Added text labels for BottomNavigationView: [#4484](https://github.com/owncloud/android/issues/4484)
+
+ Text labels have been added below the icons, and the active indicator feature is
+ implemented using the default itemActiveIndicatorStyle for better navigation
+ experience.
+
+ https://github.com/owncloud/android/issues/4484
+ https://github.com/owncloud/android/pull/4498
+
+* Enhancement - OCIS Light Users: [#4490](https://github.com/owncloud/android/issues/4490)
+
+ OCIS light users (users without personal space) are now supported in the app
+
+ https://github.com/owncloud/android/issues/4490
+ https://github.com/owncloud/android/pull/4518
+
+* Enhancement - Enforce OIDC auth flow via branding: [#4500](https://github.com/owncloud/android/issues/4500)
+
+ A new branded parameter `enforce_oidc` has been added to enforce the app to
+ follow the OIDC auth flow, and `clientId` and `clientSecret` are sent in token
+ requests when required by server. Moreover, the app now supports branded
+ redirect URIs with path due to the new branded parameter
+ `oauth2_redirect_uri_path` (legacy `oauth2_redirect_uri_path` is now
+ `oauth2_redirect_uri_host`).
+
+ https://github.com/owncloud/android/issues/4500
+ https://github.com/owncloud/android/pull/4516
+
+* Enhancement - Detekt: static code analyzer: [#4506](https://github.com/owncloud/android/issues/4506)
+
+ The Kotlin static code analyzer Detekt has been introduced with the agreed
+ rules, and the left code smells have been fixed throughout the whole code.
+
+ https://github.com/owncloud/android/issues/4506
+ https://github.com/owncloud/android/pull/4487
+
+* Enhancement - Multi-Personal (1st round): [#4514](https://github.com/owncloud/android/issues/4514)
+
+ Support for multi-personal accounts has been added. This first approach displays
+ all personal spaces in the Spaces tab, not showing project spaces. In addition,
+ the Personal tab shows an empty view since there is not a single personal space.
+
+ https://github.com/owncloud/android/issues/4514
+ https://github.com/owncloud/android/pull/4527/files
+
+* Enhancement - Technical improvements for user quota: [#4521](https://github.com/owncloud/android/issues/4521)
+
+ A new use case has been added to fetch the user quota as a flow. Also, all
+ unnecessary calls from DrawerActivity have been removed.
+
+ https://github.com/owncloud/android/issues/4521
+ https://github.com/owncloud/android/pull/4525
+
+# Changelog for ownCloud Android Client [4.4.1] (2024-10-30)
+
+The following sections list the changes in ownCloud Android Client 4.4.1 relevant to
+ownCloud admins and users.
+
+[4.4.1]: https://github.com/owncloud/android/compare/v4.4.0...v4.4.1
+
+## Summary
+
+* Bugfix - File size becomes 0 after a local update: [#4495](https://github.com/owncloud/android/issues/4495)
+
+## Details
+
+* Bugfix - File size becomes 0 after a local update: [#4495](https://github.com/owncloud/android/issues/4495)
+
+ The local copy of a file is not removed after a local update anymore. Therefore,
+ the file size has been fixed.
+
+ https://github.com/owncloud/android/issues/4495
+ https://github.com/owncloud/android/pull/4502
+
+# Changelog for ownCloud Android Client [4.4.0] (2024-09-30)
+
+The following sections list the changes in ownCloud Android Client 4.4.0 relevant to
+ownCloud admins and users.
+
+[4.4.0]: https://github.com/owncloud/android/compare/v4.3.1...v4.4.0
+
+## Summary
+
+* Bugfix - Rely on `resharing` capability: [#4397](https://github.com/owncloud/android/issues/4397)
+* Bugfix - Shares in non-root are updated correctly: [#4432](https://github.com/owncloud/android/issues/4432)
+* Bugfix - List filtering not working after rotating device: [#4441](https://github.com/owncloud/android/issues/4441)
+* Bugfix - The color of some elements is set up correctly: [#4442](https://github.com/owncloud/android/issues/4442)
+* Bugfix - Audio player does not work: [#4474](https://github.com/owncloud/android/issues/4474)
+* Bugfix - Buttons visibility in name conflicts dialog: [#4480](https://github.com/owncloud/android/pull/4480)
+* Enhancement - Improved "Remove from original folder" option in auto-upload: [#4357](https://github.com/owncloud/android/issues/4357)
+* Enhancement - Improved accessibility of information and relationships: [#4362](https://github.com/owncloud/android/issues/4362)
+* Enhancement - Changed the color of some elements to improve accessibility: [#4364](https://github.com/owncloud/android/issues/4364)
+* Enhancement - Improved SearchView accessibility: [#4365](https://github.com/owncloud/android/issues/4365)
+* Enhancement - Roles added to some elements to improve accessibility: [#4373](https://github.com/owncloud/android/issues/4373)
+* Enhancement - Hardware keyboard support: [#4438](https://github.com/owncloud/android/pull/4438)
+* Enhancement - Hardware keyboard support for passcode view: [#4447](https://github.com/owncloud/android/issues/4447)
+* Enhancement - TalkBack announces the view label correctly: [#4458](https://github.com/owncloud/android/issues/4458)
+
+## Details
+
+* Bugfix - Rely on `resharing` capability: [#4397](https://github.com/owncloud/android/issues/4397)
+
+ The request to create a new share has been fixed so that it only includes the
+ share permission by default when the resharing capability is true, and the "can
+ share" switch in the edition view of private shares is now only shown when
+ resharing is true.
+
+ https://github.com/owncloud/android/issues/4397
+ https://github.com/owncloud/android/pull/4472
+
+* Bugfix - Shares in non-root are updated correctly: [#4432](https://github.com/owncloud/android/issues/4432)
+
+ The items of the "Share" view are updated instantly when create/edit a link or
+ share with users or groups in a non-root file.
+
+ https://github.com/owncloud/android/issues/4432
+ https://github.com/owncloud/android/pull/4435
+
+* Bugfix - List filtering not working after rotating device: [#4441](https://github.com/owncloud/android/issues/4441)
+
+ Configuration changes have been handled when rotating the device so that list
+ filtering works.
+
+ https://github.com/owncloud/android/issues/4441
+ https://github.com/owncloud/android/pull/4467
+
+* Bugfix - The color of some elements is set up correctly: [#4442](https://github.com/owncloud/android/issues/4442)
+
+ The colors of the Manage Accounts header and status bar have been changed to be
+ consistent with the branding colors.
+
+ https://github.com/owncloud/android/issues/4442
+ https://github.com/owncloud/android/pull/4463
+
+* Bugfix - Audio player does not work: [#4474](https://github.com/owncloud/android/issues/4474)
+
+ Audio player in Android 14+ devices wasn't working, so some proper permissions
+ have been added in Manifest so that media can be played correctly in the
+ foreground and background in all versions.
+
+ https://github.com/owncloud/android/issues/4474
+ https://github.com/owncloud/android/pull/4479
+
+* Bugfix - Buttons visibility in name conflicts dialog: [#4480](https://github.com/owncloud/android/pull/4480)
+
+ In some languages, labels for the buttons in the name conflicts dialog were too
+ long and their visibility was very poor. These buttons have been placed in
+ vertical instead of horizontal to avoid this problem.
+
+ https://github.com/owncloud/android/pull/4480
+
+* Enhancement - Improved "Remove from original folder" option in auto-upload: [#4357](https://github.com/owncloud/android/issues/4357)
+
+ The file will be deleted locally after it has been uploaded to the server,
+ avoiding the loss of the file if an error happens during the upload.
+
+ https://github.com/owncloud/android/issues/4357
+ https://github.com/owncloud/android/pull/4437
+
+* Enhancement - Improved accessibility of information and relationships: [#4362](https://github.com/owncloud/android/issues/4362)
+
+ Headings have been added to the following views: Share, Edit/Create Share Link,
+ Standard Toolbar and Manage Accounts. The filename input field and the two
+ switches are now linked to their labels. The 'contentDescription' attributes of
+ the buttons in the Edit/Create Share Link view have also been updated.
+
+ https://github.com/owncloud/android/issues/4362
+ https://github.com/owncloud/android/issues/4363
+ https://github.com/owncloud/android/issues/4371
+ https://github.com/owncloud/android/pull/4448
+
+* Enhancement - Changed the color of some elements to improve accessibility: [#4364](https://github.com/owncloud/android/issues/4364)
+
+ The color of some UI elements has been changed to meet minimum color contrast
+ requirements.
+
+ https://github.com/owncloud/android/issues/4364
+ https://github.com/owncloud/android/pull/4429
+
+* Enhancement - Improved SearchView accessibility: [#4365](https://github.com/owncloud/android/issues/4365)
+
+ The text hint and cross button color of the SearchView has been changed to meet
+ the color contrast requirements. In addition, the SearchView includes a new
+ resource with rounded edges, using the same background color (brandable) as the
+ containing toolbar.
+
+ https://github.com/owncloud/android/issues/4365
+ https://github.com/owncloud/android/pull/4433
+
+* Enhancement - Roles added to some elements to improve accessibility: [#4373](https://github.com/owncloud/android/issues/4373)
+
+ Roles have been added to specific elements within the following views: Toolbar,
+ Spaces, Drawer Menu, Manage accounts and Floating Action Button. Improved the
+ navigation system within the passcode view.
+
+ https://github.com/owncloud/android/issues/4373
+ https://github.com/owncloud/android/pull/4454
+ https://github.com/owncloud/android/pull/4466
+
+* Enhancement - Hardware keyboard support: [#4438](https://github.com/owncloud/android/pull/4438)
+
+ Navigation via hardware keyboard has been improved so that now focus order has a
+ logical path, every element is reachable and there are no traps. These
+ improvements have been applied in main file list, spaces list, drawer menu,
+ share view and image preview.
+
+ https://github.com/owncloud/android/issues/4366
+ https://github.com/owncloud/android/issues/4367
+ https://github.com/owncloud/android/issues/4368
+ https://github.com/owncloud/android/pull/4438
+
+* Enhancement - Hardware keyboard support for passcode view: [#4447](https://github.com/owncloud/android/issues/4447)
+
+ Navigation via hardware keyboard has been added to the passcode view.
+
+ https://github.com/owncloud/android/issues/4447
+ https://github.com/owncloud/android/pull/4455
+
+* Enhancement - TalkBack announces the view label correctly: [#4458](https://github.com/owncloud/android/issues/4458)
+
+ TalkBack no longer announces "ownCloud" every time the screen changes. Now, it
+ correctly dictates the name of the current view.
+
+ https://github.com/owncloud/android/issues/4458
+ https://github.com/owncloud/android/pull/4470
+
+# Changelog for ownCloud Android Client [4.3.1] (2024-07-22)
+
+The following sections list the changes in ownCloud Android Client 4.3.1 relevant to
+ownCloud admins and users.
+
+[4.3.1]: https://github.com/owncloud/android/compare/v4.3.0...v4.3.1
+
+## Summary
+
+* Change - Bump target SDK to 34: [#4434](https://github.com/owncloud/android/issues/4434)
+
+## Details
+
+* Change - Bump target SDK to 34: [#4434](https://github.com/owncloud/android/issues/4434)
+
+ Target SDK was upgraded to 34 in order to fulfill Android platform requirements.
+
+ https://github.com/owncloud/android/issues/4434
+ https://github.com/owncloud/android/pull/4440
+
+# Changelog for ownCloud Android Client [4.3.0] (2024-07-01)
+
+The following sections list the changes in ownCloud Android Client 4.3.0 relevant to
+ownCloud admins and users.
+
+[4.3.0]: https://github.com/owncloud/android/compare/v4.2.2...v4.3.0
+
+## Summary
+
+* Bugfix - Removed unnecessary requests when the app is installed from scratch: [#4213](https://github.com/owncloud/android/issues/4213)
+* Bugfix - "Clear data" button enabled in the app settings in device settings: [#4309](https://github.com/owncloud/android/issues/4309)
+* Bugfix - Video streaming in spaces: [#4328](https://github.com/owncloud/android/issues/4328)
+* Bugfix - Retried successful uploads are cleaned up from the temporary folder: [#4335](https://github.com/owncloud/android/issues/4335)
+* Bugfix - Resolve incorrect truncation of long display names in Manage Accounts: [#4351](https://github.com/owncloud/android/issues/4351)
+* Bugfix - Av. offline files are not removed when "Local only" option is clicked: [#4353](https://github.com/owncloud/android/issues/4353)
+* Bugfix - Unwanted DELETE operations when synchronization in single file fails: [#6638](https://github.com/owncloud/enterprise/issues/6638)
+* Change - Upgrade minimum SDK version to Android 7.0 (v24): [#4230](https://github.com/owncloud/android/issues/4230)
+* Change - Automatic discovery of the account in login: [#4301](https://github.com/owncloud/android/issues/4301)
+* Change - Add new prefixes in commit messages of 3rd party contributors: [#4346](https://github.com/owncloud/android/pull/4346)
+* Change - Kotlinize PreviewTextFragment: [#4356](https://github.com/owncloud/android/issues/4356)
+* Enhancement - Add search functionality to spaces list: [#3865](https://github.com/owncloud/android/issues/3865)
+* Enhancement - Get personal space quota from GraphAPI: [#3874](https://github.com/owncloud/android/issues/3874)
+* Enhancement - Correct "Local only" option in remove dialog: [#3936](https://github.com/owncloud/android/issues/3936)
+* Enhancement - Show app provider icon from endpoint: [#4105](https://github.com/owncloud/android/issues/4105)
+* Enhancement - Improvements in Manage Accounts view: [#4148](https://github.com/owncloud/android/issues/4148)
+* Enhancement - New setting for manual removal of local storage: [#4174](https://github.com/owncloud/android/issues/4174)
+* Enhancement - New setting for automatic removal of local files: [#4175](https://github.com/owncloud/android/issues/4175)
+* Enhancement - Avoid unnecessary requests when an av. offline folder is refreshed: [#4197](https://github.com/owncloud/android/issues/4197)
+* Enhancement - Unit tests for repository classes - Part 1: [#4232](https://github.com/owncloud/android/issues/4232)
+* Enhancement - Add a warning in http connections: [#4284](https://github.com/owncloud/android/issues/4284)
+* Enhancement - Make dialog more Android-alike: [#4303](https://github.com/owncloud/android/issues/4303)
+* Enhancement - Password generator for public links in oCIS: [#4308](https://github.com/owncloud/android/issues/4308)
+* Enhancement - New UI for "Manage accounts" view: [#4312](https://github.com/owncloud/android/issues/4312)
+* Enhancement - Improvements in remove dialog: [#4342](https://github.com/owncloud/android/issues/4342)
+* Enhancement - Content description in UI elements to improve accessibility: [#4360](https://github.com/owncloud/android/issues/4360)
+* Enhancement - Added contentDescription attribute in the previewed image: [#4360](https://github.com/owncloud/android/issues/4360)
+* Enhancement - Support for URL shortcut files: [#4413](https://github.com/owncloud/android/issues/4413)
+* Enhancement - Changes in the Feedback section: [#6594](https://github.com/owncloud/enterprise/issues/6594)
+
+## Details
+
+* Bugfix - Removed unnecessary requests when the app is installed from scratch: [#4213](https://github.com/owncloud/android/issues/4213)
+
+ Some requests to the server that were not necessary when installing the app from
+ scratch have been removed.
+
+ https://github.com/owncloud/android/issues/4213
+ https://github.com/owncloud/android/pull/4385
+
+* Bugfix - "Clear data" button enabled in the app settings in device settings: [#4309](https://github.com/owncloud/android/issues/4309)
+
+ The "Clear data" button has been enabled to delete the application data from the
+ app settings in the device settings. Shared preferences, temporary files,
+ accounts and the local database will be cleared when the button is pressed.
+
+ https://github.com/owncloud/android/issues/4309
+ https://github.com/owncloud/android/pull/4350
+
+* Bugfix - Video streaming in spaces: [#4328](https://github.com/owncloud/android/issues/4328)
+
+ The URI formed to perform video streaming in spaces has been adapted to oCIS
+ accounts so that it takes into account the space where the file is located.
+
+ https://github.com/owncloud/android/issues/4328
+ https://github.com/owncloud/android/pull/4394
+
+* Bugfix - Retried successful uploads are cleaned up from the temporary folder: [#4335](https://github.com/owncloud/android/issues/4335)
+
+ Temporary files related to a failed upload are deleted after retrying it and
+ being successfully completed.
+
+ https://github.com/owncloud/android/issues/4335
+ https://github.com/owncloud/android/pull/4341
+
+* Bugfix - Resolve incorrect truncation of long display names in Manage Accounts: [#4351](https://github.com/owncloud/android/issues/4351)
+
+ Resolved the bug where long display names were truncated incorrectly in the
+ Manage Accounts view. Now, display names are properly truncated in the middle
+ with ellipsis (...) to maintain readability.
+
+ https://github.com/owncloud/android/issues/4351
+ https://github.com/owncloud/android/pull/4380
+
+* Bugfix - Av. offline files are not removed when "Local only" option is clicked: [#4353](https://github.com/owncloud/android/issues/4353)
+
+ "Local only" option in remove dialog will be displayed when the selected folder
+ contains at least one downloaded file, ignoring those available offline. If the
+ "Local only" option is displayed and clicked, available offline files will not
+ be deleted.
+
+ https://github.com/owncloud/android/issues/4353
+ https://github.com/owncloud/android/pull/4399
+
+* Bugfix - Unwanted DELETE operations when synchronization in single file fails: [#6638](https://github.com/owncloud/enterprise/issues/6638)
+
+ A new exception is now thrown and handled when the account of the network client
+ is null, avoiding DELETE requests to the server when synchronization (PROPFIND)
+ on a single file responds with 404. Also, when PROPFINDs respond with 404, the
+ delete operation has been changed to be just local and not remote too.
+
+ https://github.com/owncloud/enterprise/issues/6638
+ https://github.com/owncloud/android/pull/4408
+
+* Change - Upgrade minimum SDK version to Android 7.0 (v24): [#4230](https://github.com/owncloud/android/issues/4230)
+
+ The minimum Android version will be Android 7.0 Nougat (API 24). The application
+ will no longer support previous versions.
+
+ https://github.com/owncloud/android/issues/4230
+ https://github.com/owncloud/android/pull/4299
+
+* Change - Automatic discovery of the account in login: [#4301](https://github.com/owncloud/android/issues/4301)
+
+ Automatic account discovery is done at login. Removed the refresh account button
+ in the Manage Accounts view.
+
+ https://github.com/owncloud/android/issues/4301
+ https://github.com/owncloud/android/pull/4325
+
+* Change - Add new prefixes in commit messages of 3rd party contributors: [#4346](https://github.com/owncloud/android/pull/4346)
+
+ Dependaboy and Calens' commit messages with prefixes that fits 'Conventional
+ Commits'
+
+ https://github.com/owncloud/android/pull/4346
+
+* Change - Kotlinize PreviewTextFragment: [#4356](https://github.com/owncloud/android/issues/4356)
+
+ PreviewTextFragment class has been moved from Java to Kotlin.
+
+ https://github.com/owncloud/android/issues/4356
+ https://github.com/owncloud/android/pull/4376
+
+* Enhancement - Add search functionality to spaces list: [#3865](https://github.com/owncloud/android/issues/3865)
+
+ Search functionality was added in spaces list when you are trying to filter
+ them.
+
+ https://github.com/owncloud/android/issues/3865
+ https://github.com/owncloud/android/pull/4393
+
+* Enhancement - Get personal space quota from GraphAPI: [#3874](https://github.com/owncloud/android/issues/3874)
+
+ Personal space quota in an oCIS account has been added from GraphAPI instead of
+ propfind.
+
+ https://github.com/owncloud/android/issues/3874
+ https://github.com/owncloud/android/pull/4401
+
+* Enhancement - Correct "Local only" option in remove dialog: [#3936](https://github.com/owncloud/android/issues/3936)
+
+ "Local only" option in remove dialog will only be shown if checking selected
+ files and folders recursively, at least one file is available locally.
+
+ https://github.com/owncloud/android/issues/3936
+ https://github.com/owncloud/android/pull/4289
+
+* Enhancement - Show app provider icon from endpoint: [#4105](https://github.com/owncloud/android/issues/4105)
+
+ App provider icon fetched from the server has been added to the "Open in (web)"
+ option on the bottom sheet that appears when clicking the 3-dots button of a
+ file.
+
+ https://github.com/owncloud/android/issues/4105
+ https://github.com/owncloud/android/pull/4391
+
+* Enhancement - Improvements in Manage Accounts view: [#4148](https://github.com/owncloud/android/issues/4148)
+
+ Removed the key icon and avoid overlap account name with icons in Manage
+ Accounts. Redirect to login when snackbar appears in authentication failure.
+
+ https://github.com/owncloud/android/issues/4148
+ https://github.com/owncloud/android/pull/4330
+
+* Enhancement - New setting for manual removal of local storage: [#4174](https://github.com/owncloud/android/issues/4174)
+
+ A new icon has been added in Manage Accounts view to delete manually local
+ files.
+
+ https://github.com/owncloud/android/issues/4174
+ https://github.com/owncloud/android/pull/4334
+
+* Enhancement - New setting for automatic removal of local files: [#4175](https://github.com/owncloud/android/issues/4175)
+
+ A new setting has been created to delete automatically downloaded files, when
+ the time since their last usage exceeds the selected time in the setting.
+
+ https://github.com/owncloud/android/issues/4175
+ https://github.com/owncloud/android/pull/4320
+
+* Enhancement - Avoid unnecessary requests when an av. offline folder is refreshed: [#4197](https://github.com/owncloud/android/issues/4197)
+
+ The available offline folders will only be refreshed when their eTag from the
+ server and the corresponding one of the local database are different, avoiding
+ sending unnecessary request.
+
+ https://github.com/owncloud/android/issues/4197
+ https://github.com/owncloud/android/pull/4354
+
+* Enhancement - Unit tests for repository classes - Part 1: [#4232](https://github.com/owncloud/android/issues/4232)
+
+ Unit tests for OCAppRegistryRepository, OCAuthenticationRepository and
+ OCCapabilityRepository classes have been completed.
+
+ https://github.com/owncloud/android/issues/4232
+ https://github.com/owncloud/android/pull/4281
+
+* Enhancement - Add a warning in http connections: [#4284](https://github.com/owncloud/android/issues/4284)
+
+ Warning dialog has been added in the login screen when you are trying to connect
+ to a http server.
+
+ https://github.com/owncloud/android/issues/4284
+ https://github.com/owncloud/android/pull/4345
+
+* Enhancement - Make dialog more Android-alike: [#4303](https://github.com/owncloud/android/issues/4303)
+
+ Name conflicts dialog appearance was changed to look Android-alike and more
+ similar to other dialogs in the app.
+
+ https://github.com/owncloud/android/issues/4303
+ https://github.com/owncloud/android/pull/4336
+
+* Enhancement - Password generator for public links in oCIS: [#4308](https://github.com/owncloud/android/issues/4308)
+
+ A new password generator has been added to the public links creation view in
+ oCIS accounts, which creates passwords that fulfill all the policies coming from
+ server in a cryptographically secure way.
+
+ https://github.com/owncloud/android/issues/4308
+ https://github.com/owncloud/android/pull/4349
+
+* Enhancement - New UI for "Manage accounts" view: [#4312](https://github.com/owncloud/android/issues/4312)
+
+ A new dialog has been added to substitute the previous view for "Manage
+ accounts". In addition, all the accounts management related stuff has been
+ removed from the drawer menu in order not to show repetitive actions and make
+ this menu simpler.
+
+ https://github.com/owncloud/android/issues/4312
+ https://github.com/owncloud/android/pull/4410
+
+* Enhancement - Improvements in remove dialog: [#4342](https://github.com/owncloud/android/issues/4342)
+
+ A new remove dialog has been created by adding the thumbnail of the file to be
+ deleted. Also, when removing files in multiple selection, the number of elements
+ that are going to be removed is displayed in the dialog.
+
+ https://github.com/owncloud/android/issues/4342
+ https://github.com/owncloud/android/issues/4377
+ https://github.com/owncloud/android/pull/4348
+ https://github.com/owncloud/android/pull/4404
+
+* Enhancement - Content description in UI elements to improve accessibility: [#4360](https://github.com/owncloud/android/issues/4360)
+
+ A description of the meaning or action associated with some UI elements has been
+ included as alternative text to make the application more accessible. Views
+ improved: toolbar, file list, spaces list, share, drawer menu, manage accounts
+ and image preview.
+
+ https://github.com/owncloud/android/issues/4360
+ https://github.com/owncloud/android/pull/4387
+
+* Enhancement - Added contentDescription attribute in the previewed image: [#4360](https://github.com/owncloud/android/issues/4360)
+
+ A contentDescription attribute has been added to previewed image to make the
+ application more accessible.
+
+ https://github.com/owncloud/android/issues/4360
+ https://github.com/owncloud/android/pull/4388
+
+* Enhancement - Support for URL shortcut files: [#4413](https://github.com/owncloud/android/issues/4413)
+
+ A new option has been added in the FAB to create a shortcut file with a .url
+ extension. When the file is clicked, the URL will open in the browser.
+
+ https://github.com/owncloud/android/issues/4413
+ https://github.com/owncloud/android/pull/4420
+
+* Enhancement - Changes in the Feedback section: [#6594](https://github.com/owncloud/enterprise/issues/6594)
+
+ Based on a brandable parameter, a new dialog has been added to handle feedback.
+ Within the dialog, links to the survey, GitHub and the open forum Central will
+ be displayed.
+
+ https://github.com/owncloud/enterprise/issues/6594
+ https://github.com/owncloud/android/pull/4423
+
+# Changelog for ownCloud Android Client [4.2.2] (2024-05-30)
+
+The following sections list the changes in ownCloud Android Client 4.2.2 relevant to
+ownCloud admins and users.
+
+[4.2.2]: https://github.com/owncloud/android/compare/v4.2.1...v4.2.2
+
+## Summary
+
+* Bugfix - Downloads not working when `Content-Length` is not received: [#4352](https://github.com/owncloud/android/issues/4352)
+
+## Details
+
+* Bugfix - Downloads not working when `Content-Length` is not received: [#4352](https://github.com/owncloud/android/issues/4352)
+
+ The case when Content-Length header is not received in the response of a GET for
+ a download has been handled, and now the progress bar in images preview and
+ details view is indeterminate for those cases.
+
+ https://github.com/owncloud/android/issues/4352
+ https://github.com/owncloud/android/pull/4415
+
+# Changelog for ownCloud Android Client [4.2.1] (2024-02-22)
+
+The following sections list the changes in ownCloud Android Client 4.2.1 relevant to
+ownCloud admins and users.
+
+[4.2.1]: https://github.com/owncloud/android/compare/v4.2.0...v4.2.1
+
+## Summary
+
+* Bugfix - Some crashes in 4.2.0: [#4318](https://github.com/owncloud/android/issues/4318)
+
+## Details
+
+* Bugfix - Some crashes in 4.2.0: [#4318](https://github.com/owncloud/android/issues/4318)
+
+ Several crashes reported by Play Console in version 4.2.0 have been fixed.
+
+ https://github.com/owncloud/android/issues/4318
+ https://github.com/owncloud/android/pull/4323
+
+# Changelog for ownCloud Android Client [4.2.0] (2024-02-12)
+
+The following sections list the changes in ownCloud Android Client 4.2.0 relevant to
+ownCloud admins and users.
+
+[4.2.0]: https://github.com/owncloud/android/compare/v4.1.1...v4.2.0
+
+## Summary
+
+* Security - Improve biometric authentication security: [#4180](https://github.com/owncloud/android/issues/4180)
+* Bugfix - Fixed AlertDialog title theme in Samsung Devices: [#3192](https://github.com/owncloud/android/issues/3192)
+* Bugfix - Some Null Pointer Exceptions in MainFileListViewModel: [#4065](https://github.com/owncloud/android/issues/4065)
+* Bugfix - Bugs related to Details view: [#4188](https://github.com/owncloud/android/issues/4188)
+* Bugfix - Some Null Pointer Exceptions fixed from Google Play: [#4207](https://github.com/owncloud/android/issues/4207)
+* Bugfix - Conflict in copy with files without extension: [#4222](https://github.com/owncloud/android/issues/4222)
+* Bugfix - Add "scope" parameter to /token endpoint HTTP requests: [#4260](https://github.com/owncloud/android/pull/4260)
+* Bugfix - Fix in the handling of the base URL: [#4279](https://github.com/owncloud/android/issues/4279)
+* Bugfix - Handle Http 423 (resource locked): [#4282](https://github.com/owncloud/android/issues/4282)
+* Bugfix - Copy folder into descendant in different spaces: [#4293](https://github.com/owncloud/android/issues/4293)
+* Change - Android library as a module instead of submodule: [#3962](https://github.com/owncloud/android/issues/3962)
+* Change - Migration to Media3 from Exoplayer: [#4157](https://github.com/owncloud/android/issues/4157)
+* Enhancement - Koin DSL: [#3966](https://github.com/owncloud/android/pull/3966)
+* Enhancement - Unit tests for datasources classes - Part 1 & Fixes: [#4063](https://github.com/owncloud/android/issues/4063)
+* Enhancement - Unit tests for datasources classes - Part 3: [#4072](https://github.com/owncloud/android/issues/4072)
+* Enhancement - "Apply to all" when many name conflicts arise: [#4078](https://github.com/owncloud/android/issues/4078)
+* Enhancement - "Share to" in oCIS accounts allows upload to any space: [#4088](https://github.com/owncloud/android/issues/4088)
+* Enhancement - Auto-refresh when a file is uploaded: [#4103](https://github.com/owncloud/android/issues/4103)
+* Enhancement - Auto upload in oCIS accounts allows upload to any space: [#4117](https://github.com/owncloud/android/issues/4117)
+* Enhancement - Thumbnail improvements in grid view: [#4145](https://github.com/owncloud/android/issues/4145)
+* Enhancement - Logging changes: [#4151](https://github.com/owncloud/android/issues/4151)
+* Enhancement - Download log files on Android10+ devices: [#4155](https://github.com/owncloud/android/issues/4155)
+* Enhancement - Log file sharing allowed within ownCloud Android app: [#4156](https://github.com/owncloud/android/issues/4156)
+* Enhancement - New field "last usage" in database: [#4173](https://github.com/owncloud/android/issues/4173)
+* Enhancement - Use invoke operator to execute usecases: [#4179](https://github.com/owncloud/android/pull/4179)
+* Enhancement - Deep link open app correctly: [#4181](https://github.com/owncloud/android/issues/4181)
+* Enhancement - Select user and navigate to file when opening via deep link: [#4194](https://github.com/owncloud/android/issues/4194)
+* Enhancement - New branding/MDM parameter to show sensitive auth info in logs: [#4249](https://github.com/owncloud/android/issues/4249)
+* Enhancement - Fix in the type handling of the content-type: [#4258](https://github.com/owncloud/android/issues/4258)
+* Enhancement - Prevent that two media files are playing at the same time: [#4263](https://github.com/owncloud/android/pull/4263)
+* Enhancement - Added icon for .docxf files: [#4267](https://github.com/owncloud/android/issues/4267)
+* Enhancement - Manage password policy in live mode: [#4269](https://github.com/owncloud/android/issues/4269)
+* Enhancement - New branding/MDM parameter to send `login_hint` and `user` params: [#4288](https://github.com/owncloud/android/issues/4288)
+
+## Details
+
+* Security - Improve biometric authentication security: [#4180](https://github.com/owncloud/android/issues/4180)
+
+ Biometric authentication has been improved by checking the result received when
+ performing a successful authentication.
+
+ https://github.com/owncloud/android/issues/4180
+ https://github.com/owncloud/android/pull/4283
+
+* Bugfix - Fixed AlertDialog title theme in Samsung Devices: [#3192](https://github.com/owncloud/android/issues/3192)
+
+ Use of device default theme was removed.
+
+ https://github.com/owncloud/android/issues/3192
+ https://github.com/owncloud/android/pull/4277
+
+* Bugfix - Some Null Pointer Exceptions in MainFileListViewModel: [#4065](https://github.com/owncloud/android/issues/4065)
+
+ The MainFileListViewModel has prevented the fileById variable from crashing when
+ a null value is found.
+
+ https://github.com/owncloud/android/issues/4065
+ https://github.com/owncloud/android/pull/4241
+
+* Bugfix - Bugs related to Details view: [#4188](https://github.com/owncloud/android/issues/4188)
+
+ When coming to Details view from video or image previews, now the top bar is
+ shown correctly and navigation has the correct stack, so the back button has the
+ expected flow.
+
+ https://github.com/owncloud/android/issues/4188
+ https://github.com/owncloud/android/pull/4265
+
+* Bugfix - Some Null Pointer Exceptions fixed from Google Play: [#4207](https://github.com/owncloud/android/issues/4207)
+
+ FileDisplayActivity and ReceiverExternalFilesActivity have prevented some
+ functions from crashing when a null value is found.
+
+ https://github.com/owncloud/android/issues/4207
+ https://github.com/owncloud/android/pull/4238
+
+* Bugfix - Conflict in copy with files without extension: [#4222](https://github.com/owncloud/android/issues/4222)
+
+ The check of files names that start in the same way has been removed from the
+ copy network operation, so that the copy use case takes care of that and works
+ properly with files without extension.
+
+ https://github.com/owncloud/android/issues/4222
+ https://github.com/owncloud/android/pull/4294
+
+* Bugfix - Add "scope" parameter to /token endpoint HTTP requests: [#4260](https://github.com/owncloud/android/pull/4260)
+
+ The "scope" parameter is now always sent in the body of HTTP requests to the
+ /token endpoint, which is optional in v1 but required in v2.
+
+ https://github.com/owncloud/android/pull/4260
+
+* Bugfix - Fix in the handling of the base URL: [#4279](https://github.com/owncloud/android/issues/4279)
+
+ Base URL has been formatted in GetRemoteAppRegistryOperation when server
+ instance is installed in subfolder, so that the endpoint is formed correctly.
+
+ https://github.com/owncloud/android/issues/4279
+ https://github.com/owncloud/android/pull/4287
+
+* Bugfix - Handle Http 423 (resource locked): [#4282](https://github.com/owncloud/android/issues/4282)
+
+ App can gracefully show if the file is locked when done certain operations on
+ it.
+
+ https://github.com/owncloud/android/issues/4282
+ https://github.com/owncloud/android/pull/4285
+
+* Bugfix - Copy folder into descendant in different spaces: [#4293](https://github.com/owncloud/android/issues/4293)
+
+ Copying a folder into another folder with the same name in a different space now
+ works correctly.
+
+ https://github.com/owncloud/android/issues/4293
+ https://github.com/owncloud/android/pull/4295
+
+* Change - Android library as a module instead of submodule: [#3962](https://github.com/owncloud/android/issues/3962)
+
+ Android library, containing all networking stuff, is now the 5th module in the
+ app instead of submodule.
+
+ https://github.com/owncloud/android/issues/3962
+ https://github.com/owncloud/android/pull/4183
+
+* Change - Migration to Media3 from Exoplayer: [#4157](https://github.com/owncloud/android/issues/4157)
+
+ Media3 is the new home for Exoplayer, which has become a part of this library.
+ Media3 provides a more advanced and optimized media playback experience for
+ users, with improvements in performance and compatibility.
+
+ https://github.com/owncloud/android/issues/4157
+ https://github.com/owncloud/android/pull/4177
+
+* Enhancement - Koin DSL: [#3966](https://github.com/owncloud/android/pull/3966)
+
+ Koin DSL makes easier the dependency definition avoiding verbosity by allowing
+ you to target a class constructor directly
+
+ https://github.com/owncloud/android/pull/3966
+
+* Enhancement - Unit tests for datasources classes - Part 1 & Fixes: [#4063](https://github.com/owncloud/android/issues/4063)
+
+ Unit tests for OCLocalAppRegistryDataSource, OCRemoteAppRegistryDataSource,
+ OCLocalAuthenticationDataSource, OCRemoteAuthenticationDataSource,
+ OCLocalCapabilitiesDataSource and OCRemoteCapabilitiesDataSource classes have
+ been done and completed, and several fixes have been applied to all existent
+ unit test classes for datasources.
+
+ https://github.com/owncloud/android/issues/4063
+ https://github.com/owncloud/android/pull/4209
+
+* Enhancement - Unit tests for datasources classes - Part 3: [#4072](https://github.com/owncloud/android/issues/4072)
+
+ Unit tests of the OCFolderBackupLocalDataSource, OCRemoteOAuthDataSource,
+ OCRemoteShareeDataSource, OCLocalShareDataSource, OCRemoteShareDataSource,
+ OCLocalSpacesDataSource, OCRemoteSpacesDataSource, OCLocalTransferDataSource,
+ OCLocalUserDataSource, OCRemoteUserDataSource, OCRemoteWebFingerDatasource
+ classes have been done and completed.
+
+ https://github.com/owncloud/android/issues/4072
+ https://github.com/owncloud/android/pull/4143
+
+* 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
+
+* Enhancement - "Share to" in oCIS accounts allows upload to any space: [#4088](https://github.com/owncloud/android/issues/4088)
+
+ With this improvement, shared stuff from other apps can be uploaded to any space
+ and not only the personal one in oCIS accounts.
+
+ https://github.com/owncloud/android/issues/4088
+ https://github.com/owncloud/android/pull/4160
+
+* Enhancement - Auto-refresh when a file is uploaded: [#4103](https://github.com/owncloud/android/issues/4103)
+
+ The file list will be now refreshed automatically when an upload whose
+ destination folder is the one we are in is completed successfully.
+
+ https://github.com/owncloud/android/issues/4103
+ https://github.com/owncloud/android/pull/4199
+
+* Enhancement - Auto upload in oCIS accounts allows upload to any space: [#4117](https://github.com/owncloud/android/issues/4117)
+
+ Auto uploads of images and videos can now be uploaded to any space and not only
+ the personal one in oCIS accounts.
+
+ https://github.com/owncloud/android/issues/4117
+ https://github.com/owncloud/android/pull/4214
+
+* Enhancement - Thumbnail improvements in grid view: [#4145](https://github.com/owncloud/android/issues/4145)
+
+ Grid view was improved by adding the file name to images when the thumbnail is
+ null.
+
+ https://github.com/owncloud/android/issues/4145
+ https://github.com/owncloud/android/pull/4237
+
+* Enhancement - Logging changes: [#4151](https://github.com/owncloud/android/issues/4151)
+
+ - Updating version of com.github.AppDevNext.Logcat:LogcatCoreLib lib. - Adding
+ the hour, minutes and seconds to the log file. - Printing http logs in one line.
+ - Printing http logs with 1000000 bytes as max size. - Printing http logs in a
+ Json format.
+
+ https://github.com/owncloud/android/issues/4151
+ https://github.com/owncloud/android/pull/4204
+
+* Enhancement - Download log files on Android10+ devices: [#4155](https://github.com/owncloud/android/issues/4155)
+
+ A new icon to download a log file to the Downloads folder of the device has been
+ added to the log list screen on Android10+ devices.
+
+ https://github.com/owncloud/android/issues/4155
+ https://github.com/owncloud/android/pull/4205
+
+* Enhancement - Log file sharing allowed within ownCloud Android app: [#4156](https://github.com/owncloud/android/issues/4156)
+
+ Sharing log files to the ownCloud app itself is now possible from the logs
+ screen.
+
+ https://github.com/owncloud/android/issues/4156
+ https://github.com/owncloud/android/pull/4215
+
+* Enhancement - New field "last usage" in database: [#4173](https://github.com/owncloud/android/issues/4173)
+
+ To know the last usage of a file, a new field has been created in the database
+ to handle this specific information.
+
+ https://github.com/owncloud/android/issues/4173
+ https://github.com/owncloud/android/pull/4187
+
+* Enhancement - Use invoke operator to execute usecases: [#4179](https://github.com/owncloud/android/pull/4179)
+
+ Removes all the "execute" verbosity for use cases by using the "invoke" operator
+ instead.
+
+ https://github.com/owncloud/android/pull/4179
+
+* Enhancement - Deep link open app correctly: [#4181](https://github.com/owncloud/android/issues/4181)
+
+ Opening the app with the deep link correctly and managing if user logged or not.
+
+ https://github.com/owncloud/android/issues/4181
+ https://github.com/owncloud/android/pull/4191
+
+* Enhancement - Select user and navigate to file when opening via deep link: [#4194](https://github.com/owncloud/android/issues/4194)
+
+ Select the correct user owner of the deep link file, managing possible errors
+ and navigating to the correct file.
+
+ https://github.com/owncloud/android/issues/4194
+ https://github.com/owncloud/android/pull/4212
+
+* Enhancement - New branding/MDM parameter to show sensitive auth info in logs: [#4249](https://github.com/owncloud/android/issues/4249)
+
+ A new branding and MDM parameter has been created to decide if the sensitive
+ information put in the authorization header in HTTP requests is shown or not in
+ the logs.
+
+ https://github.com/owncloud/android/issues/4249
+ https://github.com/owncloud/android/pull/4257
+
+* Enhancement - Fix in the type handling of the content-type: [#4258](https://github.com/owncloud/android/issues/4258)
+
+ The content-type `application/jrd+json` has been added to the loggable types
+ list, so that body in some requests and responses can be correctly logged.
+
+ https://github.com/owncloud/android/issues/4258
+ https://github.com/owncloud/android/pull/4266
+
+* Enhancement - Prevent that two media files are playing at the same time: [#4263](https://github.com/owncloud/android/pull/4263)
+
+ The player handles the audio focus shifts, pausing one player if another starts.
+
+ https://github.com/owncloud/android/pull/4263
+
+* Enhancement - Added icon for .docxf files: [#4267](https://github.com/owncloud/android/issues/4267)
+
+ An icon has been added for files that have a .docxf extension.
+
+ https://github.com/owncloud/android/issues/4267
+ https://github.com/owncloud/android/pull/4297
+
+* Enhancement - Manage password policy in live mode: [#4269](https://github.com/owncloud/android/issues/4269)
+
+ Password policy for public links is handled in live mode with new items in the
+ dialog.
+
+ https://github.com/owncloud/android/issues/4269
+ https://github.com/owncloud/android/pull/4276
+
+* Enhancement - New branding/MDM parameter to send `login_hint` and `user` params: [#4288](https://github.com/owncloud/android/issues/4288)
+
+ A new branding and MDM parameter has been created to decide if `login_hint` and
+ `user` are sent as parameters in the login request, so that a value is shown in
+ the Username text field.
+
+ https://github.com/owncloud/android/issues/4288
+ https://github.com/owncloud/android/pull/4291
+
+# Changelog for ownCloud Android Client [4.1.1] (2023-10-18)
+
+The following sections list the changes in ownCloud Android Client 4.1.1 relevant to
+ownCloud admins and users.
+
+[4.1.1]: https://github.com/owncloud/android/compare/v4.1.0...v4.1.1
+
+## Summary
+
+* Bugfix - Some Null Pointer Exceptions avoided: [#4158](https://github.com/owncloud/android/issues/4158)
+* Bugfix - Thumbnails correctly shown for every user: [#4189](https://github.com/owncloud/android/pull/4189)
+
+## 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
+
+* Bugfix - Thumbnails correctly shown for every user: [#4189](https://github.com/owncloud/android/pull/4189)
+
+ Due to an error in the request, users that included the '@' character in their
+ usernames couldn't see the thumbnails of the image files. Now, every user can
+ see them correctly.
+
+ https://github.com/owncloud/android/pull/4189
+
+# 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)
+* Change - Use ViewBinding in FolderPickerActivity: [#3796](https://github.com/owncloud/android/issues/3796)
+* Change - Use ViewBinding in WhatsNewActivity: [#3796](https://github.com/owncloud/android/issues/3796)
+* Enhancement - Support for Markdown files: [#3716](https://github.com/owncloud/android/issues/3716)
+* Enhancement - Support for spaces: [#3851](https://github.com/owncloud/android/pull/3851)
+* Enhancement - Update label on Camera Uploads: [#3930](https://github.com/owncloud/android/pull/3930)
+* Enhancement - Authenticated WebFinger: [#3943](https://github.com/owncloud/android/issues/3943)
+* Enhancement - Link in drawer menu: [#3949](https://github.com/owncloud/android/pull/3949)
+* Enhancement - Send language header in all requests: [#3980](https://github.com/owncloud/android/issues/3980)
+* Enhancement - Open in specific web provider: [#3994](https://github.com/owncloud/android/issues/3994)
+* Enhancement - Create file via web: [#3995](https://github.com/owncloud/android/issues/3995)
+* Enhancement - Updated WebFinger flow: [#3998](https://github.com/owncloud/android/issues/3998)
+* Enhancement - Monochrome icon for the app: [#4001](https://github.com/owncloud/android/pull/4001)
+* Enhancement - Add prompt parameter to OIDC flow: [#4011](https://github.com/owncloud/android/pull/4011)
+* Enhancement - New setting "Access document provider": [#4032](https://github.com/owncloud/android/pull/4032)
+
+## 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 shown. Now, we show a more specific error.
+
+ https://github.com/owncloud/android/issues/3948
+ https://github.com/owncloud/android/pull/4013
+ https://github.com/owncloud/android-library/pull/558
+
+* Bugfix - Incorrect list of files in av. offline when browsing from details: [#3986](https://github.com/owncloud/android/issues/3986)
+
+ When opening the details view of a file accessed from the available offline
+ shortcut, browsing back led to a incorrect list of files. Now, browsing back
+ leads to the list of available offline files again.
+
+ https://github.com/owncloud/android/issues/3986
+ https://github.com/owncloud/android/pull/4026
+
+* Change - Bump target SDK to 33: [#3617](https://github.com/owncloud/android/issues/3617)
+
+ Target SDK was upgraded to 33 to keep the app updated with the latest android
+ changes. A new setting was introduced to manage notifications in an easier way.
+
+ https://github.com/owncloud/android/issues/3617
+ https://github.com/owncloud/android/pull/3972
+ https://developer.android.com/about/versions/13/behavior-changes-13
+
+* Change - Use ViewBinding in FolderPickerActivity: [#3796](https://github.com/owncloud/android/issues/3796)
+
+ The use of findViewById method was replaced by using ViewBinding in the
+ FolderPickerActivity.
+
+ https://github.com/owncloud/android/issues/3796
+ https://github.com/owncloud/android/pull/4014
+
+* Change - Use ViewBinding in WhatsNewActivity: [#3796](https://github.com/owncloud/android/issues/3796)
+
+ The use of findViewById method was replaced by using ViewBinding in the
+ WhatsNewActivity.
+
+ https://github.com/owncloud/android/issues/3796
+ https://github.com/owncloud/android/pull/4021
+
+* Enhancement - Support for Markdown files: [#3716](https://github.com/owncloud/android/issues/3716)
+
+ Markdown files preview will now be rendered to show its content in a prettier
+ way.
+
+ https://github.com/owncloud/android/issues/3716
+ https://github.com/owncloud/android/pull/4017
+
+* Enhancement - Support for spaces: [#3851](https://github.com/owncloud/android/pull/3851)
+
+ Spaces are now supported in oCIS accounts. A new tab has been added, which
+ allows to list and browse through all the available spaces for the current
+ account. The supported operations for files in spaces are: download, upload,
+ remove, rename, create folder, copy and move. The documents provider has been
+ adapted as well to be able to browse through spaces and perform the operations
+ already mentioned.
+
+ https://github.com/owncloud/android/pull/3851
+
+* Enhancement - Update label on Camera Uploads: [#3930](https://github.com/owncloud/android/pull/3930)
+
+ Update label on camera uploads to avoid confusions with the behavior of original
+ files. Now, it is clear that original files will be removed.
+
+ https://github.com/owncloud/android/pull/3930
+
+* Enhancement - Authenticated WebFinger: [#3943](https://github.com/owncloud/android/issues/3943)
+
+ Authenticated WebFinger was introduced into the authentication flow. Now,
+ WebFinger is used to retrieve the OpenID Connect issuer and the available
+ ownCloud instances. For the moment, multiple oC instances are not supported,
+ only the first available instance is used.
+
+ https://github.com/owncloud/android/issues/3943
+ https://github.com/owncloud/android/pull/3945
+ https://doc.owncloud.com/ocis/next/deployment/services/s-list/webfinger.html
+
+* Enhancement - Link in drawer menu: [#3949](https://github.com/owncloud/android/pull/3949)
+
+ Customers will be able now to set a personalized label and link that will appear
+ in the drawer menu, together with the drawer logo as an icon.
+
+ https://github.com/owncloud/android/issues/3907
+ https://github.com/owncloud/android/pull/3949
+
+* Enhancement - Send language header in all requests: [#3980](https://github.com/owncloud/android/issues/3980)
+
+ Added Accept-Language header to all requests so the android App can receive
+ translated content.
+
+ https://github.com/owncloud/android/issues/3980
+ https://github.com/owncloud/android/pull/3982
+ https://github.com/owncloud/android-library/pull/551
+
+* Enhancement - Open in specific web provider: [#3994](https://github.com/owncloud/android/issues/3994)
+
+ We've added the specific web app providers instead of opening the file with the
+ default web provider.
+
+ The user can open their files with any of the available specific web app
+ providers from the server. Previously, file was opened with the default one.
+
+ https://github.com/owncloud/android/issues/3994
+ https://github.com/owncloud/android/pull/3990
+ https://owncloud.dev/services/app-registry/apps/#app-registry
+
+* Enhancement - Create file via web: [#3995](https://github.com/owncloud/android/issues/3995)
+
+ A new option has been added in the FAB to create new files, for those servers
+ which support this option and have available app providers that allow the
+ creation of new files.
+
+ https://github.com/owncloud/android/issues/3995
+ https://github.com/owncloud/android/pull/4023
+ https://github.com/owncloud/android-library/pull/562
+
+* Enhancement - Updated WebFinger flow: [#3998](https://github.com/owncloud/android/issues/3998)
+
+ WebFinger call won't follow redirections. WebFinger will be requested first and
+ will skip status.php in case it's successful, and in case the lookup server is
+ not directly accessible, we will continue the authentication flow with the
+ regular status.php.
+
+ https://github.com/owncloud/android/issues/3998
+ https://github.com/owncloud/android/pull/4000
+ https://github.com/owncloud/android-library/pull/555
+
+* Enhancement - Monochrome icon for the app: [#4001](https://github.com/owncloud/android/pull/4001)
+
+ From Android 13, if the user has enabled themed app icons in their device
+ settings, the app will be shown with a monochrome icon.
+
+ https://github.com/owncloud/android/pull/4001
+
+* Enhancement - Add prompt parameter to OIDC flow: [#4011](https://github.com/owncloud/android/pull/4011)
+
+ Added prompt parameter to the authorization request in case OIDC is supported.
+ By default, select_account will be sent. It can be changed via branding or MDM.
+
+ https://github.com/owncloud/android/issues/3862
+ https://github.com/owncloud/android/issues/3984
+ https://github.com/owncloud/android/pull/4011
+
+* Enhancement - New setting "Access document provider": [#4032](https://github.com/owncloud/android/pull/4032)
+
+ A new setting has been added in the "More" settings section with a suggested app
+ to access the document provider.
+
+ https://github.com/owncloud/android/issues/4028
+ https://github.com/owncloud/android/pull/4032
+
+# Changelog for ownCloud Android Client [3.0.4] (2023-03-07)
+
+The following sections list the changes in ownCloud Android Client 3.0.4 relevant to
+ownCloud admins and users.
+
+[3.0.4]: https://github.com/owncloud/android/compare/v3.0.3...v3.0.4
+
+## Summary
+
+* Security - Fix for security issues with database: [#3952](https://github.com/owncloud/android/pull/3952)
+* Enhancement - HTTP logs show more info: [#547](https://github.com/owncloud/android-library/pull/547)
+
+## Details
+
+* Security - Fix for security issues with database: [#3952](https://github.com/owncloud/android/pull/3952)
+
+ Some fixes have been added so that now no part of the app's database can be
+ accessed from other apps.
+
+ https://github.com/owncloud/android/pull/3952
+
+* Enhancement - HTTP logs show more info: [#547](https://github.com/owncloud/android-library/pull/547)
+
+ When enabling HTTP logs, now the URL for each log will be shown as well to make
+ debugging easier.
+
+ https://github.com/owncloud/android-library/pull/547
+
+# Changelog for ownCloud Android Client [3.0.3] (2023-02-13)
+
+The following sections list the changes in ownCloud Android Client 3.0.3 relevant to
+ownCloud admins and users.
+
+[3.0.3]: https://github.com/owncloud/android/compare/v3.0.2...v3.0.3
+
+## Summary
+
+* Bugfix - Error messages too long in folders operation: [#3852](https://github.com/owncloud/android/pull/3852)
+* Bugfix - Fix problems after authentication: [#3889](https://github.com/owncloud/android/pull/3889)
+* Bugfix - Toolbar in file details view: [#3899](https://github.com/owncloud/android/pull/3899)
+
+## Details
+
+* Bugfix - Error messages too long in folders operation: [#3852](https://github.com/owncloud/android/pull/3852)
+
+ Error messages when trying to perform a non-allowed action for copying and
+ moving folders have been shortened so that they are shown completely in the
+ snackbar.
+
+ https://github.com/owncloud/android/issues/3820
+ https://github.com/owncloud/android/pull/3852
+
+* Bugfix - Fix problems after authentication: [#3889](https://github.com/owncloud/android/pull/3889)
+
+ Client for session are now fetched on demand to avoid reinitialize DI, making
+ the process smoother
+
+ https://github.com/owncloud/android/pull/3889
+
+* Bugfix - Toolbar in file details view: [#3899](https://github.com/owncloud/android/pull/3899)
+
+ When returning from the share screen to details screen, the toolbar didn't show
+ the correct options and title. Now it does.
+
+ https://github.com/owncloud/android/issues/3866
+ https://github.com/owncloud/android/pull/3899
+
+# Changelog for ownCloud Android Client [3.0.2] (2023-01-26)
+
+The following sections list the changes in ownCloud Android Client 3.0.2 relevant to
+ownCloud admins and users.
+
+[3.0.2]: https://github.com/owncloud/android/compare/v3.0.1...v3.0.2
+
+## Summary
+
+* Bugfix - Fix reauthentication prompt: [#534](https://github.com/owncloud/android-library/pull/534)
+* Enhancement - Branded scope for OpenID Connect: [#3869](https://github.com/owncloud/android/pull/3869)
+
+## Details
+
+* Bugfix - Fix reauthentication prompt: [#534](https://github.com/owncloud/android-library/pull/534)
+
+ Potential fix to oauth error after logging in for first time that makes user to
+ reauthenticate
+
+ https://github.com/owncloud/android-library/pull/534
+
+* Enhancement - Branded scope for OpenID Connect: [#3869](https://github.com/owncloud/android/pull/3869)
+
+ OpenID Connect scope is now brandable via setup.xml file or MDM
+
+ https://github.com/owncloud/android/pull/3869
+
+# Changelog for ownCloud Android Client [3.0.1] (2022-12-21)
+
+The following sections list the changes in ownCloud Android Client 3.0.1 relevant to
+ownCloud admins and users.
+
+[3.0.1]: https://github.com/owncloud/android/compare/v3.0.0...v3.0.1
+
+## Summary
+
+* Bugfix - Fix crash when upgrading from 2.18: [#3837](https://github.com/owncloud/android/pull/3837)
+* Bugfix - Fix crash when opening uploads section: [#3841](https://github.com/owncloud/android/pull/3841)
+
+## Details
+
+* Bugfix - Fix crash when upgrading from 2.18: [#3837](https://github.com/owncloud/android/pull/3837)
+
+ Upgrading from 2.18 or older versions made the app crash due to camera uploads
+ data migration. This problem has been solved and now the app upgrades correctly.
+
+ https://github.com/owncloud/android/pull/3837
+
+* Bugfix - Fix crash when opening uploads section: [#3841](https://github.com/owncloud/android/pull/3841)
+
+ When upgrading from an old version with uploads with "forget" behaviour, app
+ crashed when opening the uploads tab. Now, this has been fixed so that it works
+ correctly.
+
+ https://github.com/owncloud/android/pull/3841
+
+# Changelog for ownCloud Android Client [3.0.0] (2022-12-12)
+
+The following sections list the changes in ownCloud Android Client 3.0.0 relevant to
+ownCloud admins and users.
+
+[3.0.0]: https://github.com/owncloud/android/compare/v2.21.2...v3.0.0
+
+## Summary
+
+* Bugfix - Fix for thumbnails: [#3719](https://github.com/owncloud/android/pull/3719)
+* Enhancement - Sync engine rewritten: [#2934](https://github.com/owncloud/android/pull/2934)
+* Enhancement - Faster browser authentication: [#3632](https://github.com/owncloud/android/pull/3632)
+* Enhancement - Several transfers running simultaneously: [#3710](https://github.com/owncloud/android/pull/3710)
+* Enhancement - Empty views improved: [#3728](https://github.com/owncloud/android/pull/3728)
+* Enhancement - Automatic conflicts propagation: [#3766](https://github.com/owncloud/android/pull/3766)
+
+## Details
+
+* Bugfix - Fix for thumbnails: [#3719](https://github.com/owncloud/android/pull/3719)
+
+ Some thumbnails were not shown in the file list. Now, they are all shown
+ correctly.
+
+ https://github.com/owncloud/android/issues/2818
+ https://github.com/owncloud/android/pull/3719
+
+* Enhancement - Sync engine rewritten: [#2934](https://github.com/owncloud/android/pull/2934)
+
+ The whole synchronization engine has been refactored to a new architecture to
+ make it better structured and more efficient.
+
+ https://github.com/owncloud/android/issues/2818
+ https://github.com/owncloud/android/pull/2934
+
+* Enhancement - Faster browser authentication: [#3632](https://github.com/owncloud/android/pull/3632)
+
+ Login flow has been improved by saving a click when the server is OAuth2/OIDC
+ and it is valid. Also, when authenticating again in a OAuth2/OIDC account
+ already saved in the app, the username is already shown in the browser.
+
+ https://github.com/owncloud/android/issues/3759
+ https://github.com/owncloud/android/pull/3632
+
+* Enhancement - Several transfers running simultaneously: [#3710](https://github.com/owncloud/android/pull/3710)
+
+ With the sync engine refactor, now several downloads and uploads can run at the
+ same time, improving efficiency.
+
+ https://github.com/owncloud/android/issues/3426
+ https://github.com/owncloud/android/pull/3710
+
+* Enhancement - Empty views improved: [#3728](https://github.com/owncloud/android/pull/3728)
+
+ When the list of items is empty, we now show a more attractive view. This
+ applies to file list, available offline list, shared by link list, uploads list,
+ logs list and external share list.
+
+ https://github.com/owncloud/android/issues/3026
+ https://github.com/owncloud/android/pull/3728
+
+* Enhancement - Automatic conflicts propagation: [#3766](https://github.com/owncloud/android/pull/3766)
+
+ Conflicts are now propagated automatically to parent folders, and cleaned when
+ solved or removed. Before, it was needed to navigate to the file location for
+ the conflict to propagate. Also, move, copy and remove actions work properly
+ with conflicts.
+
+ https://github.com/owncloud/android/issues/3005
+ https://github.com/owncloud/android/pull/3766
+
+# Changelog for ownCloud Android Client [2.21.2] (2022-09-07)
+
+The following sections list the changes in ownCloud Android Client 2.21.2 relevant to
+ownCloud admins and users.
+
+[2.21.2]: https://github.com/owncloud/android/compare/v2.21.1...v2.21.2
+
+## Summary
+
+* Enhancement - Open in web: [#3672](https://github.com/owncloud/android/issues/3672)
+* Enhancement - Shares from propfind: [#3711](https://github.com/owncloud/android/issues/3711)
+* Enhancement - Private link capability: [#3732](https://github.com/owncloud/android/issues/3732)
+
+## Details
+
+* Enhancement - Open in web: [#3672](https://github.com/owncloud/android/issues/3672)
+
+ OCIS feature, to open files with mime types supported by the server in the web
+ browser using collaborative or specific tools
+
+ https://github.com/owncloud/android/issues/3672
+ https://github.com/owncloud/android/pull/3737
+
+* Enhancement - Shares from propfind: [#3711](https://github.com/owncloud/android/issues/3711)
+
+ Added a new property to the propfind, so that, we can get if the files in a
+ folder are shared directly with just one request. Previously, a propfind and
+ another additional request were needed to the shares api to retrieve the shares
+ of the folder.
+
+ https://github.com/owncloud/android/issues/3711
+ https://github.com/owncloud/android-library/pull/496
+
+* Enhancement - Private link capability: [#3732](https://github.com/owncloud/android/issues/3732)
+
+ Private link capability is now respected. Option is shown/hidden depending on
+ its value
+
+ https://github.com/owncloud/android/issues/3732
+ https://github.com/owncloud/android/pull/3738
+ https://github.com/owncloud/android-library/pull/505
+
+# Changelog for ownCloud Android Client [2.21.1] (2022-06-15)
+
+The following sections list the changes in ownCloud Android Client 2.21.1 relevant to
+ownCloud admins and users.
+
+[2.21.1]: https://github.com/owncloud/android/compare/v2.21.0...v2.21.1
+
+## Summary
+
+* Bugfix - Fix crash when opening from details screen: [#3696](https://github.com/owncloud/android/pull/3696)
+
+## Details
+
+* Bugfix - Fix crash when opening from details screen: [#3696](https://github.com/owncloud/android/pull/3696)
+
+ Fixed a crash when opening a non downloaded file from the details view.
+
+ https://github.com/owncloud/android/pull/3696
+
+# Changelog for ownCloud Android Client [2.21.0] (2022-06-07)
+
+The following sections list the changes in ownCloud Android Client 2.21.0 relevant to
+ownCloud admins and users.
+
+[2.21.0]: https://github.com/owncloud/android/compare/v2.20.0...v2.21.0
+
+## Summary
+
+* Bugfix - Prevented signed in user in the list of users to be shared: [#1419](https://github.com/owncloud/android/issues/1419)
+* Bugfix - Corrupt picture error controlled: [#3441](https://github.com/owncloud/android/issues/3441)
+* Bugfix - Security flags for recording screen: [#3468](https://github.com/owncloud/android/issues/3468)
+* Bugfix - Crash when changing orientation in Details view: [#3571](https://github.com/owncloud/android/issues/3571)
+* Bugfix - Lock displays shown again: [#3591](https://github.com/owncloud/android/issues/3591)
+* Enhancement - Support for SVG files added: [#1033](https://github.com/owncloud/android/issues/1033)
+* Enhancement - Full name is shown in shares: [#1106](https://github.com/owncloud/android/issues/1106)
+* Enhancement - Improved copy/move dialog: [#1414](https://github.com/owncloud/android/issues/1414)
+* Enhancement - Share a folder from within the folder: [#1441](https://github.com/owncloud/android/issues/1441)
+* Enhancement - New option to show or not hidden files: [#2578](https://github.com/owncloud/android/issues/2578)
+* Enhancement - What´s new option: [#3352](https://github.com/owncloud/android/issues/3352)
+* Enhancement - First steps in Android Enterprise integration: [#3415](https://github.com/owncloud/android/issues/3415)
+* Enhancement - Provide app feedback to MDM admins: [#3420](https://github.com/owncloud/android/issues/3420)
+* Enhancement - Lock delay enforced: [#3440](https://github.com/owncloud/android/issues/3440)
+* Enhancement - Release Notes: [#3442](https://github.com/owncloud/android/issues/3442)
+* Enhancement - Send for file multiselect: [#3491](https://github.com/owncloud/android/issues/3491)
+* Enhancement - Improvements for the UI in the passcode screen: [#3516](https://github.com/owncloud/android/issues/3516)
+* Enhancement - Extended security enforced: [#3543](https://github.com/owncloud/android/issues/3543)
+* Enhancement - Improvements for the UI in the pattern screen: [#3580](https://github.com/owncloud/android/issues/3580)
+* Enhancement - Prevent taking screenshots: [#3596](https://github.com/owncloud/android/issues/3596)
+* Enhancement - Option to allow screenshots or not in Android Enterprise: [#3625](https://github.com/owncloud/android/issues/3625)
+* Enhancement - Thumbnail click action in file detail: [#3653](https://github.com/owncloud/android/pull/3653)
+
+## Details
+
+* Bugfix - Prevented signed in user in the list of users to be shared: [#1419](https://github.com/owncloud/android/issues/1419)
+
+ Previously, user list for sharing contains signed in user, now this user is
+ omitted to avoid errors.
+
+ https://github.com/owncloud/android/issues/1419
+ https://github.com/owncloud/android/pull/3643
+
+* Bugfix - Corrupt picture error controlled: [#3441](https://github.com/owncloud/android/issues/3441)
+
+ Previously, If a file is not correct or is damaged, it is downloaded but not
+ previewed. An infinite spinner on a black window is shown instead. Now, an error
+ appears warning to the user.
+
+ https://github.com/owncloud/android/issues/3441
+ https://github.com/owncloud/android/pull/3644
+
+* Bugfix - Security flags for recording screen: [#3468](https://github.com/owncloud/android/issues/3468)
+
+ Previously, if passcode or pattern were enabled, no screen from the app could be
+ viewed from a recording screen app. Now, only the login, passcode and pattern
+ screens are protected against recording.
+
+ https://github.com/owncloud/android/issues/3468
+ https://github.com/owncloud/android/pull/3560
+
+* Bugfix - Crash when changing orientation in Details view: [#3571](https://github.com/owncloud/android/issues/3571)
+
+ Previously, the app crashes when changing orientation in Details view after
+ installing Now, app shows correctly the details after installing.
+
+ https://github.com/owncloud/android/issues/3571
+ https://github.com/owncloud/android/pull/3589
+
+* Bugfix - Lock displays shown again: [#3591](https://github.com/owncloud/android/issues/3591)
+
+ Previously, if you clicked on passcode or pattern lock to remove it, and then
+ you clicked on cancel, the lock display was shown again to put the passcode or
+ pattern. Now, if you cancel it, you come back to settings screen.
+
+ https://github.com/owncloud/android/issues/3591
+ https://github.com/owncloud/android/pull/3592
+
+* Enhancement - Support for SVG files added: [#1033](https://github.com/owncloud/android/issues/1033)
+
+ SVG files are supported and can be downloaded and viewed.
+
+ https://github.com/owncloud/android/issues/1033
+ https://github.com/owncloud/android/pull/3639
+
+* Enhancement - Full name is shown in shares: [#1106](https://github.com/owncloud/android/issues/1106)
+
+ Full name is shown when using public share instead of username.
+
+ https://github.com/owncloud/android/issues/1106
+ https://github.com/owncloud/android/pull/3636
+
+* Enhancement - Improved copy/move dialog: [#1414](https://github.com/owncloud/android/issues/1414)
+
+ Previously,they appeared exactly the same and there was no way of knowing which
+ was which. Now they are differentiated by the text on the action button.
+
+ https://github.com/owncloud/android/issues/1414
+ https://github.com/owncloud/android/pull/3640
+
+* Enhancement - Share a folder from within the folder: [#1441](https://github.com/owncloud/android/issues/1441)
+
+ You can share a folder clicking in the share icon inside the folder.
+
+ https://github.com/owncloud/android/issues/1441
+ https://github.com/owncloud/android/pull/3659
+
+* Enhancement - New option to show or not hidden files: [#2578](https://github.com/owncloud/android/issues/2578)
+
+ Enable it to show hidden files and folders
+
+ https://github.com/owncloud/android/issues/2578
+ https://github.com/owncloud/android/pull/3624
+
+* Enhancement - What´s new option: [#3352](https://github.com/owncloud/android/issues/3352)
+
+ New option to check what was included in the latest version.
+
+ https://github.com/owncloud/android/issues/3352
+ https://github.com/owncloud/android/pull/3616
+
+* Enhancement - First steps in Android Enterprise integration: [#3415](https://github.com/owncloud/android/issues/3415)
+
+ Two parameters (server url and server url input visibility) can be now managed
+ via MDM. These were the first parameters used to test integration with Android
+ Enterprise and Android Management API.
+
+ https://github.com/owncloud/android/issues/3415
+ https://github.com/owncloud/android/pull/3419
+
+* Enhancement - Provide app feedback to MDM admins: [#3420](https://github.com/owncloud/android/issues/3420)
+
+ Now, when a MDM configuration is applied for the first time or changed by an IT
+ administrator, the app sends feedback that will be shown in the EMM console.
+
+ https://github.com/owncloud/android/issues/3420
+ https://github.com/owncloud/android/pull/3480
+
+* Enhancement - Lock delay enforced: [#3440](https://github.com/owncloud/android/issues/3440)
+
+ A new local setup's option has been added for the application to lock after the
+ selected interval
+
+ https://github.com/owncloud/android/issues/3440
+ https://github.com/owncloud/android/pull/3547
+
+* Enhancement - Release Notes: [#3442](https://github.com/owncloud/android/issues/3442)
+
+ New release notes to show news in updates.
+
+ https://github.com/owncloud/android/issues/3442
+ https://github.com/owncloud/android/pull/3594
+
+* Enhancement - Send for file multiselect: [#3491](https://github.com/owncloud/android/issues/3491)
+
+ Send multiple files at once if they are downloaded.
+
+ https://github.com/owncloud/android/issues/3491
+ https://github.com/owncloud/android/pull/3638
+
+* Enhancement - Improvements for the UI in the passcode screen: [#3516](https://github.com/owncloud/android/issues/3516)
+
+ Redesign of the passcode screen to have the numeric keyboard in the screen
+ instead of using the Android one.
+
+ https://github.com/owncloud/android/issues/3516
+ https://github.com/owncloud/android/pull/3582
+
+* Enhancement - Extended security enforced: [#3543](https://github.com/owncloud/android/issues/3543)
+
+ New extended branding options have been added to make app lock via passcode or
+ pattern compulsory.
+
+ https://github.com/owncloud/android/issues/3543
+ https://github.com/owncloud/android/pull/3544
+
+* Enhancement - Improvements for the UI in the pattern screen: [#3580](https://github.com/owncloud/android/issues/3580)
+
+ Redesign of the pattern screen. Cancel button deleted and new back arrow in the
+ toolbar.
+
+ https://github.com/owncloud/android/issues/3580
+ https://github.com/owncloud/android/pull/3587
+
+* Enhancement - Prevent taking screenshots: [#3596](https://github.com/owncloud/android/issues/3596)
+
+ New option to prevent taking screenshots.
+
+ https://github.com/owncloud/android/issues/3596
+ https://github.com/owncloud/android/pull/3615
+
+* Enhancement - Option to allow screenshots or not in Android Enterprise: [#3625](https://github.com/owncloud/android/issues/3625)
+
+ New parameter to manage screenshots can be configured via MDM.
+
+ https://github.com/owncloud/android/issues/3625
+ https://github.com/owncloud/android/pull/3627
+
+* Enhancement - Thumbnail click action in file detail: [#3653](https://github.com/owncloud/android/pull/3653)
+
+ When a user clicks on a file's detail view thumbnail, the file is automatically
+ downloaded and previewed.
+
+ https://github.com/owncloud/android/pull/3653
+
+# Changelog for ownCloud Android Client [2.20.0] (2022-02-16)
+
+The following sections list the changes in ownCloud Android Client 2.20.0 relevant to
+ownCloud admins and users.
+
+[2.20.0]: https://github.com/owncloud/android/compare/v2.19.0...v2.20.0
+
+## Summary
+
+* Bugfix - Small glitch when side menu is full of accounts: [#3437](https://github.com/owncloud/android/pull/3437)
+* Bugfix - Small bug when privacy policy disabled: [#3542](https://github.com/owncloud/android/pull/3542)
+* Enhancement - Permission dialog removal: [#2524](https://github.com/owncloud/android/pull/2524)
+* Enhancement - Brute force protection: [#3320](https://github.com/owncloud/android/issues/3320)
+* Enhancement - Lock delay for app: [#3344](https://github.com/owncloud/android/issues/3344)
+* Enhancement - Allow access from document provider preference: [#3379](https://github.com/owncloud/android/issues/3379)
+* Enhancement - Security enforced: [#3434](https://github.com/owncloud/android/pull/3434)
+* Enhancement - Respect capability for Avatar support: [#3438](https://github.com/owncloud/android/pull/3438)
+* Enhancement - "Open with" action now allows editing: [#3475](https://github.com/owncloud/android/issues/3475)
+* Enhancement - Enable logs by default in debug mode: [#3526](https://github.com/owncloud/android/issues/3526)
+* Enhancement - Suggest the user to enable enhanced security: [#3539](https://github.com/owncloud/android/pull/3539)
+
+## Details
+
+* Bugfix - Small glitch when side menu is full of accounts: [#3437](https://github.com/owncloud/android/pull/3437)
+
+ Previously, when users set up a large number of accounts, the side menu
+ overlapped the available space quota. Now, everything is contained within a
+ scroll to avoid this.
+
+ https://github.com/owncloud/android/issues/3060
+ https://github.com/owncloud/android/pull/3437
+
+* Bugfix - Small bug when privacy policy disabled: [#3542](https://github.com/owncloud/android/pull/3542)
+
+ Previously, when privacy policy setup was disabled, the side menu showed the
+ privacy policy menu item. Now, option is hidden when privacy policy is disabled.
+
+ https://github.com/owncloud/android/issues/3521
+ https://github.com/owncloud/android/pull/3542
+
+* Enhancement - Permission dialog removal: [#2524](https://github.com/owncloud/android/pull/2524)
+
+ The old permission request dialog has been removed. It was not needed after
+ migrating the storage to scoped storage, read and write permissions are
+ guaranteed in our scoped storage.
+
+ https://github.com/owncloud/android/pull/2524
+
+* Enhancement - Brute force protection: [#3320](https://github.com/owncloud/android/issues/3320)
+
+ Previously, when setting passcode lock, an unlimited number of attempts to
+ unlock the app could be done in a row. Now, from the third incorrect attempt,
+ there will be an exponential growing waiting time until next unlock attempt.
+
+ https://github.com/owncloud/android/issues/3320
+ https://github.com/owncloud/android/pull/3463
+
+* Enhancement - Lock delay for app: [#3344](https://github.com/owncloud/android/issues/3344)
+
+ A new preference has been added to choose the interval in which the app will be
+ unlocked after having unlocked it once, making it more comfortable for those who
+ access the app frequently and have a security lock set.
+
+ https://github.com/owncloud/android/issues/3344
+ https://github.com/owncloud/android/pull/3375
+
+* Enhancement - Allow access from document provider preference: [#3379](https://github.com/owncloud/android/issues/3379)
+
+ Previously, files of ownCloud accounts couldn't be accessed via documents
+ provider when there was a lock set in the app. Now, a new preference has been
+ added to allow/disallow the access, so users have more control over their files.
+
+ https://github.com/owncloud/android/issues/3379
+ https://github.com/owncloud/android/issues/3520
+ https://github.com/owncloud/android/pull/3384
+ https://github.com/owncloud/android/pull/3538
+
+* Enhancement - Security enforced: [#3434](https://github.com/owncloud/android/pull/3434)
+
+ A new branding/MDM option has been added to make app lock via passcode or
+ pattern compulsory, whichever the user chooses.
+
+ https://github.com/owncloud/android/issues/3400
+ https://github.com/owncloud/android/pull/3434
+
+* Enhancement - Respect capability for Avatar support: [#3438](https://github.com/owncloud/android/pull/3438)
+
+ Previously, the user's avatar was shown by default. Now, it is shown or not
+ depending on a new capability.
+
+ https://github.com/owncloud/android/issues/3285
+ https://github.com/owncloud/android/pull/3438
+
+* Enhancement - "Open with" action now allows editing: [#3475](https://github.com/owncloud/android/issues/3475)
+
+ Previously, when a document file was opened and edited with an external app,
+ changes weren't saved because it didn't synchronized with the server. Now, when
+ you edit a document and navigate or refresh in the ownCloud app, it synchronizes
+ automatically, keeping consistence of your files.
+
+ https://github.com/owncloud/android/issues/3475
+ https://github.com/owncloud/android/pull/3499
+
+* Enhancement - Enable logs by default in debug mode: [#3526](https://github.com/owncloud/android/issues/3526)
+
+ Now, when the app is built in DEBUG mode, the logs are enabled by default.
+
+ https://github.com/owncloud/android/issues/3526
+ https://github.com/owncloud/android/pull/3527
+
+* Enhancement - Suggest the user to enable enhanced security: [#3539](https://github.com/owncloud/android/pull/3539)
+
+ When a user sets the passcode or pattern lock on the security screen, the
+ application suggests the user whether to enable or not a biometric lock to
+ unlock the application.
+
+ https://github.com/owncloud/android/pull/3539
+
+# Changelog for ownCloud Android Client [2.19.0] (2021-11-15)
+
+The following sections list the changes in ownCloud Android Client 2.19.0 relevant to
+ownCloud admins and users.
+
+[2.19.0]: https://github.com/owncloud/android/compare/v2.18.3...v2.19.0
+
+## Summary
+
+* Bugfix - Crash in FileDataStorageManager: [#2896](https://github.com/owncloud/android/issues/2896)
+* Bugfix - Account removed is not removed from the drawer: [#3340](https://github.com/owncloud/android/issues/3340)
+* Bugfix - Passcode input misbehaving: [#3342](https://github.com/owncloud/android/issues/3342)
+* Bugfix - Lack of back button in Logs view: [#3357](https://github.com/owncloud/android/issues/3357)
+* Bugfix - ANR after removing account with too many downloaded files: [#3362](https://github.com/owncloud/android/issues/3362)
+* Bugfix - Camera Upload manual retry: [#3418](https://github.com/owncloud/android/pull/3418)
+* Bugfix - Device rotation moves to root in folder picker: [#3431](https://github.com/owncloud/android/pull/3431)
+* Bugfix - Logging does not stop when the user deactivates it: [#3436](https://github.com/owncloud/android/pull/3436)
+* Enhancement - Instant upload only when charging: [#465](https://github.com/owncloud/android/issues/465)
+* Enhancement - Scoped Storage: [#2877](https://github.com/owncloud/android/issues/2877)
+* Enhancement - Delete old logs every week: [#3328](https://github.com/owncloud/android/issues/3328)
+* Enhancement - New Logging Screen 2.0: [#3333](https://github.com/owncloud/android/issues/3333)
+* Enhancement - Delete old user directories in order to free memory: [#3336](https://github.com/owncloud/android/pull/3336)
+
+## Details
+
+* Bugfix - Crash in FileDataStorageManager: [#2896](https://github.com/owncloud/android/issues/2896)
+
+ A possible null value with the account that caused certain crashes on Android 10
+ devices has been controlled.
+
+ https://github.com/owncloud/android/issues/2896
+ https://github.com/owncloud/android/pull/3383
+
+* Bugfix - Account removed is not removed from the drawer: [#3340](https://github.com/owncloud/android/issues/3340)
+
+ When an account was deleted from the device settings, in the accounts section,
+ it was not removed from the Navigation Drawer. Now, when deleting an account
+ from there, the Navigation Drawer is refreshed and the removed account is no
+ more shown.
+
+ https://github.com/owncloud/android/issues/3340
+ https://github.com/owncloud/android/pull/3381
+
+* Bugfix - Passcode input misbehaving: [#3342](https://github.com/owncloud/android/issues/3342)
+
+ Passcode text fields have been made not selectable once a number is written on
+ them, so that we avoid bugs with the digits of the passcode and the way of
+ entering them.
+
+ https://github.com/owncloud/android/issues/3342
+ https://github.com/owncloud/android/pull/3365
+
+* Bugfix - Lack of back button in Logs view: [#3357](https://github.com/owncloud/android/issues/3357)
+
+ A new back arrow button has been added in the toolbar in Logs screen, so that
+ now it's possible to return to the settings screen without the use of physical
+ buttons of the device.
+
+ https://github.com/owncloud/android/issues/3357
+ https://github.com/owncloud/android/pull/3363
+
+* Bugfix - ANR after removing account with too many downloaded files: [#3362](https://github.com/owncloud/android/issues/3362)
+
+ Previously, when a user account was deleted, the application could freeze when
+ trying to delete a large number of files. Now, the application has been fixed so
+ that it doesn't freeze anymore by doing this.
+
+ https://github.com/owncloud/android/issues/3362
+ https://github.com/owncloud/android/pull/3380
+
+* Bugfix - Camera Upload manual retry: [#3418](https://github.com/owncloud/android/pull/3418)
+
+ Previously, when users selected to retry a single camera upload, an error
+ message appeared. Now, the retry of a single upload is enqueued again as
+ expected.
+
+ https://github.com/owncloud/android/issues/3417
+ https://github.com/owncloud/android/pull/3418
+
+* Bugfix - Device rotation moves to root in folder picker: [#3431](https://github.com/owncloud/android/pull/3431)
+
+ Previously, when users rotate the device trying to share photos with oC
+ selecting a non-root folder, folder picker shows the root folder Now, folder
+ picker shows the folder that the user browsed.
+
+ https://github.com/owncloud/android/issues/3163
+ https://github.com/owncloud/android/pull/3431
+
+* Bugfix - Logging does not stop when the user deactivates it: [#3436](https://github.com/owncloud/android/pull/3436)
+
+ Previously, when users disabled the logging option in the settings, the
+ application would not stop logging and the size of the log files would increase.
+ Now, the option to disable it works perfectly and no logs are collected if
+ disabled.
+
+ https://github.com/owncloud/android/issues/3325
+ https://github.com/owncloud/android/pull/3436
+
+* Enhancement - Instant upload only when charging: [#465](https://github.com/owncloud/android/issues/465)
+
+ A new option has been added in the auto upload pictures/videos screen, so that
+ now it's possible to upload pictures or videos only when charging.
+
+ https://github.com/owncloud/android/issues/465
+ https://github.com/owncloud/android/issues/3315
+ https://github.com/owncloud/android/pull/3385
+
+* Enhancement - Scoped Storage: [#2877](https://github.com/owncloud/android/issues/2877)
+
+ The way to store files in the device has changed completely. Previously, the
+ files were stored in the shared storage. That means that apps that had access to
+ the shared storage, could read, write or do whatever they wanted with the
+ ownCloud files.
+
+ Now, ownCloud files are stored in the Scoped Storage, so they are safer. Other
+ apps can access ownCloud files using the Documents Provider, which is the native
+ way to do it, and that means that the ownCloud app has full control of its
+ files.
+
+ Furthermore, if the app is removed, the files downloaded to ownCloud are removed
+ too. So, files are not lost or forgotten in the device after uninstalling the
+ app.
+
+ https://github.com/owncloud/android/issues/2877
+ https://github.com/owncloud/android/pull/3269
+
+* Enhancement - Delete old logs every week: [#3328](https://github.com/owncloud/android/issues/3328)
+
+ Previously, logs were stored but never deleted. It used a lot of storage when
+ logs were enabled for some time. Now, the logs are removed periodically every
+ week.
+
+ https://github.com/owncloud/android/issues/3328
+ https://github.com/owncloud/android/pull/3337
+
+* Enhancement - New Logging Screen 2.0: [#3333](https://github.com/owncloud/android/issues/3333)
+
+ A new option has been added to the logging screen, so that now it's possible to
+ share/delete log files or open them.
+
+ https://github.com/owncloud/android/issues/3333
+ https://github.com/owncloud/android/pull/3408
+
+* Enhancement - Delete old user directories in order to free memory: [#3336](https://github.com/owncloud/android/pull/3336)
+
+ Previously, when users deleted an account the synchronized files of this account
+ stayed on the SD-Card. So if the user didn't want them anymore he had to delete
+ them manually. Now, the app automatically removes the files associated with an
+ account.
+
+ https://github.com/owncloud/android/issues/125
+ https://github.com/owncloud/android/pull/3336
+
+# Changelog for ownCloud Android Client [2.18.3] (2021-10-27)
+
+The following sections list the changes in ownCloud Android Client 2.18.3 relevant to
+ownCloud admins and users.
+
+[2.18.3]: https://github.com/owncloud/android/compare/v2.18.1...v2.18.3
+
+## Summary
+
+* Enhancement - Privacy policy button more accessible: [#3423](https://github.com/owncloud/android/pull/3423)
+
+## Details
+
+* Enhancement - Privacy policy button more accessible: [#3423](https://github.com/owncloud/android/pull/3423)
+
+ The privacy policy button has been removed from "More" settings section, and it
+ has been added to general settings screen as well as to the drawer menu, so that
+ it is easier and more accessible for users.
+
+ https://github.com/owncloud/android/issues/3422
+ https://github.com/owncloud/android/pull/3423
+
+# Changelog for ownCloud Android Client [2.18.1] (2021-07-20)
+
+The following sections list the changes in ownCloud Android Client 2.18.1 relevant to
+ownCloud admins and users.
+
+[2.18.1]: https://github.com/owncloud/android/compare/v2.18.0...v2.18.1
+
+## Summary
+
+* Security - Add PKCE support: [#3310](https://github.com/owncloud/android/pull/3310)
+* Enhancement - Replace picker to select camera folder with native one: [#2899](https://github.com/owncloud/android/issues/2899)
+* Enhancement - Hide "More" section if all options are disabled: [#3271](https://github.com/owncloud/android/issues/3271)
+* Enhancement - Note icon in music player to be branded: [#3272](https://github.com/owncloud/android/issues/3272)
+
+## Details
+
+* Security - Add PKCE support: [#3310](https://github.com/owncloud/android/pull/3310)
+
+ PKCE (Proof Key for Code Exchange) support defined in RFC-7636 was added to
+ prevent authorization code interception attacks.
+
+ https://github.com/owncloud/android/pull/3310
+
+* Enhancement - Replace picker to select camera folder with native one: [#2899](https://github.com/owncloud/android/issues/2899)
+
+ The custom picker to select the camera folder was replaced with the native one.
+ Now, it is ready for scoped storage and some problems to select a folder in the
+ SD Card were fixed. Also, a new field to show the last synchronization timestamp
+ was added.
+
+ https://github.com/owncloud/android/issues/2899
+ https://github.com/owncloud/android/pull/3293
+
+* Enhancement - Hide "More" section if all options are disabled: [#3271](https://github.com/owncloud/android/issues/3271)
+
+ A blank view was shown when all options in "More" subsection were disabled. Now,
+ the subsection is only shown if at least one option is enabled.
+
+ https://github.com/owncloud/android/issues/3271
+ https://github.com/owncloud/android/pull/3296
+
+* Enhancement - Note icon in music player to be branded: [#3272](https://github.com/owncloud/android/issues/3272)
+
+ The note icon in the music player will have the same color as the toolbar, so
+ branded apps can have the icon tinted using their custom theme.
+
+ https://github.com/owncloud/android/issues/3272
+ https://github.com/owncloud/android/pull/3297
+
+# Changelog for ownCloud Android Client [2.18.0] (2021-05-24)
+
+The following sections list the changes in ownCloud Android Client 2.18.0 relevant to
+ownCloud admins and users.
+
+
+
+## Summary
+
+* Bugfix - Snackbar in passcode view is not displayed: [#2722](https://github.com/owncloud/android/issues/2722)
+* Bugfix - Fixed problem when a file is edited externally: [#2752](https://github.com/owncloud/android/issues/2752)
+* Bugfix - Fix navbar is visible in file preview screen after rotation: [#3184](https://github.com/owncloud/android/pull/3184)
+* Bugfix - Fix a bug when some fields where not retrieved from OIDC Discovery: [#3202](https://github.com/owncloud/android/pull/3202)
+* Bugfix - Fix permissions were displayed in share creation view after rotation: [#3204](https://github.com/owncloud/android/issues/3204)
+* Change - Error handling for pattern lock: [#3215](https://github.com/owncloud/android/issues/3215)
+* Change - Hide biometrical if device does not support it: [#3217](https://github.com/owncloud/android/issues/3217)
+* Enhancement - Settings accessible even when no account is attached: [#2638](https://github.com/owncloud/android/issues/2638)
+* Enhancement - Support for apk files: [#2691](https://github.com/owncloud/android/issues/2691)
+* Enhancement - Move to AndroidX Preference and new structure for settings: [#2867](https://github.com/owncloud/android/issues/2867)
+* Enhancement - Replace blank view in music player with cover art: [#3121](https://github.com/owncloud/android/issues/3121)
+* Enhancement - Align previews actions: [#3155](https://github.com/owncloud/android/issues/3155)
+* Enhancement - Fixed account for camera uploads: [#3166](https://github.com/owncloud/android/issues/3166)
+
+## Details
+
+* Bugfix - Snackbar in passcode view is not displayed: [#2722](https://github.com/owncloud/android/issues/2722)
+
+ Snackbar telling about an error in a failed enter or reenter of the passcode
+ wasn't visible. Now, the message is shown in a text just below the passcode
+ input.
+
+ https://github.com/owncloud/android/issues/2722
+ https://github.com/owncloud/android/pull/3210
+
+* Bugfix - Fixed problem when a file is edited externally: [#2752](https://github.com/owncloud/android/issues/2752)
+
+ If an external editor modifies a file, the new size will not match when it is
+ assembled in server side. Fixed by removing the if-match header from the proper
+ place
+
+ https://github.com/owncloud/android/issues/2752
+ https://github.com/owncloud/android/pull/3220
+
+* Bugfix - Fix navbar is visible in file preview screen after rotation: [#3184](https://github.com/owncloud/android/pull/3184)
+
+ Glitch was fixed where the navigation bar became visible in a file preview
+ screen when rotating the device.
+
+ https://github.com/owncloud/android/issues/3139
+ https://github.com/owncloud/android/pull/3184
+
+* Bugfix - Fix a bug when some fields where not retrieved from OIDC Discovery: [#3202](https://github.com/owncloud/android/pull/3202)
+
+ Problem when requesting the OIDC discovery was fixed. Some fields were handled
+ as mandatory, but they are recommended according to the docs. It prevented from
+ a proper login. Now it is possible to login as expected when some fields are not
+ retrieved.
+
+ https://github.com/owncloud/android/pull/3202
+ https://github.com/owncloud/android-library/pull/392
+
+* Bugfix - Fix permissions were displayed in share creation view after rotation: [#3204](https://github.com/owncloud/android/issues/3204)
+
+ Permissions view was shown when creating a share for a file after rotation.
+ Capabilities were taken into account just once. Now, the permissions view is
+ shown only when capabilities match.
+
+ https://github.com/owncloud/android/issues/3204
+ https://github.com/owncloud/android/pull/3234
+
+* Change - Error handling for pattern lock: [#3215](https://github.com/owncloud/android/issues/3215)
+
+ Error messages when an incorrect pattern was entered were shown in a snackbar.
+ Now, they are displayed in a text below the pattern input, just like in the
+ passcode screen.
+
+ https://github.com/owncloud/android/issues/3215
+ https://github.com/owncloud/android/pull/3221
+
+* Change - Hide biometrical if device does not support it: [#3217](https://github.com/owncloud/android/issues/3217)
+
+ Biometric lock preference in "Security" settings subsection was shown even when
+ the device didn't support biometrics (if it was Android 6.0 or later versions).
+ Now, the preference is only shown if the device has the suitable hardware for
+ it.
+
+ https://github.com/owncloud/android/issues/3217
+ https://github.com/owncloud/android/pull/3230
+
+* Enhancement - Settings accessible even when no account is attached: [#2638](https://github.com/owncloud/android/issues/2638)
+
+ Now, settings can be accessed via a button in the login screen, removing the
+ necessity to have an attached account. However, auto picture and video uploads
+ won't be available until an account is registered in the app.
+
+ https://github.com/owncloud/android/issues/2638
+ https://github.com/owncloud/android/pull/3218
+
+* Enhancement - Support for apk files: [#2691](https://github.com/owncloud/android/issues/2691)
+
+ Apk files could be installed from the app after being downloaded. Installation
+ process will be triggered by the system.
+
+ https://github.com/owncloud/android/issues/2691
+ https://github.com/owncloud/android/pull/3156
+ https://github.com/owncloud/android/pull/3162
+
+* Enhancement - Move to AndroidX Preference and new structure for settings: [#2867](https://github.com/owncloud/android/issues/2867)
+
+ Settings have been updated to use the current Android's recommendation, AndroidX
+ framework. In addition, they have been reorganized into subsections for a better
+ understanding and navigation structure. Also, new features have been added: now,
+ source path and behaviour in auto uploads can be chosen differently for pictures
+ and videos.
+
+ https://github.com/owncloud/android/issues/2867
+ https://github.com/owncloud/android/pull/3143
+
+* Enhancement - Replace blank view in music player with cover art: [#3121](https://github.com/owncloud/android/issues/3121)
+
+ Blank view in the music preview player with styled up cover art was replaced.
+ For music files that does not have cover art embodied, it is displayed a
+ placeholder.
+
+ https://github.com/owncloud/android/issues/3121
+ https://github.com/owncloud/android/pull/3182
+
+* Enhancement - Align previews actions: [#3155](https://github.com/owncloud/android/issues/3155)
+
+ Behaviour was aligned through every preview fragment. Images, videos, audios and
+ texts show the same actions now.
+
+ https://github.com/owncloud/android/issues/3155
+ https://github.com/owncloud/android/pull/3177
+
+* Enhancement - Fixed account for camera uploads: [#3166](https://github.com/owncloud/android/issues/3166)
+
+ Camera uploads will be uploaded to a fixed account independently of the current
+ account. Removing the account attached to camera uploads will disable this
+ feature. User will be warned when removing an account that has camera uploads
+ attached.
+
+ https://github.com/owncloud/android/issues/3166
+ https://github.com/owncloud/android/pull/3226
+
+# Changelog for 2.17 versions and below
+
+## 2.17 (March 2021)
+- Toolbar redesign
+- Show thumbnails for every supported file type
+- Fix 301 redirections
+- Fix a crash related to pictures preview
+- Fix two bugs when sharing files with ownCloud
+- Improvements in OAuth2, including
+ + Fix a crash when migrating from OAuth2 to OIDC
+ + Fix a crash when disabling OAuth2
+ + Fix a bug where token was not refreshed properly
+ + Log authentication requests
+ + Support OIDC Dynamic Client Registration
+
+## 2.17 beta v1 (March 2021)
+- Toolbar redesign
+- Show thumbnails for every supported file type
+- Fix 301 redirections
+- Fix a crash related to pictures preview
+- Fix a bug when sharing files with ownCloud
+- Improvements in OAuth2, including
+ + Fix a crash when migrating from OAuth2 to OIDC
+ + Fix a crash when disabling OAuth2
+ + Fix a bug where token was not refreshed properly
+ + Log authentication requests
+ + Support OIDC Dynamic Client Registration
+
+## 2.16.0 (January 2021)
+- Native Android ShareSheet
+- Option to log HTTP requests and responses
+- Move sort menu from toolbar to files view
+- Update background images
+- Search when sharing with ownCloud
+- Bug fixes, including:
+ + Fix a crash while accessing a WebDAV folder
+ + Fix some crashes when rotating the device
+ + Fix a glitch where image was not refreshed properly
+ + Fix some issues when using OCIS
+
+## 2.15.3 (October 2020)
+- Bug fixes, including:
+ + Fix a crash related to downloads notifications
+ + Potential fix for ANR when retrying camera uploads
+ + Removal of legacy header http.protocol.single-cookie-header
+
+## 2.15.2 (September 2020)
+- Update logcat library
+- Bug fixes, including:
+ + Fixed a crash when browsing up
+ + Fixed a crash when logging camera upload request
+ + Fixed a crash related with available offline files
+ + Fixed a crash related with database migration
+
+## 2.15.1 (July 2020)
+- Android 10: TLS 1.3 supported
+- Update network libraries to more recent versions, OkHttp + dav4jvm (old dav4Android)
+- Rearchitecture of avatar and quota features
+- Bug fixes, including:
+ + Fixed some authentication problems regarding password edition
+ + Fixed available offline bad behaviour when the amount of files is huge
+ + Fixed a crash related with FileDataStorageManager
+ + Fixed problem related with server setting `version.hide` to allow users login if such setting is enabled.
+
+## 2.15 (June 2020)
+- Login rearchitecture
+- Support for OpenId Connect
+- Native biometrical lock
+- UI improvements, including:
+ + New bottom navigation bar
+- Support for usernames with '+' (Available since oC 10.4.1)
+- Chunking adaption to oCIS
+- End of support for Android KitKat (4.4)
+- End of support for servers older than 10 version
+- Bug fixes, including:
+ + Fix crash when changing orientation in some operations
+ + Fix OAuth2 token is not renewed after being revoked
+ + Fix occasional crash when opening share by link
+ + Fix navigation loop in shared by link and Av. Offline options
+
+## 2.15 beta v2 (May 2020)
+- Login rearchitecture
+- Support for OpenId Connect
+- Native biometrical lock
+- UI improvements, including:
+ + New bottom navigation bar
+- Support for usernames with '+' (Available since oC 10.4.1)
+- Chunking adaption to oCIS
+- End of support for Android KitKat (4.4)
+- End of support for servers older than 10 version
+- Bug fixes, including:
+ + Fix crash when changing orientation in some operations
+ + Fix OAuth2 token is not renewed after being revoked
+
+## 2.15 beta v1 (May 2020)
+- Login rearchitecture
+- Support for OpenId Connect
+- Native biometrical lock
+- UI improvements, including:
+ + New bottom navigation bar
+- Support for usernames with '+' (Available since oC 10.4.1)
+- End of support for Android KitKat (4.4)
+- End of support for servers older than 10 version
+- Bug fixes, including:
+ + Fix crash when changing orientation in some operations
+ + Fix OAuth2 token is not renewed after being revoked
+
+## 2.14.2 (January 2020)
+- Fix crash triggered when trying to connect to server secured with self signed certificate
+
+## 2.14.1 (December 2019)
+- Some improvements in wizard
+
+## 2.14 (December 2019)
+- Splash screen
+- Shortcut to shared by link files from side menu (contribution)
+- Use new server parameter to set a minimum number of characters for searching users, groups or federated shares
+- End of support for SAML authentication.
+- UI improvements, including:
+ + Mix files and folders when sorting them by date (contribution) or size
+ + Redesign logs view with new tabs, filters and share options (contribution)
+ + Resize cloud image in side menu to not overlap the new side menu options
+- Bug fixes, including:
+ + Avoid overwritten files with the same name during copy or move operations
+ + Retry camera uploads when recovering wifi connectivity and "Upload with wifi only" option is enabled
+
+## 2.13.1 (October 2019)
+- Improve oAuth user experience flow and wording when token expires or becomes invalid
+
+## 2.13 (September 2019)
+- Copy and move files from other third-party apps or internal storage to an ownCloud account through Downloads or Files app
+- Save files in an ownCloud account from third-party apps
+- Copy and move files within the same ownCloud account through Downloads or Files app
+- Add more logs coverage to gather information about known but difficult to reproduce issues
+- UI improvements, including:
+ + Show date and size for every file in Available Offline option from side menu
+
+## 2.12 (August 2019)
+- Shares rearchitecture
+- UI improvements, including:
+ + Private link accessible when share API is disabled
+- Bug fixes, including:
+ + Fix images not detected in Android 9 gallery after being downloaded
+
+## 2.12 beta v1 (August 2019)
+- Shares rearchitecture
+- UI improvements, including:
+ + Private link accessible when share API is disabled
+- Bug fixes, including:
+ + Fix images not detected in Android 9 gallery after being downloaded
+
+## 2.11.1 (June 2019)
+- Fix crash triggered when notifying upload results
+
+## 2.11 (June 2019)
+- Replace ownCloud file picker with the Android native one when uploading files (contribution)
+- Send logs to support, enable it via new developer menu (contribution)
+- Logs search (contribution)
+- Shortcut to available offline files from side menu
+- Document provider: files and folders rename, edition and deletion.
+- Document provider: folder creation
+- Document provider: multiaccount support
+- UI improvements, including:
+ + Notch support
+ + Batched permission errors when deleting multiple files (contribution)
+- Bug fixes, including:
+ + Fix just created folder disappears when synchronizing parent folder
+ + Fix crash when clearing successful/failed uploads (contribution)
+ + Fix download progress bar still visible after successful download
+ + Fix UI glitch in warning icon when sharing a file publicly (contribution)
+ + Fix crash when sharing files with ownCloud and creating new folder (contribution)
+ + Fix canceling dialog in settings turns on setting (contribution)
+ + Bring back select all and select inverse icons to the app bar (contribution)
+ + Fix folder with brackets [ ] does not show the content
+ + Fix login fails with "§" in password
+
+## 2.11 beta v1 (May 2019)
+- Send logs to support, enable it via new developer menu (contribution)
+- Logs search (contribution)
+- Shortcut to available offline files from side menu
+- Document provider: files and folders rename, edition and deletion.
+- Document provider: folder creation
+- Document provider: multiaccount support
+- UI improvements, including:
+ + Notch support
+- Bug fixes, including:
+ + Fix download progress bar still visible after successful download
+ + Fix UI glitch in warning icon when sharing a file publicly (contribution)
+ + Fix crash when sharing files with ownCloud and creating new folder (contribution)
+ + Fix canceling dialog in settings turns on setting (contribution)
+ + Bring back select all and select inverse icons to the app bar (contribution)
+ + Fix folder with brackets [ ] does not show the content
+ + Fix login fails with "§" in password
+
+## 2.10.1 (April 2019)
+- Content provider improvements
+
+## 2.10.0 (March 2019)
+- Android 9 (P) support (contribution)
+- Allow light filtering apps (optional)
+- Show additional info (user ID, email) when sharing with users with same display name
+- Support more options to enforce password when sharing publicly
+- Select all and inverse when uploading files (contribution)
+- Sorting options in sharing view (contribution)
+- Batched notifications for file deletions (contribution)
+- Commit hash in settings (contribution)
+- UI improvements, including:
+ + Disable log in button when credentials are empty (contribution)
+ + Warning to properly set camera folder in camera uploads
+- Bug fixes, including:
+ + Some camera upload issues in Android 9 (P) (contribution)
+ + Fix eye icon not visible to show/hide password in public shares (contribution)
+ + Fix welcome wizard rotation (contribution)
+
+## 2.10.0 beta v1 (February 2019)
+- Android 9 (P) support (contribution)
+- Select all and inverse when uploading files (contribution)
+- Sorting options in sharing view (contribution)
+- Batched notifications for file deletions (contribution)
+- Commit hash in settings (contribution)
+- UI improvements, including:
+ + Disable log in button when credentials are empty (contribution)
+ + Warning to properly set camera folder in camera uploads
+- Bug fixes, including:
+ + Some camera upload issues in Android 9 (P) (contribution)
+ + Fix eye icon not visible to show/hide password in public shares (contribution)
+ + Fix welcome wizard rotation (contribution)
+
+## 2.9.3 (November 2018)
+- Bug fixes for users with username containing @ character
+
+## 2.9.2 (November 2018)
+- Bug fixes for users with username containing spaces
+
+## 2.9.1 (November 2018)
+- Bug fixes for LDAP users using uid:
+ + Fix login not working
+ + Fix empty list of files
+
+## 2.9.0 (November 2018)
+- Search in current folder
+- Select all/inverse files (contribution)
+- Improve available offline files synchronization and conflict resolution (Android 5 or higher required)
+- Sort files in file picker when uploading (contribution)
+- Access ownCloud files from files apps, even with files not downloaded
+- New login view
+- Show re-shares
+- Switch apache and jackrabbit deprecated network libraries to more modern and active library, OkHttp + Dav4Android
+- UI improvements, including:
+ + Change edit share icon
+ + New gradient in top of the list of files (contribution)
+ + More accurate message when creating folders with the same name (contribution)
+- Bug fixes, including:
+ + Fix some crashes:
+ - When rebooting the device
+ - When copying, moving files or choosing a folder within camera uploads feature
+ - When creating private/public link
+ + Fix some failing downloads
+ + Fix pattern lock being asked very often after disabling fingerprint lock (contribution)
+
+## 2.9.0 beta v2 (October 2018)
+- Bug fixes, including:
+ + Fix some crashes:
+ - When rebooting the device
+ - When copying, moving files or choosing a folder within camera uploads feature
+ + Fix some failing downloads
+ + Fix pattern lock being asked very often after disabling fingerprint lock
+
+## 2.9.0 beta v1 (September 2018)
+- Switch apache and jackrabbit deprecated libraries to more modern and active library, OkHttp
+- Search in current folder
+- Select all/inverse files
+- New login view
+- Show re-shares
+- UI improvements, including:
+ + Change edit share icon
+ + New gradient in top of the list of files
+
+## 2.8.0 (July 2018)
+- Side menu redesign
+- User quota in side menu
+- Descending option when sorting
+- New downloaded/offline icons and pins
+- One panel design for tablets
+- Custom tabs for OAuth
+- Improve public link sharing permissions for folders
+- Redirect to login view when SAML session expires
+- UI improvements, including:
+ + Fab button above snackbar
+ + Toggle to control password visibility when sharing via link
+ + Adaptive icons support (Android 8 required)
+- Bug fixes, including:
+ + Fix block for deleted basic/oauth accounts
+ + Fix available offline when renaming files
+ + Fix camera directory not selectable in root
+ + Fix guest account showing an empty file list
+ + Hide keyboard when going back from select user view
+ + Fix black "downloading screen" message when downloading an image offline
+ + Show proper timestamp in uploads/downloads notification
+ + Fix sharing when disabling files versioning app in server
+
+## 2.8.0 beta v1 (May 2018)
+- Side menu redesign
+- User quota in side menu
+- Descending option when sorting
+- New downloaded/offline icons and pins
+- One panel design for tablets
+- Custom tabs for OAuth
+- UI improvements, including:
+ + Fab button above snackbar
+ + Toggle to control password visibility when sharing via link
+- Bug fixes, including:
+ + Fix block for deleted basic/oauth accounts
+ + Fix available offline when renaming files
+ + Fix camera directory not selectable in root
+ + Fix guest account showing an empty file list
+ + Hide keyboard when going back from select user view
+ + Fix black "downloading screen" message when downloading an image offline.
+
+## 2.7.0 (April 2018)
+- Fingerprint lock
+- Pattern lock (contribution)
+- Upload picture directly from camera (contribution)
+- GIF support
+- New features wizard
+- UI improvements, including:
+ + Display file size during upload (contribution)
+ + Animations when switching folders
+- Bug fixes, including:
+ + Hide always visible notification in Android 8
+
+## 2.7.0 beta v1 (March 2018)
+- Fingerprint lock
+- Pattern lock (contribution)
+- Upload picture directly from camera (contribution)
+- GIF support
+- New features wizard
+- UI improvements, including:
+ + Display file size during upload (contribution)
+- Bug fixes, including:
+ + Hide always visible notification in Android 8
+
+## 2.6.0 (February 2018)
+- Camera uploads, replacing instant uploads (Android 5 or higher required)
+- Android 8 support
+- Notification channels (Android 8 required)
+- Private link (OC X required)
+- Fixed typos in some translations
+
+## 2.5.1 beta v1 (November 2017)
+- Camera uploads (replacing instant uploads)
+- Android O support
+- Notification channels (Android O required)
+- Private link (OC X required)
+- Fixed typos in some translations
+
+## 2.5.0 (October 2017)
+- OAuth2 support
+- Show file listing option (anonymous upload) when sharing a folder (OC X required)
+- First approach to fix instant uploads
+- UI improvements, including:
+ + Hide share icon when resharing is forbidden
+ + Improve feedback when uploading infected files
+- Bug fixes
+
+## 2.4.0 (May 2017)
+- Video streaming
+- Multiple public links per file (OC X required)
+- Share with custom groups (OC X required)
+- Automated retry of failed transfers in Android 6 and 7
+- Save shared text as new file
+- File count per section in uploads view
+- UI improvements, including:
+ + Share view update
+- Bug fixes
+
+## 2.3.0 (March 2017)
+- Included privacy policy.
+- Error messages improvement.
+- Design/UI improvement: snackbars replace toasts.
+- Bugs fixed, including:
+ + Crash when other app uses same account name.
+
+## 2.2.0 (December 2016)
+- Set folders as Available Offline
+- New navigation drawer, with avatar and account switch.
+- New account manager, accessible from navigation drawer.
+- Set edit permissions in federated shares of folders (OC server >= 9.1)
+- Monitor and revoke session from web UI (OC server >= 9.1)
+- Improved look and contents of file menu.
+- Bugs fixed, including:
+ + Keep modification time of uploaded files.
+ + Stop audio when file is deleted.
+ + Upload of big files.
+
+## 2.1.2 (September 2016)
+- Instant uploads fixed in Android 6.
+
+## 2.1.1 (September 2016)
+- Instant uploads work in Android 7.
+- Select your camera folder to upload pictures or videos from any
+ camera app.
+- Multi-Window support for Android 7.
+- Size of folders shown in list of files.
+- Sort by size your list of files.
+
+## 2.1.0 (August 2016)
+- Select and handle multiple files
+- Sync files on tap
+- Access files through Documents Provider
+- "Can share" option for federated shares (server 9.1+)
+- Full name shown instead of user name
+- New icon
+- Style and sorting fixes
+- Bugs fixed, including:
+ + Icon "available offline" shown when set
+ + Trim blanks of username in login view
+ + Protect password field from suggestions
+
## 2.0.1 (June 2016)
- Favorite files are now called AVAILABLE OFFLINE
- New overlay icons
@@ -6,7 +3852,7 @@
+ Passwords with non-alphanumeric characters work fine
+ Sending files from other apps does not duplicate them
+ Favorite setting is not lost after uploading
- + Instant uploads waiting for Wi-Fi are not shown as failed
+ + Instant uploads waiting for Wi-Fi are not shown as failed
## 2.0.0 (April 2016)
- Uploads view: track the progress of your uploads and handle failures
@@ -69,7 +3915,7 @@
- Bugs fixed:
+ Frequent crashes in folder with several images
+ Sync error in servers with huge quota and external storage enable
- + Share by link error
+ + Share by link error
+ Some other crashes and minor bugs
## 1.7.1 (April 2015)
@@ -93,7 +3939,7 @@
- Uploads:
+ Local thumbnails in section "Files"
+ Multiple selection in "Content from other apps" (Android 4.3+)
-- Gallery:
+- Gallery:
+ proper handling of EXIF
+ obey sorting in the list of files
- Settings view updated
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 000000000000..47dc3e3d863c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000000..ce144bf1fa06
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,8 @@
+# Code of Conduct
+
+This project follows the ownCloud Code of Conduct.
+
+Please read the full Code of Conduct at:
+** **
+
+By participating in this project, you agree to abide by its terms.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 620c2ab9d63c..26b60b34b467 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,66 +1,70 @@
-## Submitting issues
+# Contributing
-If you have questions about how to use ownCloud, please direct these to the [mailing list][mailinglist] or our [forum][forum]. We are also available on [IRC][irc].
+Thank you for your interest in contributing to this project!
-### Guidelines
-* [Report the issue](https://github.com/owncloud/android/issues/new) using our [template][template], it includes all the informations we need to track down the issue.
-* This repository is *only* for issues within the ownCloud Android app code. Issues in other compontents should be reported in their own repositores:
- - [ownCloud core](https://github.com/owncloud/core/issues)
- - [iOS client](https://github.com/owncloud/ios-issues/issues)
- - [Desktop client](https://github.com/owncloud/mirall/issues)
- - [ownCloud apps](https://github.com/owncloud/apps/issues) (e.g. Calendar, Contacts...)
-* Search the [existing issues](https://github.com/owncloud/android/issues) first, it's likely that your issue was already reported.
-
-If your issue appears to be a bug, and hasn't been reported, open a new issue.
-
-Help us to maximize the effort we can spend fixing issues and adding new features, by not reporting duplicate issues.
+Please read the full contributing guidelines at:
+** **
-[template]: https://raw.github.com/owncloud/android/master/issue_template.md
-[mailinglist]: https://mailman.owncloud.org/mailman/listinfo/user
-[forum]: http://forum.owncloud.org/
-[irc]: http://webchat.freenode.net/?channels=owncloud&uio=d4
+For development setup, coding standards, and pull request process,
+see the [README](https://github.com/owncloud/android/blob/master/README.md) in this repository.
## Contributing to Source Code
-Thanks for wanting to contribute source code to ownCloud. That's great!
+### Guidelines
-Before we're able to merge your code into the ownCloud app for Android, you need to sign our [Contributor Agreement][agreement].
+* Contribute your code in a feature, fix, improvement or technical enhancement branch by using one of the following branch names:
-### Guidelines
-* Contribute your code in the branch 'master'. It will give us a better chance to test your code before merging it with stable code.
-* For your first contribution, start a pull request on master and send us the signed [Contributor Agreement][agreement].
+ - ```feature/feature_name``` → new features in the app
+ - ```fix/fix_name``` → fixing problems or bugs, always welcome!
+ - ```improvement/improvement_name``` → make even better an existing feature
+ - ```technical/technical_description``` → code review, DB... technical stuff improved
+
+ 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 OneFlow as branching model. It's something as useful as easy:
+
+ * `master` will stay as main branch. Everything will work around it.
+ * Feature branch: new branch created from `master`. Once it is finished and DoD accomplished, rebased and merged into `master`.
+ * Release branch: will work as any feature branch. Before rebasing and merging into `master`, release tag must be signed.
+ * Hotfix branch: created from latest tag. Once it is finished, tag must be signed. Then, rebased and merged into `master`.
+ * The way to get an specific version is browsing through the tags.
-[agreement]: http://owncloud.org/about/contributor-agreement/
+ Interesting [link](https://www.endoflineblog.com/oneflow-a-git-branching-model-and-workflow) about this.
### 1. Fork and download android/master repository:
* Please follow [SETUP.md](https://github.com/owncloud/android/blob/master/SETUP.md) to setup ownCloud Android app work environment.
-
### 2. Create pull request:
-NOTE: You must sign the [Contributor Agreement][agreement] before your changes can be accepted!
-
-* Commit your changes locally: ```git commit -a```
-* Push your changes to your GitHub repo: ```git push```
-* Browse to https://github.com/YOURGITHUBNAME/android/pulls and issue pull request
+* Create new feature, fix, improvement or technical enhancement branch from your master branch: ```git checkout -b feature/feature_name```
+* Register your changes: `git add filename`
+* Commit your changes locally. Please, if posible use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to add descriptive messages to the commits. Take the simplest approach:
+ - Feature commits: `feat: brief description of the changes performed`
+ - Fix commits: `fix: brief description of applied fix`
+ - Test commits: `test: brief description of developed tests`
+ - Calens commits: `chore: add calens file`
+
+ Submit the commit with ```git commit -m "commit message"```
+* Push your changes to your GitHub repo: ```git push origin feature/feature_name```
+* Browse to `https://github.com/YOURGITHUBNAME/android/pulls` and issue pull request
* Enter description and send pull request.
-### 3. Create another pull request:
-
-To make sure your new pull request does not contain commits which are already contained in previous PRs, create a new branch which is a clone of upstream/master.
+### 3. Update your contribution branch with master changes:
-* ```git fetch upstream```
-* ```git checkout -b my_new_master_branch upstream/master```
-* If you want to rename that branch later: ```git checkout -b my_new_master_branch_with_new_name```
-* Push branch to server: ```git push -u origin name_of_local_master_branch```
-* Use GitHub to issue PR
+It is possible you see the next message from time to time.
+
+To fix this and make sure your contribution branch is updated with official android/master, you need to perform the next steps:
-## Translations
-Please submit translations via [Transifex][transifex].
+* Checkout your master branch: ```git checkout master```
+* Get and apply official android/master branch changes in your master branch: ```git fetch upstream``` + ```git rebase upstream/master```. Now you have your master branch updated with official master branch changes.
+* Checkout your contribution branch: ```git checkout feature/feature_name```
+* Rebase contribution branch with master to put your contribution commits after the last commit of master branch, ensuring a clean commits history: ```git rebase master```. If there's some conflicts, solve it by using rebase in different steps.
+* Push branch to server: ```git push -f origin feature/feature_name```. At this point, the message ```This branch is out-of-date with the base branch``` should disappear.
-[transifex]: https://www.transifex.com/projects/p/owncloud/
+## Versioning
+In order to check or review the stable versions, all available tags can be fetched with the command `git fetch --tags` and listed with the command `git tag`. The tag `latest` is also available pointing to the latest released version.
diff --git a/README.md b/README.md
index 1fc54a108267..d73b48ace24f 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,115 @@
-# [ownCloud](https://owncloud.org) Android app
-[](https://play.google.com/store/apps/details?id=com.owncloud.android)
+# ownCloud Android App
-[](https://play.google.com/store/apps/details?id=com.owncloud.android) [](https://play.google.com/store/apps/details?id=com.owncloud.android) [](https://play.google.com/store/apps/details?id=com.owncloud.android)
+
-## Join development!
+[](LICENSE.txt) [](https://kiteworks.com/opensource)
-**Build status:** master  stable 
+[](https://github.com/owncloud/android/actions/workflows/android-unit-tests.yml) [](https://github.com/owncloud/android/actions/workflows/android-instrumented-data-tests.yml) [](https://github.com/owncloud/android/actions/workflows/detekt.yml) [](https://github.com/owncloud/android/actions/workflows/conventional-commits.yml)
-**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 [junior jobs](https://github.com/owncloud/android/issues?q=is%3Aopen+is%3Aissue+label%3A%22Junior+Job%22).
+The ownCloud Android app enables users to access, sync, and share files stored on their ownCloud server directly from Android devices. Built with Kotlin, it supports ownCloud Infinite Scale (oCIS) and ownCloud Classic servers, featuring file browsing, photo galleries, Spaces, sharing, passcode lock, and biometric authentication. The app is available on:
-**IRC channel:** [#owncloud-android](https://webchat.freenode.net/?channels=owncloud-android) on freenode IRC
+
-**Beta version** [beta version](https://github.com/owncloud/android/raw/beta/apks/latest.apk)
+| | | | |
+| ---------------------------------------------- | -------------------------------------------- | ------------------------------------------- | ------------------------------------------- |
-**License:** [GPLv2](https://github.com/owncloud/android/blob/master/LICENSE.txt)
+## Part of Mobile (Android)
+
+This is the official [ownCloud Android app](https://github.com/owncloud/android), the primary mobile client for Android users. It connects to both [ownCloud Infinite Scale (oCIS)](https://github.com/owncloud/ocis) and [ownCloud Classic](https://github.com/owncloud/core).
+
+## Getting Started
+
+1. Read [SETUP.md](https://github.com/owncloud/android/blob/master/SETUP.md) for development environment setup
+2. Fork the repository and clone it locally
+3. Open the project in Android Studio
+4. Build and run using `./gradlew assembleDebug`
+
+### Beta Testing
+
+- **Play Store:** Download the app from Play Store, scroll down to the beta section, and tap "I'm in"
+- **F-Droid:** Open the ownCloud tab in F-Droid and download the latest beta version
+
+## Documentation
+
+- [SETUP.md](https://github.com/owncloud/android/blob/master/SETUP.md) - Development setup guide
+- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guidelines
+- [CHANGELOG.md](https://github.com/owncloud/android/blob/master/CHANGELOG.md) - Release history
+- [ownCloud documentation](https://doc.owncloud.com)
+
+## Community & Support
+
+**[Star](https://github.com/owncloud/android)** this repo and **Watch** for release notifications!
+
+- [ownCloud Website](https://owncloud.com)
+- [Community Discussions](https://github.com/orgs/owncloud/discussions)
+- [Matrix Chat](https://app.element.io/#/room/#owncloud:matrix.org)
+- [Documentation](https://doc.owncloud.com)
+- [Enterprise Support](https://owncloud.com/contact-us/)
+- [OSPO Home](https://kiteworks.com/opensource)
+
+## Contributing
+
+We welcome contributions! Please read the [Contributing Guidelines](CONTRIBUTING.md)
+and our [Code of Conduct](CODE_OF_CONDUCT.md) before getting started.
+
+### Workflow
+
+- **Rebase Early, Rebase Often!** We use a rebase workflow. Always rebase on the target branch before submitting a PR.
+- **Dependabot**: Automated dependency updates are managed via Dependabot. Review and merge dependency PRs promptly.
+- **Signed Commits**: All commits **must** be PGP/GPG signed. See [GitHub's signing guide](https://docs.github.com/en/authentication/managing-commit-signature-verification).
+- **DCO Sign-off**: Every commit must carry a `Signed-off-by` line:
+ ```
+ git commit -s -S -m "your commit message"
+ ```
+- **GitHub Actions Policy**: Workflows may only use actions that are (a) owned by `owncloud`, (b) created by GitHub (`actions/*`), or (c) verified in the GitHub Marketplace.
+
+## Translations
+
+Help translate this project on Transifex:
+** **
+
+Please submit translations via Transifex -- do not open pull requests for translation changes.
+
+## Security
+
+**Do not open a public GitHub issue for security vulnerabilities.**
+
+Report vulnerabilities at **** -- see [SECURITY.md](SECURITY.md).
+
+Bug bounty: [YesWeHack ownCloud Program](https://yeswehack.com/programs/owncloud-bug-bounty-program)
+
+## License
+
+This project is licensed under the [GPL-2.0](LICENSE.txt).
+
+## About the ownCloud OSPO
+
+The [Kiteworks Open Source Program Office](https://kiteworks.com/opensource), operating under
+the [ownCloud](https://owncloud.com) brand, launched on May 5, 2026, to steward the open source
+ecosystem around ownCloud's products. The OSPO ensures transparent governance, license compliance,
+community health, and sustainable collaboration between the open source community and
+[Kiteworks](https://www.kiteworks.com), which acquired ownCloud in 2023.
+
+- **OSPO Home**:
+- **GitHub**:
+- **ownCloud**:
+
+For questions about the OSPO or licensing, contact ospo@kiteworks.com.
+
+### License Migration to Apache 2.0
+
+The OSPO is driving a strategic relicensing of ownCloud repositories toward the
+[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0), following
+the [Apache Software Foundation's third-party license policy](https://www.apache.org/legal/resolved.html).
+
+Individual repositories will migrate as their audit is completed. The LICENSE file
+in each repo reflects its **current** license status (not the target).
+
+**Current license: GPL-2.0** (Category X per Apache policy -- cannot be included in Apache-2.0 works).
+
+Migration prerequisites for this repository:
+
+- **CLA/DCO coverage**: All past contributors must have signed agreements permitting relicensing
+- **Copyleft dependency audit**: All GPL dependencies must be replaced or isolated
+- **KDE heritage review**: Any code with KDE-era copyrights requires legal analysis
+- **Complete relicensing**: GPL-2.0 is a strong copyleft license; migration requires full relicensing of all files
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000000..78094ae4caf5
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,11 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+**Do NOT open a public GitHub issue for security vulnerabilities.**
+
+Please report security issues responsibly via:
+****
+
+You can also report vulnerabilities through our YesWeHack bug bounty program:
+****
diff --git a/SETUP.md b/SETUP.md
index 5e79f9aef8c8..424f134e22e7 100644
--- a/SETUP.md
+++ b/SETUP.md
@@ -1,3 +1,5 @@
+### Setup Information
+
These instructions will help you to set up your development environment, get the source code of the ownCloud for Android app and build it by yourself. If you want to help developing the app take a look to the [contribution guidelines][0].
Sections 1) and 2) are common for any environment. The rest of the sections describe how to set up a project in different tool environments. Nowadays we recommend to use Android Studio (section 2), but you can also build the app from the command line (section 3).
@@ -16,8 +18,8 @@ The [Android SDK][3] is necessary to build the app. There are different options
Open a terminal and type 'android' to start the Android SDK Manager. To build the ownCloud for Android app you will need to install at least the next SDK packages:
* Android SDK Tools and Android SDK Platform-tools (already installed); upgrade to their last versions is usually a good idea.
-* Android SDK Build-Tools; any version from 23 or later should work fine; avoid preview versions, if any available.
-* Android 6.0 (API 23), SDK Platform; needed to build the owncloud app.
+* No longer need to specify a version for the build tools, Gradle plugin uses the minimum required version by default.
+* Android 12.0 (API 31), SDK Platform; needed to build the owncloud app.
Install any other package you consider interesting, such as emulators.
@@ -32,34 +34,32 @@ Next steps will assume you have a Github account and that you will get the code
* In a web browser, go to https://github.com/owncloud/android, and click the 'Fork' button near the top right corner.
* Open a terminal and go on with the next steps in it.
-* Clone your forked repository: ```git clone --recursive git@github.com:YOURGITHUBNAME/android.git```.
+* Clone your forked repository: ```git clone https://github.com/YOURGITHUBNAME/android.git```.
* Move to the project folder with ```cd android```.
-* Pull any changes from your remote branch 'master': ```git pull origin master```
-* Make official ownCloud repo known as upstream: ```git remote add upstream git@github.com:owncloud/android.git```
-* Make sure to get the latest changes from official android/master branch: ```git pull upstream master```
+* Fetch and apply any changes from your remote branch 'master': ```git fetch``` + ```git rebase```
+* Make official ownCloud repo known as upstream: ```git remote add upstream https://github.com/owncloud/android.git```
+* Make sure to get and apply the latest changes from official android/master branch: ```git fetch upstream``` + ```git rebase upstream/master```
At this point you can continue using different tools to build the project. Section 2 and 3 describe the existing alternatives.
### 2. Working with Android Studio.
-[Android Studio][5] is currently the official Android IDE. Due to this, we recommend it as the IDE to use in your development environment. Follow the installation instructions [here][6].
+[Android Studio][5] is currently the official Android IDE. Due to this, we recommend it as the IDE to use in your development environment.
We recommend to use the last version available in the stable channel of Android Studio updates. See what update channel is your Android Studio checking for updates in the menu path 'Help'/'Check for Update...'/link 'Updates' in the dialog.
To set up the project in Android Studio follow the next steps:
-* Make sure you have called ```git submodule update``` whenever you switched branches
* Open Android Studio and select 'Import Project (Eclipse ADT, Gradle, etc)'. Browse through your file system to the folder 'android' where the project is located. Android Studio will then create the '.iml' files it needs. If you ever close the project but the files are still there, you just select 'Open Project...'. The file chooser will show an Android face as the folder icon, which you can select to reopen the project.
* Android Studio will try to build the project directly after importing it. To build it manually, follow the menu path 'Build'/'Make Project', or just click the 'Play' button in the tool bar to build and run it in a mobile device or an emulator. The resulting APK file will be saved in the 'build/outputs/apk/' subdirectory in the project folder.
### 3. Working in a terminal with Gradle:
-[Gradle][7] is the build system used by Android Studio to manage the building operations on Android apps. You do not need to install Gradle in your system, and Google recommends not to do it, but instead trusting on the [Graddle wrapper][8] included in the project.
+[Gradle][6] is the build system used by Android Studio to manage the building operations on Android apps. You do not need to install Gradle in your system, and Google recommends not to do it, but instead trusting on the [Gradle wrapper][7] included in the project.
* Open a terminal and go to the 'android' directory that contains the repository.
-* Make sure you have called ```git submodule update``` whenever you switched branches
* Run the 'clean' and 'build' tasks using the Gradle wrapper provided
- Windows: ```gradlew.bat clean build```
- Mac OS/Linux: ```./gradlew clean build```
@@ -74,9 +74,6 @@ The generated APK file is saved in android/build/outputs/apk as android-debug.ap
[2]: https://git-scm.com/downloads
[3]: https://developer.android.com/sdk/index.html
[4]: https://developer.android.com/sdk/installing/index.html
-[5]: https://developer.android.com/tools/studio/index.html
-[6]: https://developer.android.com/sdk/installing/index.html?pkg=studio
-[7]: https://gradle.org/
-[8]: https://docs.gradle.org/current/userguide/gradle_wrapper.html
-[9]: https://eclipse.org/
-[10]: http://developer.android.com/sdk/installing/installing-adt.html
+[5]: https://developer.android.com/studio
+[6]: https://gradle.org/
+[7]: https://docs.gradle.org/current/userguide/gradle_wrapper.html
diff --git a/SUPPORT.md b/SUPPORT.md
new file mode 100644
index 000000000000..d87c0ca865a8
--- /dev/null
+++ b/SUPPORT.md
@@ -0,0 +1,10 @@
+# Support
+
+For support with this project, please use the following channels:
+
+- **Enterprise Support**:
+- **Community discussions**: https://github.com/orgs/owncloud/discussions
+- **Matrix Chat**:
+- **Documentation**:
+
+Please do not use GitHub issues for general support questions.
diff --git a/THIRD_PARTY.txt b/THIRD_PARTY.txt
index 7b21bfc321d4..57bd31e343e6 100644
--- a/THIRD_PARTY.txt
+++ b/THIRD_PARTY.txt
@@ -1,7 +1,7 @@
###################################################################
- ownCloud Android client
+ ownCloud Android client
- Copyright (C) 2012-2013 ownCloud Inc.
+ Copyright (C) 2020 ownCloud GmbH.
Copyright (C) 2012 Bartek Przybylski
###################################################################
@@ -11,7 +11,7 @@
###########
This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License verions 2,
+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,
@@ -20,7 +20,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
The source distribution of this program should include a full copy
-of the GNU GPL version 2 license in the LICENSE.txt file located
+of the GNU GPL version 2 license in the LICENSE.txt file located
in its root directory. If not, see .
@@ -30,20 +30,12 @@ in its root directory. If not, see .
Both the source and binary distributions of this software contain
some third party software. All the third party software included
-or linked is redistributed under the terms and conditions of their
-original licenses. These licenses are compatible the GPL license
+or linked is redistributed under the terms and conditions of their
+original licenses. These licenses are compatible the GPL license
that govern this software, for the purposes they are being used.
The third party software included and used by this project is:
- * Apache JackRabbit, version 2.2.5.
- Copyright (C) 2004-2010 The Apache Software Foundation.
- Licensed under Apache License, Version 2.0.
- Placed at libs/jackrabbit-webdav-2.2.5-jar-with-dependencies.jar
- The jar file must be included in the ownCloud client APK.
- Original license document included at libs/LICENSE.txt
- See http://jackrabbit.apache.org/
-
* Transifex client.
Copyright (C) Transifex.
Licensed under GNU General Public License.
@@ -51,16 +43,37 @@ The third party software included and used by this project is:
Used as a helper tool, not included in the ownCloud client APK.
Original license document included at third_party/transifex-client/LICENSE.
See http://help.transifex.com/features/client/
-
- * TouchImageView, commit 6dbeac4f11936185ba374c73144ac431c23c9aab
- Copyright (c) 2012 Michael Ortiz
- Licensed under MIT License
- JAR file libs/touch-image-view.jar has been generated by ownCloud Inc., including without
- modifications com.ortiz.touch.ExtendedViewPager and com.ortiz.touch.TouchImageView classes.
- See https://github.com/MikeOrtiz/TouchImageView
-
+
* floatingactionbutton 1.10.1.
Copyright (c) 2014 Jerzy Chalupski
Licensed under Apache License, Version 2.0.
- placed at libs/com-getbase-floatingactionbutton-1-10-0-exploded-aar has been exploded by ownCloud Inc.
- See https://github.com/futuresimple/android-floating-action-button
\ No newline at end of file
+ placed at libs/com-getbase-floatingactionbutton-1-10-0-exploded-aar has been exploded by ownCloud GmbH.
+ See https://github.com/futuresimple/android-floating-action-button
+
+* PatternLockView, version 1.0.0
+ Copyright (C) 2017 aritraroy
+ Licensed under Apache License, Version 2.0.
+ Included in the ownCloud client APK.
+ See https://github.com/aritraroy/PatternLockView
+
+ * Glide, version 4.6.1
+ Copyright (C) 2014 Google, Inc.
+ Licensed under BSD, part MIT and Apache 2.0.
+ Included in the ownCloud client APK.
+ See https://github.com/bumptech/glide
+
+ * PhotoView, version 2.0.0
+ Copyright (C) 2017 Chris Banes
+ Licensed under Apache License, Version 2.0.
+ Included in the ownCloud client APK.
+ See https://github.com/chrisbanes/PhotoView
+
+ * OkHttp, version 4.6.0
+ Licensed under Apache License, Version 2.0.
+ Included in the ownCloud client APK.
+ See https://github.com/square/okhttp
+
+ * Dav4Jvm
+ Licensed under Mozilla Public License, Version 2.0.
+ Included in the ownCloud client APK.
+ See https://gitlab.com/bitfireAT/dav4jvm
diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md
new file mode 100644
index 000000000000..951f36de9a59
--- /dev/null
+++ b/TROUBLESHOOTING.md
@@ -0,0 +1,4 @@
+#Troubleshooting
+
+Check the documentation for troubleshooting information:
+https://doc.owncloud.com/android/troubleshooting.html
\ No newline at end of file
diff --git a/androidTest/AndroidManifest.xml b/androidTest/AndroidManifest.xml
deleted file mode 100644
index 93c2f505ff67..000000000000
--- a/androidTest/AndroidManifest.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityTest.java b/androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityTest.java
deleted file mode 100644
index d367976d8fb1..000000000000
--- a/androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityTest.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.authentication;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.RemoteException;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.test.uiautomator.UiDevice;
-import android.test.suitebuilder.annotation.LargeTest;
-
-import static org.junit.Assert.assertTrue;
-import com.owncloud.android.R;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.reflect.Field;
-
-import android.app.Activity;
-
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.action.ViewActions.click;
-import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
-import static android.support.test.espresso.action.ViewActions.typeText;
-
-import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static org.hamcrest.Matchers.not;
-
-@RunWith(AndroidJUnit4.class)
-@LargeTest
-public class AuthenticatorActivityTest {
-
- public static final String EXTRA_ACTION = "ACTION";
- public static final String EXTRA_ACCOUNT = "ACCOUNT";
-
- private static final int WAIT_LOGIN = 5000;
-
- private static final String ERROR_MESSAGE = "Activity not finished";
- private static final String RESULT_CODE = "mResultCode";
-
-
- @Rule
- public ActivityTestRule mActivityRule = new ActivityTestRule(
- AuthenticatorActivity.class){
- @Override
- protected Intent getActivityIntent() {
-
- Context targetContext = InstrumentationRegistry.getInstrumentation()
- .getTargetContext();
- Intent result = new Intent(targetContext, AuthenticatorActivity.class);
- result.putExtra(EXTRA_ACTION, AuthenticatorActivity.ACTION_CREATE);
- result.putExtra(EXTRA_ACCOUNT, "");
- return result;
- }
- };
-
- @Before
- public void init(){
- UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
- /*Point[] coordinates = new Point[4];
- coordinates[0] = new Point(248, 1020);
- coordinates[1] = new Point(248, 429);
- coordinates[2] = new Point(796, 1020);
- coordinates[3] = new Point(796, 429);*/
- try {
- if (!uiDevice.isScreenOn()) {
- uiDevice.wakeUp();
- //uiDevice.swipe(coordinates, 10);
- }
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
-
- @Test
- public void check_login()
- throws InterruptedException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
- Bundle arguments = InstrumentationRegistry.getArguments();
-
- // Get values passed
- String testUser = arguments.getString("TEST_USER");
- String testPassword = arguments.getString("TEST_PASSWORD");
- String testServerURL = arguments.getString("TEST_SERVER_URL");
-
- // Check that login button is disabled
- onView(withId(R.id.buttonOK))
- .check(matches(not(isEnabled())));
-
- // Type server url
- onView(withId(R.id.hostUrlInput))
- .perform(typeText(testServerURL), closeSoftKeyboard());
- onView(withId(R.id.account_username)).perform(click());
-
- // Type user
- onView(withId(R.id.account_username))
- .perform(typeText(testUser), closeSoftKeyboard());
-
- // Type user pass
- onView(withId(R.id.account_password))
- .perform(typeText(testPassword), closeSoftKeyboard());
- onView(withId(R.id.buttonOK)).perform(click());
-
- // Check that the Activity ends after clicking
-
- Thread.sleep(WAIT_LOGIN);
- Field f = Activity.class.getDeclaredField(RESULT_CODE);
- f.setAccessible(true);
- int mResultCode = f.getInt(mActivityRule.getActivity());
-
- assertTrue(ERROR_MESSAGE, mResultCode == Activity.RESULT_OK);
-
- }
-}
\ No newline at end of file
diff --git a/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java b/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java
deleted file mode 100644
index a66dac922fc4..000000000000
--- a/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author David A. Velasco
- * Copyright (C) 2016 ownCloud Inc.
- *
- * 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.datamodel;
-
-import android.os.Parcel;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-
-/**
- * Instrumented unit test, to be run in an Android emulator or device.
- *
- * At the moment, it's a sample to validate the automatic test environment, in the scope of instrumented unit tests.
- *
- * Don't take it as an example of completeness.
- *
- * See http://developer.android.com/intl/es/training/testing/unit-testing/instrumented-unit-tests.html .
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class OCFileUnitTest {
-
- private final static String PATH = "/path/to/a/file.txt";
- private static final long ID = 12345L;
- private static final long PARENT_ID = 567890L;
- private static final String STORAGE_PATH = "/mnt/sd/localpath/to/a/file.txt";
- private static final String MIME_TYPE = "text/plain";
- private static final long FILE_LENGTH = 9876543210L;
- private static final long CREATION_TIMESTAMP = 8765432109L;
- private static final long MODIFICATION_TIMESTAMP = 7654321098L;
- private static final long MODIFICATION_TIMESTAMP_AT_LAST_SYNC_FOR_DATA = 6543210987L;
- private static final long LAST_SYNC_DATE_FOR_PROPERTIES = 5432109876L;
- private static final long LAST_SYNC_DATE_FOR_DATA = 4321098765L;
- private static final String ETAG = "adshfas98ferqw8f9yu2";
- private static final String PUBLIC_LINK = "https://fake.url.net/owncloud/987427448712984sdas29";
- private static final String PERMISSIONS = "SRKNVD";
- private static final String REMOTE_ID = "jadñgiadf8203:9jrp98v2mn3er2089fh";
- private static final String ETAG_IN_CONFLICT = "2adshfas98ferqw8f9yu";
-
- private OCFile mFile;
-
- @Before
- public void createDefaultOCFile() {
- mFile = new OCFile(PATH);
- }
-
-
- @Test
- public void writeThenReadAsParcelable() {
-
- // Set up mFile with not-default values
- mFile.setFileId(ID);
- mFile.setParentId(PARENT_ID);
- mFile.setStoragePath(STORAGE_PATH);
- mFile.setMimetype(MIME_TYPE);
- mFile.setFileLength(FILE_LENGTH);
- mFile.setCreationTimestamp(CREATION_TIMESTAMP);
- mFile.setModificationTimestamp(MODIFICATION_TIMESTAMP);
- mFile.setModificationTimestampAtLastSyncForData(MODIFICATION_TIMESTAMP_AT_LAST_SYNC_FOR_DATA);
- mFile.setLastSyncDateForProperties(LAST_SYNC_DATE_FOR_PROPERTIES);
- mFile.setLastSyncDateForData(LAST_SYNC_DATE_FOR_DATA);
- mFile.setFavorite(true);
- mFile.setEtag(ETAG);
- mFile.setShareViaLink(true);
- mFile.setShareWithSharee(true);
- mFile.setPublicLink(PUBLIC_LINK);
- mFile.setPermissions(PERMISSIONS);
- mFile.setRemoteId(REMOTE_ID);
- mFile.setNeedsUpdateThumbnail(true);
- mFile.setDownloading(true);
- mFile.setEtagInConflict(ETAG_IN_CONFLICT);
-
-
- // Write the file data in a Parcel
- Parcel parcel = Parcel.obtain();
- mFile.writeToParcel(parcel, mFile.describeContents());
-
- // Read the data from the parcel
- parcel.setDataPosition(0);
- OCFile fileReadFromParcel = OCFile.CREATOR.createFromParcel(parcel);
-
- // Verify that the received data are correct
- assertThat(fileReadFromParcel.getRemotePath(), is(PATH));
- assertThat(fileReadFromParcel.getFileId(), is(ID));
- assertThat(fileReadFromParcel.getParentId(), is(PARENT_ID));
- assertThat(fileReadFromParcel.getStoragePath(), is(STORAGE_PATH));
- assertThat(fileReadFromParcel.getMimetype(), is(MIME_TYPE));
- assertThat(fileReadFromParcel.getFileLength(), is(FILE_LENGTH));
- assertThat(fileReadFromParcel.getCreationTimestamp(), is(CREATION_TIMESTAMP));
- assertThat(fileReadFromParcel.getModificationTimestamp(), is(MODIFICATION_TIMESTAMP));
- assertThat(
- fileReadFromParcel.getModificationTimestampAtLastSyncForData(),
- is(MODIFICATION_TIMESTAMP_AT_LAST_SYNC_FOR_DATA)
- );
- assertThat(fileReadFromParcel.getLastSyncDateForProperties(), is(LAST_SYNC_DATE_FOR_PROPERTIES));
- assertThat(fileReadFromParcel.getLastSyncDateForData(), is(LAST_SYNC_DATE_FOR_DATA));
- assertThat(fileReadFromParcel.isFavorite(), is(true));
- assertThat(fileReadFromParcel.getEtag(), is(ETAG));
- assertThat(fileReadFromParcel.isSharedViaLink(), is(true));
- assertThat(fileReadFromParcel.isSharedWithSharee(), is(true));
- assertThat(fileReadFromParcel.getPublicLink(), is(PUBLIC_LINK));
- assertThat(fileReadFromParcel.getPermissions(), is(PERMISSIONS));
- assertThat(fileReadFromParcel.getRemoteId(), is(REMOTE_ID));
- assertThat(fileReadFromParcel.needsUpdateThumbnail(), is(true));
- assertThat(fileReadFromParcel.isDownloading(), is(true));
- assertThat(fileReadFromParcel.getEtagInConflict(), is(ETAG_IN_CONFLICT));
-
- }
-}
diff --git a/androidTest/java/com/owncloud/android/uiautomator/InitialTest.java b/androidTest/java/com/owncloud/android/uiautomator/InitialTest.java
deleted file mode 100644
index 7dcc9c9d14d2..000000000000
--- a/androidTest/java/com/owncloud/android/uiautomator/InitialTest.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.uiautomator;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject;
-import android.support.test.uiautomator.UiObjectNotFoundException;
-import android.support.test.uiautomator.UiSelector;
-import android.support.test.uiautomator.Until;
-
-
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.junit.Assert.assertThat;
-
-/**
- * UI Automator tests
- */
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 18)
-public class InitialTest {
-
- private static final String OWNCLOUD_APP_PACKAGE = "com.owncloud.android";
- private static final String ANDROID_SETTINGS_PACKAGE = "com.android.settings";
- private static final String SETTINGS_DATA_USAGE_OPTION = "Data usage";
-
- private static final int LAUNCH_TIMEOUT = 5000;
-
- private UiDevice mDevice;
-
- @Before
- public void initializeDevice() {
- // Initialize UiDevice instance
- mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-
- }
-
- @Test
- public void checkPreconditions() {
- assertThat(mDevice, notNullValue());
- }
-
- /**
- * Start owncloud app
- */
- @Test
- public void startAppFromHomeScreen() {
- // Perform a short press on the HOME button
- mDevice.pressHome();
-
- // Wait for launcher
- final String launcherPackage = getLauncherPackageName();
- assertThat(launcherPackage, notNullValue());
- mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT);
-
- // Launch the app
- Context context = InstrumentationRegistry.getContext();
- final Intent intent = context.getPackageManager()
- .getLaunchIntentForPackage(OWNCLOUD_APP_PACKAGE);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
- context.startActivity(intent);
-
- // Wait for the app to appear
- mDevice.wait(Until.hasObject(By.pkg(OWNCLOUD_APP_PACKAGE).depth(0)), LAUNCH_TIMEOUT);
- }
-
- /**
- * Start Settings app
- *
- * @throws UiObjectNotFoundException
- */
- @Test
- public void startSettingsFromHomeScreen() throws UiObjectNotFoundException {
-
- mDevice.pressHome();
-
- // Wait for launcher
- final String launcherPackage = getLauncherPackageName();
- assertThat(launcherPackage, notNullValue());
- mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT);
-
- // Launch the app
- Context context = InstrumentationRegistry.getContext();
- final Intent intent = context.getPackageManager()
- .getLaunchIntentForPackage(ANDROID_SETTINGS_PACKAGE);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
- context.startActivity(intent);
-
- clickByText(SETTINGS_DATA_USAGE_OPTION);
-
- }
-
- /**
- * Uses package manager to find the package name of the device launcher. Usually this package
- * is "com.android.launcher" but can be different at times. This is a generic solution which
- * works on all platforms.`
- */
- private String getLauncherPackageName() {
- // Create launcher Intent
- final Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.addCategory(Intent.CATEGORY_HOME);
-
- // Use PackageManager to get the launcher package name
- PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
- ResolveInfo resolveInfo = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
- return resolveInfo.activityInfo.packageName;
- }
-
- /**
- * Helper to click on objects that match the content-description text
- *
- * @param text
- * @throws UiObjectNotFoundException
- */
- private void clickByDescription(String text) throws UiObjectNotFoundException {
- UiObject obj = new UiObject(new UiSelector().description(text));
- obj.clickAndWaitForNewWindow();
- }
-
- /**
- * Helper to click on object that match the text value
- *
- * @param text
- * @throws UiObjectNotFoundException
- */
- private void clickByText(String text) throws UiObjectNotFoundException {
- UiObject obj = new UiObject(new UiSelector().text(text));
- obj.clickAndWaitForNewWindow();
- }
-}
diff --git a/androidx.databinding_viewbinding_7.4.2@aar b/androidx.databinding_viewbinding_7.4.2@aar
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/automationTest/.classpath b/automationTest/.classpath
deleted file mode 100644
index c52bcedcbe60..000000000000
--- a/automationTest/.classpath
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/automationTest/.gitignore b/automationTest/.gitignore
deleted file mode 100644
index 487ae52fe62d..000000000000
--- a/automationTest/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-
-target/
-ScreenShots/
-.DS_Store
-Users/
\ No newline at end of file
diff --git a/automationTest/.project b/automationTest/.project
deleted file mode 100644
index b1e58409f8a6..000000000000
--- a/automationTest/.project
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
- androidtest
-
-
-
-
-
- org.eclipse.jdt.core.javabuilder
-
-
-
-
- org.eclipse.m2e.core.maven2Builder
-
-
-
-
-
- org.eclipse.jdt.core.javanature
- org.eclipse.m2e.core.maven2Nature
-
-
diff --git a/automationTest/.settings/org.eclipse.jdt.core.prefs b/automationTest/.settings/org.eclipse.jdt.core.prefs
deleted file mode 100644
index 62492222ad2e..000000000000
--- a/automationTest/.settings/org.eclipse.jdt.core.prefs
+++ /dev/null
@@ -1,12 +0,0 @@
-eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
-org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=1.7
-org.eclipse.jdt.core.compiler.debug.lineNumber=generate
-org.eclipse.jdt.core.compiler.debug.localVariable=generate
-org.eclipse.jdt.core.compiler.debug.sourceFile=generate
-org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
-org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.source=1.7
diff --git a/automationTest/.settings/org.eclipse.m2e.core.prefs b/automationTest/.settings/org.eclipse.m2e.core.prefs
deleted file mode 100644
index f897a7f1cb23..000000000000
--- a/automationTest/.settings/org.eclipse.m2e.core.prefs
+++ /dev/null
@@ -1,4 +0,0 @@
-activeProfiles=
-eclipse.preferences.version=1
-resolveWorkspaceProjects=true
-version=1
diff --git a/automationTest/README.md b/automationTest/README.md
deleted file mode 100644
index 899bb8dc7991..000000000000
--- a/automationTest/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-** Work in progress
-
-This project contains a set of automatic tests operating in the UI level.
-
-Tests are to be run with the tool Appium. Check [here][0] to install it and all its dependencies (including Maven).
-
-You will need to modify the constants in automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/Config.java to assign appropiate values for your test server and accounts.
-You will need to include the ownCloud.apk to test in automationTest/src/test/resources/.
-
-To run the tests from command line, plug a device to your computer or start and emulator. Then type
-
-mvn clean tests
-
-To run only one category of the test
-
-mvn clean -Dtest=RunSmokeTests test
-
-The project may also be imported in Eclipse, with the appropiate plug-ins, and run from it.
-
-[0]: http://appium.io/slate/en/master/?java#about-appium
\ No newline at end of file
diff --git a/automationTest/pom.xml b/automationTest/pom.xml
deleted file mode 100644
index 548da37e84c2..000000000000
--- a/automationTest/pom.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-4.0.0
-
-com.owncloud
-androidtest
-1.0-SNAPSHOT
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- 2.18.1
-
-
- **/*Test**.java
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.3
-
- 1.7
- 1.7
-
-
-
-
-
-
-
- junit
- junit
- 4.11
-
-
- org.seleniumhq.selenium
- selenium-java
- 2.45.0
-
-
- io.selendroid
- 0.9.0
- selendroid-standalone
-
-
- io.selendroid
- 0.9.0
- selendroid-client
-
-
- io.appium
- java-client
- 2.2.0
-
-
- commons-lang
- commons-lang
- 2.6
-
-
- org.apache.httpcomponents
- httpclient
- 4.3.1
-
-
- com.google.android
- android
- 4.1.1.4
- provided
-
-
-
\ No newline at end of file
diff --git a/automationTest/resources/.gitignore b/automationTest/resources/.gitignore
deleted file mode 100644
index 86d0cb2726c6..000000000000
--- a/automationTest/resources/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-# Ignore everything in this directory
-*
-# Except this file
-!.gitignore
\ No newline at end of file
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/actions/Actions.java b/automationTest/src/test/java/com/owncloud/android/test/ui/actions/Actions.java
deleted file mode 100644
index ecec7f5625ff..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/actions/Actions.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.actions;
-
-import java.util.HashMap;
-
-import org.openqa.selenium.By;
-import org.openqa.selenium.NoSuchElementException;
-import org.openqa.selenium.ScreenOrientation;
-import org.openqa.selenium.remote.RemoteWebElement;
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import org.openqa.selenium.support.ui.ExpectedConditions;
-import org.openqa.selenium.support.ui.WebDriverWait;
-import com.owncloud.android.test.ui.models.CertificatePopUp;
-import com.owncloud.android.test.ui.models.ElementMenuOptions;
-import com.owncloud.android.test.ui.models.GmailSendMailView;
-import com.owncloud.android.test.ui.models.ShareView;
-import com.owncloud.android.test.ui.models.UploadFilesView;
-import com.owncloud.android.test.ui.models.LoginForm;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.MenuList;
-import com.owncloud.android.test.ui.models.NewFolderPopUp;
-import com.owncloud.android.test.ui.models.RemoveConfirmationView;
-import com.owncloud.android.test.ui.models.SettingsView;
-import com.owncloud.android.test.ui.models.WaitAMomentPopUp;
-import com.owncloud.android.test.ui.testSuites.Common;
-import com.owncloud.android.test.ui.testSuites.Config;
-
-public class Actions {
-
- public static FileListView login(String url, String user, String password,
- Boolean isTrusted, AndroidDriver driver)
- throws InterruptedException {
- LoginForm loginForm = new LoginForm(driver);
- CertificatePopUp certificatePopUp = loginForm.typeHostUrl(url);
- if(!isTrusted){
- WebDriverWait wait = new WebDriverWait(driver, 30);
- //sometimes the certificate has been already accept
- //and it doesn't appear again
- try {
- wait.until(ExpectedConditions
- .visibilityOf(certificatePopUp.getOkButtonElement()));
- //we need to repaint the screen
- //because of some element are misplaced
- driver.rotate(ScreenOrientation.LANDSCAPE);
- driver.rotate(ScreenOrientation.PORTRAIT);
- certificatePopUp.clickOnOkButton();
- }catch (NoSuchElementException e) {
-
- }
-
- }
- loginForm.typeUserName(user);
- loginForm.typePassword(password);
- //TODO. Assert related to check the connection?
- return loginForm.clickOnConnectButton();
- }
-
- public static WaitAMomentPopUp createFolder(String folderName,
- FileListView fileListView){
- NewFolderPopUp newFolderPopUp = fileListView.clickOnNewFolderButton();
- newFolderPopUp.typeNewFolderName(folderName);
- WaitAMomentPopUp waitAMomentPopUp = newFolderPopUp
- .clickOnNewFolderOkButton();
- //TODO. assert here
- return waitAMomentPopUp;
- }
-
-
- public static AndroidElement scrollTillFindElement (String elementName,
- AndroidElement element, AndroidDriver driver) {
- AndroidElement fileElement;
-
- if(element.getAttribute("scrollable").equals("true")){
- HashMap scrollObject = new HashMap();
- scrollObject.put("text", elementName);
- scrollObject.put("element", ( (RemoteWebElement) element).getId());
- driver.executeScript("mobile: scrollTo", scrollObject);
- }
- try {
- fileElement = (AndroidElement) driver
- .findElementByName(elementName);
- } catch (NoSuchElementException e) {
- fileElement = null;
- }
- return fileElement;
- }
-
-
- public static void deleteAccount (int accountPosition,FileListView fileListView) {
- MenuList menulist = fileListView.clickOnMenuButton();
- SettingsView settingView = menulist.clickOnSettingsButton();
- deleteAccount(accountPosition,settingView);
- }
-
- public static void deleteAccount (int accountPosition, SettingsView settingsView) {
- settingsView.tapOnAccountElement(accountPosition,1, 1000);
- settingsView.clickOnDeleteAccountElement();
- }
-
- public static void clickOnMainLayout(AndroidDriver driver){
- driver.tap(1, 0, 0, 1);
- }
-
-
- public static AndroidElement deleteElement(String elementName,
- FileListView fileListView, AndroidDriver driver) throws Exception{
- AndroidElement fileElement;
- WaitAMomentPopUp waitAMomentPopUp;
- try{
- //To open directly the "file list view" and
- //we don't need to know in which view we are
- driver.startActivity("com.owncloud.android",
- ".ui.activity.FileDisplayActivity");
- fileElement = (AndroidElement) driver
- .findElementByName(elementName);
- ElementMenuOptions menuOptions = fileListView
- .longPressOnElement(elementName);
- RemoveConfirmationView removeConfirmationView = menuOptions
- .clickOnRemove();;
- waitAMomentPopUp = removeConfirmationView
- .clickOnRemoteAndLocalButton();
- Common.waitTillElementIsNotPresent(
- waitAMomentPopUp.getWaitAMomentTextElement(), 100);
- }catch(NoSuchElementException e){
- fileElement=null;
- }
- return fileElement;
- }
-
- public static AndroidElement shareLinkElementByGmail(String elementName,
- FileListView fileListView, AndroidDriver driver, Common common)
- throws Exception{
- try{
- //To open directly the "file list view" and
- //we don't need to know in which view we are
- driver.startActivity("com.owncloud.android",
- ".ui.activity.FileDisplayActivity");
- ElementMenuOptions menuOptions = fileListView
- .longPressOnElement(elementName);
- ShareView shareView = menuOptions.clickOnShareLinkElement();
- Actions.scrollTillFindElement("Gmail", shareView
- .getListViewLayout(), driver).click();
- GmailSendMailView gmailSendMailView = new GmailSendMailView(driver);
- gmailSendMailView.typeToEmailAdress(Config.gmailAccount);
- gmailSendMailView.clickOnSendButton();
- Common.waitTillElementIsNotPresentWithoutTimeout(fileListView
- .getProgressCircular(), 1000);
- common.wait.until(ExpectedConditions.visibilityOf(
- fileListView.getFileElementLayout()
- .findElement(By.id(FileListView
- .getSharedElementIndicator()))));
-
- }catch(NoSuchElementException e){
- return null;
- }
- return (AndroidElement) fileListView.getFileElementLayout()
- .findElement(By.id(FileListView.getSharedElementIndicator()));
- }
-
- public static AndroidElement shareLinkElementByCopyLink(String elementName,
- FileListView fileListView, AndroidDriver driver, Common common)
- throws Exception{
- try{
- //To open directly the "file list view" and
- //we don't need to know in which view we are
- driver.startActivity("com.owncloud.android",
- ".ui.activity.FileDisplayActivity");
- ElementMenuOptions menuOptions = fileListView
- .longPressOnElement(elementName);
- ShareView shareView = menuOptions.clickOnShareLinkElement();
- Actions.scrollTillFindElement("Copy link", shareView.getListViewLayout(),
- driver).click();
- WaitAMomentPopUp waitAMomentPopUp = new WaitAMomentPopUp(driver);
- Common.waitTillElementIsNotPresentWithoutTimeout(waitAMomentPopUp
- .getWaitAMomentTextElement(), 100);
- common.wait.until(ExpectedConditions.visibilityOf(
- fileListView.getFileElementLayout()
- .findElement(By.id(FileListView.getSharedElementIndicator()))));
- }catch(NoSuchElementException e){
- return null;
- }
- return (AndroidElement) fileListView.getFileElementLayout()
- .findElement(By.id(FileListView.getSharedElementIndicator()));
- }
-
-
- public static void unshareLinkElement(String elementName,
- FileListView fileListView, AndroidDriver driver, Common common)
- throws Exception{
- try{
- //To open directly the "file list view" and
- //we don't need to know in which view we are
- driver.startActivity("com.owncloud.android",
- ".ui.activity.FileDisplayActivity");
- ElementMenuOptions menuOptions = fileListView
- .longPressOnElement(elementName);
- WaitAMomentPopUp waitAMomentPopUp = menuOptions
- .clickOnUnshareLinkElement();
- Common.waitTillElementIsNotPresentWithoutTimeout(waitAMomentPopUp
- .getWaitAMomentTextElement(), 100);
- Common.waitTillElementIsNotPresent((AndroidElement) fileListView
- .getFileElementLayout()
- .findElement(By.id(FileListView.getSharedElementIndicator())
- ),100);
- }catch(NoSuchElementException e){
-
- }
- }
-
-
- public static FileListView uploadFile(String elementName,
- FileListView fileListView) throws InterruptedException{
- fileListView.clickOnUploadButton();
- UploadFilesView uploadFilesView = fileListView
- .clickOnFilesElementUploadFile();
- uploadFilesView.clickOnFileName(elementName);
- FileListView fileListViewAfterUploadFile = uploadFilesView
- .clickOnUploadButton();
- //TO DO. detect when the file is successfully uploaded
- Thread.sleep(15000);
- return fileListViewAfterUploadFile;
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/FailingTestCategory.java b/automationTest/src/test/java/com/owncloud/android/test/ui/groups/FailingTestCategory.java
deleted file mode 100644
index 2a91e7ffc94e..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/FailingTestCategory.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.groups;
-
-public interface FailingTestCategory extends IgnoreTestCategory {}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/FlexibleCategories.java b/automationTest/src/test/java/com/owncloud/android/test/ui/groups/FlexibleCategories.java
deleted file mode 100644
index 94a019afffd1..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/FlexibleCategories.java
+++ /dev/null
@@ -1,138 +0,0 @@
-package com.owncloud.android.test.ui.groups;
-
-import java.lang.annotation.Annotation;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import org.junit.Test;
-import org.junit.experimental.categories.Categories.CategoryFilter;
-import org.junit.experimental.categories.Categories.ExcludeCategory;
-import org.junit.experimental.categories.Categories.IncludeCategory;
-import org.junit.experimental.categories.Category;
-import org.junit.runner.Description;
-import org.junit.runner.manipulation.NoTestsRemainException;
-import org.junit.runners.Suite;
-import org.junit.runners.model.InitializationError;
-import org.junit.runners.model.RunnerBuilder;
-
-/**
- * This class is based on org.junit.experimental.categories.Categories from JUnit 4.10.
- *
- * All anotations and inner classes from the original class Categories are removed,
- * since they will be re-used.
- * Unfortunately sub-classing Categories did not work.
- */
-public class FlexibleCategories extends Suite {
-
- /**
- * Specifies the package which should be scanned for test classes (e.g. @TestScanPackage("my.package")).
- * This annotation is required.
- */
- @Retention(RetentionPolicy.RUNTIME)
- public @interface TestScanPackage {
- public String value();
- }
-
- /**
- * Specifies the prefix of matching class names (e.g. @TestClassPrefix("Test")).
- * This annotation is optional (default: "").
- */
- @Retention(RetentionPolicy.RUNTIME)
- public @interface TestClassPrefix {
- public String value();
- }
-
- /**
- * Specifies the suffix of matching class names (e.g. @TestClassSuffix("Test")).
- * This annotation is optional (default: "Test").
- */
- @Retention(RetentionPolicy.RUNTIME)
- public @interface TestClassSuffix {
- public String value();
- }
-
- /**
- * Specifies an annotation for methods which must be present in a matching class (e.g. @TestMethodAnnotationFilter(Test.class)).
- * This annotation is optional (default: org.junit.Test.class).
- */
- @Retention(RetentionPolicy.RUNTIME)
- public @interface TestMethodAnnotation {
- public Class extends Annotation> value();
- }
-
- public FlexibleCategories(Class> clazz, RunnerBuilder builder)
- throws InitializationError {
- this(builder, clazz, PatternClasspathClassesFinder.getSuiteClasses(
- getTestScanPackage(clazz), getTestClassPrefix(clazz), getTestClassSuffix(clazz),
- getTestMethodAnnotation(clazz)));
- try {
- filter(new CategoryFilter(getIncludedCategory(clazz),
- getExcludedCategory(clazz)));
- } catch (NoTestsRemainException e) {
- // Ignore all classes with no matching tests.
- }
- assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription());
- }
-
- public FlexibleCategories(RunnerBuilder builder, Class> clazz,
- Class>[] suiteClasses) throws InitializationError {
- super(builder, clazz, suiteClasses);
- }
-
- private static String getTestScanPackage(Class> clazz) throws InitializationError {
- TestScanPackage annotation = clazz.getAnnotation(TestScanPackage.class);
- if (annotation == null) {
- throw new InitializationError("No package given to scan for tests!\nUse the annotation @TestScanPackage(\"my.package\") on the test suite " + clazz + ".");
- }
- return annotation.value();
- }
-
- private static String getTestClassPrefix(Class> clazz) {
- TestClassPrefix annotation = clazz.getAnnotation(TestClassPrefix.class);
- return annotation == null ? "" : annotation.value();
- }
-
- private static String getTestClassSuffix(Class> clazz) {
- TestClassSuffix annotation = clazz.getAnnotation(TestClassSuffix.class);
- return annotation == null ? "Test" : annotation.value();
- }
-
- private static Class extends Annotation> getTestMethodAnnotation(Class> clazz) {
- TestMethodAnnotation annotation = clazz.getAnnotation(TestMethodAnnotation.class);
- return annotation == null ? Test.class : annotation.value();
- }
-
- private Class> getIncludedCategory(Class> clazz) {
- IncludeCategory annotation= clazz.getAnnotation(IncludeCategory.class);
- return annotation == null ? null : annotation.value();
- }
-
- private Class> getExcludedCategory(Class> clazz) {
- ExcludeCategory annotation= clazz.getAnnotation(ExcludeCategory.class);
- return annotation == null ? null : annotation.value();
- }
-
- private void assertNoCategorizedDescendentsOfUncategorizeableParents(Description description) throws InitializationError {
- if (!canHaveCategorizedChildren(description))
- assertNoDescendantsHaveCategoryAnnotations(description);
- for (Description each : description.getChildren())
- assertNoCategorizedDescendentsOfUncategorizeableParents(each);
- }
-
- private void assertNoDescendantsHaveCategoryAnnotations(Description description) throws InitializationError {
- for (Description each : description.getChildren()) {
- if (each.getAnnotation(Category.class) != null)
- throw new InitializationError("Category annotations on Parameterized classes are not supported on individual methods.");
- assertNoDescendantsHaveCategoryAnnotations(each);
- }
- }
-
- // If children have names like [0], our current magical category code can't determine their
- // parentage.
- private static boolean canHaveCategorizedChildren(Description description) {
- for (Description each : description.getChildren())
- if (each.getTestClass() == null)
- return false;
- return true;
- }
-}
\ No newline at end of file
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/IgnoreTestCategory.java b/automationTest/src/test/java/com/owncloud/android/test/ui/groups/IgnoreTestCategory.java
deleted file mode 100644
index 2586edb7d570..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/IgnoreTestCategory.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.groups;
-
-public interface IgnoreTestCategory {}
-
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/InProgressCategory.java b/automationTest/src/test/java/com/owncloud/android/test/ui/groups/InProgressCategory.java
deleted file mode 100644
index 850ff45139f2..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/InProgressCategory.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.owncloud.android.test.ui.groups;
-
-public interface InProgressCategory extends IgnoreTestCategory{
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/NoIgnoreTestCategory.java b/automationTest/src/test/java/com/owncloud/android/test/ui/groups/NoIgnoreTestCategory.java
deleted file mode 100644
index 321fb0885547..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/NoIgnoreTestCategory.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.groups;
-
-public interface NoIgnoreTestCategory {
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/OtherTestCategory.java b/automationTest/src/test/java/com/owncloud/android/test/ui/groups/OtherTestCategory.java
deleted file mode 100644
index b14c36e48b6b..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/OtherTestCategory.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.groups;
-
-public interface OtherTestCategory extends IgnoreTestCategory {}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/PatternClasspathClassesFinder.java b/automationTest/src/test/java/com/owncloud/android/test/ui/groups/PatternClasspathClassesFinder.java
deleted file mode 100644
index b59888309dec..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/PatternClasspathClassesFinder.java
+++ /dev/null
@@ -1,141 +0,0 @@
-package com.owncloud.android.test.ui.groups;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-
-/**
- *
- * Modified version of ClasspathClassesFinder from:
- * http://linsolas.free.fr/wordpress/index.php/2011/02/how-to-categorize-junit-tests-with-maven/
- *
- * The difference is, that it does not search for annotated classes but for classes with a certain
- * class name prefix and suffix.
- */
-public final class PatternClasspathClassesFinder {
-
- /**
- * Get the list of classes of a given package name, and that are annotated
- * by a given annotation.
- *
- * @param packageName
- * The package name of the classes.
- * @param classPrefix
- * The prefix of the class name.
- * @param classSuffix
- * The suffix of the class name.
- * @param methodAnnotation
- * Only return classes containing methods annotated with methodAnnotation.
- * @return The List of classes that matches the requirements.
- */
- public static Class>[] getSuiteClasses(String packageName,
- String classPrefix, String classSuffix,
- Class extends Annotation> methodAnnotation) {
- try {
- return getClasses(packageName, classPrefix, classSuffix, methodAnnotation);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return null;
- }
-
- /**
- * Get the list of classes of a given package name, and that are annotated
- * by a given annotation.
- *
- * @param packageName
- * The package name of the classes.
- * @param classPrefix
- * The prefix of the class name.
- * @param classSuffix
- * The suffix of the class name.
- * @param methodAnnotation
- * Only return classes containing methods annotated with methodAnnotation.
- * @return The List of classes that matches the requirements.
- * @throws ClassNotFoundException
- * If something goes wrong...
- * @throws IOException
- * If something goes wrong...
- */
- private static Class>[] getClasses(String packageName,
- String classPrefix, String classSuffix,
- Class extends Annotation> methodAnnotation)
- throws ClassNotFoundException, IOException {
- ClassLoader classLoader = Thread.currentThread()
- .getContextClassLoader();
- String path = packageName.replace('.', '/');
- // Get classpath
- Enumeration resources = classLoader.getResources(path);
- List dirs = new ArrayList();
- while (resources.hasMoreElements()) {
- URL resource = resources.nextElement();
- dirs.add(new File(resource.getFile()));
- }
- // For each classpath, get the classes.
- ArrayList> classes = new ArrayList>();
- for (File directory : dirs) {
- classes.addAll(findClasses(directory, packageName, classPrefix, classSuffix, methodAnnotation));
- }
- return classes.toArray(new Class[classes.size()]);
- }
-
- /**
- * Find classes, in a given directory (recursively), for a given package
- * name, that are annotated by a given annotation.
- *
- * @param directory
- * The directory where to look for.
- * @param packageName
- * The package name of the classes.
- * @param classPrefix
- * The prefix of the class name.
- * @param classSuffix
- * The suffix of the class name.
- * @param methodAnnotation
- * Only return classes containing methods annotated with methodAnnotation.
- * @return The List of classes that matches the requirements.
- * @throws ClassNotFoundException
- * If something goes wrong...
- */
- private static List> findClasses(File directory,
- String packageName, String classPrefix, String classSuffix,
- Class extends Annotation> methodAnnotation)
- throws ClassNotFoundException {
- List> classes = new ArrayList>();
- if (!directory.exists()) {
- return classes;
- }
- File[] files = directory.listFiles();
- for (File file : files) {
- if (file.isDirectory()) {
- classes.addAll(findClasses(file,
- packageName + "." + file.getName(), classPrefix, classSuffix, methodAnnotation));
- } else if (file.getName().startsWith(classPrefix) && file.getName().endsWith(classSuffix + ".class")) {
- // We remove the .class at the end of the filename to get the
- // class name...
- Class> clazz = Class.forName(packageName
- + '.'
- + file.getName().substring(0,
- file.getName().length() - 6));
-
- // Check, if class contains test methods (prevent "No runnable methods" exception):
- boolean classHasTest = false;
- for (Method method : clazz.getMethods()) {
- if (method.getAnnotation(methodAnnotation) != null) {
- classHasTest = true;
- break;
- }
- }
- if (classHasTest) {
- classes.add(clazz);
- }
- }
- }
- return classes;
- }
-}
\ No newline at end of file
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/SmokeTestCategory.java b/automationTest/src/test/java/com/owncloud/android/test/ui/groups/SmokeTestCategory.java
deleted file mode 100644
index ebac5fecf453..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/SmokeTestCategory.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.groups;
-
-public interface SmokeTestCategory {
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/UnfinishedTestCategory.java b/automationTest/src/test/java/com/owncloud/android/test/ui/groups/UnfinishedTestCategory.java
deleted file mode 100644
index 2a1451d557a5..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/groups/UnfinishedTestCategory.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.groups;
-
-public interface UnfinishedTestCategory extends IgnoreTestCategory{
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/CertificatePopUp.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/CertificatePopUp.java
deleted file mode 100644
index 1908ac9feb04..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/CertificatePopUp.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.PageFactory;
-
-public class CertificatePopUp {
- final AndroidDriver driver;
-
- @AndroidFindBy(name = "OK")
- private AndroidElement okButton;
-
- public CertificatePopUp (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public void clickOnOkButton () {
- okButton.click();
- }
-
- public AndroidElement getOkButtonElement () {
- return okButton;
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/ElementMenuOptions.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/ElementMenuOptions.java
deleted file mode 100644
index 7480360706e8..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/ElementMenuOptions.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.PageFactory;
-
-public class ElementMenuOptions {
-
- final AndroidDriver driver;
-
- @AndroidFindBy(name = "Share link")
- private AndroidElement shareLinkElement;
-
- @AndroidFindBy(name = "Unshare link")
- private AndroidElement unshareLinkElement;
-
- @AndroidFindBy(name = "Details")
- private AndroidElement detailsFileElement;
-
- @AndroidFindBy(name = "Rename")
- private AndroidElement renameFileElement;
-
- @AndroidFindBy(name = "Remove")
- private AndroidElement removeFileElement;
-
- @AndroidFindBy(name = "Move")
- private AndroidElement moveElement;
-
- public ElementMenuOptions (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public FileDetailsView clickOnDetails () {
- detailsFileElement.click();
- FileDetailsView fileDetailsView = new FileDetailsView(driver);
- return fileDetailsView;
- }
-
- public RemoveConfirmationView clickOnRemove () {
- removeFileElement.click();
- RemoveConfirmationView removeConfirmationView =
- new RemoveConfirmationView(driver);
- return removeConfirmationView;
- }
-
-
- public MoveView clickOnMove () {
- moveElement.click();
- MoveView moveView = new MoveView(driver);
- return moveView;
- }
-
- public NewFolderPopUp clickOnRename () {
- renameFileElement.click();
- NewFolderPopUp newFolderPopUp = new NewFolderPopUp(driver);
- return newFolderPopUp;
- }
-
- public ShareView clickOnShareLinkElement () {
- shareLinkElement.click();
- ShareView shareView = new ShareView(driver);
- return shareView;
- }
-
- public WaitAMomentPopUp clickOnUnshareLinkElement () {
- unshareLinkElement.click();
- WaitAMomentPopUp waitAMomentPopUp = new WaitAMomentPopUp(driver);
- return waitAMomentPopUp;
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/FileDetailsView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/FileDetailsView.java
deleted file mode 100644
index 46da42fd29d6..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/FileDetailsView.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-
-public class FileDetailsView {
- final AndroidDriver driver;
-
- @CacheLookup
- @AndroidFindBy(name = "Keep file up to date")
- private AndroidElement keepFileUpToDateCheckbox;
-
- @AndroidFindBy(id = "com.owncloud.android:id/fdProgressBar")
- private AndroidElement progressBar;
-
- public FileDetailsView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public void checkKeepFileUpToDateCheckbox () {
- if(keepFileUpToDateCheckbox.getAttribute("checked").equals("false")){
- keepFileUpToDateCheckbox.click();
- }
- }
-
- public void unCheckKeepFileUpToDateCheckbox () {
- if(keepFileUpToDateCheckbox.getAttribute("checked").equals("true")){
- keepFileUpToDateCheckbox.click();
- }
- }
-
- public AndroidElement getProgressBar (){
- return progressBar;
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/FileListView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/FileListView.java
deleted file mode 100644
index 3c33864e0b88..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/FileListView.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import java.util.List;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.android.AndroidKeyCode;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.NoSuchElementException;
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-import org.openqa.selenium.Point;
-
-import com.owncloud.android.test.ui.actions.Actions;
-
-public class FileListView {
- final AndroidDriver driver;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".description(\"More options\")")
- private AndroidElement menuButton;
-
- @CacheLookup
- @AndroidFindBy(id = "com.owncloud.android:id/list_root")
- private AndroidElement filesLayout;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".resourceId(\"android:id/action_bar_title\")")
- private AndroidElement titleText;
-
- @AndroidFindBy(id = "android:id/progress_circular")
- private AndroidElement progressCircular;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".description(\"New folder\")")
- private AndroidElement newFolderButton;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector().description(\"Upload\")")
- private AndroidElement uploadButton;
-
- private AndroidElement waitAMomentText;
-
- @AndroidFindBy(id = "com.owncloud.android:id/ListItemLayout")
- private List listItemLayout;
-
- @AndroidFindBy(id = "com.owncloud.android:id/list_root")
- private AndroidElement listRootLayout;
-
- @AndroidFindBy(name = "Files")
- private AndroidElement filesElementUploadFile;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".description(\"List Layout\")")
- private AndroidElement listLayout;
-
- @AndroidFindBy(uiAutomator = "new UiSelector().className(\"android.widget.FrameLayout\").index(0)")
- private AndroidElement deviceScreen;
-
- private AndroidElement fileElement;
-
- private AndroidElement fileElementLayout;
-
- private static String localFileIndicator =
- "com.owncloud.android:id/localFileIndicator";
- private static String favoriteFileIndicator =
- "com.owncloud.android:id/favoriteIcon";
- private static String sharedElementIndicator =
- "com.owncloud.android:id/sharedIcon";
-
-
- public FileListView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public MenuList clickOnMenuButton () {
- //if the menu option is not in the actionBar, it is opening again
- try {
- menuButton.click();
- } catch (NoSuchElementException e){
- driver.sendKeyEvent(AndroidKeyCode.MENU);
- }
- MenuList menuList = new MenuList (driver);
- return menuList;
- }
-
- public SettingsView getSettingsView () {
- SettingsView settingsView = new SettingsView(driver);
- return settingsView;
- }
-
- public NewFolderPopUp clickOnNewFolderButton () {
- newFolderButton.click();
- NewFolderPopUp newFolderPopUp = new NewFolderPopUp(driver);
- return newFolderPopUp;
- }
-
- public void clickOnUploadButton () {
- uploadButton.click();
- }
-
- public UploadFilesView clickOnFilesElementUploadFile () {
- filesElementUploadFile.click();
- UploadFilesView uploadFilesView = new UploadFilesView(driver);
- return uploadFilesView;
- }
-
- public AndroidElement getTitleTextElement () {
- return titleText;
- }
-
- public AndroidElement getUploadButton () {
- return uploadButton;
- }
-
- public AndroidElement getWaitAMomentTextElement () {
- return waitAMomentText;
- }
-
- public AndroidElement getListRootElement () {
- return listRootLayout;
- }
-
- public List getListItemLayout () {
- return listItemLayout;
- }
-
- public AndroidElement getFileElement () {
- return fileElement;
- }
-
- public ElementMenuOptions longPressOnElement (String elementName) {
- scrollTillFindElement(elementName).tap(1, 1000);
- //fileElement.tap(1, 1000);
- ElementMenuOptions menuOptions = new ElementMenuOptions(driver);
- return menuOptions;
- }
-
- public AndroidElement scrollTillFindElement (String elementName) {
- fileElement = Actions
- .scrollTillFindElement (elementName,filesLayout,driver);
- try {
- fileElementLayout = (AndroidElement) driver
- .findElementByAndroidUIAutomator("new UiSelector()"
- + ".description(\"LinearLayout-"+ elementName +"\")");
- } catch (NoSuchElementException e) {
- fileElementLayout = null;
- }
- return fileElement;
- }
-
- public AndroidElement getFileElementLayout () {
- return fileElementLayout;
- }
-
- public AndroidElement getProgressCircular () {
- return progressCircular;
- }
-
- public static String getLocalFileIndicator() {
- return localFileIndicator;
- }
-
- public static String getFavoriteFileIndicator() {
- return favoriteFileIndicator;
- }
-
- public static String getSharedElementIndicator() {
- return sharedElementIndicator;
- }
- public void pulldownToRefresh () throws InterruptedException {
- Point listLocation = listLayout.getLocation();
- driver.swipe(listLocation.getX(),listLocation.getY(),
- listLocation.getX(),listLocation.getY()+1000, 5000);
- }
-
-
-
- public void pulldownToSeeNotification () throws InterruptedException {
- Point listLocation = deviceScreen.getLocation();
- driver.swipe(listLocation.getX(),listLocation.getY(),
- listLocation.getX(),listLocation.getY()+1000, 5000);
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/GmailEmailListView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/GmailEmailListView.java
deleted file mode 100644
index a6b6975141aa..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/GmailEmailListView.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.NoSuchElementException;
-import org.openqa.selenium.support.PageFactory;
-
-public class GmailEmailListView {
-
- final AndroidDriver driver;
-
- @AndroidFindBy(uiAutomator = "new UiSelector().description(\""
- + "me about UploadFile, on May 11, conversation read\")")
- private AndroidElement emailAmericanFormatDate;
- @AndroidFindBy(uiAutomator = "new UiSelector().description(\""
- + "me about UploadFile, on 11 May, conversation read\")")
- private AndroidElement emailEuropeanFormatDate;
-
-
- public GmailEmailListView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public GmailEmailView clickOnEmail (){
- try{
- emailAmericanFormatDate.click();
- }catch (NoSuchElementException e) {
- emailEuropeanFormatDate.click();
- }
- GmailEmailView gmailEmailView = new GmailEmailView(driver);
- return gmailEmailView;
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/GmailEmailView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/GmailEmailView.java
deleted file mode 100644
index 6d6acd797369..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/GmailEmailView.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-
-import com.owncloud.android.test.ui.testSuites.Config;
-
-public class GmailEmailView {
- final AndroidDriver driver;
-
- @CacheLookup
- @AndroidFindBy(name = Config.fileToTestSendByEmailName)
- private AndroidElement fileButton;
-
- public GmailEmailView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public ImageView clickOnfileButton (){
- fileButton.click();
- ImageView imageView = new ImageView(driver);
- return imageView;
- }
-}
\ No newline at end of file
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/GmailSendMailView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/GmailSendMailView.java
deleted file mode 100644
index a8688157cf14..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/GmailSendMailView.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-
-public class GmailSendMailView {
- final AndroidDriver driver;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector().description(\"To\")")
- private AndroidElement toTextField;
-
- @CacheLookup
- @AndroidFindBy(name = "Subject")
- private AndroidElement subjectTextField;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector().description(\"Send\")")
- private AndroidElement sendButton;
-
- public GmailSendMailView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public void typeToEmailAdress (String email) {
- toTextField.sendKeys(email + "\n");
- }
-
- public void clickOnSendButton () {
- sendButton.click();
- }
-
- public void typeSubject (String subject) {
- subjectTextField.clear();
- subjectTextField.sendKeys(subject);
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/ImageView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/ImageView.java
deleted file mode 100644
index c0f13fcc14ef..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/ImageView.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.testSuites.Common;
-
-
-public class ImageView {
- final AndroidDriver driver;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".description(\"More options\")")
- private AndroidElement optionsButton;
-
- @AndroidFindBy(name = "Share")
- private AndroidElement shareButton;
-
- @AndroidFindBy(name = "ownCloud")
- private AndroidElement ownCloudButton;
-
- @AndroidFindBy(name = "Share with ownCloud")
- private AndroidElement shareWithOwnCloudButton;
-
- @AndroidFindBy(name = "Just once")
- private AndroidElement justOnceButton;
-
- @AndroidFindBy(id = "android:id/resolver_list")
- private AndroidElement sharingAppsLayout;
-
- public ImageView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public void clickOnOptionsButton(){
- optionsButton.click();
- }
-
- public void clickOnShareButton(){
- shareButton.click();
- }
-
- public void clickOnOwnCloudButton(){
- if(Common.isElementPresent(ownCloudButton)){
- Actions.scrollTillFindElement("ownCloud",sharingAppsLayout,driver);
- ownCloudButton.click();
- }else if(Common.isElementPresent(shareWithOwnCloudButton)){}
- }
-
- public void clickOnJustOnceButton(){
- justOnceButton.click();
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/LoginForm.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/LoginForm.java
deleted file mode 100644
index aaef6aedfd53..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/LoginForm.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-public class LoginForm {
- final AndroidDriver driver;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".description(\"Server address\")")
- private AndroidElement hostUrlInput;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector().description(\"Username\")")
- private AndroidElement userNameInput;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector().description(\"Password\")")
- private AndroidElement passwordInput;
-
- @CacheLookup
- @AndroidFindBy(uiAutomator = "new UiSelector().description(\"Connect\")")
- private AndroidElement connectButton;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".description(\"Testing connection\")")
- private AndroidElement serverStatusText;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".description(\"Wrong username or password\")")
- private AndroidElement authStatusText;
-
- public LoginForm (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public CertificatePopUp typeHostUrl (String hostUrl) {
- hostUrlInput.clear();
- hostUrlInput.sendKeys(hostUrl + "\n");
- CertificatePopUp certificatePopUp = new CertificatePopUp(driver);
- return certificatePopUp;
- }
-
- public void clickOnUserName () {
- userNameInput.click();
- }
-
- public void typeUserName (String userName) {
- userNameInput.clear();
- //using the \n , it not need to hide the keyboard
- //which sometimes gives problems
- userNameInput.sendKeys(userName + "\n");
- //driver.hideKeyboard();
- }
-
- public void typePassword (String password) {
- passwordInput.clear();
- passwordInput.sendKeys(password + "\n");
- //driver.hideKeyboard();
- }
-
- public FileListView clickOnConnectButton () {
- connectButton.click();
- FileListView fileListView = new FileListView(driver);
- return fileListView;
- }
-
- public AndroidElement gethostUrlInput () {
- return hostUrlInput;
- }
-
- public AndroidElement getUserNameInput () {
- return userNameInput;
- }
-
- public AndroidElement getPasswordInput () {
- return passwordInput;
- }
-
-
- public AndroidElement getServerStatusTextElement () {
- return serverStatusText;
- }
-
- public AndroidElement getAuthStatusText () {
- return authStatusText;
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/MenuList.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/MenuList.java
deleted file mode 100644
index 132e6717cfd6..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/MenuList.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import org.openqa.selenium.support.PageFactory;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-public class MenuList {
-
- final AndroidDriver driver;
-
- @AndroidFindBy(name = "Settings")
- private AndroidElement settingsButton;
-
- public MenuList (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public SettingsView clickOnSettingsButton () {
- settingsButton.click();
- SettingsView settingsView = new SettingsView(driver);
- return settingsView;
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/MoveView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/MoveView.java
deleted file mode 100644
index 222a268effbd..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/MoveView.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-
-import com.owncloud.android.test.ui.actions.Actions;
-
-public class MoveView {
- final AndroidDriver driver;
-
- @CacheLookup
- @AndroidFindBy(id = "com.owncloud.android:id/list_root")
- private AndroidElement filesLayout;
-
- @AndroidFindBy(name = "Choose")
- private AndroidElement chooseButton;
-
- public MoveView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public WaitAMomentPopUp clickOnChoose () {
- chooseButton.click();
- WaitAMomentPopUp waitAMomentPopUp = new WaitAMomentPopUp(driver);
- return waitAMomentPopUp;
- }
-
- public AndroidElement scrollTillFindElement (String elementName) {
- return Actions.scrollTillFindElement (elementName,filesLayout,driver);
- }
-}
\ No newline at end of file
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/NewFolderPopUp.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/NewFolderPopUp.java
deleted file mode 100644
index fb280662728d..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/NewFolderPopUp.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import org.openqa.selenium.support.PageFactory;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-public class NewFolderPopUp {
-
- final AndroidDriver driver;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".resourceId(\"android:id/button1\")")
- private AndroidElement newFolderOkButton;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".resourceId(\"com.owncloud.android:id/user_input\")")
- private AndroidElement newFolderNameField;
-
- public NewFolderPopUp (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public void typeNewFolderName (String newFolderName) {
- newFolderNameField.clear();
- newFolderNameField.sendKeys(newFolderName + "\n");
- //driver.hideKeyboard();
- }
-
- public WaitAMomentPopUp clickOnNewFolderOkButton () {
- newFolderOkButton.click();
- WaitAMomentPopUp waitAMomentPopUp = new WaitAMomentPopUp(driver);
- return waitAMomentPopUp;
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/NotificationView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/NotificationView.java
deleted file mode 100644
index 35de73c861bf..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/NotificationView.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.owncloud.android.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.PageFactory;
-
-public class NotificationView {
-final AndroidDriver driver;
-
- @AndroidFindBy(name = "Upload succeeded")
- private static AndroidElement uploadSucceededNotification;
-
- @AndroidFindBy(name = "Uploading ?")
- private static AndroidElement uploadingNotification;
-
- @AndroidFindBy(uiAutomator = "new UiSelector().description(\"Clear all notifications.\")")
- private AndroidElement clearAllNotificationButton;
-
- @AndroidFindBy(uiAutomator = "new UiSelector().className(\"android.widget.FrameLayout\").index(0)")
- private AndroidElement notificationArea;
-
-
- public NotificationView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
-
- public AndroidElement getUploadSucceededNotification() {
- return uploadSucceededNotification;
- }
-
- public AndroidElement getUploadingNotification() {
- return uploadingNotification;
- }
-
- public AndroidElement getClearAllNotificationButton() {
- return clearAllNotificationButton;
- }
-
- public void tapOnClearAllNotification () {
- clearAllNotificationButton.tap(1, 1000);
- }
-
- public void tapOnBottomNotificationArea(){
- //TODO. it is not working
- notificationArea.getSize();
- notificationArea.tap(1, 1000);
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/PassCodeRequestView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/PassCodeRequestView.java
deleted file mode 100644
index 52744cb5689c..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/PassCodeRequestView.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import org.openqa.selenium.support.PageFactory;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-public class PassCodeRequestView {
-final AndroidDriver driver;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".className(\"android.widget.EditText\").index(0)")
- private AndroidElement codeElement1;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".className(\"android.widget.EditText\").index(1)")
- private AndroidElement codeElement2;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".className(\"android.widget.EditText\").index(2)")
- private AndroidElement codeElement3;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".className(\"android.widget.EditText\").index(3)")
- private AndroidElement codeElement4;
-
- public PassCodeRequestView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public void enterPasscode(String codeNumber1, String codeNumber2,
- String codeNumber3, String codeNumber4){
- codeElement1
- .sendKeys(codeNumber1 + codeNumber1 + codeNumber1 + codeNumber1);
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/PassCodeView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/PassCodeView.java
deleted file mode 100644
index 6f84fca582f0..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/PassCodeView.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.PageFactory;
-
-public class PassCodeView {
- final AndroidDriver driver;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".className(\"android.widget.EditText\").index(0)")
- private AndroidElement codeElement1;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".className(\"android.widget.EditText\").index(1)")
- private AndroidElement codeElement2;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".className(\"android.widget.EditText\").index(2)")
- private AndroidElement codeElement3;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".className(\"android.widget.EditText\").index(3)")
- private AndroidElement codeElement4;
-
- @AndroidFindBy(name = "Cancel")
- private AndroidElement cancelButton;
-
- public PassCodeView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public PassCodeView enterPasscode(String codeNumber1, String codeNumber2,
- String codeNumber3, String codeNumber4){
- codeElement1
- .sendKeys(codeNumber1 + codeNumber1 + codeNumber1 + codeNumber1);
- return this;
- }
- public SettingsView reenterPasscode(String codeNumber1,
- String codeNumber2, String codeNumber3, String codeNumber4){
- codeElement1
- .sendKeys(codeNumber1 + codeNumber1 + codeNumber1 + codeNumber1);
- SettingsView settingsView = new SettingsView(driver);
- return settingsView;
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/RemoveConfirmationView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/RemoveConfirmationView.java
deleted file mode 100644
index 5b0c06617bee..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/RemoveConfirmationView.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.PageFactory;
-
-public class RemoveConfirmationView {
- final AndroidDriver driver;
-
- @AndroidFindBy(name = "Remote and local")
- private AndroidElement remoteAndLocalButton;
-
- public RemoveConfirmationView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public WaitAMomentPopUp clickOnRemoteAndLocalButton () {
- remoteAndLocalButton.click();
- WaitAMomentPopUp waitAMomentPopUp = new WaitAMomentPopUp(driver);
- return waitAMomentPopUp;
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/SettingsView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/SettingsView.java
deleted file mode 100644
index 900c03b2a04c..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/SettingsView.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-
-import com.owncloud.android.test.ui.testSuites.Config;
-
-public class SettingsView {
- final AndroidDriver driver;
-
- @CacheLookup
- @AndroidFindBy(name = Config.userAccount)
- private AndroidElement accountElement;
-
- @CacheLookup
- @AndroidFindBy(name = Config.userAccount2)
- private AndroidElement accountElement2;
-
- @AndroidFindBy(name = "Delete account")
- private AndroidElement deleteAccountElement;
-
- @AndroidFindBy(name = "Change password")
- private AndroidElement changePasswordElement;
-
- @AndroidFindBy(name = "Add account")
- private AndroidElement addAccountElement;
-
- @AndroidFindBy(uiAutomator = "new UiSelector()"
- + ".className(\"android.widget.CheckBox\").index(0)")
- private AndroidElement passcodeCheckbox;
-
- public SettingsView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public void tapOnAccountElement (int accountPosition, int fingers, int milliSeconds) {
- if(accountPosition==1)
- accountElement.tap(fingers, milliSeconds);
- else
- accountElement2.tap(fingers, milliSeconds);
- }
-
- public void tapOnAddAccount (int fingers, int milliSeconds) {
- addAccountElement.tap(fingers, milliSeconds);
- }
-
- public LoginForm clickOnDeleteAccountElement () {
- deleteAccountElement.click();
- LoginForm loginForm = new LoginForm(driver);
- return loginForm;
- }
-
- public LoginForm clickOnChangePasswordElement () {
- changePasswordElement.click();
- LoginForm loginForm = new LoginForm(driver);
- return loginForm;
- }
-
- public PassCodeView EnablePassCode(){
- if(!passcodeCheckbox.isSelected()){
- passcodeCheckbox.click();
- }
- PassCodeView passcodeview = new PassCodeView(driver);
- return passcodeview;
- }
-
- public PassCodeView DisablePassCode(){
- if(passcodeCheckbox.isSelected()){
- passcodeCheckbox.click();
- }
- PassCodeView passcodeview = new PassCodeView(driver);
- return passcodeview;
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/ShareView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/ShareView.java
deleted file mode 100644
index 650671ba35a2..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/ShareView.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.FindBy;
-import org.openqa.selenium.support.PageFactory;
-
-public class ShareView {
- final AndroidDriver driver;
-
- @CacheLookup
- @FindBy(id = "android:id/select_dialog_listview")
- private AndroidElement listViewLayout;
-
- public ShareView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public AndroidElement getListViewLayout () {
- return listViewLayout;
- }
-
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/UploadFilesView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/UploadFilesView.java
deleted file mode 100644
index 599e90c1da34..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/UploadFilesView.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-
-import com.owncloud.android.test.ui.actions.Actions;
-
-public class UploadFilesView{
- final AndroidDriver driver;
-
- @CacheLookup
- @AndroidFindBy(id = "com.owncloud.android:id/list_root")
- private AndroidElement filesLayout;
-
- @CacheLookup
- @AndroidFindBy(id = "com.owncloud.android:id/upload_files_btn_upload")
- private AndroidElement uploadButton;
-
- private AndroidElement fileElement;
-
- public UploadFilesView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public FileListView clickOnUploadButton () {
- uploadButton.click();
- FileListView fileListView = new FileListView (driver);
- return fileListView;
- }
-
- //change to scrollTillFindElement
- public void scrollTillFindFile (String fileName) {
- fileElement = Actions
- .scrollTillFindElement(fileName,filesLayout,driver);
- }
-
- public void clickOnFileName (String fileName) {
- scrollTillFindFile(fileName);
- fileElement.click();
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/UploadView.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/UploadView.java
deleted file mode 100644
index 5975c9af7dc8..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/UploadView.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-import org.openqa.selenium.support.CacheLookup;
-import org.openqa.selenium.support.PageFactory;
-
-public class UploadView {
- final AndroidDriver driver;
-
- @CacheLookup
- @AndroidFindBy(name = "Upload")
- private AndroidElement uploadButton;
-
- public UploadView (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public void clickOUploadButton () {
- uploadButton.click();
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/models/WaitAMomentPopUp.java b/automationTest/src/test/java/com/owncloud/android/test/ui/models/WaitAMomentPopUp.java
deleted file mode 100644
index d296235b5714..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/models/WaitAMomentPopUp.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.models;
-
-import org.openqa.selenium.support.PageFactory;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-import io.appium.java_client.pagefactory.AndroidFindBy;
-import io.appium.java_client.pagefactory.AppiumFieldDecorator;
-
-public class WaitAMomentPopUp {
- final AndroidDriver driver;
-
- @AndroidFindBy(name = "Wait a moment")
- private AndroidElement waitAMomentText;
-
- public WaitAMomentPopUp (AndroidDriver driver) {
- this.driver = driver;
- PageFactory.initElements(new AppiumFieldDecorator(driver), this);
- }
-
- public AndroidElement getWaitAMomentTextElement () {
- return waitAMomentText;
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/.gitignore b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/.gitignore
deleted file mode 100644
index fa0f66dfc9e4..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-Config.java
\ No newline at end of file
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/Common.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/Common.java
deleted file mode 100644
index 7510cd99b26f..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/Common.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URL;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.commons.io.FileUtils;
-import org.openqa.selenium.By;
-import org.openqa.selenium.NoSuchElementException;
-import org.openqa.selenium.OutputType;
-import org.openqa.selenium.TimeoutException;
-import org.openqa.selenium.remote.DesiredCapabilities;
-import org.openqa.selenium.remote.RemoteWebDriver;
-import org.openqa.selenium.support.ui.WebDriverWait;
-
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-
-public class Common{
- AndroidDriver driver;
- static int waitingTime = 30;
-
- public WebDriverWait wait;
-
- protected AndroidDriver setUpCommonDriver () throws Exception {
- File rootPath = new File(System.getProperty("user.dir"));
- File appDir = new File(rootPath,"src/test/resources");
- File app = new File(appDir,"ownCloud.apk");
- DesiredCapabilities capabilities = new DesiredCapabilities();
- capabilities.setCapability("platformName", "Android");
- capabilities.setCapability("deviceName", "test");
- capabilities.setCapability("app", app.getAbsolutePath());
- capabilities.setCapability("appPackage", "com.owncloud.android");
- capabilities.setCapability("appActivity",
- ".ui.activity.FileDisplayActivity");
- capabilities.setCapability("appWaitActivity",
- ".authentication.AuthenticatorActivity");
- driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"),
- capabilities);
- driver.manage().timeouts().implicitlyWait(waitingTime,
- TimeUnit.SECONDS);
- wait = new WebDriverWait(driver, waitingTime, 50);
- return driver;
-
- }
-
- protected boolean waitForTextPresent(String text, AndroidElement element)
- throws InterruptedException{
- for (int second = 0;;second++){
- if (second >= waitingTime)
- return false;
- try{
- if (text.equals(element.getText()))
- break;
- } catch (Exception e){
-
- }
- Thread.sleep(1000);
- }
- return true;
- }
-
- protected boolean isElementPresent(AndroidElement element, By by) {
- try {
- element.findElement(by);
- return true;
- } catch (NoSuchElementException e) {
- return false;
- }
- }
-
- public static boolean isElementPresent(AndroidElement element) {
- try{
- element.isDisplayed();
- } catch (NoSuchElementException e){
- return false;
- }
- return true;
- }
-
- //pollingTime in milliseconds
- public static void waitTillElementIsNotPresent (AndroidElement element,
- int pollingTime) throws Exception {
- for (int time = 0;time <= waitingTime * 1000;time += pollingTime){
- try{
- element.isDisplayed();
- } catch (NoSuchElementException e){
- return;
- }
- Thread.sleep(pollingTime);
- }
- throw new TimeoutException();
- }
-
- public static void waitTillElementIsNotPresentWithoutTimeout (
- AndroidElement element,int pollingTime)
- throws InterruptedException {
- for (int time = 0;time <= waitingTime * 1000;time += pollingTime){
- try{
- element.isDisplayed();
- } catch (NoSuchElementException e){
- return;
- }
- Thread.sleep(pollingTime);
- }
- }
-
- public static void waitTillElementIsPresent (
- AndroidElement element,int pollingTime)
- throws InterruptedException {
- for (int time = 0;time <= waitingTime * 1000;time += pollingTime){
- try{
- if(element.isDisplayed()){
- return;
- }
- } catch (NoSuchElementException e){
-
- }
- Thread.sleep(pollingTime);
- }
- }
-
- protected void takeScreenShotOnFailed (String testName)
- throws IOException {
- File file = ((RemoteWebDriver) driver)
- .getScreenshotAs(OutputType.FILE);
- SimpleDateFormat dt1 = new SimpleDateFormat("yyyy-MM-dd");
- Date today = Calendar.getInstance().getTime();
- String screenShotName = "ScreenShots/" + dt1.format(today) + "/"
- + testName + ".png";
- FileUtils.copyFile(file, new File(screenShotName));
- }
-
- protected void assertIsInFileListView() throws InterruptedException {
- //waitForTextPresent("Wrong username or password",
- // changePasswordForm.getAuthStatusText());
- Thread.sleep(2000);
- assertTrue(waitForTextPresent("ownCloud", (AndroidElement) driver
- .findElementByAndroidUIAutomator("new UiSelector()"
- + ".resourceId(\"android:id/action_bar_title\")")));
- assertTrue(isElementPresent((AndroidElement) driver
- .findElementByAndroidUIAutomator("new UiSelector()"
- + ".description(\"Upload\")")));
- }
-
- protected void assertIsNotInFileListView() throws InterruptedException {
- AndroidElement fileElement;
- assertTrue(waitForTextPresent("ownCloud", (AndroidElement) driver
- .findElementByAndroidUIAutomator("new UiSelector()"
- + ".resourceId(\"android:id/action_bar_title\")")));
- try {
- fileElement = (AndroidElement) driver
- .findElementByAndroidUIAutomator("new UiSelector()"
- + ".description(\"Upload\")");
- } catch (NoSuchElementException e) {
- fileElement = null;
- }
- assertNull(fileElement);
- }
-
- protected void assertIsPasscodeRequestView() throws InterruptedException {
- assertTrue(waitForTextPresent("ownCloud", (AndroidElement) driver
- .findElementByAndroidUIAutomator("new UiSelector()"
- + ".resourceId(\"android:id/action_bar_title\")")));
- assertTrue(((AndroidElement) driver.findElementByAndroidUIAutomator(
- "new UiSelector().text(\"Please, insert your pass code\")"))
- .isDisplayed());
-
- }
-
- protected void assertIsInSettingsView() throws InterruptedException {
- assertTrue(waitForTextPresent("Settings", (AndroidElement) driver
- .findElementByAndroidUIAutomator("new UiSelector()"
- + ".resourceId(\"android:id/action_bar_title\")")));
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/Config.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/Config.java
deleted file mode 100644
index 4674fab20949..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/Config.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-public final class Config {
-
- //without http or https
- public static final String URL = "owncloudServerVar";
- public static boolean isTrusted = true;
-
- //without http or https
- public static final String URL2 = "owncloudServer2Var";
- public static boolean isTrusted2 = true;
-
- public static final String user = "owncloudUserVar";
- public static final String password = "owncloudPasswordVar";
- public static final String user2 = "owncloudUser2Var";
- public static final String password2 = "owncloudPassword2Var";
- public static final String userAccount = user + "@"+ URL;
- public static final String userAccount2 = user2 + "@"+ URL2;
-
- public static final String gmailAccount = "gmailAccountVar";
-
- public static final String fileWhichIsInTheServer1 ="test";
- public static final String fileWhichIsInTheServer2 ="test";
-
- public static final String fileToTestName = "test";
- public static final String fileToTestSendByEmailName = "test";
- public static final String bigFileToTestName = "test";
-
- public static final String passcode1 = "passcode1";
- public static final String passcode2 = "passcode2";
- public static final String passcode3 = "passcode3";
- public static final String passcode4 = "passcode4";
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/CreateFolderTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/CreateFolderTestSuite.java
deleted file mode 100644
index 139487555488..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/CreateFolderTestSuite.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.WaitAMomentPopUp;
-
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class CreateFolderTestSuite{
-
- AndroidDriver driver;
- Common common;
- private Boolean folderHasBeenCreated = false;
- private final String FOLDER_NAME = "testCreateFolder";
- private String CurrentCreatedFolder = "";
-
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testCreateNewFolder () throws Exception {
- String NEW_FOLDER_NAME = "testCreateFolder";
-
- FileListView fileListView = Actions.login(Config.URL,
- Config.user,Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //check if the folder already exists and if true, delete them
- Actions.deleteElement(NEW_FOLDER_NAME, fileListView, driver);
-
- WaitAMomentPopUp waitAMomentPopUp = Actions
- .createFolder(NEW_FOLDER_NAME, fileListView);
- Common.waitTillElementIsNotPresentWithoutTimeout(waitAMomentPopUp
- .getWaitAMomentTextElement(), 100);
- fileListView.scrollTillFindElement(FOLDER_NAME);
- assertNotNull(fileListView.getFileElement());
- assertTrue(
- folderHasBeenCreated=fileListView.getFileElement().isDisplayed());
- CurrentCreatedFolder = FOLDER_NAME;
- assertEquals(FOLDER_NAME , fileListView.getFileElement().getText());
- }
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- if (folderHasBeenCreated) {
- FileListView fileListView = new FileListView(driver);
- Actions.deleteElement(CurrentCreatedFolder, fileListView, driver);
- }
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/DeleteFileTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/DeleteFileTestSuite.java
deleted file mode 100644
index 5970ec970b5a..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/DeleteFileTestSuite.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.openqa.selenium.By;
-import org.openqa.selenium.support.ui.ExpectedConditions;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.models.FileListView;
-
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class DeleteFileTestSuite{
-
- AndroidDriver driver;
- Common common;
- private final String FILE_NAME = Config.fileToTestName;
-
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testDeleteFile () throws Exception {
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //TODO. if the file already exists, do not upload
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(FILE_NAME, fileListView);
-
- fileListViewAfterUploadFile.scrollTillFindElement(FILE_NAME);
- Common.waitTillElementIsNotPresentWithoutTimeout(
- fileListViewAfterUploadFile.getProgressCircular(), 1000);
- common.wait.until(ExpectedConditions.visibilityOf(
- fileListViewAfterUploadFile.getFileElementLayout()
- .findElement(By.id(FileListView.getLocalFileIndicator()))));
-
- Actions.deleteElement(FILE_NAME,fileListViewAfterUploadFile, driver);
- assertFalse(fileListViewAfterUploadFile.getFileElement().isDisplayed());
- }
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-}
\ No newline at end of file
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/DeleteFolderTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/DeleteFolderTestSuite.java
deleted file mode 100644
index 375de02a9ab5..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/DeleteFolderTestSuite.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.WaitAMomentPopUp;
-
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class DeleteFolderTestSuite{
- AndroidDriver driver;
- Common common;
- private Boolean folderHasBeenCreated = false;
- private final String FOLDER_NAME = "testCreateFolder";
-
- @Rule public TestName name = new TestName();
-
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testDeleteFolder () throws Exception {
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //TODO. if the folder already exists, do no created
- //create the folder
- WaitAMomentPopUp waitAMomentPopUp = Actions
- .createFolder(FOLDER_NAME, fileListView);
- Common.waitTillElementIsNotPresentWithoutTimeout(
- waitAMomentPopUp.getWaitAMomentTextElement(), 100);
- fileListView.scrollTillFindElement(FOLDER_NAME);
- assertTrue(
- folderHasBeenCreated = fileListView.getFileElement().isDisplayed());
-
- //delete the folder
- Actions.deleteElement(FOLDER_NAME, fileListView, driver);
- assertFalse(
- folderHasBeenCreated =fileListView.getFileElement().isDisplayed());
- }
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- if(folderHasBeenCreated){
- FileListView fileListView = new FileListView(driver);
- Actions.deleteElement(FOLDER_NAME, fileListView, driver);
- }
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/LoginTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/LoginTestSuite.java
deleted file mode 100644
index d9a95535c0ab..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/LoginTestSuite.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.openqa.selenium.ScreenOrientation;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.*;
-import com.owncloud.android.test.ui.models.LoginForm;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.MenuList;
-import com.owncloud.android.test.ui.models.SettingsView;
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class LoginTestSuite{
- AndroidDriver driver;
- Common common;
-
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class})
- public void test1LoginPortrait () throws Exception {
- driver.rotate(ScreenOrientation.PORTRAIT);
-
- Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class})
- public void test2LoginLandscape () throws Exception {
- driver.rotate(ScreenOrientation.LANDSCAPE);
- Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testLoginAndShowFiles () throws Exception {
- driver.rotate(ScreenOrientation.PORTRAIT);
-
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- fileListView.scrollTillFindElement(Config.fileWhichIsInTheServer1);
- assertTrue(fileListView.getFileElement().isDisplayed());
- }
-
-
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void test3MultiAccountRotate () throws Exception {
- driver.rotate(ScreenOrientation.LANDSCAPE);
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- driver.rotate(ScreenOrientation.PORTRAIT);
- MenuList menu = fileListView.clickOnMenuButton();
- SettingsView settingsView = menu.clickOnSettingsButton();
-
- settingsView.tapOnAddAccount(1, 1000);
- fileListView = Actions.login(Config.URL2, Config.user2,
- Config.password2, Config.isTrusted2, driver);
- common.assertIsInSettingsView();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testMultiAccountAndShowFiles () throws Exception {
- driver.rotate(ScreenOrientation.LANDSCAPE);
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
- fileListView.scrollTillFindElement(Config.fileWhichIsInTheServer1);
- assertTrue(fileListView.getFileElement().isDisplayed());
-
- driver.rotate(ScreenOrientation.PORTRAIT);
- MenuList menu = fileListView.clickOnMenuButton();
- SettingsView settingsView = menu.clickOnSettingsButton();
-
- settingsView.tapOnAddAccount(1, 1000);
- fileListView = Actions.login(Config.URL2, Config.user2,
- Config.password2, Config.isTrusted2, driver);
- common.assertIsInSettingsView();
- settingsView.tapOnAccountElement(2,1, 100);
- common.assertIsInFileListView();
-
- fileListView.scrollTillFindElement(Config.fileWhichIsInTheServer2);
- assertTrue(fileListView.getFileElement().isDisplayed());
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class})
- public void test4ExistingAccountRotate () throws Exception {
- driver.rotate(ScreenOrientation.PORTRAIT);
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- driver.rotate(ScreenOrientation.LANDSCAPE);
- MenuList menu = fileListView.clickOnMenuButton();
- SettingsView settingsView = menu.clickOnSettingsButton();
- settingsView.tapOnAddAccount(1, 1000);
-
- LoginForm loginForm = new LoginForm(driver);
- fileListView = Actions.login(Config.URL, Config.user,Config.password,
- Config.isTrusted, driver);
- assertTrue(common.waitForTextPresent("An account for the same user and"
- + " server already exists in the device",
- loginForm.getAuthStatusText()));
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class})
- public void test5ChangePasswordWrong () throws Exception {
- driver.rotate(ScreenOrientation.PORTRAIT);
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
- MenuList menu = fileListView.clickOnMenuButton();
- SettingsView settingsView = menu.clickOnSettingsButton();
- settingsView.tapOnAccountElement(1,1, 1000);
- LoginForm changePasswordForm = settingsView
- .clickOnChangePasswordElement();
- changePasswordForm.typePassword("WrongPassword");
- changePasswordForm.clickOnConnectButton();
- assertTrue(common.waitForTextPresent("Wrong username or password",
- changePasswordForm.getAuthStatusText()));
- }
-
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/LogoutTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/LogoutTestSuite.java
deleted file mode 100644
index 8927dacecf11..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/LogoutTestSuite.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.models.LoginForm;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.MenuList;
-import com.owncloud.android.test.ui.models.SettingsView;
-
-public class LogoutTestSuite{
-
- AndroidDriver driver;
- Common common;
-
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testLogout () throws Exception {
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
- MenuList menulist = fileListView.clickOnMenuButton();
- SettingsView settingsView = menulist.clickOnSettingsButton();
- settingsView.tapOnAccountElement(1,1, 1000);
- LoginForm loginForm = settingsView.clickOnDeleteAccountElement();
- assertEquals("Server address https://…",
- loginForm.gethostUrlInput().getText());
- assertEquals("Username", loginForm.getUserNameInput().getText());
- assertEquals("", loginForm.getPasswordInput().getText());
- }
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- //driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/MoveFileTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/MoveFileTestSuite.java
deleted file mode 100644
index 38c004b580fc..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/MoveFileTestSuite.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.models.ElementMenuOptions;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.MoveView;
-import com.owncloud.android.test.ui.models.WaitAMomentPopUp;
-
-
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class MoveFileTestSuite{
- AndroidDriver driver;
- Common common;
- private String FOLDER_WHERE_MOVE = "folderWhereMove";
- private String FILE_NAME = Config.fileToTestName;
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testMoveFile () throws Exception {
- WaitAMomentPopUp waitAMomentPopUp;
-
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //Common.waitTillElementIsNotPresentWithoutTimeout(
- //fileListView.getProgressCircular(), 1000);
-
- //check if the folder already exists and if true, delete them
- Actions.deleteElement(FOLDER_WHERE_MOVE, fileListView, driver);
- Actions.deleteElement(FILE_NAME, fileListView, driver);
-
- //Create the folder where the other is gone to be moved
- waitAMomentPopUp = Actions
- .createFolder(FOLDER_WHERE_MOVE, fileListView);
- Common.waitTillElementIsNotPresentWithoutTimeout(
- waitAMomentPopUp.getWaitAMomentTextElement(), 100);
- fileListView.scrollTillFindElement(FOLDER_WHERE_MOVE);
- assertTrue(fileListView.getFileElement().isDisplayed());
-
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(FILE_NAME, fileListView);
- fileListViewAfterUploadFile.scrollTillFindElement(FILE_NAME);
- assertTrue(fileListViewAfterUploadFile.getFileElement().isDisplayed());
-
- //select to move the file
- ElementMenuOptions menuOptions = fileListView
- .longPressOnElement(FILE_NAME);
- MoveView moveView = menuOptions.clickOnMove();
-
- //to move to a folder
- moveView.scrollTillFindElement(FOLDER_WHERE_MOVE).tap(1,1);
- waitAMomentPopUp = moveView.clickOnChoose();
- Common.waitTillElementIsNotPresentWithoutTimeout(
- waitAMomentPopUp.getWaitAMomentTextElement(), 100);
-
- //check that the folder moved is inside the other
- fileListView.scrollTillFindElement(FOLDER_WHERE_MOVE).tap(1,1);
- Common.waitTillElementIsNotPresentWithoutTimeout(fileListView.getProgressCircular(),
- 1000);
- Thread.sleep(1000);
- fileListView.scrollTillFindElement(FILE_NAME);
- assertEquals(FILE_NAME , fileListView.getFileElement().getText());
-
- }
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- FileListView fileListView = new FileListView(driver);
- driver.sendKeyEvent(android.view.KeyEvent.KEYCODE_BACK);
- Actions.deleteElement(FOLDER_WHERE_MOVE, fileListView, driver);
- Actions.deleteElement(FILE_NAME, fileListView, driver);
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/MoveFolderTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/MoveFolderTestSuite.java
deleted file mode 100644
index a6f8769f5713..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/MoveFolderTestSuite.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.models.ElementMenuOptions;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.MoveView;
-import com.owncloud.android.test.ui.models.WaitAMomentPopUp;
-
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class MoveFolderTestSuite{
- AndroidDriver driver;
- Common common;
- private String FOLDER_TO_MOVE = "folderToMove";
- private String FOLDER_WHERE_MOVE = "folderWhereMove";
-
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testMoveFolder () throws Exception {
- WaitAMomentPopUp waitAMomentPopUp;
-
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //Common.waitTillElementIsNotPresentWithoutTimeout(
- //fileListView.getProgressCircular(), 1000);
-
- //check if the folder already exists and if true, delete them
- Actions.deleteElement(FOLDER_WHERE_MOVE, fileListView, driver);
- Actions.deleteElement(FOLDER_TO_MOVE, fileListView, driver);
-
- //Create the folder where the other is gone to be moved
- waitAMomentPopUp = Actions
- .createFolder(FOLDER_WHERE_MOVE, fileListView);
- Common.waitTillElementIsNotPresentWithoutTimeout(
- waitAMomentPopUp.getWaitAMomentTextElement(), 100);
- fileListView.scrollTillFindElement(FOLDER_WHERE_MOVE);
- assertTrue(fileListView.getFileElement().isDisplayed());
-
- //Create the folder which is going to be moved
- waitAMomentPopUp = Actions.createFolder(FOLDER_TO_MOVE, fileListView);
- Common.waitTillElementIsNotPresent(
- waitAMomentPopUp.getWaitAMomentTextElement(), 100);
- fileListView.scrollTillFindElement(FOLDER_TO_MOVE);
- assertTrue(fileListView.getFileElement().isDisplayed());
-
- //select to move the folder
- ElementMenuOptions menuOptions = fileListView
- .longPressOnElement(FOLDER_TO_MOVE);
- MoveView moveView = menuOptions.clickOnMove();
-
- //to move to a folder
- moveView.scrollTillFindElement(FOLDER_WHERE_MOVE).tap(1,1);
- waitAMomentPopUp = moveView.clickOnChoose();
- Common.waitTillElementIsNotPresentWithoutTimeout(waitAMomentPopUp
- .getWaitAMomentTextElement(), 100);
-
- //check that the folder moved is inside the other
- fileListView.scrollTillFindElement(FOLDER_WHERE_MOVE).tap(1,1);
- Common.waitTillElementIsNotPresentWithoutTimeout(fileListView.getProgressCircular(),
- 1000);
- Thread.sleep(1000);
- fileListView.scrollTillFindElement(FOLDER_TO_MOVE);
- assertEquals(FOLDER_TO_MOVE , fileListView.getFileElement().getText());
- }
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- FileListView fileListView = new FileListView(driver);
- driver.sendKeyEvent(android.view.KeyEvent.KEYCODE_BACK);
- Actions.deleteElement(FOLDER_WHERE_MOVE, fileListView, driver);
- Actions.deleteElement(FOLDER_TO_MOVE, fileListView, driver);
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/PasscodeTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/PasscodeTestSuite.java
deleted file mode 100644
index 9f671ce8063c..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/PasscodeTestSuite.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.openqa.selenium.ScreenOrientation;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.MenuList;
-import com.owncloud.android.test.ui.models.PassCodeRequestView;
-import com.owncloud.android.test.ui.models.PassCodeView;
-import com.owncloud.android.test.ui.models.SettingsView;
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class PasscodeTestSuite {
- AndroidDriver driver;
- Common common;
-
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class})
- public void testPincodeEnable () throws Exception {
- driver.rotate(ScreenOrientation.PORTRAIT);
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- MenuList menu = fileListView.clickOnMenuButton();
- SettingsView settingsView = menu.clickOnSettingsButton();
-
- PassCodeView passCodeview = settingsView.EnablePassCode();
- PassCodeView passCodeview2 = passCodeview.enterPasscode(
- Config.passcode1, Config.passcode2, Config.passcode3,
- Config.passcode4);
- passCodeview2.reenterPasscode(Config.passcode1, Config.passcode2,
- Config.passcode3, Config.passcode4);
-
- driver.sendKeyEvent(android.view.KeyEvent.KEYCODE_HOME);
- //TO DO. Open the app instead of start an activity
- driver.startActivity("com.owncloud.android",
- ".ui.activity.FileDisplayActivity");
- //here we check that we are not in the fileDisplayActivity,
- //because pincode is asked
- common.assertIsNotInFileListView();
- common.assertIsPasscodeRequestView();
-
- PassCodeRequestView passCodeReequestView = new
- PassCodeRequestView(driver);
- passCodeReequestView.enterPasscode(Config.passcode1, Config.passcode2,
- Config.passcode3, Config.passcode4);
- common.assertIsInFileListView();
- }
-
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RefreshFolderTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RefreshFolderTestSuite.java
deleted file mode 100644
index 850a75727120..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RefreshFolderTestSuite.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.UnfinishedTestCategory;
-import com.owncloud.android.test.ui.models.FileListView;
-
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class RefreshFolderTestSuite{
- AndroidDriver driver;
- Common common;
-
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
-
- @Test
- @Category({UnfinishedTestCategory.class})
- public void testPulldownToRefreshFolder () throws Exception {
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
- //TODO. Remove the sleep and check why is not working the assert
- //when using waitTillElementIsNotPresent
- Thread.sleep(5000);
- //waitTillElementIsNotPresentWithoutTimeout(fileListView.getProgressCircular(), 1000);
- fileListView.pulldownToRefresh();
- assertTrue(fileListView.getProgressCircular().isDisplayed());
- //TODO insert a file in the web, and check that it's shown here
- }
-
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RenameFileTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RenameFileTestSuite.java
deleted file mode 100644
index d82cd1191b7b..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RenameFileTestSuite.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.openqa.selenium.By;
-import org.openqa.selenium.support.ui.ExpectedConditions;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.models.ElementMenuOptions;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.NewFolderPopUp;
-import com.owncloud.android.test.ui.models.WaitAMomentPopUp;
-
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class RenameFileTestSuite{
-
- AndroidDriver driver;
- Common common;
- private Boolean fileHasBeenCreated = false;
- private final String OLD_FILE_NAME = Config.fileToTestName;
- private final String FILE_NAME = "newNameFile";
- private String CurrentCreatedFile = "";
-
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testRenameFile () throws Exception {
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //TODO. if the file already exists, do not upload
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(OLD_FILE_NAME, fileListView);
-
- //check if the file with the new name already exists, if true delete it
- Actions.deleteElement(FILE_NAME, fileListView, driver);
-
- fileListViewAfterUploadFile.scrollTillFindElement(OLD_FILE_NAME);
- assertTrue(fileHasBeenCreated = fileListViewAfterUploadFile
- .getFileElement().isDisplayed());
- CurrentCreatedFile = OLD_FILE_NAME;
- Common.waitTillElementIsNotPresentWithoutTimeout(fileListViewAfterUploadFile
- .getProgressCircular(), 1000);
- common.wait.until(ExpectedConditions.visibilityOf(
- fileListViewAfterUploadFile.getFileElementLayout()
- .findElement(By.id(FileListView.getLocalFileIndicator()))));
- ElementMenuOptions menuOptions = fileListViewAfterUploadFile
- .longPressOnElement(OLD_FILE_NAME);
- NewFolderPopUp newFolderPopUp = menuOptions.clickOnRename();
- newFolderPopUp.typeNewFolderName(FILE_NAME);
- WaitAMomentPopUp waitAMomentPopUp = newFolderPopUp
- .clickOnNewFolderOkButton();
- Common.waitTillElementIsNotPresentWithoutTimeout(waitAMomentPopUp
- .getWaitAMomentTextElement(), 100);
- fileListViewAfterUploadFile.scrollTillFindElement(FILE_NAME);
- assertNotNull(fileListViewAfterUploadFile.getFileElement());
- assertTrue(fileListViewAfterUploadFile.getFileElement().isDisplayed());
- assertEquals(FILE_NAME , fileListViewAfterUploadFile.getFileElement()
- .getText());
- CurrentCreatedFile = FILE_NAME;
- }
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- if (fileHasBeenCreated) {
- FileListView fileListView = new FileListView(driver);
- Actions.deleteElement(CurrentCreatedFile,fileListView, driver);
- }
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RenameFolderTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RenameFolderTestSuite.java
deleted file mode 100644
index 271295f00c0b..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RenameFolderTestSuite.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.models.ElementMenuOptions;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.NewFolderPopUp;
-import com.owncloud.android.test.ui.models.WaitAMomentPopUp;
-
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class RenameFolderTestSuite{
-
- AndroidDriver driver;
- Common common;
- private Boolean folderHasBeenCreated = false;
- private final String OLD_FOLDER_NAME = "beforeRemoving";
- private final String FOLDER_NAME = "testCreateFolder";
- private String CurrentCreatedFolder = "";
-
- @Rule public TestName name = new TestName();
-
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testRenameFolder () throws Exception {
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //TODO. if the folder already exists, do no created
- //create the folder to rename
- WaitAMomentPopUp waitAMomentPopUp = Actions
- .createFolder(OLD_FOLDER_NAME, fileListView);
- Common.waitTillElementIsNotPresentWithoutTimeout(
- waitAMomentPopUp.getWaitAMomentTextElement(), 100);
- fileListView.scrollTillFindElement(OLD_FOLDER_NAME);
-
- assertTrue(
- folderHasBeenCreated = fileListView.getFileElement().isDisplayed());
-
- //check if the folder with the new name already exists
- //and if true, delete them
- Actions.deleteElement(FOLDER_NAME, fileListView, driver);
-
- CurrentCreatedFolder = OLD_FOLDER_NAME;
- ElementMenuOptions menuOptions = fileListView
- .longPressOnElement(OLD_FOLDER_NAME);
- NewFolderPopUp FolderPopUp = menuOptions.clickOnRename();
- FolderPopUp.typeNewFolderName(FOLDER_NAME);
- FolderPopUp.clickOnNewFolderOkButton();
- CurrentCreatedFolder = FOLDER_NAME;
- Common.waitTillElementIsNotPresentWithoutTimeout(waitAMomentPopUp
- .getWaitAMomentTextElement(), 100);
- fileListView.scrollTillFindElement(FOLDER_NAME);
- assertNotNull(fileListView.getFileElement());
- assertTrue(
- folderHasBeenCreated = fileListView.getFileElement().isDisplayed());
- assertEquals(FOLDER_NAME , fileListView.getFileElement().getText());
- }
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- if(folderHasBeenCreated){
- FileListView fileListView = new FileListView(driver);
- Actions.deleteElement(CurrentCreatedFolder, fileListView, driver);
- }
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunFailingTests.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunFailingTests.java
deleted file mode 100644
index 9633abca3ef1..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunFailingTests.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-
-import org.junit.experimental.categories.Categories.ExcludeCategory;
-import org.junit.experimental.categories.Categories.IncludeCategory;
-import org.junit.runner.RunWith;
-
-import com.owncloud.android.test.ui.groups.FailingTestCategory;
-import com.owncloud.android.test.ui.groups.FlexibleCategories;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestClassPrefix;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestClassSuffix;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestScanPackage;
-
-
-@RunWith(FlexibleCategories.class)
-@ExcludeCategory(NoIgnoreTestCategory.class)
-@IncludeCategory(FailingTestCategory.class)
-@TestScanPackage("com.owncloud.android.test.ui.testSuites")
-@TestClassPrefix("")
-@TestClassSuffix("TestSuite")
-public class RunFailingTests {
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunInProgressTest.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunInProgressTest.java
deleted file mode 100644
index 9fb14a053f19..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunInProgressTest.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.owncloud.android.test.ui.testSuites;
-
-import org.junit.experimental.categories.Categories.IncludeCategory;
-import org.junit.runner.RunWith;
-import com.owncloud.android.test.ui.groups.FlexibleCategories;
-import com.owncloud.android.test.ui.groups.InProgressCategory;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestClassPrefix;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestClassSuffix;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestScanPackage;
-
-
-@RunWith(FlexibleCategories.class)
-@IncludeCategory(InProgressCategory.class)
-@TestScanPackage("com.owncloud.android.test.ui.testSuites")
-@TestClassPrefix("")
-@TestClassSuffix("TestSuite")
-public class RunInProgressTest {
-
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunNoIgnoreTests.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunNoIgnoreTests.java
deleted file mode 100644
index 4d9043876084..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunNoIgnoreTests.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import org.junit.experimental.categories.Categories.ExcludeCategory;
-import org.junit.experimental.categories.Categories.IncludeCategory;
-import org.junit.runner.RunWith;
-
-import com.owncloud.android.test.ui.groups.FlexibleCategories;
-import com.owncloud.android.test.ui.groups.IgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestClassPrefix;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestClassSuffix;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestScanPackage;
-
-
-@RunWith(FlexibleCategories.class)
-@ExcludeCategory(IgnoreTestCategory.class)
-@IncludeCategory(NoIgnoreTestCategory.class)
-@TestScanPackage("com.owncloud.android.test.ui.testSuites")
-@TestClassPrefix("")
-@TestClassSuffix("TestSuite")
-public class RunNoIgnoreTests {
-}
\ No newline at end of file
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunSmokeTests.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunSmokeTests.java
deleted file mode 100644
index 5c2e6ebdba01..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/RunSmokeTests.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import org.junit.experimental.categories.Categories.ExcludeCategory;
-import org.junit.experimental.categories.Categories.IncludeCategory;
-import org.junit.runner.RunWith;
-
-import com.owncloud.android.test.ui.groups.FlexibleCategories;
-import com.owncloud.android.test.ui.groups.IgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestClassPrefix;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestClassSuffix;
-import com.owncloud.android.test.ui.groups.FlexibleCategories.TestScanPackage;
-
-
-@RunWith(FlexibleCategories.class)
-@ExcludeCategory(IgnoreTestCategory.class)
-@IncludeCategory(SmokeTestCategory.class)
-@TestScanPackage("com.owncloud.android.test.ui.testSuites")
-@TestClassPrefix("")
-@TestClassSuffix("TestSuite")
-public class RunSmokeTests {
-}
\ No newline at end of file
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/ShareLinkFileTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/ShareLinkFileTestSuite.java
deleted file mode 100644
index 5e604cd32633..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/ShareLinkFileTestSuite.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-import static org.junit.Assert.*;
-import io.appium.java_client.android.AndroidDriver;
-import io.appium.java_client.android.AndroidElement;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.IgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.InProgressCategory;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.models.FileListView;;
-
-public class ShareLinkFileTestSuite{
-
- AndroidDriver driver;
- Common common;
- private final String FILE_NAME = Config.fileToTestName;
- private Boolean fileHasBeenCreated = false;
-
- @Rule public TestName name = new TestName();
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class})
- public void testShareLinkFileByGmail () throws Exception {
- AndroidElement sharedElementIndicator;
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //TODO. if the file already exists, do not upload
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(FILE_NAME, fileListView);
-
- fileListViewAfterUploadFile.scrollTillFindElement(FILE_NAME);
- assertTrue(fileHasBeenCreated = fileListViewAfterUploadFile
- .getFileElement().isDisplayed());
-
- sharedElementIndicator = Actions.shareLinkElementByGmail(FILE_NAME,
- fileListViewAfterUploadFile,driver,common);
- assertTrue(sharedElementIndicator.isDisplayed());
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testShareLinkFileByCopyLink () throws Exception {
- AndroidElement sharedElementIndicator;
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //TODO. if the file already exists, do not upload
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(FILE_NAME, fileListView);
-
- fileListViewAfterUploadFile.scrollTillFindElement(FILE_NAME);
- assertTrue(fileHasBeenCreated = fileListViewAfterUploadFile
- .getFileElement().isDisplayed());
-
- sharedElementIndicator = Actions.shareLinkElementByCopyLink(FILE_NAME,
- fileListViewAfterUploadFile,driver,common);
- assertTrue(sharedElementIndicator.isDisplayed());
- }
-
- @Test
- @Category({IgnoreTestCategory.class, SmokeTestCategory.class})
- public void testUnshareLinkFile () throws Exception {
- AndroidElement sharedElementIndicator;
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //TODO. if the file already exists, do not upload
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(FILE_NAME, fileListView);
-
- fileListViewAfterUploadFile.scrollTillFindElement(FILE_NAME);
- assertTrue(fileHasBeenCreated = fileListViewAfterUploadFile
- .getFileElement().isDisplayed());
-
- sharedElementIndicator = Actions.shareLinkElementByCopyLink(FILE_NAME,
- fileListViewAfterUploadFile,driver,common);
- assertTrue(sharedElementIndicator.isDisplayed());
- Actions.unshareLinkElement(FILE_NAME,
- fileListViewAfterUploadFile,driver,common);
- assertFalse(sharedElementIndicator.isDisplayed());
-
- }
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- if (fileHasBeenCreated) {
- FileListView fileListView = new FileListView(driver);
- Actions.deleteElement(FILE_NAME,fileListView, driver);
- }
- driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-}
diff --git a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/UploadTestSuite.java b/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/UploadTestSuite.java
deleted file mode 100644
index d8ffeff57221..000000000000
--- a/automationTest/src/test/java/com/owncloud/android/test/ui/testSuites/UploadTestSuite.java
+++ /dev/null
@@ -1,285 +0,0 @@
-/**
- * ownCloud Android client application
- *
- * @author purigarcia
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.test.ui.testSuites;
-
-
-import static org.junit.Assert.*;
-import io.appium.java_client.MobileBy;
-import io.appium.java_client.android.AndroidDriver;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.TestName;
-import org.junit.runners.MethodSorters;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.openqa.selenium.By;
-import org.openqa.selenium.NoSuchElementException;
-import org.openqa.selenium.support.ui.ExpectedConditions;
-
-import com.owncloud.android.test.ui.actions.Actions;
-import com.owncloud.android.test.ui.groups.FailingTestCategory;
-import com.owncloud.android.test.ui.groups.InProgressCategory;
-import com.owncloud.android.test.ui.groups.NoIgnoreTestCategory;
-import com.owncloud.android.test.ui.groups.SmokeTestCategory;
-import com.owncloud.android.test.ui.groups.UnfinishedTestCategory;
-import com.owncloud.android.test.ui.models.FileDetailsView;
-import com.owncloud.android.test.ui.models.ElementMenuOptions;
-import com.owncloud.android.test.ui.models.GmailEmailListView;
-import com.owncloud.android.test.ui.models.GmailEmailView;
-import com.owncloud.android.test.ui.models.ImageView;
-import com.owncloud.android.test.ui.models.FileListView;
-import com.owncloud.android.test.ui.models.NotificationView;
-import com.owncloud.android.test.ui.models.SettingsView;
-import com.owncloud.android.test.ui.models.UploadView;
-
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Category({NoIgnoreTestCategory.class})
-public class UploadTestSuite{
-
- AndroidDriver driver;
- Common common;
- String FILE_NAME = Config.fileToTestName;
- String BIG_FILE_NAME = Config.bigFileToTestName;
- String FILE_GMAIL_NAME = Config.fileToTestSendByEmailName;
- private Boolean fileHasBeenUploadedFromGmail = false;
- private Boolean fileHasBeenUploaded = false;
-
- @Rule public TestName name = new TestName();
-
-
- @Before
- public void setUp() throws Exception {
- common=new Common();
- driver=common.setUpCommonDriver();
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class, SmokeTestCategory.class})
- public void testUploadFile () throws Exception {
-
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //check if the file already exists and if true, delete it
- Actions.deleteElement(FILE_NAME, fileListView, driver);
-
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(FILE_NAME, fileListView);
-
- fileListViewAfterUploadFile.scrollTillFindElement(FILE_NAME);
- assertTrue(fileListViewAfterUploadFile.getFileElement().isDisplayed());
- Common.waitTillElementIsNotPresentWithoutTimeout(
- fileListViewAfterUploadFile.getProgressCircular(), 1000);
- common.wait.until(ExpectedConditions.visibilityOf(
- fileListViewAfterUploadFile.getFileElementLayout()
- .findElement(By.id(FileListView.getLocalFileIndicator()))));
- assertTrue(fileListViewAfterUploadFile.getFileElementLayout()
- .findElement(By.id(FileListView.getLocalFileIndicator()))
- .isDisplayed());
- fileListView = new FileListView(driver);
- fileListView.scrollTillFindElement(FILE_NAME);
- assertTrue(
- fileHasBeenUploaded = fileListView.getFileElement().isDisplayed());
- }
-
-
-
- @Test
- @Category({UnfinishedTestCategory.class})
- public void testUploadBigFile () throws Exception {
-
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- //check if the file already exists and if true, delete it
- Actions.deleteElement(BIG_FILE_NAME, fileListView, driver);
-
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(BIG_FILE_NAME, fileListView);
-
-
- driver.openNotifications();
- NotificationView notificationView = new NotificationView(driver);
-
- try{
- if(notificationView.getUploadingNotification().isDisplayed()){
- Common.waitTillElementIsPresent(
- notificationView.getUploadSucceededNotification(),300000);
- driver.sendKeyEvent(android.view.KeyEvent.KEYCODE_HOME);
- driver.startActivity("com.owncloud.android",
- ".ui.activity.FileDisplayActivity");
-
- }
- } catch (NoSuchElementException e) {
- driver.sendKeyEvent(android.view.KeyEvent.KEYCODE_HOME);
- driver.startActivity("com.owncloud.android",
- ".ui.activity.FileDisplayActivity");
- }
-
- fileListViewAfterUploadFile.scrollTillFindElement(BIG_FILE_NAME);
-
- assertTrue(fileListViewAfterUploadFile.getFileElement().isDisplayed());
-
- Common.waitTillElementIsNotPresentWithoutTimeout(
- fileListViewAfterUploadFile.getProgressCircular(), 1000);
- common.wait.until(ExpectedConditions.visibilityOf(
- fileListViewAfterUploadFile.getFileElementLayout()
- .findElement(By.id(FileListView.getLocalFileIndicator()))));
- assertTrue(fileListViewAfterUploadFile.getFileElementLayout()
- .findElement(By.id(FileListView.getLocalFileIndicator()))
- .isDisplayed());
- fileListView = new FileListView(driver);
- fileListView.scrollTillFindElement(BIG_FILE_NAME);
- assertTrue(
- fileHasBeenUploaded = fileListView.getFileElement().isDisplayed());
- }
-
-
- @Test
- @Category(UnfinishedTestCategory.class)
- public void testUploadFromGmail () throws Exception {
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- driver.startActivity("com.google.android.gm",
- ".ConversationListActivityGmail");
- GmailEmailListView gmailEmailListView = new GmailEmailListView(driver);
- Thread.sleep(3000);
- GmailEmailView gmailEmailView = gmailEmailListView.clickOnEmail();
- ImageView imageView = gmailEmailView.clickOnfileButton();
- imageView.clickOnOptionsButton();
- imageView.clickOnShareButton();
- imageView.clickOnOwnCloudButton();
- //justonce button do not appear always
- try{
- imageView.clickOnJustOnceButton();
- }catch (NoSuchElementException e) {
- }
- UploadView uploadView = new UploadView(driver);
- uploadView.clickOUploadButton();
- driver.sendKeyEvent(android.view.KeyEvent.KEYCODE_HOME);
- driver.startActivity("com.owncloud.android",
- ".ui.activity.FileDisplayActivity");
- common.wait.until(ExpectedConditions
- .visibilityOfAllElementsLocatedBy(By.name(FILE_GMAIL_NAME)));
- assertEquals(Config.fileToTestSendByEmailName ,
- driver.findElementByName(FILE_GMAIL_NAME).getText());
- fileListView = new FileListView(driver);
- fileListView.scrollTillFindElement(FILE_GMAIL_NAME);
- assertTrue(fileHasBeenUploadedFromGmail = fileListView
- .getFileElement().isDisplayed());
- //TODO. correct assert if fileListView is shown in grid mode
- }
-
-
- @Test
- @Category({FailingTestCategory.class})
- public void testKeepFileUpToDate () throws Exception {
-
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- Common.waitTillElementIsNotPresentWithoutTimeout(fileListView.getProgressCircular(),
- 1000);
-
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(FILE_NAME, fileListView);
- fileListViewAfterUploadFile.scrollTillFindElement(FILE_NAME);
- assertTrue(fileHasBeenUploaded = fileListViewAfterUploadFile
- .getFileElement().isDisplayed());
-
- ElementMenuOptions menuOptions = fileListViewAfterUploadFile
- .longPressOnElement(FILE_NAME);
- FileDetailsView fileDetailsView = menuOptions.clickOnDetails();
- fileDetailsView.checkKeepFileUpToDateCheckbox();
- Thread.sleep(3000);
- driver.sendKeyEvent(android.view.KeyEvent.KEYCODE_BACK);
- assertTrue(common.isElementPresent(
- fileListViewAfterUploadFile.getFileElementLayout(),
- MobileBy.id(FileListView.getFavoriteFileIndicator())));
- assertTrue(fileListViewAfterUploadFile.getFileElementLayout()
- .findElement(By.id(FileListView.getFavoriteFileIndicator()))
- .isDisplayed());
- }
-
- @Test
- @Category({NoIgnoreTestCategory.class})
- public void testKeepFileUpToDateAndRefresh () throws Exception {
-
- FileListView fileListView = Actions.login(Config.URL, Config.user,
- Config.password, Config.isTrusted, driver);
- common.assertIsInFileListView();
-
- Common.waitTillElementIsNotPresentWithoutTimeout(fileListView.getProgressCircular(),
- 1000);
-
- FileListView fileListViewAfterUploadFile = Actions
- .uploadFile(FILE_NAME, fileListView);
- fileListViewAfterUploadFile.scrollTillFindElement(FILE_NAME);
- assertTrue(fileHasBeenUploaded = fileListViewAfterUploadFile
- .getFileElement().isDisplayed());
-
- ElementMenuOptions menuOptions = fileListViewAfterUploadFile
- .longPressOnElement(FILE_NAME);
- FileDetailsView fileDetailsView = menuOptions.clickOnDetails();
- fileDetailsView.checkKeepFileUpToDateCheckbox();
- Thread.sleep(3000);
- driver.sendKeyEvent(android.view.KeyEvent.KEYCODE_BACK);
-
- fileListViewAfterUploadFile.pulldownToRefresh();
- //assertTrue(fileListView.getProgressCircular().isDisplayed());
- Common.waitTillElementIsNotPresentWithoutTimeout(fileListView.getProgressCircular(),
- 100);
-
- assertTrue(common.isElementPresent(
- fileListViewAfterUploadFile.getFileElementLayout(),
- MobileBy.id(FileListView.getFavoriteFileIndicator())));
- assertTrue(fileListViewAfterUploadFile.getFileElementLayout()
- .findElement(By.id(FileListView.getFavoriteFileIndicator()))
- .isDisplayed());
- }
-
-
- @After
- public void tearDown() throws Exception {
- common.takeScreenShotOnFailed(name.getMethodName());
- FileListView fileListView = new FileListView(driver);
- if (fileHasBeenUploadedFromGmail) {
- Actions.deleteElement(FILE_GMAIL_NAME,fileListView, driver);
- }
- if(fileHasBeenUploaded){
- Actions.deleteElement(FILE_NAME,fileListView, driver);
- Actions.deleteElement(BIG_FILE_NAME,fileListView, driver);
- }
-
- //driver.removeApp("com.owncloud.android");
- driver.quit();
- }
-
-
-}
-
diff --git a/automationTest/src/test/resources/.gitignore b/automationTest/src/test/resources/.gitignore
deleted file mode 100644
index 86d0cb2726c6..000000000000
--- a/automationTest/src/test/resources/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-# Ignore everything in this directory
-*
-# Except this file
-!.gitignore
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 9c9bbea0bf29..4722cbdb6c6c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,188 +1,57 @@
-// Gradle build file
-//
-// This project was started in Eclipse and later moved to Android Studio. In the transition, both IDEs were supported.
-// Due to this, the files layout is not the usual in new projects created with Android Studio / gradle. This file
-// merges declarations usually split in two separates build.gradle file, one for global settings of the project in
-// its root folder, another one for the app module in subfolder of root.
-
buildscript {
+ ext {
+ // SDK
+ sdkCompileVersion = 36
+ sdkMinVersion = 28
+ sdkTargetVersion = 36
+ }
+
repositories {
- mavenCentral()
+ google()
+ maven { url "https://plugins.gradle.org/m2/" }
}
+
dependencies {
- classpath 'com.android.tools.build:gradle:2.1.0'
+ classpath libs.android.gradlePlugin
+ classpath libs.kotlin.gradlePlugin
+ classpath libs.ktlint.gradlePlugin
}
}
-apply plugin: 'com.android.application'
-
-ext {
- supportLibraryVersion = '23.1.1'
+plugins {
+ alias libs.plugins.sonarqube
+ alias libs.plugins.ksp apply false
+ alias libs.plugins.detekt
+ alias libs.plugins.cyclonedx
}
-repositories {
- mavenCentral()
-
- flatDir {
- dirs 'libs'
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven {
+ url "https://jitpack.io"
+ }
}
}
-dependencies {
- /// dependencies for app building
- compile name: 'touch-image-view'
- compile project(':owncloud-android-library')
- compile "com.android.support:support-v4:${supportLibraryVersion}"
- compile "com.android.support:design:${supportLibraryVersion}"
- compile 'com.jakewharton:disklrucache:2.0.2'
- compile "com.android.support:appcompat-v7:${supportLibraryVersion}"
- compile 'com.getbase:floatingactionbutton:1.10.1'
-
-
- /// dependencies for local unit tests
- testCompile 'junit:junit:4.12'
- testCompile 'org.mockito:mockito-core:1.10.19'
-
-
- /// dependencies for instrumented tests
- // JUnit4 Rules
- androidTestCompile 'com.android.support.test:rules:0.5'
-
- // Android JUnit Runner
- androidTestCompile 'com.android.support.test:runner:0.5'
-
- // Espresso core
- androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
-
- // UIAutomator - for cross-app UI tests, and to grant screen is turned on in Espresso tests
- androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
-
-}
-
-tasks.withType(Test) {
- /// increased logging for tests
- testLogging {
- events "passed", "skipped", "failed"
- }
+subprojects {
+ apply plugin: "com.google.devtools.ksp"
+ apply plugin: "org.jlleitschuh.gradle.ktlint"
+ apply plugin: "org.sonarqube"
+ apply plugin: "io.gitlab.arturbosch.detekt"
}
-android {
- compileSdkVersion 23
- buildToolsVersion "23.0.3"
-
- defaultConfig {
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
-
- // arguments to be passed to functional tests
- testInstrumentationRunnerArgument "TEST_USER", "\"$System.env.OCTEST_APP_USERNAME\""
- testInstrumentationRunnerArgument "TEST_PASSWORD", "\"$System.env.OCTEST_APP_PASSWORD\""
- testInstrumentationRunnerArgument "TEST_SERVER_URL", "\"$System.env.OCTEST_SERVER_BASE_URL\""
- }
-
- // adapt structure from Eclipse to Gradle/Android Studio expectations;
- // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure
- sourceSets {
- main {
- manifest.srcFile 'AndroidManifest.xml'
- java.srcDirs = ['src']
- resources.srcDirs = ['src']
- aidl.srcDirs = ['src']
- renderscript.srcDirs = ['src']
- res.srcDirs = ['res']
- assets.srcDirs = ['assets']
- }
-
-
- // move whole local unit tests structure as a whole from src/test/* to test/*
- test.setRoot('test')
-
- // move whole instrumented tests structure as a whole from src/androidTest/* to androidTest/*
- androidTest.setRoot('androidTest')
-
-
- // Move the build types to build-types/
- // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...
- // This moves them out of them default location under src//... which would
- // conflict with src/ being used by the main source set.
- // Adding new build types or product flavors should be accompanied
- // by a similar customization.
- debug.setRoot('build-types/debug')
- release.setRoot('build-types/release')
- }
-
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_7
- targetCompatibility JavaVersion.VERSION_1_7
- }
-
- lintOptions {
- abortOnError false
- }
-
- packagingOptions {
- exclude 'META-INF/LICENSE.txt'
- }
-
- signingConfigs {
- release {
- if (System.env.OC_RELEASE_KEYSTORE) {
- storeFile file(System.env.OC_RELEASE_KEYSTORE) // use an absolute path
- storePassword System.env.OC_RELEASE_KEYSTORE_PASSWORD
- keyAlias System.env.OC_RELEASE_KEY_ALIAS
- keyPassword System.env.OC_RELEASE_KEY_PASSWORD
- }
- }
- }
-
- buildTypes {
- release {
- if (System.env.OC_RELEASE_KEYSTORE) {
- signingConfig signingConfigs.release
- }
- }
- }
-
- applicationVariants.all { variant ->
- def appName = System.env.OC_APP_NAME
- setOutputFileName(variant, appName, project)
+sonarqube {
+ properties {
+ property "sonar.projectKey", "owncloud_android"
+ property "sonar.organization", "owncloud-1"
+ property "sonar.projectVersion", getBranchName()
+ property "sonar.host.url", "https://sonarcloud.io"
}
-
}
-// Updates output file names of a given variant to format
-// [appName].[variant.versionName].[OC_BUILD_NUMBER]-[variant.name].apk.
-//
-// OC_BUILD_NUMBER is an environment variable read directly in this method. If undefined, it's not added.
-//
-// @param variant Build variant instance which output file name will be updated.
-// @param appName String to use as first part of the new file name. May be undefined, the original
-// project.archivesBaseName property will be used instead.
-// @param callerProject Caller project.
-
-def setOutputFileName(variant, appName, callerProject) {
- logger.info("Setting new name for output of variant $variant.name")
-
- def originalFile = variant.outputs[0].outputFile;
- def originalName = originalFile.name;
- logger.info("$variant.name: originalName is $originalName")
-
- def newName = ""
-
- if (appName) {
- newName += appName
- } else {
- newName += callerProject.archivesBaseName
- }
-
- newName += "_$variant.versionName"
-
- def buildNumber = System.env.OC_BUILD_NUMBER
- if (buildNumber) {
- newName += ".$buildNumber"
- }
-
- newName += originalName.substring(callerProject.archivesBaseName.length())
-
- logger.info("$variant.name: newName is $newName")
- variant.outputs[0].outputFile = new File(originalFile.parent, newName)
+def getBranchName() {
+ def name = "git rev-parse --abbrev-ref HEAD".execute()
+ return name.text.toString().trim()
}
diff --git a/changelog/2.18.0_2021-05-24/3121 b/changelog/2.18.0_2021-05-24/3121
new file mode 100644
index 000000000000..564abdeeb07f
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3121
@@ -0,0 +1,6 @@
+Enhancement: Replace blank view in music player with cover art
+
+Blank view in the music preview player with styled up cover art was replaced.
+For music files that does not have cover art embodied, it is displayed a placeholder.
+
+https://github.com/owncloud/android/issues/3121 https://github.com/owncloud/android/pull/3182
diff --git a/changelog/2.18.0_2021-05-24/3143 b/changelog/2.18.0_2021-05-24/3143
new file mode 100644
index 000000000000..30243062ae87
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3143
@@ -0,0 +1,8 @@
+Enhancement: Move to AndroidX Preference and new structure for settings
+
+Settings have been updated to use the current Android's recommendation, AndroidX framework.
+In addition, they have been reorganized into subsections for a better understanding and
+navigation structure. Also, new features have been added: now, source path and behaviour
+in auto uploads can be chosen differently for pictures and videos.
+
+https://github.com/owncloud/android/issues/2867 https://github.com/owncloud/android/pull/3143
diff --git a/changelog/2.18.0_2021-05-24/3162 b/changelog/2.18.0_2021-05-24/3162
new file mode 100644
index 000000000000..27d03aa2ad19
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3162
@@ -0,0 +1,7 @@
+Enhancement: Support for apk files
+
+Apk files could be installed from the app after being downloaded. Installation process will be triggered by the system.
+
+https://github.com/owncloud/android/issues/2691
+https://github.com/owncloud/android/pull/3156
+https://github.com/owncloud/android/pull/3162
diff --git a/changelog/2.18.0_2021-05-24/3177 b/changelog/2.18.0_2021-05-24/3177
new file mode 100644
index 000000000000..2220b653c6db
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3177
@@ -0,0 +1,7 @@
+Enhancement: Align previews actions
+
+Behaviour was aligned through every preview fragment. Images, videos, audios and texts show the same actions now.
+
+
+https://github.com/owncloud/android/issues/3155
+https://github.com/owncloud/android/pull/3177
\ No newline at end of file
diff --git a/changelog/2.18.0_2021-05-24/3184 b/changelog/2.18.0_2021-05-24/3184
new file mode 100644
index 000000000000..b94a9a34ae9d
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3184
@@ -0,0 +1,6 @@
+Bugfix: Fix navbar is visible in file preview screen after rotation
+
+Glitch was fixed where the navigation bar became visible in a file preview
+screen when rotating the device.
+
+https://github.com/owncloud/android/pull/3184 https://github.com/owncloud/android/issues/3139
diff --git a/changelog/2.18.0_2021-05-24/3202 b/changelog/2.18.0_2021-05-24/3202
new file mode 100644
index 000000000000..6ac642e30cb9
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3202
@@ -0,0 +1,8 @@
+Bugfix: Fix a bug when some fields where not retrieved from OIDC Discovery
+
+Problem when requesting the OIDC discovery was fixed. Some fields were handled
+as mandatory, but they are recommended according to the docs. It prevented from
+a proper login. Now it is possible to login as expected when some fields are not retrieved.
+
+https://github.com/owncloud/android/pull/3202
+https://github.com/owncloud/android-library/pull/392
diff --git a/changelog/2.18.0_2021-05-24/3210 b/changelog/2.18.0_2021-05-24/3210
new file mode 100644
index 000000000000..0ae6e38c9806
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3210
@@ -0,0 +1,6 @@
+Bugfix: Snackbar in passcode view is not displayed
+
+Snackbar telling about an error in a failed enter or reenter of the passcode
+wasn't visible. Now, the message is shown in a text just below the passcode input.
+
+https://github.com/owncloud/android/issues/2722 https://github.com/owncloud/android/pull/3210
diff --git a/changelog/2.18.0_2021-05-24/3218 b/changelog/2.18.0_2021-05-24/3218
new file mode 100644
index 000000000000..fcf5bd62148e
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3218
@@ -0,0 +1,7 @@
+Enhancement: Settings accessible even when no account is attached
+
+Now, settings can be accessed via a button in the login screen, removing the necessity to
+have an attached account. However, auto picture and video uploads won't be available until
+an account is registered in the app.
+
+https://github.com/owncloud/android/issues/2638 https://github.com/owncloud/android/pull/3218
diff --git a/changelog/2.18.0_2021-05-24/3220 b/changelog/2.18.0_2021-05-24/3220
new file mode 100644
index 000000000000..8dc7e4328c1f
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3220
@@ -0,0 +1,8 @@
+Bugfix: Fixed problem when a file is edited externally
+
+If an external editor modifies a file, the new size will not match when it is
+assembled in server side. Fixed by removing the if-match header from the proper place
+
+https://github.com/owncloud/android/issues/2752
+https://github.com/owncloud/android/pull/3220
+
diff --git a/changelog/2.18.0_2021-05-24/3221 b/changelog/2.18.0_2021-05-24/3221
new file mode 100644
index 000000000000..ba09e4d49fb6
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3221
@@ -0,0 +1,6 @@
+Change: Error handling for pattern lock
+
+Error messages when an incorrect pattern was entered were shown in a snackbar. Now,
+they are displayed in a text below the pattern input, just like in the passcode screen.
+
+https://github.com/owncloud/android/issues/3215 https://github.com/owncloud/android/pull/3221
diff --git a/changelog/2.18.0_2021-05-24/3226 b/changelog/2.18.0_2021-05-24/3226
new file mode 100644
index 000000000000..7abb7c68ad84
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3226
@@ -0,0 +1,8 @@
+Enhancement: Fixed account for camera uploads
+
+Camera uploads will be uploaded to a fixed account independently of the current account.
+Removing the account attached to camera uploads will disable this feature.
+User will be warned when removing an account that has camera uploads attached.
+
+https://github.com/owncloud/android/issues/3166
+https://github.com/owncloud/android/pull/3226
diff --git a/changelog/2.18.0_2021-05-24/3230 b/changelog/2.18.0_2021-05-24/3230
new file mode 100644
index 000000000000..ed898abd6991
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3230
@@ -0,0 +1,7 @@
+Change: Hide biometrical if device does not support it
+
+Biometric lock preference in "Security" settings subsection was shown even when the
+device didn't support biometrics (if it was Android 6.0 or later versions). Now, the
+preference is only shown if the device has the suitable hardware for it.
+
+https://github.com/owncloud/android/issues/3217 https://github.com/owncloud/android/pull/3230
diff --git a/changelog/2.18.0_2021-05-24/3234 b/changelog/2.18.0_2021-05-24/3234
new file mode 100644
index 000000000000..43eb3092c8e5
--- /dev/null
+++ b/changelog/2.18.0_2021-05-24/3234
@@ -0,0 +1,6 @@
+Bugfix: Fix permissions were displayed in share creation view after rotation
+
+Permissions view was shown when creating a share for a file after rotation.
+Capabilities were taken into account just once. Now, the permissions view is shown only when capabilities match.
+
+https://github.com/owncloud/android/issues/3204 https://github.com/owncloud/android/pull/3234
diff --git a/changelog/2.18.1_2021-07-20/3293 b/changelog/2.18.1_2021-07-20/3293
new file mode 100644
index 000000000000..69c4f2c9fa0c
--- /dev/null
+++ b/changelog/2.18.1_2021-07-20/3293
@@ -0,0 +1,7 @@
+Enhancement: Replace picker to select camera folder with native one
+
+The custom picker to select the camera folder was replaced with the native one. Now, it is ready for
+scoped storage and some problems to select a folder in the SD Card were fixed. Also, a new field to show
+the last synchronization timestamp was added.
+
+https://github.com/owncloud/android/issues/2899 https://github.com/owncloud/android/pull/3293
\ No newline at end of file
diff --git a/changelog/2.18.1_2021-07-20/3296 b/changelog/2.18.1_2021-07-20/3296
new file mode 100644
index 000000000000..4dc42fa01fde
--- /dev/null
+++ b/changelog/2.18.1_2021-07-20/3296
@@ -0,0 +1,6 @@
+Enhancement: Hide "More" section if all options are disabled
+
+A blank view was shown when all options in "More" subsection were disabled. Now, the
+subsection is only shown if at least one option is enabled.
+
+https://github.com/owncloud/android/issues/3271 https://github.com/owncloud/android/pull/3296
diff --git a/changelog/2.18.1_2021-07-20/3297 b/changelog/2.18.1_2021-07-20/3297
new file mode 100644
index 000000000000..12fcc89a872c
--- /dev/null
+++ b/changelog/2.18.1_2021-07-20/3297
@@ -0,0 +1,6 @@
+Enhancement: Note icon in music player to be branded
+
+The note icon in the music player will have the same color as the toolbar,
+so branded apps can have the icon tinted using their custom theme.
+
+https://github.com/owncloud/android/issues/3272 https://github.com/owncloud/android/pull/3297
diff --git a/changelog/2.18.1_2021-07-20/3310 b/changelog/2.18.1_2021-07-20/3310
new file mode 100644
index 000000000000..1f5bc0f491e4
--- /dev/null
+++ b/changelog/2.18.1_2021-07-20/3310
@@ -0,0 +1,5 @@
+Security: Add PKCE support
+
+PKCE (Proof Key for Code Exchange) support defined in RFC-7636 was added to prevent authorization code interception attacks.
+
+https://github.com/owncloud/android/pull/3310
\ No newline at end of file
diff --git a/changelog/2.18.3_2021-10-27/3423 b/changelog/2.18.3_2021-10-27/3423
new file mode 100644
index 000000000000..7e3a67348de4
--- /dev/null
+++ b/changelog/2.18.3_2021-10-27/3423
@@ -0,0 +1,8 @@
+Enhancement: Privacy policy button more accessible
+
+The privacy policy button has been removed from "More" settings section,
+and it has been added to general settings screen as well as to the drawer menu, so
+that it is easier and more accessible for users.
+
+https://github.com/owncloud/android/pull/3423
+https://github.com/owncloud/android/issues/3422
\ No newline at end of file
diff --git a/changelog/2.19.0_2021-11-15/3336 b/changelog/2.19.0_2021-11-15/3336
new file mode 100644
index 000000000000..9d6d0b00cc6e
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3336
@@ -0,0 +1,9 @@
+Enhancement: Delete old user directories in order to free memory
+
+Previously, when users deleted an account the synchronized files of this account
+stayed on the SD-Card. So if the user didn't want them anymore he had to delete
+them manually. Now, the app automatically removes the files associated with an
+account.
+
+https://github.com/owncloud/android/pull/3336
+https://github.com/owncloud/android/issues/125
diff --git a/changelog/2.19.0_2021-11-15/3337 b/changelog/2.19.0_2021-11-15/3337
new file mode 100644
index 000000000000..843672fab25e
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3337
@@ -0,0 +1,6 @@
+Enhancement: Delete old logs every week
+
+Previously, logs were stored but never deleted. It used a lot of storage when logs were enabled for some time. Now, the
+logs are removed periodically every week.
+
+https://github.com/owncloud/android/issues/3328 https://github.com/owncloud/android/pull/3337
diff --git a/changelog/2.19.0_2021-11-15/3363 b/changelog/2.19.0_2021-11-15/3363
new file mode 100644
index 000000000000..5244ed4e7cf2
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3363
@@ -0,0 +1,8 @@
+Bugfix: Lack of back button in Logs view
+
+A new back arrow button has been added in the toolbar in Logs
+screen, so that now it's possible to return to the settings screen without
+the use of physical buttons of the device.
+
+https://github.com/owncloud/android/issues/3357
+https://github.com/owncloud/android/pull/3363
\ No newline at end of file
diff --git a/changelog/2.19.0_2021-11-15/3365 b/changelog/2.19.0_2021-11-15/3365
new file mode 100644
index 000000000000..c73d1c1a2e6d
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3365
@@ -0,0 +1,8 @@
+Bugfix: Passcode input misbehaving
+
+Passcode text fields have been made not selectable once a number is written on
+them, so that we avoid bugs with the digits of the passcode and the way of
+entering them.
+
+https://github.com/owncloud/android/issues/3342
+https://github.com/owncloud/android/pull/3365
\ No newline at end of file
diff --git a/changelog/2.19.0_2021-11-15/3380 b/changelog/2.19.0_2021-11-15/3380
new file mode 100644
index 000000000000..32f5c1017eab
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3380
@@ -0,0 +1,8 @@
+Bugfix: ANR after removing account with too many downloaded files
+
+Previously, when a user account was deleted, the application could freeze when trying to delete a large number of files.
+Now, the application has been fixed so that it doesn't freeze anymore by doing this.
+
+https://github.com/owncloud/android/issues/3362
+https://github.com/owncloud/android/pull/3380
+
diff --git a/changelog/2.19.0_2021-11-15/3381 b/changelog/2.19.0_2021-11-15/3381
new file mode 100644
index 000000000000..70a38118925a
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3381
@@ -0,0 +1,7 @@
+Bugfix: Account removed is not removed from the drawer
+
+When an account was deleted from the device settings, in the accounts section, it was not removed from the Navigation Drawer.
+Now, when deleting an account from there, the Navigation Drawer is refreshed and the removed account is no more shown.
+
+https://github.com/owncloud/android/issues/3340
+https://github.com/owncloud/android/pull/3381
diff --git a/changelog/2.19.0_2021-11-15/3383 b/changelog/2.19.0_2021-11-15/3383
new file mode 100644
index 000000000000..04cfc52a1203
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3383
@@ -0,0 +1,7 @@
+Bugfix: Crash in FileDataStorageManager
+
+A possible null value with the account
+that caused certain crashes on Android 10 devices has been controlled.
+
+https://github.com/owncloud/android/issues/2896
+https://github.com/owncloud/android/pull/3383
diff --git a/changelog/2.19.0_2021-11-15/3385 b/changelog/2.19.0_2021-11-15/3385
new file mode 100644
index 000000000000..d463333d3eaa
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3385
@@ -0,0 +1,9 @@
+Enhancement: Instant upload only when charging
+
+A new option has been added in the auto upload pictures/videos
+screen, so that now it's possible to upload pictures or videos
+only when charging.
+
+https://github.com/owncloud/android/issues/465
+https://github.com/owncloud/android/issues/3315
+https://github.com/owncloud/android/pull/3385
diff --git a/changelog/2.19.0_2021-11-15/3396 b/changelog/2.19.0_2021-11-15/3396
new file mode 100644
index 000000000000..1c02e4f05947
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3396
@@ -0,0 +1,15 @@
+Enhancement: Scoped Storage
+
+The way to store files in the device has changed completely.
+Previously, the files were stored in the shared storage. That means that apps that had
+access to the shared storage, could read, write or do whatever they wanted with the ownCloud files.
+
+Now, ownCloud files are stored in the Scoped Storage, so they are safer.
+Other apps can access ownCloud files using the Documents Provider, which is the native way
+to do it, and that means that the ownCloud app has full control of its files.
+
+Furthermore, if the app is removed, the files downloaded to ownCloud are removed too.
+So, files are not lost or forgotten in the device after uninstalling the app.
+
+https://github.com/owncloud/android/issues/2877
+https://github.com/owncloud/android/pull/3269
\ No newline at end of file
diff --git a/changelog/2.19.0_2021-11-15/3408 b/changelog/2.19.0_2021-11-15/3408
new file mode 100644
index 000000000000..9e596ad15aa5
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3408
@@ -0,0 +1,8 @@
+Enhancement: New Logging Screen 2.0
+
+A new option has been added to the logging screen,
+so that now it's possible to share/delete log files
+or open them.
+
+https://github.com/owncloud/android/issues/3333
+https://github.com/owncloud/android/pull/3408
diff --git a/changelog/2.19.0_2021-11-15/3418 b/changelog/2.19.0_2021-11-15/3418
new file mode 100644
index 000000000000..5239fc4d2cbe
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3418
@@ -0,0 +1,7 @@
+Bugfix: Camera Upload manual retry
+
+Previously, when users selected to retry a single camera upload, an error message appeared.
+Now, the retry of a single upload is enqueued again as expected.
+
+https://github.com/owncloud/android/pull/3418
+https://github.com/owncloud/android/issues/3417
diff --git a/changelog/2.19.0_2021-11-15/3431 b/changelog/2.19.0_2021-11-15/3431
new file mode 100644
index 000000000000..0d52cb8933cc
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3431
@@ -0,0 +1,8 @@
+Bugfix: Device rotation moves to root in folder picker
+
+Previously, when users rotate the device trying to share photos with oC selecting a non-root folder,
+folder picker shows the root folder
+Now, folder picker shows the folder that the user browsed.
+
+https://github.com/owncloud/android/pull/3431
+https://github.com/owncloud/android/issues/3163
diff --git a/changelog/2.19.0_2021-11-15/3436 b/changelog/2.19.0_2021-11-15/3436
new file mode 100644
index 000000000000..b8fca5954294
--- /dev/null
+++ b/changelog/2.19.0_2021-11-15/3436
@@ -0,0 +1,8 @@
+Bugfix: Logging does not stop when the user deactivates it
+
+Previously, when users disabled the logging option in the settings,
+the application would not stop logging and the size of the log files would increase.
+Now, the option to disable it works perfectly and no logs are collected if disabled.
+
+https://github.com/owncloud/android/pull/3436
+https://github.com/owncloud/android/issues/3325
diff --git a/changelog/2.20.0_2022-02-16/2524 b/changelog/2.20.0_2022-02-16/2524
new file mode 100644
index 000000000000..ab4864a24356
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/2524
@@ -0,0 +1,7 @@
+Enhancement: Permission dialog removal
+
+The old permission request dialog has been removed.
+It was not needed after migrating the storage to scoped storage,
+read and write permissions are guaranteed in our scoped storage.
+
+https://github.com/owncloud/android/pull/2524
\ No newline at end of file
diff --git a/changelog/2.20.0_2022-02-16/3375 b/changelog/2.20.0_2022-02-16/3375
new file mode 100644
index 000000000000..03e5add394c2
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3375
@@ -0,0 +1,9 @@
+Enhancement: Lock delay for app
+
+A new preference has been added to choose the interval in which the
+app will be unlocked after having unlocked it once, making it more
+comfortable for those who access the app frequently and have a security
+lock set.
+
+https://github.com/owncloud/android/issues/3344
+https://github.com/owncloud/android/pull/3375
\ No newline at end of file
diff --git a/changelog/2.20.0_2022-02-16/3434 b/changelog/2.20.0_2022-02-16/3434
new file mode 100644
index 000000000000..659f261d594b
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3434
@@ -0,0 +1,7 @@
+Enhancement: Security enforced
+
+A new branding/MDM option has been added to make app
+lock via passcode or pattern compulsory, whichever the user chooses.
+
+https://github.com/owncloud/android/pull/3434
+https://github.com/owncloud/android/issues/3400
diff --git a/changelog/2.20.0_2022-02-16/3437 b/changelog/2.20.0_2022-02-16/3437
new file mode 100644
index 000000000000..71900e6e2cb7
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3437
@@ -0,0 +1,8 @@
+Bugfix: Small glitch when side menu is full of accounts
+
+Previously, when users set up a large number of accounts,
+the side menu overlapped the available space quota.
+Now, everything is contained within a scroll to avoid this.
+
+https://github.com/owncloud/android/pull/3437
+https://github.com/owncloud/android/issues/3060
diff --git a/changelog/2.20.0_2022-02-16/3438 b/changelog/2.20.0_2022-02-16/3438
new file mode 100644
index 000000000000..eced7ccf63be
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3438
@@ -0,0 +1,7 @@
+Enhancement: Respect capability for Avatar support
+
+Previously, the user's avatar was shown by default.
+Now, it is shown or not depending on a new capability.
+
+https://github.com/owncloud/android/pull/3438
+https://github.com/owncloud/android/issues/3285
diff --git a/changelog/2.20.0_2022-02-16/3463 b/changelog/2.20.0_2022-02-16/3463
new file mode 100644
index 000000000000..ca5468119609
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3463
@@ -0,0 +1,9 @@
+Enhancement: Brute force protection
+
+Previously, when setting passcode lock, an unlimited number of attempts
+to unlock the app could be done in a row. Now, from the third incorrect
+attempt, there will be an exponential growing waiting time until next
+unlock attempt.
+
+https://github.com/owncloud/android/issues/3320
+https://github.com/owncloud/android/pull/3463
\ No newline at end of file
diff --git a/changelog/2.20.0_2022-02-16/3499 b/changelog/2.20.0_2022-02-16/3499
new file mode 100644
index 000000000000..627fa47b0a3c
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3499
@@ -0,0 +1,9 @@
+Enhancement: "Open with" action now allows editing
+
+Previously, when a document file was opened and edited with an external
+app, changes weren't saved because it didn't synchronized with the server.
+Now, when you edit a document and navigate or refresh in the ownCloud app,
+it synchronizes automatically, keeping consistence of your files.
+
+https://github.com/owncloud/android/issues/3475
+https://github.com/owncloud/android/pull/3499
\ No newline at end of file
diff --git a/changelog/2.20.0_2022-02-16/3527 b/changelog/2.20.0_2022-02-16/3527
new file mode 100644
index 000000000000..788ef6584f26
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3527
@@ -0,0 +1,6 @@
+Enhancement: Enable logs by default in debug mode
+
+Now, when the app is built in DEBUG mode, the logs are enabled by default.
+
+https://github.com/owncloud/android/issues/3526
+https://github.com/owncloud/android/pull/3527
\ No newline at end of file
diff --git a/changelog/2.20.0_2022-02-16/3538 b/changelog/2.20.0_2022-02-16/3538
new file mode 100644
index 000000000000..90613b7a7d55
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3538
@@ -0,0 +1,10 @@
+Enhancement: Allow access from document provider preference
+
+Previously, files of ownCloud accounts couldn't be accessed via documents provider
+when there was a lock set in the app. Now, a new preference has been
+added to allow/disallow the access, so users have more control over their files.
+
+https://github.com/owncloud/android/issues/3379
+https://github.com/owncloud/android/issues/3520
+https://github.com/owncloud/android/pull/3384
+https://github.com/owncloud/android/pull/3538
\ No newline at end of file
diff --git a/changelog/2.20.0_2022-02-16/3539 b/changelog/2.20.0_2022-02-16/3539
new file mode 100644
index 000000000000..81c411bc9989
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3539
@@ -0,0 +1,7 @@
+Enhancement: Suggest the user to enable enhanced security
+
+When a user sets the passcode or pattern lock
+on the security screen, the application suggests the user
+whether to enable or not a biometric lock to unlock the application.
+
+https://github.com/owncloud/android/pull/3539
diff --git a/changelog/2.20.0_2022-02-16/3542 b/changelog/2.20.0_2022-02-16/3542
new file mode 100644
index 000000000000..e04610713e6e
--- /dev/null
+++ b/changelog/2.20.0_2022-02-16/3542
@@ -0,0 +1,8 @@
+Bugfix: Small bug when privacy policy disabled
+
+Previously, when privacy policy setup was disabled,
+the side menu showed the privacy policy menu item.
+Now, option is hidden when privacy policy is disabled.
+
+https://github.com/owncloud/android/pull/3542
+https://github.com/owncloud/android/issues/3521
diff --git a/changelog/2.21.0_2022-06-07/3419 b/changelog/2.21.0_2022-06-07/3419
new file mode 100644
index 000000000000..55b083bf591b
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3419
@@ -0,0 +1,8 @@
+Enhancement: First steps in Android Enterprise integration
+
+Two parameters (server url and server url input visibility) can be now
+managed via MDM. These were the first parameters used to test integration
+with Android Enterprise and Android Management API.
+
+https://github.com/owncloud/android/issues/3415
+https://github.com/owncloud/android/pull/3419
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3440 b/changelog/2.21.0_2022-06-07/3440
new file mode 100644
index 000000000000..542c056f46b5
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3440
@@ -0,0 +1,7 @@
+Enhancement: Lock delay enforced
+
+A new local setup's option has been added for the
+application to lock after the selected interval
+
+https://github.com/owncloud/android/issues/3440
+https://github.com/owncloud/android/pull/3547
diff --git a/changelog/2.21.0_2022-06-07/3480 b/changelog/2.21.0_2022-06-07/3480
new file mode 100644
index 000000000000..cff20be96b33
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3480
@@ -0,0 +1,8 @@
+Enhancement: Provide app feedback to MDM admins
+
+Now, when a MDM configuration is applied for the first time
+or changed by an IT administrator, the app sends feedback that
+will be shown in the EMM console.
+
+https://github.com/owncloud/android/issues/3420
+https://github.com/owncloud/android/pull/3480
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3544 b/changelog/2.21.0_2022-06-07/3544
new file mode 100644
index 000000000000..c69940581c95
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3544
@@ -0,0 +1,7 @@
+Enhancement: Extended security enforced
+
+New extended branding options have been added to make app
+lock via passcode or pattern compulsory.
+
+https://github.com/owncloud/android/issues/3543
+https://github.com/owncloud/android/pull/3544
diff --git a/changelog/2.21.0_2022-06-07/3560 b/changelog/2.21.0_2022-06-07/3560
new file mode 100644
index 000000000000..3b6549f56e52
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3560
@@ -0,0 +1,8 @@
+Bugfix: Security flags for recording screen
+
+Previously, if passcode or pattern were enabled, no screen from the app could be viewed
+from a recording screen app. Now, only the login, passcode and pattern screens are protected
+against recording.
+
+https://github.com/owncloud/android/issues/3468
+https://github.com/owncloud/android/pull/3560
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3582 b/changelog/2.21.0_2022-06-07/3582
new file mode 100644
index 000000000000..7578f5dcdc84
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3582
@@ -0,0 +1,6 @@
+Enhancement: Improvements for the UI in the passcode screen
+
+Redesign of the passcode screen to have the numeric keyboard in the screen instead of using the Android one.
+
+https://github.com/owncloud/android/issues/3516
+https://github.com/owncloud/android/pull/3582
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3587 b/changelog/2.21.0_2022-06-07/3587
new file mode 100644
index 000000000000..b24d32ea4907
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3587
@@ -0,0 +1,7 @@
+Enhancement: Improvements for the UI in the pattern screen
+
+Redesign of the pattern screen.
+Cancel button deleted and new back arrow in the toolbar.
+
+https://github.com/owncloud/android/issues/3580
+https://github.com/owncloud/android/pull/3587
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3589 b/changelog/2.21.0_2022-06-07/3589
new file mode 100644
index 000000000000..cd17910cdf4b
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3589
@@ -0,0 +1,7 @@
+Bugfix: Crash when changing orientation in Details view
+
+Previously, the app crashes when changing orientation in Details view after installing
+Now, app shows correctly the details after installing.
+
+https://github.com/owncloud/android/issues/3571
+https://github.com/owncloud/android/pull/3589
diff --git a/changelog/2.21.0_2022-06-07/3592 b/changelog/2.21.0_2022-06-07/3592
new file mode 100644
index 000000000000..013bceae727f
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3592
@@ -0,0 +1,8 @@
+Bugfix: Lock displays shown again
+
+Previously, if you clicked on passcode or pattern lock to remove it, and then you clicked on cancel,
+the lock display was shown again to put the passcode or pattern. Now, if you cancel it, you come back
+to settings screen.
+
+https://github.com/owncloud/android/issues/3591
+https://github.com/owncloud/android/pull/3592
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3594 b/changelog/2.21.0_2022-06-07/3594
new file mode 100644
index 000000000000..a85257294b21
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3594
@@ -0,0 +1,6 @@
+Enhancement: Release Notes
+
+New release notes to show news in updates.
+
+https://github.com/owncloud/android/issues/3442
+https://github.com/owncloud/android/pull/3594
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3615 b/changelog/2.21.0_2022-06-07/3615
new file mode 100644
index 000000000000..0e0c150970f0
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3615
@@ -0,0 +1,6 @@
+Enhancement: Prevent taking screenshots
+
+New option to prevent taking screenshots.
+
+https://github.com/owncloud/android/issues/3596
+https://github.com/owncloud/android/pull/3615
diff --git a/changelog/2.21.0_2022-06-07/3616 b/changelog/2.21.0_2022-06-07/3616
new file mode 100644
index 000000000000..5953d88aef38
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3616
@@ -0,0 +1,6 @@
+Enhancement: What´s new option
+
+New option to check what was included in the latest version.
+
+https://github.com/owncloud/android/issues/3352
+https://github.com/owncloud/android/pull/3616
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3624 b/changelog/2.21.0_2022-06-07/3624
new file mode 100644
index 000000000000..ec892e4c4855
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3624
@@ -0,0 +1,6 @@
+Enhancement: New option to show or not hidden files
+
+Enable it to show hidden files and folders
+
+https://github.com/owncloud/android/issues/2578
+https://github.com/owncloud/android/pull/3624
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3627 b/changelog/2.21.0_2022-06-07/3627
new file mode 100644
index 000000000000..b87cef187f7a
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3627
@@ -0,0 +1,6 @@
+Enhancement: Option to allow screenshots or not in Android Enterprise
+
+New parameter to manage screenshots can be configured via MDM.
+
+https://github.com/owncloud/android/issues/3625
+https://github.com/owncloud/android/pull/3627
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3636 b/changelog/2.21.0_2022-06-07/3636
new file mode 100644
index 000000000000..c12cfb2fd311
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3636
@@ -0,0 +1,6 @@
+Enhancement: Full name is shown in shares
+
+Full name is shown when using public share instead of username.
+
+https://github.com/owncloud/android/issues/1106
+https://github.com/owncloud/android/pull/3636
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3638 b/changelog/2.21.0_2022-06-07/3638
new file mode 100644
index 000000000000..191f360cfb5a
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3638
@@ -0,0 +1,6 @@
+Enhancement: Send for file multiselect
+
+Send multiple files at once if they are downloaded.
+
+https://github.com/owncloud/android/issues/3491
+https://github.com/owncloud/android/pull/3638
\ No newline at end of file
diff --git a/changelog/2.21.0_2022-06-07/3639 b/changelog/2.21.0_2022-06-07/3639
new file mode 100644
index 000000000000..b5bcb3804caf
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3639
@@ -0,0 +1,6 @@
+Enhancement: Support for SVG files added
+
+SVG files are supported and can be downloaded and viewed.
+
+https://github.com/owncloud/android/issues/1033
+https://github.com/owncloud/android/pull/3639
diff --git a/changelog/2.21.0_2022-06-07/3640 b/changelog/2.21.0_2022-06-07/3640
new file mode 100644
index 000000000000..5350a9a284df
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3640
@@ -0,0 +1,7 @@
+Enhancement: Improved copy/move dialog
+
+Previously,they appeared exactly the same and there was no way of knowing which was which.
+Now they are differentiated by the text on the action button.
+
+https://github.com/owncloud/android/issues/1414
+https://github.com/owncloud/android/pull/3640
diff --git a/changelog/2.21.0_2022-06-07/3643 b/changelog/2.21.0_2022-06-07/3643
new file mode 100644
index 000000000000..4e9520cdbaac
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3643
@@ -0,0 +1,7 @@
+Bugfix: Prevented signed in user in the list of users to be shared
+
+Previously, user list for sharing contains signed in user,
+now this user is omitted to avoid errors.
+
+https://github.com/owncloud/android/issues/1419
+https://github.com/owncloud/android/pull/3643
diff --git a/changelog/2.21.0_2022-06-07/3644 b/changelog/2.21.0_2022-06-07/3644
new file mode 100644
index 000000000000..bb463bfb279e
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3644
@@ -0,0 +1,9 @@
+Bugfix: Corrupt picture error controlled
+
+Previously, If a file is not correct or is damaged,
+it is downloaded but not previewed.
+An infinite spinner on a black window is shown instead.
+Now, an error appears warning to the user.
+
+https://github.com/owncloud/android/issues/3441
+https://github.com/owncloud/android/pull/3644
diff --git a/changelog/2.21.0_2022-06-07/3653 b/changelog/2.21.0_2022-06-07/3653
new file mode 100644
index 000000000000..df16ea37578a
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3653
@@ -0,0 +1,6 @@
+Enhancement: Thumbnail click action in file detail
+
+When a user clicks on a file's detail view thumbnail,
+the file is automatically downloaded and previewed.
+
+https://github.com/owncloud/android/pull/3653
diff --git a/changelog/2.21.0_2022-06-07/3659 b/changelog/2.21.0_2022-06-07/3659
new file mode 100644
index 000000000000..7de5b02b5cda
--- /dev/null
+++ b/changelog/2.21.0_2022-06-07/3659
@@ -0,0 +1,6 @@
+Enhancement: Share a folder from within the folder
+
+You can share a folder clicking in the share icon inside the folder.
+
+https://github.com/owncloud/android/issues/1441
+https://github.com/owncloud/android/pull/3659
diff --git a/changelog/2.21.1_2022-06-15/3696 b/changelog/2.21.1_2022-06-15/3696
new file mode 100644
index 000000000000..e3627f284229
--- /dev/null
+++ b/changelog/2.21.1_2022-06-15/3696
@@ -0,0 +1,5 @@
+Bugfix: Fix crash when opening from details screen
+
+Fixed a crash when opening a non downloaded file from the details view.
+
+https://github.com/owncloud/android/pull/3696
diff --git a/changelog/2.21.2_2022-09-07/3711 b/changelog/2.21.2_2022-09-07/3711
new file mode 100644
index 000000000000..47580dfca319
--- /dev/null
+++ b/changelog/2.21.2_2022-09-07/3711
@@ -0,0 +1,7 @@
+Enhancement: Shares from propfind
+
+Added a new property to the propfind, so that, we can get if the files in a folder are shared directly with just one request.
+Previously, a propfind and another additional request were needed to the shares api to retrieve the shares of the folder.
+
+https://github.com/owncloud/android/issues/3711
+https://github.com/owncloud/android-library/pull/496
diff --git a/changelog/2.21.2_2022-09-07/3737 b/changelog/2.21.2_2022-09-07/3737
new file mode 100644
index 000000000000..2e75427652b0
--- /dev/null
+++ b/changelog/2.21.2_2022-09-07/3737
@@ -0,0 +1,6 @@
+Enhancement: Open in web
+
+oCIS feature, to open files with mime types supported by the server in the web browser using collaborative or specific tools
+
+https://github.com/owncloud/android/issues/3672
+https://github.com/owncloud/android/pull/3737
diff --git a/changelog/2.21.2_2022-09-07/3738 b/changelog/2.21.2_2022-09-07/3738
new file mode 100644
index 000000000000..e6db75c28d5f
--- /dev/null
+++ b/changelog/2.21.2_2022-09-07/3738
@@ -0,0 +1,7 @@
+Enhancement: Private link capability
+
+Private link capability is now respected. Option is shown/hidden depending on its value
+
+https://github.com/owncloud/android/issues/3732
+https://github.com/owncloud/android/pull/3738
+https://github.com/owncloud/android-library/pull/505
diff --git a/changelog/3.0.0_2022-12-12/2934 b/changelog/3.0.0_2022-12-12/2934
new file mode 100644
index 000000000000..bca620750d2d
--- /dev/null
+++ b/changelog/3.0.0_2022-12-12/2934
@@ -0,0 +1,7 @@
+Enhancement: Sync engine rewritten
+
+The whole synchronization engine has been refactored to a new architecture to make it
+better structured and more efficient.
+
+https://github.com/owncloud/android/pull/2934
+https://github.com/owncloud/android/issues/2818
diff --git a/changelog/3.0.0_2022-12-12/3632 b/changelog/3.0.0_2022-12-12/3632
new file mode 100644
index 000000000000..85ebf48a4c4d
--- /dev/null
+++ b/changelog/3.0.0_2022-12-12/3632
@@ -0,0 +1,8 @@
+Enhancement: Faster browser authentication
+
+Login flow has been improved by saving a click when the server is OAuth2/OIDC and it is valid. Also,
+when authenticating again in a OAuth2/OIDC account already saved in the app, the username is already shown
+in the browser.
+
+https://github.com/owncloud/android/pull/3632
+https://github.com/owncloud/android/issues/3759
diff --git a/changelog/3.0.0_2022-12-12/3710 b/changelog/3.0.0_2022-12-12/3710
new file mode 100644
index 000000000000..de0c1ceadf95
--- /dev/null
+++ b/changelog/3.0.0_2022-12-12/3710
@@ -0,0 +1,7 @@
+Enhancement: Several transfers running simultaneously
+
+With the sync engine refactor, now several downloads and uploads can run at the same time, improving
+efficiency.
+
+https://github.com/owncloud/android/pull/3710
+https://github.com/owncloud/android/issues/3426
diff --git a/changelog/3.0.0_2022-12-12/3719 b/changelog/3.0.0_2022-12-12/3719
new file mode 100644
index 000000000000..e7d896b809cb
--- /dev/null
+++ b/changelog/3.0.0_2022-12-12/3719
@@ -0,0 +1,6 @@
+Bugfix: Fix for thumbnails
+
+Some thumbnails were not shown in the file list. Now, they are all shown correctly.
+
+https://github.com/owncloud/android/pull/3719
+https://github.com/owncloud/android/issues/2818
diff --git a/changelog/3.0.0_2022-12-12/3728 b/changelog/3.0.0_2022-12-12/3728
new file mode 100644
index 000000000000..d88e29b3b61f
--- /dev/null
+++ b/changelog/3.0.0_2022-12-12/3728
@@ -0,0 +1,7 @@
+Enhancement: Empty views improved
+
+When the list of items is empty, we now show a more attractive view. This applies to file list,
+available offline list, shared by link list, uploads list, logs list and external share list.
+
+https://github.com/owncloud/android/pull/3728
+https://github.com/owncloud/android/issues/3026
diff --git a/changelog/3.0.0_2022-12-12/3766 b/changelog/3.0.0_2022-12-12/3766
new file mode 100644
index 000000000000..204d6d31204f
--- /dev/null
+++ b/changelog/3.0.0_2022-12-12/3766
@@ -0,0 +1,8 @@
+Enhancement: Automatic conflicts propagation
+
+Conflicts are now propagated automatically to parent folders, and cleaned when solved or removed. Before,
+it was needed to navigate to the file location for the conflict to propagate. Also, move, copy and remove
+actions work properly with conflicts.
+
+https://github.com/owncloud/android/pull/3766
+https://github.com/owncloud/android/issues/3005
diff --git a/changelog/3.0.1_2022-12-21/3837 b/changelog/3.0.1_2022-12-21/3837
new file mode 100644
index 000000000000..7f7fc196ebeb
--- /dev/null
+++ b/changelog/3.0.1_2022-12-21/3837
@@ -0,0 +1,6 @@
+Bugfix: Fix crash when upgrading from 2.18
+
+Upgrading from 2.18 or older versions made the app crash due to camera uploads data migration.
+This problem has been solved and now the app upgrades correctly.
+
+https://github.com/owncloud/android/pull/3837
diff --git a/changelog/3.0.1_2022-12-21/3841 b/changelog/3.0.1_2022-12-21/3841
new file mode 100644
index 000000000000..c34ae3c6d930
--- /dev/null
+++ b/changelog/3.0.1_2022-12-21/3841
@@ -0,0 +1,6 @@
+Bugfix: Fix crash when opening uploads section
+
+When upgrading from an old version with uploads with "forget" behaviour, app crashed
+when opening the uploads tab. Now, this has been fixed so that it works correctly.
+
+https://github.com/owncloud/android/pull/3841
diff --git a/changelog/3.0.2_2023-01-26/3869 b/changelog/3.0.2_2023-01-26/3869
new file mode 100644
index 000000000000..0e10dd94f2ad
--- /dev/null
+++ b/changelog/3.0.2_2023-01-26/3869
@@ -0,0 +1,5 @@
+Enhancement: Branded scope for OpenID Connect
+
+OpenID Connect scope is now brandable via setup.xml file or MDM
+
+https://github.com/owncloud/android/pull/3869
diff --git a/changelog/3.0.2_2023-01-26/534 b/changelog/3.0.2_2023-01-26/534
new file mode 100644
index 000000000000..c50cd261e3f2
--- /dev/null
+++ b/changelog/3.0.2_2023-01-26/534
@@ -0,0 +1,5 @@
+Bugfix: Fix reauthentication prompt
+
+Potential fix to oauth error after logging in for first time that makes user to reauthenticate
+
+https://github.com/owncloud/android-library/pull/534
diff --git a/changelog/3.0.3_2023-02-13/3852 b/changelog/3.0.3_2023-02-13/3852
new file mode 100644
index 000000000000..6ee8fd087490
--- /dev/null
+++ b/changelog/3.0.3_2023-02-13/3852
@@ -0,0 +1,7 @@
+Bugfix: Error messages too long in folders operation
+
+Error messages when trying to perform a non-allowed action for copying and moving folders
+have been shortened so that they are shown completely in the snackbar.
+
+https://github.com/owncloud/android/pull/3852
+https://github.com/owncloud/android/issues/3820
diff --git a/changelog/3.0.3_2023-02-13/3889 b/changelog/3.0.3_2023-02-13/3889
new file mode 100644
index 000000000000..16ece13faa01
--- /dev/null
+++ b/changelog/3.0.3_2023-02-13/3889
@@ -0,0 +1,5 @@
+Bugfix: Fix problems after authentication
+
+Client for session are now fetched on demand to avoid reinitialize DI, making the process smoother
+
+https://github.com/owncloud/android/pull/3889
diff --git a/changelog/3.0.3_2023-02-13/3899 b/changelog/3.0.3_2023-02-13/3899
new file mode 100644
index 000000000000..54ef933714be
--- /dev/null
+++ b/changelog/3.0.3_2023-02-13/3899
@@ -0,0 +1,7 @@
+Bugfix: Toolbar in file details view
+
+When returning from the share screen to details screen, the toolbar didn't show
+the correct options and title. Now it does.
+
+https://github.com/owncloud/android/pull/3899
+https://github.com/owncloud/android/issues/3866
diff --git a/changelog/3.0.4_2023-03-07/3952 b/changelog/3.0.4_2023-03-07/3952
new file mode 100644
index 000000000000..03bd0b557b44
--- /dev/null
+++ b/changelog/3.0.4_2023-03-07/3952
@@ -0,0 +1,6 @@
+Security: Fix for security issues with database
+
+Some fixes have been added so that now no part of the app's database
+can be accessed from other apps.
+
+https://github.com/owncloud/android/pull/3952
diff --git a/changelog/3.0.4_2023-03-07/547 b/changelog/3.0.4_2023-03-07/547
new file mode 100644
index 000000000000..f992364a39ed
--- /dev/null
+++ b/changelog/3.0.4_2023-03-07/547
@@ -0,0 +1,6 @@
+Enhancement: HTTP logs show more info
+
+When enabling HTTP logs, now the URL for each log will be shown as well to
+make debugging easier.
+
+https://github.com/owncloud/android-library/pull/547
diff --git a/changelog/4.0.0_2023-05-29/3851 b/changelog/4.0.0_2023-05-29/3851
new file mode 100644
index 000000000000..cd3813eac4a2
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/3851
@@ -0,0 +1,9 @@
+Enhancement: Support for spaces
+
+Spaces are now supported in oCIS accounts. A new tab has been added, which allows to list and
+browse through all the available spaces for the current account. The supported operations
+for files in spaces are: download, upload, remove, rename, create folder, copy and move. The
+documents provider has been adapted as well to be able to browse through spaces and perform
+the operations already mentioned.
+
+https://github.com/owncloud/android/pull/3851
diff --git a/changelog/4.0.0_2023-05-29/3930 b/changelog/4.0.0_2023-05-29/3930
new file mode 100644
index 000000000000..909f53cbcbfa
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/3930
@@ -0,0 +1,6 @@
+Enhancement: Update label on Camera Uploads
+
+Update label on camera uploads to avoid confusions with the behavior of original files.
+Now, it is clear that original files will be removed.
+
+https://github.com/owncloud/android/pull/3930
diff --git a/changelog/4.0.0_2023-05-29/3945 b/changelog/4.0.0_2023-05-29/3945
new file mode 100644
index 000000000000..c78802490a8e
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/3945
@@ -0,0 +1,9 @@
+Enhancement: Authenticated WebFinger
+
+Authenticated WebFinger was introduced into the authentication flow.
+Now, WebFinger is used to retrieve the OpenID Connect issuer and the available ownCloud instances.
+For the moment, multiple oC instances are not supported, only the first available instance is used.
+
+https://github.com/owncloud/android/issues/3943
+https://github.com/owncloud/android/pull/3945
+https://doc.owncloud.com/ocis/next/deployment/services/s-list/webfinger.html
diff --git a/changelog/4.0.0_2023-05-29/3949 b/changelog/4.0.0_2023-05-29/3949
new file mode 100644
index 000000000000..72b53603c35f
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/3949
@@ -0,0 +1,7 @@
+Enhancement: Link in drawer menu
+
+Customers will be able now to set a personalized label and link that will
+appear in the drawer menu, together with the drawer logo as an icon.
+
+https://github.com/owncloud/android/pull/3949
+https://github.com/owncloud/android/issues/3907
diff --git a/changelog/4.0.0_2023-05-29/3973 b/changelog/4.0.0_2023-05-29/3973
new file mode 100644
index 000000000000..eb8493b5498f
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/3973
@@ -0,0 +1,8 @@
+Change: Bump target SDK to 33
+
+Target SDK was upgraded to 33 to keep the app updated with the latest android changes.
+A new setting was introduced to manage notifications in an easier way.
+
+https://github.com/owncloud/android/issues/3617
+https://github.com/owncloud/android/pull/3972
+https://developer.android.com/about/versions/13/behavior-changes-13
diff --git a/changelog/4.0.0_2023-05-29/3982 b/changelog/4.0.0_2023-05-29/3982
new file mode 100644
index 000000000000..bc0e816a5645
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/3982
@@ -0,0 +1,7 @@
+Enhancement: Send language header in all requests
+
+Added Accept-Language header to all requests so the android App can receive translated content.
+
+https://github.com/owncloud/android/issues/3980
+https://github.com/owncloud/android/pull/3982
+https://github.com/owncloud/android-library/pull/551
diff --git a/changelog/4.0.0_2023-05-29/3990 b/changelog/4.0.0_2023-05-29/3990
new file mode 100644
index 000000000000..e568aa2bf556
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/3990
@@ -0,0 +1,10 @@
+Enhancement: Open in specific web provider
+
+We've added the specific web app providers instead of opening the file with the default web provider.
+
+The user can open their files with any of the available specific web app providers from the server.
+Previously, file was opened with the default one.
+
+https://github.com/owncloud/android/issues/3994
+https://github.com/owncloud/android/pull/3990
+https://owncloud.dev/services/app-registry/apps/#app-registry
diff --git a/changelog/4.0.0_2023-05-29/4000 b/changelog/4.0.0_2023-05-29/4000
new file mode 100644
index 000000000000..7ba929e75197
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4000
@@ -0,0 +1,9 @@
+Enhancement: Updated WebFinger flow
+
+WebFinger call won't follow redirections. WebFinger will be requested first and will skip status.php
+in case it's successful, and in case the lookup server is not directly accessible, we will continue
+the authentication flow with the regular status.php.
+
+https://github.com/owncloud/android/issues/3998
+https://github.com/owncloud/android/pull/4000
+https://github.com/owncloud/android-library/pull/555
diff --git a/changelog/4.0.0_2023-05-29/4001 b/changelog/4.0.0_2023-05-29/4001
new file mode 100644
index 000000000000..fbcc4f0a7812
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4001
@@ -0,0 +1,6 @@
+Enhancement: Monochrome icon for the app
+
+From Android 13, if the user has enabled themed app icons in their device settings,
+the app will be shown with a monochrome icon.
+
+https://github.com/owncloud/android/pull/4001
diff --git a/changelog/4.0.0_2023-05-29/4011 b/changelog/4.0.0_2023-05-29/4011
new file mode 100644
index 000000000000..a4c9a43f08f8
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4011
@@ -0,0 +1,8 @@
+Enhancement: Add prompt parameter to OIDC flow
+
+Added prompt parameter to the authorization request in case OIDC is supported.
+By default, select_account will be sent. It can be changed via branding or MDM.
+
+https://github.com/owncloud/android/pull/4011
+https://github.com/owncloud/android/issues/3862
+https://github.com/owncloud/android/issues/3984
diff --git a/changelog/4.0.0_2023-05-29/4013 b/changelog/4.0.0_2023-05-29/4013
new file mode 100644
index 000000000000..a42b529c01f9
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4013
@@ -0,0 +1,8 @@
+Bugfix: Error message for protocol exception
+
+Previously, when the network connection is lost while uploading a file, "Unknown error" was
+shown. Now, we show a more specific error.
+
+https://github.com/owncloud/android/issues/3948
+https://github.com/owncloud/android/pull/4013
+https://github.com/owncloud/android-library/pull/558
diff --git a/changelog/4.0.0_2023-05-29/4014 b/changelog/4.0.0_2023-05-29/4014
new file mode 100644
index 000000000000..45d51c08b981
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4014
@@ -0,0 +1,7 @@
+Change: Use ViewBinding in FolderPickerActivity
+
+The use of findViewById method was replaced by using ViewBinding in the
+FolderPickerActivity.
+
+https://github.com/owncloud/android/issues/3796
+https://github.com/owncloud/android/pull/4014
diff --git a/changelog/4.0.0_2023-05-29/4017 b/changelog/4.0.0_2023-05-29/4017
new file mode 100644
index 000000000000..2648dc5290c0
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4017
@@ -0,0 +1,6 @@
+Enhancement: Support for Markdown files
+
+Markdown files preview will now be rendered to show its content in a prettier way.
+
+https://github.com/owncloud/android/issues/3716
+https://github.com/owncloud/android/pull/4017
diff --git a/changelog/4.0.0_2023-05-29/4021 b/changelog/4.0.0_2023-05-29/4021
new file mode 100644
index 000000000000..c2aece5ed719
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4021
@@ -0,0 +1,7 @@
+Change: Use ViewBinding in WhatsNewActivity
+
+The use of findViewById method was replaced by using ViewBinding in the
+WhatsNewActivity.
+
+https://github.com/owncloud/android/issues/3796
+https://github.com/owncloud/android/pull/4021
diff --git a/changelog/4.0.0_2023-05-29/4023 b/changelog/4.0.0_2023-05-29/4023
new file mode 100644
index 000000000000..b23cb64956d1
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4023
@@ -0,0 +1,9 @@
+Enhancement: Create file via web
+
+A new option has been added in the FAB to create new files, for those servers
+which support this option and have available app providers that allow the creation
+of new files.
+
+https://github.com/owncloud/android/issues/3995
+https://github.com/owncloud/android/pull/4023
+https://github.com/owncloud/android-library/pull/562
diff --git a/changelog/4.0.0_2023-05-29/4026 b/changelog/4.0.0_2023-05-29/4026
new file mode 100644
index 000000000000..3f151e4d26fa
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4026
@@ -0,0 +1,8 @@
+Bugfix: Incorrect list of files in av. offline when browsing from details
+
+When opening the details view of a file accessed from the available offline shortcut,
+browsing back led to a incorrect list of files. Now, browsing back leads to the
+list of available offline files again.
+
+https://github.com/owncloud/android/issues/3986
+https://github.com/owncloud/android/pull/4026
diff --git a/changelog/4.0.0_2023-05-29/4032 b/changelog/4.0.0_2023-05-29/4032
new file mode 100644
index 000000000000..5de774954e64
--- /dev/null
+++ b/changelog/4.0.0_2023-05-29/4032
@@ -0,0 +1,7 @@
+Enhancement: New setting "Access document provider"
+
+A new setting has been added in the "More" settings section with a
+suggested app to access the document provider.
+
+https://github.com/owncloud/android/pull/4032
+https://github.com/owncloud/android/issues/4028
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 000000000000..8f18510da70b
--- /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 000000000000..6151699e6934
--- /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 000000000000..5dce06738e89
--- /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 000000000000..3182ba645f4c
--- /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 000000000000..007d6ae2eca2
--- /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 000000000000..18c98f38c53b
--- /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 000000000000..3a83f66c1fe7
--- /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 000000000000..a7ef93331bfc
--- /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 000000000000..b8aaf479beb7
--- /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 000000000000..c84ac614fc9f
--- /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 000000000000..3b96933dcea5
--- /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 000000000000..17eb324ebdef
--- /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 000000000000..9fa42ecdf92b
--- /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 000000000000..c87f5ea0a28e
--- /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 000000000000..ecdb3ff209a9
--- /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 000000000000..3235d0908a7f
--- /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 000000000000..92c9b4c34a8d
--- /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 000000000000..673852c47770
--- /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 000000000000..6fa55b21cb16
--- /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 000000000000..63c2b4833260
--- /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 000000000000..e91b92287f00
--- /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 000000000000..cc918b83578b
--- /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 000000000000..8563f5872244
--- /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 000000000000..f8a296dcc292
--- /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 000000000000..7b52d37f790d
--- /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 000000000000..05e3dfbafcde
--- /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 000000000000..60a41a29f01a
--- /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/4.1.1_2023-10-18/4170 b/changelog/4.1.1_2023-10-18/4170
new file mode 100644
index 000000000000..e7ad28c99a7d
--- /dev/null
+++ b/changelog/4.1.1_2023-10-18/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/changelog/4.1.1_2023-10-18/4189 b/changelog/4.1.1_2023-10-18/4189
new file mode 100644
index 000000000000..2f0d3bede906
--- /dev/null
+++ b/changelog/4.1.1_2023-10-18/4189
@@ -0,0 +1,6 @@
+Bugfix: Thumbnails correctly shown for every user
+
+Due to an error in the request, users that included the '@' character in their usernames couldn't
+see the thumbnails of the image files. Now, every user can see them correctly.
+
+https://github.com/owncloud/android/pull/4189
diff --git a/changelog/4.2.0_2024-02-12/3966 b/changelog/4.2.0_2024-02-12/3966
new file mode 100644
index 000000000000..df68f2c91d13
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/3966
@@ -0,0 +1,5 @@
+Enhancement: Koin DSL
+
+Koin DSL makes easier the dependency definition avoiding verbosity by allowing you to target a class constructor directly
+
+https://github.com/owncloud/android/pull/3966
diff --git a/changelog/4.2.0_2024-02-12/4138 b/changelog/4.2.0_2024-02-12/4138
new file mode 100644
index 000000000000..bdfd816ab3a2
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/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/4.2.0_2024-02-12/4143 b/changelog/4.2.0_2024-02-12/4143
new file mode 100644
index 000000000000..30b6577074e1
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4143
@@ -0,0 +1,8 @@
+Enhancement: Unit tests for datasources classes - Part 3
+
+Unit tests of the OCFolderBackupLocalDataSource, OCRemoteOAuthDataSource, OCRemoteShareeDataSource, OCLocalShareDataSource,
+OCRemoteShareDataSource, OCLocalSpacesDataSource, OCRemoteSpacesDataSource, OCLocalTransferDataSource, OCLocalUserDataSource,
+OCRemoteUserDataSource, OCRemoteWebFingerDatasource classes have been done and completed.
+
+https://github.com/owncloud/android/issues/4072
+https://github.com/owncloud/android/pull/4143
diff --git a/changelog/4.2.0_2024-02-12/4160 b/changelog/4.2.0_2024-02-12/4160
new file mode 100644
index 000000000000..5524ff0159a6
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4160
@@ -0,0 +1,6 @@
+Enhancement: "Share to" in oCIS accounts allows upload to any space
+
+With this improvement, shared stuff from other apps can be uploaded to any space and not only the personal one in oCIS accounts.
+
+https://github.com/owncloud/android/issues/4088
+https://github.com/owncloud/android/pull/4160
diff --git a/changelog/4.2.0_2024-02-12/4177 b/changelog/4.2.0_2024-02-12/4177
new file mode 100644
index 000000000000..1fb7636f0fbd
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4177
@@ -0,0 +1,8 @@
+Change: Migration to Media3 from Exoplayer
+
+Media3 is the new home for Exoplayer, which has become a part of this library.
+Media3 provides a more advanced and optimized media playback experience for users,
+with improvements in performance and compatibility.
+
+https://github.com/owncloud/android/issues/4157
+https://github.com/owncloud/android/pull/4177
diff --git a/changelog/4.2.0_2024-02-12/4179 b/changelog/4.2.0_2024-02-12/4179
new file mode 100644
index 000000000000..586537211493
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4179
@@ -0,0 +1,5 @@
+Enhancement: Use invoke operator to execute usecases
+
+Removes all the "execute" verbosity for use cases by using the "invoke" operator instead.
+
+https://github.com/owncloud/android/pull/4179
diff --git a/changelog/4.2.0_2024-02-12/4183 b/changelog/4.2.0_2024-02-12/4183
new file mode 100644
index 000000000000..1978f4d0f9ed
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4183
@@ -0,0 +1,6 @@
+Change: Android library as a module instead of submodule
+
+Android library, containing all networking stuff, is now the 5th module in the app instead of submodule.
+
+https://github.com/owncloud/android/issues/3962
+https://github.com/owncloud/android/pull/4183
diff --git a/changelog/4.2.0_2024-02-12/4187 b/changelog/4.2.0_2024-02-12/4187
new file mode 100644
index 000000000000..eff45a4bab56
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4187
@@ -0,0 +1,6 @@
+Enhancement: New field "last usage" in database
+
+To know the last usage of a file, a new field has been created in the database to handle this specific information.
+
+https://github.com/owncloud/android/issues/4173
+https://github.com/owncloud/android/pull/4187
diff --git a/changelog/4.2.0_2024-02-12/4191 b/changelog/4.2.0_2024-02-12/4191
new file mode 100644
index 000000000000..f90e1b0986b7
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4191
@@ -0,0 +1,6 @@
+Enhancement: Deep link open app correctly
+
+Opening the app with the deep link correctly and managing if user logged or not.
+
+https://github.com/owncloud/android/issues/4181
+https://github.com/owncloud/android/pull/4191
diff --git a/changelog/4.2.0_2024-02-12/4199 b/changelog/4.2.0_2024-02-12/4199
new file mode 100644
index 000000000000..961dc41f0126
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4199
@@ -0,0 +1,7 @@
+Enhancement: Auto-refresh when a file is uploaded
+
+The file list will be now refreshed automatically when an upload whose destination folder
+is the one we are in is completed successfully.
+
+https://github.com/owncloud/android/issues/4103
+https://github.com/owncloud/android/pull/4199
diff --git a/changelog/4.2.0_2024-02-12/4204 b/changelog/4.2.0_2024-02-12/4204
new file mode 100644
index 000000000000..21610b86da4b
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4204
@@ -0,0 +1,10 @@
+Enhancement: Logging changes
+
+- Updating version of com.github.AppDevNext.Logcat:LogcatCoreLib lib.
+- Adding the hour, minutes and seconds to the log file.
+- Printing http logs in one line.
+- Printing http logs with 1000000 bytes as max size.
+- Printing http logs in a Json format.
+
+https://github.com/owncloud/android/issues/4151
+https://github.com/owncloud/android/pull/4204
diff --git a/changelog/4.2.0_2024-02-12/4205 b/changelog/4.2.0_2024-02-12/4205
new file mode 100644
index 000000000000..74f95a60627d
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4205
@@ -0,0 +1,7 @@
+Enhancement: Download log files on Android10+ devices
+
+A new icon to download a log file to the Downloads folder of the device has been added to the log list screen
+on Android10+ devices.
+
+https://github.com/owncloud/android/issues/4155
+https://github.com/owncloud/android/pull/4205
diff --git a/changelog/4.2.0_2024-02-12/4209 b/changelog/4.2.0_2024-02-12/4209
new file mode 100644
index 000000000000..bac92ded45c3
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4209
@@ -0,0 +1,9 @@
+Enhancement: Unit tests for datasources classes - Part 1 & Fixes
+
+Unit tests for OCLocalAppRegistryDataSource, OCRemoteAppRegistryDataSource, OCLocalAuthenticationDataSource,
+OCRemoteAuthenticationDataSource, OCLocalCapabilitiesDataSource and OCRemoteCapabilitiesDataSource classes have
+been done and completed, and several fixes have been applied to all existent unit test classes for
+datasources.
+
+https://github.com/owncloud/android/issues/4063
+https://github.com/owncloud/android/pull/4209
diff --git a/changelog/4.2.0_2024-02-12/4212 b/changelog/4.2.0_2024-02-12/4212
new file mode 100644
index 000000000000..fa5ffd474ce8
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4212
@@ -0,0 +1,6 @@
+Enhancement: Select user and navigate to file when opening via deep link
+
+Select the correct user owner of the deep link file, managing possible errors and navigating to the correct file.
+
+https://github.com/owncloud/android/issues/4194
+https://github.com/owncloud/android/pull/4212
diff --git a/changelog/4.2.0_2024-02-12/4214 b/changelog/4.2.0_2024-02-12/4214
new file mode 100644
index 000000000000..8aa97ce71256
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4214
@@ -0,0 +1,6 @@
+Enhancement: Auto upload in oCIS accounts allows upload to any space
+
+Auto uploads of images and videos can now be uploaded to any space and not only the personal one in oCIS accounts.
+
+https://github.com/owncloud/android/issues/4117
+https://github.com/owncloud/android/pull/4214
diff --git a/changelog/4.2.0_2024-02-12/4215 b/changelog/4.2.0_2024-02-12/4215
new file mode 100644
index 000000000000..f207b6bd7871
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4215
@@ -0,0 +1,6 @@
+Enhancement: Log file sharing allowed within ownCloud Android app
+
+Sharing log files to the ownCloud app itself is now possible from the logs screen.
+
+https://github.com/owncloud/android/issues/4156
+https://github.com/owncloud/android/pull/4215
diff --git a/changelog/4.2.0_2024-02-12/4237 b/changelog/4.2.0_2024-02-12/4237
new file mode 100644
index 000000000000..5e8e0f998ac0
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4237
@@ -0,0 +1,6 @@
+Enhancement: Thumbnail improvements in grid view
+
+Grid view was improved by adding the file name to images when the thumbnail is null.
+
+https://github.com/owncloud/android/issues/4145
+https://github.com/owncloud/android/pull/4237
diff --git a/changelog/4.2.0_2024-02-12/4238 b/changelog/4.2.0_2024-02-12/4238
new file mode 100644
index 000000000000..563ff5020fe1
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4238
@@ -0,0 +1,6 @@
+Bugfix: Some Null Pointer Exceptions fixed from Google Play
+
+FileDisplayActivity and ReceiverExternalFilesActivity have prevented some functions from crashing when a null value is found.
+
+https://github.com/owncloud/android/issues/4207
+https://github.com/owncloud/android/pull/4238
diff --git a/changelog/4.2.0_2024-02-12/4241 b/changelog/4.2.0_2024-02-12/4241
new file mode 100644
index 000000000000..43a0baea6116
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4241
@@ -0,0 +1,6 @@
+Bugfix: Some Null Pointer Exceptions in MainFileListViewModel
+
+The MainFileListViewModel has prevented the fileById variable from crashing when a null value is found.
+
+https://github.com/owncloud/android/issues/4065
+https://github.com/owncloud/android/pull/4241
diff --git a/changelog/4.2.0_2024-02-12/4257 b/changelog/4.2.0_2024-02-12/4257
new file mode 100644
index 000000000000..ffbbd4c7b5ac
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4257
@@ -0,0 +1,7 @@
+Enhancement: New branding/MDM parameter to show sensitive auth info in logs
+
+A new branding and MDM parameter has been created to decide if the sensitive information put in the
+authorization header in HTTP requests is shown or not in the logs.
+
+https://github.com/owncloud/android/issues/4249
+https://github.com/owncloud/android/pull/4257
diff --git a/changelog/4.2.0_2024-02-12/4260 b/changelog/4.2.0_2024-02-12/4260
new file mode 100644
index 000000000000..9e11fd2d78f6
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4260
@@ -0,0 +1,6 @@
+Bugfix: Add "scope" parameter to /token endpoint HTTP requests
+
+The "scope" parameter is now always sent in the body of HTTP requests to the /token endpoint,
+which is optional in v1 but required in v2.
+
+https://github.com/owncloud/android/pull/4260
diff --git a/changelog/4.2.0_2024-02-12/4263 b/changelog/4.2.0_2024-02-12/4263
new file mode 100644
index 000000000000..3bbad65dd29e
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4263
@@ -0,0 +1,5 @@
+Enhancement: Prevent that two media files are playing at the same time
+
+The player handles the audio focus shifts, pausing one player if another starts.
+
+https://github.com/owncloud/android/pull/4263
diff --git a/changelog/4.2.0_2024-02-12/4265 b/changelog/4.2.0_2024-02-12/4265
new file mode 100644
index 000000000000..f219b6ade4d6
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4265
@@ -0,0 +1,7 @@
+Bugfix: Bugs related to Details view
+
+When coming to Details view from video or image previews, now the top bar is shown correctly
+and navigation has the correct stack, so the back button has the expected flow.
+
+https://github.com/owncloud/android/issues/4188
+https://github.com/owncloud/android/pull/4265
diff --git a/changelog/4.2.0_2024-02-12/4266 b/changelog/4.2.0_2024-02-12/4266
new file mode 100644
index 000000000000..09259ed25d39
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4266
@@ -0,0 +1,7 @@
+Enhancement: Fix in the type handling of the content-type
+
+The content-type `application/jrd+json` has been added to the loggable types list, so that body in
+some requests and responses can be correctly logged.
+
+https://github.com/owncloud/android/issues/4258
+https://github.com/owncloud/android/pull/4266
diff --git a/changelog/4.2.0_2024-02-12/4276 b/changelog/4.2.0_2024-02-12/4276
new file mode 100644
index 000000000000..78edb7220046
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4276
@@ -0,0 +1,6 @@
+Enhancement: Manage password policy in live mode
+
+Password policy for public links is handled in live mode with new items in the dialog.
+
+https://github.com/owncloud/android/issues/4269
+https://github.com/owncloud/android/pull/4276
diff --git a/changelog/4.2.0_2024-02-12/4277 b/changelog/4.2.0_2024-02-12/4277
new file mode 100644
index 000000000000..1b1cde38bb5f
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4277
@@ -0,0 +1,6 @@
+Bugfix: Fixed AlertDialog title theme in Samsung Devices
+
+Use of device default theme was removed.
+
+https://github.com/owncloud/android/issues/3192
+https://github.com/owncloud/android/pull/4277
diff --git a/changelog/4.2.0_2024-02-12/4283 b/changelog/4.2.0_2024-02-12/4283
new file mode 100644
index 000000000000..cf3a805630a0
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4283
@@ -0,0 +1,7 @@
+Security: Improve biometric authentication security
+
+Biometric authentication has been improved by checking the result received when performing a successful
+authentication.
+
+https://github.com/owncloud/android/issues/4180
+https://github.com/owncloud/android/pull/4283
diff --git a/changelog/4.2.0_2024-02-12/4285 b/changelog/4.2.0_2024-02-12/4285
new file mode 100644
index 000000000000..feb9fe15bb38
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4285
@@ -0,0 +1,6 @@
+Bugfix: Handle Http 423 (resource locked)
+
+App can gracefully show if the file is locked when done certain operations on it.
+
+https://github.com/owncloud/android/issues/4282
+https://github.com/owncloud/android/pull/4285
diff --git a/changelog/4.2.0_2024-02-12/4287 b/changelog/4.2.0_2024-02-12/4287
new file mode 100644
index 000000000000..3864427a7c65
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4287
@@ -0,0 +1,7 @@
+Bugfix: Fix in the handling of the base URL
+
+Base URL has been formatted in GetRemoteAppRegistryOperation
+when server instance is installed in subfolder, so that the endpoint is formed correctly.
+
+https://github.com/owncloud/android/issues/4279
+https://github.com/owncloud/android/pull/4287
diff --git a/changelog/4.2.0_2024-02-12/4291 b/changelog/4.2.0_2024-02-12/4291
new file mode 100644
index 000000000000..981f6a79e9e2
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4291
@@ -0,0 +1,7 @@
+Enhancement: New branding/MDM parameter to send `login_hint` and `user` params
+
+A new branding and MDM parameter has been created to decide if `login_hint` and `user` are
+sent as parameters in the login request, so that a value is shown in the Username text field.
+
+https://github.com/owncloud/android/issues/4288
+https://github.com/owncloud/android/pull/4291
diff --git a/changelog/4.2.0_2024-02-12/4294 b/changelog/4.2.0_2024-02-12/4294
new file mode 100644
index 000000000000..ba4d97f43751
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4294
@@ -0,0 +1,8 @@
+Bugfix: Conflict in copy with files without extension
+
+The check of files names that start in the same way has been removed from the copy
+network operation, so that the copy use case takes care of that and works properly with
+files without extension.
+
+https://github.com/owncloud/android/issues/4222
+https://github.com/owncloud/android/pull/4294
diff --git a/changelog/4.2.0_2024-02-12/4295 b/changelog/4.2.0_2024-02-12/4295
new file mode 100644
index 000000000000..faa1db7e745c
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4295
@@ -0,0 +1,6 @@
+Bugfix: Copy folder into descendant in different spaces
+
+Copying a folder into another folder with the same name in a different space now works correctly.
+
+https://github.com/owncloud/android/issues/4293
+https://github.com/owncloud/android/pull/4295
diff --git a/changelog/4.2.0_2024-02-12/4297 b/changelog/4.2.0_2024-02-12/4297
new file mode 100644
index 000000000000..4868d0a75eb5
--- /dev/null
+++ b/changelog/4.2.0_2024-02-12/4297
@@ -0,0 +1,6 @@
+Enhancement: Added icon for .docxf files
+
+An icon has been added for files that have a .docxf extension.
+
+https://github.com/owncloud/android/issues/4267
+https://github.com/owncloud/android/pull/4297
diff --git a/changelog/4.2.1_2024-02-22/4323 b/changelog/4.2.1_2024-02-22/4323
new file mode 100644
index 000000000000..261f21dc889b
--- /dev/null
+++ b/changelog/4.2.1_2024-02-22/4323
@@ -0,0 +1,6 @@
+Bugfix: Some crashes in 4.2.0
+
+Several crashes reported by Play Console in version 4.2.0 have been fixed.
+
+https://github.com/owncloud/android/issues/4318
+https://github.com/owncloud/android/pull/4323
diff --git a/changelog/4.2.2_2024-05-30/4415 b/changelog/4.2.2_2024-05-30/4415
new file mode 100644
index 000000000000..d7098b46337f
--- /dev/null
+++ b/changelog/4.2.2_2024-05-30/4415
@@ -0,0 +1,7 @@
+Bugfix: Downloads not working when `Content-Length` is not received
+
+The case when Content-Length header is not received in the response of a GET for a download has been
+handled, and now the progress bar in images preview and details view is indeterminate for those cases.
+
+https://github.com/owncloud/android/issues/4352
+https://github.com/owncloud/android/pull/4415
diff --git a/changelog/4.3.0_2024-07-01/4281 b/changelog/4.3.0_2024-07-01/4281
new file mode 100644
index 000000000000..6bee1fd88723
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4281
@@ -0,0 +1,6 @@
+Enhancement: Unit tests for repository classes - Part 1
+
+Unit tests for OCAppRegistryRepository, OCAuthenticationRepository and OCCapabilityRepository classes have been completed.
+
+https://github.com/owncloud/android/issues/4232
+https://github.com/owncloud/android/pull/4281
diff --git a/changelog/4.3.0_2024-07-01/4289 b/changelog/4.3.0_2024-07-01/4289
new file mode 100644
index 000000000000..65b2c8f3be7e
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4289
@@ -0,0 +1,6 @@
+Enhancement: Correct "Local only" option in remove dialog
+
+"Local only" option in remove dialog will only be shown if checking selected files and folders recursively, at least one file is available locally.
+
+https://github.com/owncloud/android/issues/3936
+https://github.com/owncloud/android/pull/4289
diff --git a/changelog/4.3.0_2024-07-01/4299 b/changelog/4.3.0_2024-07-01/4299
new file mode 100644
index 000000000000..05e1f434686c
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4299
@@ -0,0 +1,6 @@
+Change: Upgrade minimum SDK version to Android 7.0 (v24)
+
+The minimum Android version will be Android 7.0 Nougat (API 24). The application will no longer support previous versions.
+
+https://github.com/owncloud/android/issues/4230
+https://github.com/owncloud/android/pull/4299
diff --git a/changelog/4.3.0_2024-07-01/4320 b/changelog/4.3.0_2024-07-01/4320
new file mode 100644
index 000000000000..c0ded6c67c45
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4320
@@ -0,0 +1,6 @@
+Enhancement: New setting for automatic removal of local files
+
+A new setting has been created to delete automatically downloaded files, when the time since their last usage exceeds the selected time in the setting.
+
+https://github.com/owncloud/android/issues/4175
+https://github.com/owncloud/android/pull/4320
diff --git a/changelog/4.3.0_2024-07-01/4325 b/changelog/4.3.0_2024-07-01/4325
new file mode 100644
index 000000000000..6ecd1a530650
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4325
@@ -0,0 +1,6 @@
+Change: Automatic discovery of the account in login
+
+Automatic account discovery is done at login. Removed the refresh account button in the Manage Accounts view.
+
+https://github.com/owncloud/android/issues/4301
+https://github.com/owncloud/android/pull/4325
diff --git a/changelog/4.3.0_2024-07-01/4330 b/changelog/4.3.0_2024-07-01/4330
new file mode 100644
index 000000000000..83a15537d6f8
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4330
@@ -0,0 +1,7 @@
+Enhancement: Improvements in Manage Accounts view
+
+Removed the key icon and avoid overlap account name with icons in Manage Accounts.
+Redirect to login when snackbar appears in authentication failure.
+
+https://github.com/owncloud/android/issues/4148
+https://github.com/owncloud/android/pull/4330
diff --git a/changelog/4.3.0_2024-07-01/4334 b/changelog/4.3.0_2024-07-01/4334
new file mode 100644
index 000000000000..89be1a9a3dd1
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4334
@@ -0,0 +1,6 @@
+Enhancement: New setting for manual removal of local storage
+
+A new icon has been added in Manage Accounts view to delete manually local files.
+
+https://github.com/owncloud/android/issues/4174
+https://github.com/owncloud/android/pull/4334
diff --git a/changelog/4.3.0_2024-07-01/4336 b/changelog/4.3.0_2024-07-01/4336
new file mode 100644
index 000000000000..5fe383a54ce9
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4336
@@ -0,0 +1,6 @@
+Enhancement: Make dialog more Android-alike
+
+Name conflicts dialog appearance was changed to look Android-alike and more similar to other dialogs in the app.
+
+https://github.com/owncloud/android/issues/4303
+https://github.com/owncloud/android/pull/4336
diff --git a/changelog/4.3.0_2024-07-01/4341 b/changelog/4.3.0_2024-07-01/4341
new file mode 100644
index 000000000000..06049fff366e
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4341
@@ -0,0 +1,6 @@
+Bugfix: Retried successful uploads are cleaned up from the temporary folder
+
+Temporary files related to a failed upload are deleted after retrying it and being successfully completed.
+
+https://github.com/owncloud/android/issues/4335
+https://github.com/owncloud/android/pull/4341
diff --git a/changelog/4.3.0_2024-07-01/4345 b/changelog/4.3.0_2024-07-01/4345
new file mode 100644
index 000000000000..20a821c7d0ea
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4345
@@ -0,0 +1,6 @@
+Enhancement: Add a warning in http connections
+
+Warning dialog has been added in the login screen when you are trying to connect to a http server.
+
+https://github.com/owncloud/android/issues/4284
+https://github.com/owncloud/android/pull/4345
diff --git a/changelog/4.3.0_2024-07-01/4346 b/changelog/4.3.0_2024-07-01/4346
new file mode 100644
index 000000000000..51de5e6688ea
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4346
@@ -0,0 +1,5 @@
+Change: Add new prefixes in commit messages of 3rd party contributors
+
+Dependaboy and Calens' commit messages with prefixes that fits 'Conventional Commits'
+
+https://github.com/owncloud/android/pull/4346
diff --git a/changelog/4.3.0_2024-07-01/4349 b/changelog/4.3.0_2024-07-01/4349
new file mode 100644
index 000000000000..f7436b62cdd3
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4349
@@ -0,0 +1,7 @@
+Enhancement: Password generator for public links in oCIS
+
+A new password generator has been added to the public links creation view in oCIS accounts, which creates
+passwords that fulfill all the policies coming from server in a cryptographically secure way.
+
+https://github.com/owncloud/android/issues/4308
+https://github.com/owncloud/android/pull/4349
diff --git a/changelog/4.3.0_2024-07-01/4350 b/changelog/4.3.0_2024-07-01/4350
new file mode 100644
index 000000000000..370c7aee6e8e
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4350
@@ -0,0 +1,7 @@
+Bugfix: "Clear data" button enabled in the app settings in device settings
+
+The "Clear data" button has been enabled to delete the application data from the app settings in the device settings.
+Shared preferences, temporary files, accounts and the local database will be cleared when the button is pressed.
+
+https://github.com/owncloud/android/issues/4309
+https://github.com/owncloud/android/pull/4350
diff --git a/changelog/4.3.0_2024-07-01/4351 b/changelog/4.3.0_2024-07-01/4351
new file mode 100644
index 000000000000..592a382e281d
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4351
@@ -0,0 +1,7 @@
+Bugfix: Resolve incorrect truncation of long display names in Manage Accounts
+
+Resolved the bug where long display names were truncated incorrectly in the Manage Accounts view.
+Now, display names are properly truncated in the middle with ellipsis (...) to maintain readability.
+
+https://github.com/owncloud/android/issues/4351
+https://github.com/owncloud/android/pull/4380
diff --git a/changelog/4.3.0_2024-07-01/4354 b/changelog/4.3.0_2024-07-01/4354
new file mode 100644
index 000000000000..94c800e9c8f4
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4354
@@ -0,0 +1,7 @@
+Enhancement: Avoid unnecessary requests when an av. offline folder is refreshed
+
+The available offline folders will only be refreshed when their eTag from the server and the corresponding one of the local database are different,
+avoiding sending unnecessary request.
+
+https://github.com/owncloud/android/issues/4197
+https://github.com/owncloud/android/pull/4354
diff --git a/changelog/4.3.0_2024-07-01/4376 b/changelog/4.3.0_2024-07-01/4376
new file mode 100644
index 000000000000..e3ebe0f22e8d
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4376
@@ -0,0 +1,6 @@
+Change: Kotlinize PreviewTextFragment
+
+PreviewTextFragment class has been moved from Java to Kotlin.
+
+https://github.com/owncloud/android/issues/4356
+https://github.com/owncloud/android/pull/4376
diff --git a/changelog/4.3.0_2024-07-01/4385 b/changelog/4.3.0_2024-07-01/4385
new file mode 100644
index 000000000000..b68d32114356
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4385
@@ -0,0 +1,6 @@
+Bugfix: Removed unnecessary requests when the app is installed from scratch
+
+Some requests to the server that were not necessary when installing the app from scratch have been removed.
+
+https://github.com/owncloud/android/issues/4213
+https://github.com/owncloud/android/pull/4385
diff --git a/changelog/4.3.0_2024-07-01/4387 b/changelog/4.3.0_2024-07-01/4387
new file mode 100644
index 000000000000..b5c793b415ce
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4387
@@ -0,0 +1,7 @@
+Enhancement: Content description in UI elements to improve accessibility
+
+A description of the meaning or action associated with some UI elements has been included as alternative text to make the application more accessible.
+Views improved: toolbar, file list, spaces list, share, drawer menu, manage accounts and image preview.
+
+https://github.com/owncloud/android/issues/4360
+https://github.com/owncloud/android/pull/4387
diff --git a/changelog/4.3.0_2024-07-01/4388 b/changelog/4.3.0_2024-07-01/4388
new file mode 100644
index 000000000000..327f5b4bae80
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4388
@@ -0,0 +1,6 @@
+Enhancement: Added contentDescription attribute in the previewed image
+
+A contentDescription attribute has been added to previewed image to make the application more accessible.
+
+https://github.com/owncloud/android/issues/4360
+https://github.com/owncloud/android/pull/4388
diff --git a/changelog/4.3.0_2024-07-01/4391 b/changelog/4.3.0_2024-07-01/4391
new file mode 100644
index 000000000000..448aebea2cb2
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4391
@@ -0,0 +1,6 @@
+Enhancement: Show app provider icon from endpoint
+
+App provider icon fetched from the server has been added to the "Open in (web)" option on the bottom sheet that appears when clicking the 3-dots button of a file.
+
+https://github.com/owncloud/android/issues/4105
+https://github.com/owncloud/android/pull/4391
diff --git a/changelog/4.3.0_2024-07-01/4393 b/changelog/4.3.0_2024-07-01/4393
new file mode 100644
index 000000000000..50a456a06428
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4393
@@ -0,0 +1,6 @@
+Enhancement: Add search functionality to spaces list
+
+Search functionality was added in spaces list when you are trying to filter them.
+
+https://github.com/owncloud/android/issues/3865
+https://github.com/owncloud/android/pull/4393
diff --git a/changelog/4.3.0_2024-07-01/4394 b/changelog/4.3.0_2024-07-01/4394
new file mode 100644
index 000000000000..a6564b5567d9
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4394
@@ -0,0 +1,7 @@
+Bugfix: Video streaming in spaces
+
+The URI formed to perform video streaming in spaces has been adapted to oCIS accounts so that it takes into account the
+space where the file is located.
+
+https://github.com/owncloud/android/issues/4328
+https://github.com/owncloud/android/pull/4394
diff --git a/changelog/4.3.0_2024-07-01/4399 b/changelog/4.3.0_2024-07-01/4399
new file mode 100644
index 000000000000..2b26aa795584
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4399
@@ -0,0 +1,7 @@
+Bugfix: Av. offline files are not removed when "Local only" option is clicked
+
+"Local only" option in remove dialog will be displayed when the selected folder contains at least one downloaded file, ignoring those available offline.
+If the "Local only" option is displayed and clicked, available offline files will not be deleted.
+
+https://github.com/owncloud/android/issues/4353
+https://github.com/owncloud/android/pull/4399
diff --git a/changelog/4.3.0_2024-07-01/4401 b/changelog/4.3.0_2024-07-01/4401
new file mode 100644
index 000000000000..5832d6b56280
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4401
@@ -0,0 +1,6 @@
+Enhancement: Get personal space quota from GraphAPI
+
+Personal space quota in an oCIS account has been added from GraphAPI instead of propfind.
+
+https://github.com/owncloud/android/issues/3874
+https://github.com/owncloud/android/pull/4401
diff --git a/changelog/4.3.0_2024-07-01/4404 b/changelog/4.3.0_2024-07-01/4404
new file mode 100644
index 000000000000..d8316ee1e411
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4404
@@ -0,0 +1,9 @@
+Enhancement: Improvements in remove dialog
+
+A new remove dialog has been created by adding the thumbnail of the file to be deleted.
+Also, when removing files in multiple selection, the number of elements that are going to be removed is displayed in the dialog.
+
+https://github.com/owncloud/android/issues/4342
+https://github.com/owncloud/android/pull/4348
+https://github.com/owncloud/android/issues/4377
+https://github.com/owncloud/android/pull/4404
diff --git a/changelog/4.3.0_2024-07-01/4408 b/changelog/4.3.0_2024-07-01/4408
new file mode 100644
index 000000000000..74a7dfad9446
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4408
@@ -0,0 +1,8 @@
+Bugfix: Unwanted DELETE operations when synchronization in single file fails
+
+A new exception is now thrown and handled when the account of the network client is null, avoiding
+DELETE requests to the server when synchronization (PROPFIND) on a single file responds with 404. Also,
+when PROPFINDs respond with 404, the delete operation has been changed to be just local and not remote too.
+
+https://github.com/owncloud/enterprise/issues/6638
+https://github.com/owncloud/android/pull/4408
diff --git a/changelog/4.3.0_2024-07-01/4410 b/changelog/4.3.0_2024-07-01/4410
new file mode 100644
index 000000000000..8199fb4ab6a2
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4410
@@ -0,0 +1,8 @@
+Enhancement: New UI for "Manage accounts" view
+
+A new dialog has been added to substitute the previous view for "Manage accounts". In addition,
+all the accounts management related stuff has been removed from the drawer menu in order not to
+show repetitive actions and make this menu simpler.
+
+https://github.com/owncloud/android/issues/4312
+https://github.com/owncloud/android/pull/4410
diff --git a/changelog/4.3.0_2024-07-01/4420 b/changelog/4.3.0_2024-07-01/4420
new file mode 100644
index 000000000000..93e01533ef56
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4420
@@ -0,0 +1,6 @@
+Enhancement: Support for URL shortcut files
+
+A new option has been added in the FAB to create a shortcut file with a .url extension. When the file is clicked, the URL will open in the browser.
+
+https://github.com/owncloud/android/issues/4413
+https://github.com/owncloud/android/pull/4420
diff --git a/changelog/4.3.0_2024-07-01/4423 b/changelog/4.3.0_2024-07-01/4423
new file mode 100644
index 000000000000..4a41d96eee34
--- /dev/null
+++ b/changelog/4.3.0_2024-07-01/4423
@@ -0,0 +1,6 @@
+Enhancement: Changes in the Feedback section
+
+Based on a brandable parameter, a new dialog has been added to handle feedback. Within the dialog, links to the survey, GitHub and the open forum Central will be displayed.
+
+https://github.com/owncloud/enterprise/issues/6594
+https://github.com/owncloud/android/pull/4423
diff --git a/changelog/4.3.1_2024-07-22/4440 b/changelog/4.3.1_2024-07-22/4440
new file mode 100644
index 000000000000..ab60e62e898f
--- /dev/null
+++ b/changelog/4.3.1_2024-07-22/4440
@@ -0,0 +1,6 @@
+Change: Bump target SDK to 34
+
+Target SDK was upgraded to 34 in order to fulfill Android platform requirements.
+
+https://github.com/owncloud/android/issues/4434
+https://github.com/owncloud/android/pull/4440
diff --git a/changelog/4.4.0_2024-09-30/4429 b/changelog/4.4.0_2024-09-30/4429
new file mode 100644
index 000000000000..0e908d2ec6b4
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4429
@@ -0,0 +1,6 @@
+Enhancement: Changed the color of some elements to improve accessibility
+
+The color of some UI elements has been changed to meet minimum color contrast requirements.
+
+https://github.com/owncloud/android/issues/4364
+https://github.com/owncloud/android/pull/4429
diff --git a/changelog/4.4.0_2024-09-30/4433 b/changelog/4.4.0_2024-09-30/4433
new file mode 100644
index 000000000000..e6baf5f3cef3
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4433
@@ -0,0 +1,7 @@
+Enhancement: Improved SearchView accessibility
+
+The text hint and cross button color of the SearchView has been changed to meet the color contrast requirements.
+In addition, the SearchView includes a new resource with rounded edges, using the same background color (brandable) as the containing toolbar.
+
+https://github.com/owncloud/android/issues/4365
+https://github.com/owncloud/android/pull/4433
diff --git a/changelog/4.4.0_2024-09-30/4435 b/changelog/4.4.0_2024-09-30/4435
new file mode 100644
index 000000000000..ae238bdf7836
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4435
@@ -0,0 +1,6 @@
+Bugfix: Shares in non-root are updated correctly
+
+The items of the "Share" view are updated instantly when create/edit a link or share with users or groups in a non-root file.
+
+https://github.com/owncloud/android/issues/4432
+https://github.com/owncloud/android/pull/4435
\ No newline at end of file
diff --git a/changelog/4.4.0_2024-09-30/4437 b/changelog/4.4.0_2024-09-30/4437
new file mode 100644
index 000000000000..e259e7c374b6
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4437
@@ -0,0 +1,6 @@
+Enhancement: Improved "Remove from original folder" option in auto-upload
+
+The file will be deleted locally after it has been uploaded to the server, avoiding the loss of the file if an error happens during the upload.
+
+https://github.com/owncloud/android/issues/4357
+https://github.com/owncloud/android/pull/4437
diff --git a/changelog/4.4.0_2024-09-30/4438 b/changelog/4.4.0_2024-09-30/4438
new file mode 100644
index 000000000000..532bd7df6b2e
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4438
@@ -0,0 +1,10 @@
+Enhancement: Hardware keyboard support
+
+Navigation via hardware keyboard has been improved so that now focus order has a logical path, every element
+is reachable and there are no traps. These improvements have been applied in main file list, spaces list,
+drawer menu, share view and image preview.
+
+https://github.com/owncloud/android/pull/4438
+https://github.com/owncloud/android/issues/4366
+https://github.com/owncloud/android/issues/4367
+https://github.com/owncloud/android/issues/4368
diff --git a/changelog/4.4.0_2024-09-30/4448 b/changelog/4.4.0_2024-09-30/4448
new file mode 100644
index 000000000000..3f6662d2dc94
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4448
@@ -0,0 +1,10 @@
+Enhancement: Improved accessibility of information and relationships
+
+Headings have been added to the following views: Share, Edit/Create Share Link, Standard Toolbar and Manage Accounts.
+The filename input field and the two switches are now linked to their labels.
+The 'contentDescription' attributes of the buttons in the Edit/Create Share Link view have also been updated.
+
+https://github.com/owncloud/android/issues/4362
+https://github.com/owncloud/android/issues/4363
+https://github.com/owncloud/android/issues/4371
+https://github.com/owncloud/android/pull/4448
diff --git a/changelog/4.4.0_2024-09-30/4454 b/changelog/4.4.0_2024-09-30/4454
new file mode 100644
index 000000000000..c3f88bac6c3e
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4454
@@ -0,0 +1,8 @@
+Enhancement: Roles added to some elements to improve accessibility
+
+Roles have been added to specific elements within the following views: Toolbar, Spaces, Drawer Menu, Manage accounts and Floating Action Button.
+Improved the navigation system within the passcode view.
+
+https://github.com/owncloud/android/issues/4373
+https://github.com/owncloud/android/pull/4454
+https://github.com/owncloud/android/pull/4466
diff --git a/changelog/4.4.0_2024-09-30/4455 b/changelog/4.4.0_2024-09-30/4455
new file mode 100644
index 000000000000..397188ea260e
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4455
@@ -0,0 +1,6 @@
+Enhancement: Hardware keyboard support for passcode view
+
+Navigation via hardware keyboard has been added to the passcode view.
+
+https://github.com/owncloud/android/issues/4447
+https://github.com/owncloud/android/pull/4455
diff --git a/changelog/4.4.0_2024-09-30/4463 b/changelog/4.4.0_2024-09-30/4463
new file mode 100644
index 000000000000..bf2ff5d461da
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4463
@@ -0,0 +1,6 @@
+Bugfix: The color of some elements is set up correctly
+
+The colors of the Manage Accounts header and status bar have been changed to be consistent with the branding colors.
+
+https://github.com/owncloud/android/issues/4442
+https://github.com/owncloud/android/pull/4463
diff --git a/changelog/4.4.0_2024-09-30/4467 b/changelog/4.4.0_2024-09-30/4467
new file mode 100644
index 000000000000..0eeb0ef41dcc
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4467
@@ -0,0 +1,6 @@
+Bugfix: List filtering not working after rotating device
+
+Configuration changes have been handled when rotating the device so that list filtering works.
+
+https://github.com/owncloud/android/issues/4441
+https://github.com/owncloud/android/pull/4467
diff --git a/changelog/4.4.0_2024-09-30/4470 b/changelog/4.4.0_2024-09-30/4470
new file mode 100644
index 000000000000..b282b4164b30
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4470
@@ -0,0 +1,6 @@
+Enhancement: TalkBack announces the view label correctly
+
+TalkBack no longer announces "ownCloud" every time the screen changes. Now, it correctly dictates the name of the current view.
+
+https://github.com/owncloud/android/issues/4458
+https://github.com/owncloud/android/pull/4470
diff --git a/changelog/4.4.0_2024-09-30/4472 b/changelog/4.4.0_2024-09-30/4472
new file mode 100644
index 000000000000..935a3e297641
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4472
@@ -0,0 +1,8 @@
+Bugfix: Rely on `resharing` capability
+
+The request to create a new share has been fixed so that it only includes the share permission
+by default when the resharing capability is true, and the "can share" switch in the edition view
+of private shares is now only shown when resharing is true.
+
+https://github.com/owncloud/android/issues/4397
+https://github.com/owncloud/android/pull/4472
diff --git a/changelog/4.4.0_2024-09-30/4479 b/changelog/4.4.0_2024-09-30/4479
new file mode 100644
index 000000000000..978b60bb827a
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4479
@@ -0,0 +1,7 @@
+Bugfix: Audio player does not work
+
+Audio player in Android 14+ devices wasn't working, so some proper permissions have been added
+in Manifest so that media can be played correctly in the foreground and background in all versions.
+
+https://github.com/owncloud/android/issues/4474
+https://github.com/owncloud/android/pull/4479
diff --git a/changelog/4.4.0_2024-09-30/4480 b/changelog/4.4.0_2024-09-30/4480
new file mode 100644
index 000000000000..2c152e83f548
--- /dev/null
+++ b/changelog/4.4.0_2024-09-30/4480
@@ -0,0 +1,7 @@
+Bugfix: Buttons visibility in name conflicts dialog
+
+In some languages, labels for the buttons in the name conflicts dialog were too long and their
+visibility was very poor. These buttons have been placed in vertical instead of horizontal to avoid
+this problem.
+
+https://github.com/owncloud/android/pull/4480
diff --git a/changelog/4.4.1_2024-10-30/4502 b/changelog/4.4.1_2024-10-30/4502
new file mode 100644
index 000000000000..8151bea5f108
--- /dev/null
+++ b/changelog/4.4.1_2024-10-30/4502
@@ -0,0 +1,6 @@
+Bugfix: File size becomes 0 after a local update
+
+The local copy of a file is not removed after a local update anymore. Therefore, the file size has been fixed.
+
+https://github.com/owncloud/android/issues/4495
+https://github.com/owncloud/android/pull/4502
diff --git a/changelog/4.5.0_2025-03-24/4389 b/changelog/4.5.0_2025-03-24/4389
new file mode 100644
index 000000000000..1a9bb42118a0
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4389
@@ -0,0 +1,6 @@
+Enhancement: Unit tests for repository classes - Part 2
+
+Unit tests for OCFileRepository class have been completed.
+
+https://github.com/owncloud/android/issues/4233
+https://github.com/owncloud/android/pull/4389
diff --git a/changelog/4.5.0_2025-03-24/4482 b/changelog/4.5.0_2025-03-24/4482
new file mode 100644
index 000000000000..5ca06e49a178
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4482
@@ -0,0 +1,7 @@
+Enhancement: Add status message when (un)setting av. offline from preview
+
+A message has been added in all previews when the (un)setting av. offline buttons are clicked.
+The options menu has been updated in all previews depending on the file status.
+
+https://github.com/owncloud/android/issues/4382
+https://github.com/owncloud/android/pull/4482
diff --git a/changelog/4.5.0_2025-03-24/4484 b/changelog/4.5.0_2025-03-24/4484
new file mode 100644
index 000000000000..405ee38a12db
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4484
@@ -0,0 +1,7 @@
+Enhancement: Added text labels for BottomNavigationView
+
+Text labels have been added below the icons, and the active indicator feature is implemented using the default itemActiveIndicatorStyle for better navigation experience.
+
+
+https://github.com/owncloud/android/issues/4484
+https://github.com/owncloud/android/pull/4498
diff --git a/changelog/4.5.0_2025-03-24/4487 b/changelog/4.5.0_2025-03-24/4487
new file mode 100644
index 000000000000..ea233bb9b43b
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4487
@@ -0,0 +1,7 @@
+Enhancement: Detekt: static code analyzer
+
+The Kotlin static code analyzer Detekt has been introduced with the agreed rules, and
+the left code smells have been fixed throughout the whole code.
+
+https://github.com/owncloud/android/issues/4506
+https://github.com/owncloud/android/pull/4487
diff --git a/changelog/4.5.0_2025-03-24/4492 b/changelog/4.5.0_2025-03-24/4492
new file mode 100644
index 000000000000..3eb5162e14d1
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4492
@@ -0,0 +1,6 @@
+Change: replace auto-uploads with automatic uploads
+
+Wording change in the feature name, in order to make it clearer in translations and documentation
+
+https://github.com/owncloud/android/issues/4252
+https://github.com/owncloud/android/pull/4492
diff --git a/changelog/4.5.0_2025-03-24/4496 b/changelog/4.5.0_2025-03-24/4496
new file mode 100644
index 000000000000..28c51469d0cd
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4496
@@ -0,0 +1,7 @@
+Enhancement: Quota improvements from GraphAPI
+
+The quota in the drawer has been updated depending on its status and also when a file is removed, copied, moved and after a refresh operation.
+In addition, the quota value for each account has been added in the manage accounts dialog.
+
+https://github.com/owncloud/android/issues/4411
+https://github.com/owncloud/android/pull/4496
diff --git a/changelog/4.5.0_2025-03-24/4507 b/changelog/4.5.0_2025-03-24/4507
new file mode 100644
index 000000000000..9b9367603a36
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4507
@@ -0,0 +1,7 @@
+Enhancement: Upgraded AGP version to 8.7.2
+
+The Android Gradle Plugin version has been upgraded to 8.7.2, together with Gradle version (updated to 8.9) and
+JDK version (updated to JBR 17).
+
+https://github.com/owncloud/android/issues/4478
+https://github.com/owncloud/android/pull/4507
diff --git a/changelog/4.5.0_2025-03-24/4516 b/changelog/4.5.0_2025-03-24/4516
new file mode 100644
index 000000000000..1bb48453012f
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4516
@@ -0,0 +1,9 @@
+Enhancement: Enforce OIDC auth flow via branding
+
+A new branded parameter `enforce_oidc` has been added to enforce the app to follow the OIDC auth flow,
+and `clientId` and `clientSecret` are sent in token requests when required by server. Moreover, the
+app now supports branded redirect URIs with path due to the new branded parameter
+`oauth2_redirect_uri_path` (legacy `oauth2_redirect_uri_path` is now `oauth2_redirect_uri_host`).
+
+https://github.com/owncloud/android/issues/4500
+https://github.com/owncloud/android/pull/4516
diff --git a/changelog/4.5.0_2025-03-24/4518 b/changelog/4.5.0_2025-03-24/4518
new file mode 100644
index 000000000000..5bb03006a0b7
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4518
@@ -0,0 +1,6 @@
+Enhancement: oCIS Light Users
+
+oCIS light users (users without personal space) are now supported in the app
+
+https://github.com/owncloud/android/issues/4490
+https://github.com/owncloud/android/pull/4518
diff --git a/changelog/4.5.0_2025-03-24/4523 b/changelog/4.5.0_2025-03-24/4523
new file mode 100644
index 000000000000..9dadb4b2bd4c
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4523
@@ -0,0 +1,7 @@
+Enhancement: Unit tests for repository classes - Part 3
+
+Unit tests for OCFolderBackupRepository, OCOAuthRepository, OCServerInfoRepository,
+OCShareeRepository, OCShareRepository classes have been completed.
+
+https://github.com/owncloud/android/issues/4234
+https://github.com/owncloud/android/pull/4523
diff --git a/changelog/4.5.0_2025-03-24/4525 b/changelog/4.5.0_2025-03-24/4525
new file mode 100644
index 000000000000..20f5b8e346bb
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4525
@@ -0,0 +1,7 @@
+Enhancement: Technical improvements for user quota
+
+A new use case has been added to fetch the user quota as a flow.
+Also, all unnecessary calls from DrawerActivity have been removed.
+
+https://github.com/owncloud/android/issues/4521
+https://github.com/owncloud/android/pull/4525
diff --git a/changelog/4.5.0_2025-03-24/4527 b/changelog/4.5.0_2025-03-24/4527
new file mode 100644
index 000000000000..c95b07638bad
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4527
@@ -0,0 +1,8 @@
+Enhancement: Multi-Personal (1st round)
+
+Support for multi-personal accounts has been added. This first approach displays all personal
+spaces in the Spaces tab, not showing project spaces. In addition, the Personal tab shows
+an empty view since there is not a single personal space.
+
+https://github.com/owncloud/android/issues/4514
+https://github.com/owncloud/android/pull/4527/files
diff --git a/changelog/4.5.0_2025-03-24/4535 b/changelog/4.5.0_2025-03-24/4535
new file mode 100644
index 000000000000..8612f747c82a
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4535
@@ -0,0 +1,7 @@
+Bugfix: Navigation in automatic uploads folder picker
+
+The button in the toolbar for going up when choosing an upload path has been added
+when needed, since there were some cases in which it didn't appear.
+
+https://github.com/owncloud/android/issues/4340
+https://github.com/owncloud/android/pull/4535
diff --git a/changelog/4.5.0_2025-03-24/4537 b/changelog/4.5.0_2025-03-24/4537
new file mode 100644
index 000000000000..e82244408b9a
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4537
@@ -0,0 +1,7 @@
+Enhancement: Unit tests for repository classes - Part 4
+
+Unit tests for OCSpacesRepository, OCTransferRepository,
+OCUserRepository and OCWebFingerRepository classes have been completed.
+
+https://github.com/owncloud/android/issues/4235
+https://github.com/owncloud/android/pull/4537
diff --git a/changelog/4.5.0_2025-03-24/4542 b/changelog/4.5.0_2025-03-24/4542
new file mode 100644
index 000000000000..04bd45109254
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4542
@@ -0,0 +1,8 @@
+Bugfix: Crash from Google Play Store
+
+The androidx-appcompat version has been upgraded from 1.5.1 to 1.6.1
+in order to fix one crash reported by Play Console which is related to
+the FileDataStorageManager constructor
+
+https://github.com/owncloud/android/issues/4333
+https://github.com/owncloud/android/pull/4542
diff --git a/changelog/4.5.0_2025-03-24/4548 b/changelog/4.5.0_2025-03-24/4548
new file mode 100644
index 000000000000..65a073e2dd6c
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4548
@@ -0,0 +1,7 @@
+Bugfix: Downloading non-previewable files in details view leads to empty list
+
+The error that led to an empty file list after downloading a file in details view,
+due to the bottom sheet "Open with", has been fixed.
+
+https://github.com/owncloud/android/issues/4428
+https://github.com/owncloud/android/pull/4548
diff --git a/changelog/4.5.0_2025-03-24/4549 b/changelog/4.5.0_2025-03-24/4549
new file mode 100644
index 000000000000..d5f0f771e0c0
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4549
@@ -0,0 +1,7 @@
+Change: Removed survey and chat from feedback
+
+Survey and chat have been removed from the feedback dialog due to they are
+not maintained anymore or they have low traffic.
+
+https://github.com/owncloud/android/issues/4540
+https://github.com/owncloud/android/pull/4549
diff --git a/changelog/4.5.0_2025-03-24/4553 b/changelog/4.5.0_2025-03-24/4553
new file mode 100644
index 000000000000..5fbf524c4615
--- /dev/null
+++ b/changelog/4.5.0_2025-03-24/4553
@@ -0,0 +1,7 @@
+Bugfix: Ensure folder size updates automatically after file replacement
+
+The folder size has been updated automatically after replacing a file during a move operation,
+eliminating the need for a manual refresh.
+
+https://github.com/owncloud/android/issues/4505
+https://github.com/owncloud/android/pull/4553
diff --git a/changelog/4.5.1_2025-04-03/4562 b/changelog/4.5.1_2025-04-03/4562
new file mode 100644
index 000000000000..272924c97514
--- /dev/null
+++ b/changelog/4.5.1_2025-04-03/4562
@@ -0,0 +1,7 @@
+Bugfix: Confusing behaviour when creating new files using apps provider
+
+The error that appeared when creating a new file using the apps provider has been fixed.
+Now, the custom tab is opened correctly with the file content.
+
+https://github.com/owncloud/android/issues/4560
+https://github.com/owncloud/android/pull/4562
diff --git a/changelog/4.5.1_2025-04-03/4564 b/changelog/4.5.1_2025-04-03/4564
new file mode 100644
index 000000000000..d97ddf6181ae
--- /dev/null
+++ b/changelog/4.5.1_2025-04-03/4564
@@ -0,0 +1,7 @@
+Bugfix: App crashes at start when biometrics fail
+
+The crash that happened when biometrics failed due to a system error has been handled.
+In this case, an error is shown and pattern or passcode unlock are used instead of biometrics.
+
+https://github.com/owncloud/enterprise/issues/7134
+https://github.com/owncloud/android/pull/4564
diff --git a/changelog/4.6.0_2025-07-22/4556 b/changelog/4.6.0_2025-07-22/4556
new file mode 100644
index 000000000000..263122ea737c
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4556
@@ -0,0 +1,6 @@
+Change: Bump target SDK to 35
+
+Target SDK has been upgraded to 35 in order to fulfill Android platform requirements.
+
+https://github.com/owncloud/android/issues/4529
+https://github.com/owncloud/android/pull/4556
diff --git a/changelog/4.6.0_2025-07-22/4558 b/changelog/4.6.0_2025-07-22/4558
new file mode 100644
index 000000000000..e85bb5891e9d
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4558
@@ -0,0 +1,6 @@
+Change: Replace dav4android location
+
+dav4android location has been moved from GitLab to GitHub.
+
+https://github.com/owncloud/android/issues/4536
+https://github.com/owncloud/android/pull/4558
diff --git a/changelog/4.6.0_2025-07-22/4567 b/changelog/4.6.0_2025-07-22/4567
new file mode 100644
index 000000000000..799e3fcbbb00
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4567
@@ -0,0 +1,6 @@
+Bugfix: Add bottom margin for used quota in account dialog
+
+Added bottom margin to the container holding used quota view when multi account is disabled
+
+https://github.com/owncloud/android/issues/4566
+https://github.com/owncloud/android/pull/4567
diff --git a/changelog/4.6.0_2025-07-22/4569 b/changelog/4.6.0_2025-07-22/4569
new file mode 100644
index 000000000000..7914c7d88175
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4569
@@ -0,0 +1,6 @@
+Enhancement: QA variant
+
+A new flavor for QA has been created in order to make automatic tests easier.
+
+https://github.com/owncloud/android/issues/3791
+https://github.com/owncloud/android/pull/4569
diff --git a/changelog/4.6.0_2025-07-22/4571 b/changelog/4.6.0_2025-07-22/4571
new file mode 100644
index 000000000000..d3097798a922
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4571
@@ -0,0 +1,7 @@
+Bugfix: Changes in the automatic uploads algorithm to prevent duplications
+
+The timestamp for automatic uploads is now updated at the beginning of the upload process instead of at the end. Additionally, the filter
+used in AutomaticUploadsWorker to select the files to upload has been modified in order to reduce time and charge when evaluating all files.
+
+https://github.com/owncloud/android/issues/3983
+https://github.com/owncloud/android/pull/4571
diff --git a/changelog/4.6.0_2025-07-22/4573 b/changelog/4.6.0_2025-07-22/4573
new file mode 100644
index 000000000000..6b98f7c2216f
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4573
@@ -0,0 +1,6 @@
+Enhancement: Accessibility reports in 4.5.1
+
+Some content descriptions that were missing have been added to provide a better accessibility experience.
+
+https://github.com/owncloud/android/issues/4568
+https://github.com/owncloud/android/pull/4573
diff --git a/changelog/4.6.0_2025-07-22/4574 b/changelog/4.6.0_2025-07-22/4574
new file mode 100644
index 000000000000..1adfb9855833
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4574
@@ -0,0 +1,7 @@
+Bugfix: Content in Spaces not shown from third-party apps
+
+The root of the spaces has been synchronized before displaying
+the file list when a file is shared from a third-party app.
+
+https://github.com/owncloud/android/issues/4522
+https://github.com/owncloud/android/pull/4574
diff --git a/changelog/4.6.0_2025-07-22/4578 b/changelog/4.6.0_2025-07-22/4578
new file mode 100644
index 000000000000..52775433ce7f
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4578
@@ -0,0 +1,7 @@
+Change: Modify biometrics fail source string
+
+The string that appears when biometric unlocking is not
+available has been changed in order to make it clearer.
+
+https://github.com/owncloud/android/issues/4572
+https://github.com/owncloud/android/pull/4578
diff --git a/changelog/4.6.0_2025-07-22/4579 b/changelog/4.6.0_2025-07-22/4579
new file mode 100644
index 000000000000..4f04e7436829
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4579
@@ -0,0 +1,7 @@
+Enhancement: Shares space in Android native file explorer
+
+The Shares space has been added to the spaces list shown in the Documents Provider, the Android
+native file explorer.
+
+https://github.com/owncloud/android/issues/4515
+https://github.com/owncloud/android/pull/4579
diff --git a/changelog/4.6.0_2025-07-22/4580 b/changelog/4.6.0_2025-07-22/4580
new file mode 100644
index 000000000000..1c5a99d65c24
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4580
@@ -0,0 +1,6 @@
+Bugfix: Side menu collapses info in landscape
+
+Two empty and visual items have been added to prevent the drawer from collapsing in landscape mode.
+
+https://github.com/owncloud/android/issues/4513
+https://github.com/owncloud/android/pull/4580
diff --git a/changelog/4.6.0_2025-07-22/4581 b/changelog/4.6.0_2025-07-22/4581
new file mode 100644
index 000000000000..b0caadfb56ed
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4581
@@ -0,0 +1,6 @@
+Bugfix: Infinite edges in Android 15
+
+Infinite edges feature, enabled by default starting from Android 15, has been disabled in the app.
+
+https://github.com/owncloud/android/issues/4576
+https://github.com/owncloud/android/pull/4581
diff --git a/changelog/4.6.0_2025-07-22/4586 b/changelog/4.6.0_2025-07-22/4586
new file mode 100644
index 000000000000..39e2e9ec1f7c
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4586
@@ -0,0 +1,7 @@
+Bugfix: Token request with Bearer returns error
+
+A new condition has been added into the network client to check if the network request comes from TokenRequestRemoteOperation
+before setting the authorization header. This allows users to have more than one logged-in account on the same server.
+
+https://github.com/owncloud/android/issues/4080
+https://github.com/owncloud/android/pull/4586
diff --git a/changelog/4.6.0_2025-07-22/4587 b/changelog/4.6.0_2025-07-22/4587
new file mode 100644
index 000000000000..1e2994745574
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4587
@@ -0,0 +1,6 @@
+Bugfix: No message when uploading a file with no quota
+
+A message has been added in the file list when uploading a file (from file system, camera or shortcut) without available quota
+
+https://github.com/owncloud/android/issues/4582
+https://github.com/owncloud/android/pull/4587
diff --git a/changelog/4.6.0_2025-07-22/4589 b/changelog/4.6.0_2025-07-22/4589
new file mode 100644
index 000000000000..308552ac0492
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4589
@@ -0,0 +1,6 @@
+Enhancement: Support for Kiteworks servers without client secret
+
+Support for connecting to Kiteworks servers without requiring client secret has been added to the app.
+
+https://github.com/owncloud/android/issues/4588
+https://github.com/owncloud/android/pull/4589
diff --git a/changelog/4.6.0_2025-07-22/4594 b/changelog/4.6.0_2025-07-22/4594
new file mode 100644
index 000000000000..bad58f4f3302
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4594
@@ -0,0 +1,7 @@
+Bugfix: Crash from Google Play Console in PreviewImageFragment
+
+In order to prevent app crashes when file variable is null, a nullability check
+has been added in onPrepareOptionsMenu method from PreviewImageFragment
+
+https://github.com/owncloud/android/issues/4577
+https://github.com/owncloud/android/pull/4594
diff --git a/changelog/4.6.0_2025-07-22/4599 b/changelog/4.6.0_2025-07-22/4599
new file mode 100644
index 000000000000..255bd1a7f066
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4599
@@ -0,0 +1,6 @@
+Enhancement: SBOM (Software Bill of Materials)
+
+SBOM to be generated via GitHub Actions with the list of all dependencies used in the code, powered by cyclonedx. Finally, it is pushed to the repo's root folder if changes are detected.
+
+https://github.com/owncloud/android/issues/4598
+https://github.com/owncloud/android/pull/4641
diff --git a/changelog/4.6.0_2025-07-22/4600 b/changelog/4.6.0_2025-07-22/4600
new file mode 100644
index 000000000000..4f97e4cf8601
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4600
@@ -0,0 +1,7 @@
+Bugfix: Crash from Google Play Console in PreviewImagePagerAdapter
+
+In order to prevent app crashes, a validation has been added in onPageSelected method
+from PreviewImageActivity to ensure the image list contains items before using it.
+
+https://github.com/owncloud/android/issues/4596
+https://github.com/owncloud/android/pull/4600
diff --git a/changelog/4.6.0_2025-07-22/4602 b/changelog/4.6.0_2025-07-22/4602
new file mode 100644
index 000000000000..f73a0befc196
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4602
@@ -0,0 +1,7 @@
+Enhancement: Integration of instrumented tests in GitHub Actions
+
+A new workflow has been added to run instrumented tests in GitHub Actions
+in order to have a more consistent CI pipeline in the project.
+
+https://github.com/owncloud/android/issues/4595
+https://github.com/owncloud/android/pull/4602
diff --git a/changelog/4.6.0_2025-07-22/4608 b/changelog/4.6.0_2025-07-22/4608
new file mode 100644
index 000000000000..4578a4205431
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4608
@@ -0,0 +1,7 @@
+Enhancement: Polish UI and sync operations over Kiteworks servers
+
+The UI and navigation behaviour after performing a sync operation
+have been refined for accounts associated with Kiteworks servers.
+
+https://github.com/owncloud/android/issues/4591
+https://github.com/owncloud/android/pull/4608
diff --git a/changelog/4.6.0_2025-07-22/4632 b/changelog/4.6.0_2025-07-22/4632
new file mode 100644
index 000000000000..7ada92c2713f
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4632
@@ -0,0 +1,6 @@
+Enhancement: New set of configurations for Kiteworks servers
+
+A new set of configurations in setup file has been added to support the connection to Kiteworks servers.
+
+https://github.com/owncloud/android/issues/4622
+https://github.com/owncloud/android/pull/4632
diff --git a/changelog/4.6.0_2025-07-22/4637 b/changelog/4.6.0_2025-07-22/4637
new file mode 100644
index 000000000000..a59f38fd27f1
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4637
@@ -0,0 +1,6 @@
+Change: Update CI badges in README file
+
+Bitrise badges in README file have been replaced by GitHub Actions badges.
+
+https://github.com/owncloud/android/issues/4623
+https://github.com/owncloud/android/pull/4637
diff --git a/changelog/4.6.0_2025-07-22/4640 b/changelog/4.6.0_2025-07-22/4640
new file mode 100644
index 000000000000..ae1180ec91d8
--- /dev/null
+++ b/changelog/4.6.0_2025-07-22/4640
@@ -0,0 +1,7 @@
+Bugfix: Change space icon in file details view for personal space
+
+The space icon in the file details view has been replaced by the
+folder icon when the space that contains the file is personal.
+
+https://github.com/owncloud/android/issues/4624
+https://github.com/owncloud/android/pull/4640
diff --git a/changelog/4.6.1_2025-08-01/4649 b/changelog/4.6.1_2025-08-01/4649
new file mode 100644
index 000000000000..cd3ab1d8f3aa
--- /dev/null
+++ b/changelog/4.6.1_2025-08-01/4649
@@ -0,0 +1,7 @@
+Bugfix: Mini-fab for creating new document not always available
+
+The productName property has been made nullable to handle cases where it is not present
+in the JSON response, allowing the mini-fab for creating new documents to be properly shown.
+
+https://github.com/owncloud/enterprise/issues/7277
+https://github.com/owncloud/android/pull/4649
diff --git a/changelog/4.6.1_2025-08-01/4652 b/changelog/4.6.1_2025-08-01/4652
new file mode 100644
index 000000000000..00345667db31
--- /dev/null
+++ b/changelog/4.6.1_2025-08-01/4652
@@ -0,0 +1,7 @@
+Bugfix: Crash from Google Play Console in TransferListFragment
+
+In order to prevent app crashes, the account parameter from TransferListFragment has
+been removed and the newInstance method has been created to pass parameters safely.
+
+https://github.com/owncloud/android/issues/4650
+https://github.com/owncloud/android/pull/4652
diff --git a/changelog/4.6.1_2025-08-01/4654 b/changelog/4.6.1_2025-08-01/4654
new file mode 100644
index 000000000000..48b9f9bcbffd
--- /dev/null
+++ b/changelog/4.6.1_2025-08-01/4654
@@ -0,0 +1,7 @@
+Bugfix: Re-login not working properly when credentials expire
+
+The startActivityForResult method has been replaced with startActivity
+to make re-login work properly when credentials expire.
+
+https://github.com/owncloud/enterprise/issues/7277
+https://github.com/owncloud/android/pull/4654
diff --git a/changelog/4.6.2_2025-08-13/4657 b/changelog/4.6.2_2025-08-13/4657
new file mode 100644
index 000000000000..850620291747
--- /dev/null
+++ b/changelog/4.6.2_2025-08-13/4657
@@ -0,0 +1,6 @@
+Bugfix: Removed files in failed uploads
+
+Files have been prevented from being deleted from the source folder on upload retries by using the createdBy property.
+
+https://github.com/owncloud/android/issues/4653
+https://github.com/owncloud/android/pull/4657
diff --git a/changelog/4.7.0_2025-11-17/4661 b/changelog/4.7.0_2025-11-17/4661
new file mode 100644
index 000000000000..6435890365d1
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4661
@@ -0,0 +1,7 @@
+Enhancement: Add account ID to the user information
+
+The account ID has been added to the Account Manager only for Infinite Scale users.
+This information will be used to fetch all permissions related to space management.
+
+https://github.com/owncloud/android/issues/4605
+https://github.com/owncloud/android/pull/4661
diff --git a/changelog/4.7.0_2025-11-17/4664 b/changelog/4.7.0_2025-11-17/4664
new file mode 100644
index 000000000000..4cf7c618d5ab
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4664
@@ -0,0 +1,6 @@
+Change: SBOM workflow runs on dependabot PRs
+
+Changes to dependency versions will be managed within Dependabot PRs, so they can be updated together.
+
+https://github.com/owncloud/android/pull/4664
+https://github.com/owncloud/android/pull/4666
diff --git a/changelog/4.7.0_2025-11-17/4665 b/changelog/4.7.0_2025-11-17/4665
new file mode 100644
index 000000000000..5ef2754dbf0f
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4665
@@ -0,0 +1,7 @@
+Enhancement: New layout for spaces list
+
+A three-dot button has been added to each space card, allowing a bottom sheet dialog to
+be opened when clicked. In addition, the spaces layout has been changed to horizontal.
+
+https://github.com/owncloud/android/issues/4604
+https://github.com/owncloud/android/pull/4665
diff --git a/changelog/4.7.0_2025-11-17/4671 b/changelog/4.7.0_2025-11-17/4671
new file mode 100644
index 000000000000..350fedaba7b9
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4671
@@ -0,0 +1,7 @@
+Bugfix: Set authorization header for token request only when necessary
+
+A new field has been added to the token request to indicate whether the authorization
+header should be applied in case `client_secret_post` method is supported or not.
+
+https://github.com/owncloud/android/issues/4575
+https://github.com/owncloud/android/pull/4671
diff --git a/changelog/4.7.0_2025-11-17/4675 b/changelog/4.7.0_2025-11-17/4675
new file mode 100644
index 000000000000..708b47f6690e
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4675
@@ -0,0 +1,5 @@
+Bugfix: Add explicit permission to Detekt workflow
+
+Read permission has been added to the Detekt workflow. Without this explicit setting, the default permissions would apply, which unnecessarily include write access.
+
+https://github.com/owncloud/android/pull/4675
diff --git a/changelog/4.7.0_2025-11-17/4683 b/changelog/4.7.0_2025-11-17/4683
new file mode 100644
index 000000000000..605eafd840e7
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4683
@@ -0,0 +1,9 @@
+Enhancement: Create a new space
+
+A new floating action button has been added to the spaces list, visible only to users with right permissions.
+In addition, a new dialog for space creation has been implemented and, it is shown when the floating action button is tapped.
+
+https://github.com/owncloud/android/issues/4606
+https://github.com/owncloud/android/issues/4688
+https://github.com/owncloud/android/pull/4683
+https://github.com/owncloud/android/pull/4694
diff --git a/changelog/4.7.0_2025-11-17/4685 b/changelog/4.7.0_2025-11-17/4685
new file mode 100644
index 000000000000..b4755352896d
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4685
@@ -0,0 +1,6 @@
+Bugfix: Handling of HTTP code response 413
+
+A new exception has been added to handle cases where the payload exceeds the server limit (HTTP code 413)
+
+https://github.com/owncloud/android/issues/4094
+https://github.com/owncloud/android/pull/4685
diff --git a/changelog/4.7.0_2025-11-17/4687 b/changelog/4.7.0_2025-11-17/4687
new file mode 100644
index 000000000000..b81803da3603
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4687
@@ -0,0 +1,11 @@
+Enhancement: Edit a space
+
+A new edit space option has been added to the bottom sheet, available only to users
+with the required permissions when the three-dot menu button is tapped.
+
+https://github.com/owncloud/android/issues/4607
+https://github.com/owncloud/android/issues/4688
+https://github.com/owncloud/android/issues/4695
+https://github.com/owncloud/android/pull/4687
+https://github.com/owncloud/android/pull/4694
+https://github.com/owncloud/android/pull/4713
diff --git a/changelog/4.7.0_2025-11-17/4696 b/changelog/4.7.0_2025-11-17/4696
new file mode 100644
index 000000000000..885f287614b0
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4696
@@ -0,0 +1,7 @@
+Enhancement: Disable/Remove a space
+
+New options to disable, enable and remove spaces have been added to the bottom sheet, available only to users
+with the required permissions when the three-dot menu button is tapped.
+
+https://github.com/owncloud/android/issues/4611
+https://github.com/owncloud/android/pull/4696
diff --git a/changelog/4.7.0_2025-11-17/4698 b/changelog/4.7.0_2025-11-17/4698
new file mode 100644
index 000000000000..9ec0d4b1158c
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4698
@@ -0,0 +1,5 @@
+Enhancement: Add user role to spaces
+
+A new field representing the user role has been added to the space model and to the spaces table in the database.
+
+https://github.com/owncloud/android/pull/4698
diff --git a/changelog/4.7.0_2025-11-17/4704 b/changelog/4.7.0_2025-11-17/4704
new file mode 100644
index 000000000000..d2f2840e6c48
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4704
@@ -0,0 +1,7 @@
+Enhancement: Show space quota
+
+The used and total values of the space quota have been added to the bottom
+sheet dialog, displayed when the three-dot menu button is tapped.
+
+https://github.com/owncloud/android/issues/4693
+https://github.com/owncloud/android/pull/4704
diff --git a/changelog/4.7.0_2025-11-17/4705 b/changelog/4.7.0_2025-11-17/4705
new file mode 100644
index 000000000000..84baad63235b
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4705
@@ -0,0 +1,7 @@
+Enhancement: Update space image
+
+A new option to update the space image has been added to the bottom sheet, available
+only to users with the required permissions when the three-dot menu button is tapped.
+
+https://github.com/owncloud/android/issues/4691
+https://github.com/owncloud/android/pull/4705
diff --git a/changelog/4.7.0_2025-11-17/4712 b/changelog/4.7.0_2025-11-17/4712
new file mode 100644
index 000000000000..099798abcf53
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4712
@@ -0,0 +1,5 @@
+Enhancement: Cancellation policy for CI workflows
+
+A new cancellation policy has been set to CI workflows. Only one concurrent execution of every workflow in every branch.
+
+https://github.com/owncloud/android/pull/4712
diff --git a/changelog/4.7.0_2025-11-17/4714 b/changelog/4.7.0_2025-11-17/4714
new file mode 100644
index 000000000000..cfcf01a0bdfd
--- /dev/null
+++ b/changelog/4.7.0_2025-11-17/4714
@@ -0,0 +1,7 @@
+Enhancement: New setting to show disabled spaces
+
+A new setting has been added to the advanced section that allows space
+managers to show or hide disabled spaces in the spaces list.
+
+https://github.com/owncloud/android/issues/4711
+https://github.com/owncloud/android/pull/4714
diff --git a/changelog/4.8.0_2026-05-18/4708 b/changelog/4.8.0_2026-05-18/4708
new file mode 100644
index 000000000000..8a036669811e
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4708
@@ -0,0 +1,7 @@
+Enhancement: Set emoji as space image
+
+A new option to set an emoji as space image has been added to the bottom sheet, available
+only to users with the required permissions when the three-dot menu button is tapped.
+
+https://github.com/owncloud/android/issues/4707
+https://github.com/owncloud/android/pull/4708
diff --git a/changelog/4.8.0_2026-05-18/4722 b/changelog/4.8.0_2026-05-18/4722
new file mode 100644
index 000000000000..7fe78a4a3c75
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4722
@@ -0,0 +1,6 @@
+Change: Migrate tests to the new kotlinx-coroutines-test API
+
+Some tests from view model classes have been refactored in order to use the new kotlinx-coroutines-test API.
+
+https://github.com/owncloud/android/issues/4710
+https://github.com/owncloud/android/pull/4722
diff --git a/changelog/4.8.0_2026-05-18/4728 b/changelog/4.8.0_2026-05-18/4728
new file mode 100644
index 000000000000..e44d58ded3cc
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4728
@@ -0,0 +1,15 @@
+Enhancement: Show members of a space
+
+A new option to view all members of a space has been added to the bottom sheet, available when the three-dot menu button is tapped.
+This new option opens a new view that shows information like member names, roles, and expiration dates (if available).
+
+https://github.com/owncloud/android/issues/4612
+https://github.com/owncloud/android/issues/4763
+https://github.com/owncloud/android/issues/4772
+https://github.com/owncloud/android/issues/4782
+https://github.com/owncloud/android/pull/4728
+https://github.com/owncloud/android/pull/4765
+https://github.com/owncloud/android/pull/4779
+https://github.com/owncloud/android/pull/4784
+https://github.com/owncloud/android/pull/4809
+https://github.com/owncloud/android/pull/4841
diff --git a/changelog/4.8.0_2026-05-18/4744 b/changelog/4.8.0_2026-05-18/4744
new file mode 100644
index 000000000000..3c013bb17865
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4744
@@ -0,0 +1,5 @@
+Change: Increase rating dialog delay
+
+The time before the rating dialog appears has been increased to 7 days, in order to make it less intrusive for the user.
+
+https://github.com/owncloud/android/pull/4744
diff --git a/changelog/4.8.0_2026-05-18/4750 b/changelog/4.8.0_2026-05-18/4750
new file mode 100644
index 000000000000..c987f6d98b0c
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4750
@@ -0,0 +1,6 @@
+Bugfix: Edit space option available when there is no connection
+
+A new condition has been added to hide the edit space option when no network connection is available.
+
+https://github.com/owncloud/android/issues/4742
+https://github.com/owncloud/android/pull/4750
diff --git a/changelog/4.8.0_2026-05-18/4751 b/changelog/4.8.0_2026-05-18/4751
new file mode 100644
index 000000000000..d12b44415fcd
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4751
@@ -0,0 +1,5 @@
+Enhancement: Workflow to build APK
+
+A new workflow to build unsigned APKs has been added, using variant and version as inputs and generating the artifact and commit hash as outputs.
+
+https://github.com/owncloud/android/pull/4751
diff --git a/changelog/4.8.0_2026-05-18/4754 b/changelog/4.8.0_2026-05-18/4754
new file mode 100644
index 000000000000..cbf951683a7c
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4754
@@ -0,0 +1,11 @@
+Enhancement: Add a member to a space
+
+A new option to add members to a space has been added. It will be only visible for users with proper permissions.
+
+https://github.com/owncloud/android/issues/4613
+https://github.com/owncloud/android/issues/4740
+https://github.com/owncloud/android/issues/4770
+https://github.com/owncloud/android/pull/4754
+https://github.com/owncloud/android/pull/4764
+https://github.com/owncloud/android/pull/4777
+https://github.com/owncloud/android/pull/4847
diff --git a/changelog/4.8.0_2026-05-18/4759 b/changelog/4.8.0_2026-05-18/4759
new file mode 100644
index 000000000000..e6880b66c782
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4759
@@ -0,0 +1,5 @@
+Enhancement: Workflow to check Conventional Commits
+
+A new workflow has been added to check that commit names in a PR fits the Conventional Commits convention.
+
+https://github.com/owncloud/android/pull/4759
diff --git a/changelog/4.8.0_2026-05-18/4768 b/changelog/4.8.0_2026-05-18/4768
new file mode 100644
index 000000000000..4667a44717ae
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4768
@@ -0,0 +1,6 @@
+Enhancement: Remove a space member
+
+A new option to remove members from a space has been added. It will be only visible for users with proper permissions.
+
+https://github.com/owncloud/android/issues/4725
+https://github.com/owncloud/android/pull/4768
diff --git a/changelog/4.8.0_2026-05-18/4774 b/changelog/4.8.0_2026-05-18/4774
new file mode 100644
index 000000000000..6641421095b4
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4774
@@ -0,0 +1,6 @@
+Enhancement: Edit a space member
+
+A new option to edit members from a space has been added. It will be only visible for users with proper permissions.
+
+https://github.com/owncloud/android/issues/4724
+https://github.com/owncloud/android/pull/4774
diff --git a/changelog/4.8.0_2026-05-18/4776 b/changelog/4.8.0_2026-05-18/4776
new file mode 100644
index 000000000000..3e7f49ed12c3
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4776
@@ -0,0 +1,5 @@
+Enhancement: QA Content Provider
+
+A new content provider has been added to the QA variant in order to work with the sharing preferences with testing purposes.
+
+https://github.com/owncloud/android/pull/4776
diff --git a/changelog/4.8.0_2026-05-18/4778 b/changelog/4.8.0_2026-05-18/4778
new file mode 100644
index 000000000000..a2e4981efddf
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4778
@@ -0,0 +1,6 @@
+Enhancement: Copy permanent link of a space
+
+A new option to copy and share the permanent link of a space has been added next to the space header in the members section.
+
+https://github.com/owncloud/android/issues/4758
+https://github.com/owncloud/android/pull/4778
diff --git a/changelog/4.8.0_2026-05-18/4783 b/changelog/4.8.0_2026-05-18/4783
new file mode 100644
index 000000000000..3aa23cca02ca
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4783
@@ -0,0 +1,9 @@
+Enhancement: List links over a space
+
+The list of all available space public links has been added below the list of space members.
+This new section shows information like public link names, types and expiration date (if available).
+
+https://github.com/owncloud/android/issues/4752
+https://github.com/owncloud/android/pull/4783
+https://github.com/owncloud/android/pull/4834
+https://github.com/owncloud/android/pull/4845
diff --git a/changelog/4.8.0_2026-05-18/4785 b/changelog/4.8.0_2026-05-18/4785
new file mode 100644
index 000000000000..9ececc77e9f7
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4785
@@ -0,0 +1,6 @@
+Bugfix: Sort is not alphabetically correct
+
+Locale-aware collation has been applied to file sorting by name to handle accented characters correctly.
+
+https://github.com/owncloud/android/issues/4760
+https://github.com/owncloud/android/pull/4785
diff --git a/changelog/4.8.0_2026-05-18/4786 b/changelog/4.8.0_2026-05-18/4786
new file mode 100644
index 000000000000..1b6c3de7b012
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4786
@@ -0,0 +1,6 @@
+Bugfix: Option "Copy here" in spaces list after rotating device
+
+The "Copy here" button has been prevented from being displayed after device rotation in the spaces list.
+
+https://github.com/owncloud/android/issues/4178
+https://github.com/owncloud/android/pull/4786
diff --git a/changelog/4.8.0_2026-05-18/4790 b/changelog/4.8.0_2026-05-18/4790
new file mode 100644
index 000000000000..e433266e4caa
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4790
@@ -0,0 +1,6 @@
+Bugfix: Reduce space header size after rotating device
+
+The space header height has been reduced in landscape orientation.
+
+https://github.com/owncloud/android/issues/4625
+https://github.com/owncloud/android/pull/4790
diff --git a/changelog/4.8.0_2026-05-18/4794 b/changelog/4.8.0_2026-05-18/4794
new file mode 100644
index 000000000000..0bc88dde22db
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4794
@@ -0,0 +1,6 @@
+Enhancement: Add a public link over a space
+
+A new option to add public links over a space has been added. It will be only visible for users with proper permissions.
+
+https://github.com/owncloud/android/issues/4753
+https://github.com/owncloud/android/pull/4794
diff --git a/changelog/4.8.0_2026-05-18/4799 b/changelog/4.8.0_2026-05-18/4799
new file mode 100644
index 000000000000..6e1b4a5b0fac
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4799
@@ -0,0 +1,6 @@
+Change: Stream handling and double-close in UploadFileFromContentUriWorker
+
+Both the content resolver input stream and the file output stream from UploadFileFromContentUriWorker have been closed via use blocks, and the redundant double-close of the output stream has been removed.
+
+https://github.com/owncloud/android/issues/4798
+https://github.com/owncloud/android/pull/4799
diff --git a/changelog/4.8.0_2026-05-18/4802 b/changelog/4.8.0_2026-05-18/4802
new file mode 100644
index 000000000000..7cb096d68a4a
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4802
@@ -0,0 +1,6 @@
+Enhancement: Show destination folder snackbar for copy/move operations
+
+A snackbar message has been displayed after copy or move operations with an action button that allows users to quickly navigate to the destination folder.
+
+https://github.com/owncloud/android/issues/4379
+https://github.com/owncloud/android/pull/4802
diff --git a/changelog/4.8.0_2026-05-18/4804 b/changelog/4.8.0_2026-05-18/4804
new file mode 100644
index 000000000000..06062053788d
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4804
@@ -0,0 +1,5 @@
+Change: Update actions dependencies to be compliant with Node24 requirements
+
+Some dependencies and "uses" clauses in GitHub Actions workflows have been updated to newer versions that support Node24
+
+https://github.com/owncloud/android/pull/4804
diff --git a/changelog/4.8.0_2026-05-18/4806 b/changelog/4.8.0_2026-05-18/4806
new file mode 100644
index 000000000000..264b40e3e73c
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4806
@@ -0,0 +1,6 @@
+Change: Resource leak in CopyAndUploadContentUrisTask
+
+Input and output streams have been closed per URI iteration using try-with-resources to prevent file descriptor and memory leaks when copying multiple content URIs.
+
+https://github.com/owncloud/android/issues/4797
+https://github.com/owncloud/android/pull/4806
diff --git a/changelog/4.8.0_2026-05-18/4807 b/changelog/4.8.0_2026-05-18/4807
new file mode 100644
index 000000000000..6df2af2457ba
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4807
@@ -0,0 +1,5 @@
+Enhancement: Workflow to validate source strings
+
+A new workflow has been added to validate the source strings and prevent errors in translation CI system from the first step
+
+https://github.com/owncloud/android/pull/4807
diff --git a/changelog/4.8.0_2026-05-18/4808 b/changelog/4.8.0_2026-05-18/4808
new file mode 100644
index 000000000000..8551d745058e
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4808
@@ -0,0 +1,5 @@
+Bugfix: Conventional commits' action ignores SBOM automatic commits
+
+Conventional commits' GitHub Action has been modified to ignore commits generated by SBOM action because the leading capital.
+
+https://github.com/owncloud/android/pull/4808
diff --git a/changelog/4.8.0_2026-05-18/4814 b/changelog/4.8.0_2026-05-18/4814
new file mode 100644
index 000000000000..0dd8388ee10a
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4814
@@ -0,0 +1,7 @@
+Enhancement: Resource leaks in fragment view bindings
+
+View binding references have been cleared in onDestroyView() across fragments to
+prevent memory leaks when fragment instances outlive their views.
+
+https://github.com/owncloud/android/issues/4813
+https://github.com/owncloud/android/pull/4814
diff --git a/changelog/4.8.0_2026-05-18/4815 b/changelog/4.8.0_2026-05-18/4815
new file mode 100644
index 000000000000..3765850aa3d3
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4815
@@ -0,0 +1,5 @@
+Change: Actions with SHA commits instead of versions
+
+Commit SHA references have been added to GitHub Actions workflows to be compliant with new security policies
+
+https://github.com/owncloud/android/pull/4815
diff --git a/changelog/4.8.0_2026-05-18/4816 b/changelog/4.8.0_2026-05-18/4816
new file mode 100644
index 000000000000..26b0a5c59c27
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4816
@@ -0,0 +1,6 @@
+Enhancement: Remove space link
+
+A new option to remove space public links from a space has been added. It will be only visible for users with proper permissions.
+
+https://github.com/owncloud/android/issues/4757
+https://github.com/owncloud/android/pull/4816
diff --git a/changelog/4.8.0_2026-05-18/4817 b/changelog/4.8.0_2026-05-18/4817
new file mode 100644
index 000000000000..be6ed5b3e0a3
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4817
@@ -0,0 +1,6 @@
+Enhancement: Edit space link
+
+A new option to edit space public links has been added. It will be only visible for users with proper permissions.
+
+https://github.com/owncloud/android/issues/4756
+https://github.com/owncloud/android/pull/4817
diff --git a/changelog/4.8.0_2026-05-18/4818 b/changelog/4.8.0_2026-05-18/4818
new file mode 100644
index 000000000000..7b5ce37e8844
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4818
@@ -0,0 +1,7 @@
+Enhancement: Workflow to use the translation-sync reusable workflow
+
+A new workflow that uses the translation-sync reusable workflow has been added in order to update translations from transifex
+
+https://github.com/owncloud/android/pull/4818
+https://github.com/owncloud/android/pull/4831
+https://github.com/owncloud/android/pull/4846
diff --git a/changelog/4.8.0_2026-05-18/4824 b/changelog/4.8.0_2026-05-18/4824
new file mode 100644
index 000000000000..86b755254540
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4824
@@ -0,0 +1,6 @@
+Change: Replace PATs for tokens generated by a GitHub app
+
+PATs have been replaced by tokens generated by a GitHub app in order to not depend on personal setup
+
+https://github.com/owncloud/android/pull/4824
+https://github.com/owncloud/android/pull/4830
diff --git a/changelog/4.8.0_2026-05-18/4825 b/changelog/4.8.0_2026-05-18/4825
new file mode 100644
index 000000000000..d6910fc4b11a
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4825
@@ -0,0 +1,8 @@
+Change: Use reusable workflow for Calens
+
+A new reusable workflow for calens changelog changes has been added to the current workflow in order to be used by all clients
+
+https://github.com/owncloud/android/pull/4825
+https://github.com/owncloud/android/pull/4826
+https://github.com/owncloud/android/pull/4829
+https://github.com/owncloud/android/pull/4853
diff --git a/changelog/4.8.0_2026-05-18/4832 b/changelog/4.8.0_2026-05-18/4832
new file mode 100644
index 000000000000..40fcc751a5a8
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4832
@@ -0,0 +1,5 @@
+Change: Use reusable workflow for Conventional commits
+
+A new reusable workflow to validate Conventional commits has been added to the current workflow in order to be used by all clients
+
+https://github.com/owncloud/android/pull/4832
diff --git a/changelog/4.8.0_2026-05-18/4836 b/changelog/4.8.0_2026-05-18/4836
new file mode 100644
index 000000000000..891a99a9fb01
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4836
@@ -0,0 +1,7 @@
+Enhancement: Pull to refresh members and links
+
+A pull to refresh has been added to the space members and links lists
+in order to refresh the data and permissions with a simple gesture.
+
+https://github.com/owncloud/android/issues/4773
+https://github.com/owncloud/android/pull/4836
diff --git a/changelog/4.8.0_2026-05-18/4837 b/changelog/4.8.0_2026-05-18/4837
new file mode 100644
index 000000000000..a08d598cf502
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4837
@@ -0,0 +1,5 @@
+Change: SBOM to be updated in a separate branch with signed commits
+
+SBOM workflow in GitHub Actions has been modified to accomplish security policies, assuring that commits are verified and pushing them to a specific branch
+
+https://github.com/owncloud/android/pull/4837
diff --git a/changelog/4.8.0_2026-05-18/4849 b/changelog/4.8.0_2026-05-18/4849
new file mode 100644
index 000000000000..04efaa9f21c2
--- /dev/null
+++ b/changelog/4.8.0_2026-05-18/4849
@@ -0,0 +1,5 @@
+Change: Weekly dependabot updates
+
+Dependabot workflow in GitHub Actions has been modified to be triggered weekly with maximum 5 PRs to be created
+
+https://github.com/owncloud/android/pull/4849
diff --git a/changelog/4.8.1_2026-06-09/4861 b/changelog/4.8.1_2026-06-09/4861
new file mode 100644
index 000000000000..8bd429bf3b90
--- /dev/null
+++ b/changelog/4.8.1_2026-06-09/4861
@@ -0,0 +1,6 @@
+Security: SSL certificate verification and trusted host handling
+
+SSL certificate verification flow has been improved by enhancing
+host validation and trusted certificate handling.
+
+https://github.com/owncloud/android/pull/4861
diff --git a/changelog/4.8.1_2026-06-09/4862 b/changelog/4.8.1_2026-06-09/4862
new file mode 100644
index 000000000000..28f9e58515b9
--- /dev/null
+++ b/changelog/4.8.1_2026-06-09/4862
@@ -0,0 +1,6 @@
+Change: Bump target SDK to 36
+
+Target SDK has been upgraded to 36 in order to fulfill Android platform requirements.
+
+https://github.com/owncloud/android/issues/4743
+https://github.com/owncloud/android/pull/4862
diff --git a/changelog/4.8.1_2026-06-09/4863 b/changelog/4.8.1_2026-06-09/4863
new file mode 100644
index 000000000000..2a9a3c643057
--- /dev/null
+++ b/changelog/4.8.1_2026-06-09/4863
@@ -0,0 +1,5 @@
+Enhancement: Add gradle ecosystem to dependabot
+
+Dependabot workflow in GitHub Actions has been modified to update gradle dependencies weekly
+
+https://github.com/owncloud/android/pull/4863
diff --git a/changelog/4.8.1_2026-06-09/4869 b/changelog/4.8.1_2026-06-09/4869
new file mode 100644
index 000000000000..332a236053b1
--- /dev/null
+++ b/changelog/4.8.1_2026-06-09/4869
@@ -0,0 +1,5 @@
+Enhancement: Remove unused resources
+
+Useless strings and resources in the project have been removed in order to keep the codebase cleaner
+
+https://github.com/owncloud/android/pull/4869
diff --git a/changelog/4.8.1_2026-06-09/4871 b/changelog/4.8.1_2026-06-09/4871
new file mode 100644
index 000000000000..fea65f1f46db
--- /dev/null
+++ b/changelog/4.8.1_2026-06-09/4871
@@ -0,0 +1,6 @@
+Enhancement: Passcode and pattern screens in landscape mode
+
+Orientation restrictions defined in the manifest have been removed, and support for both
+orientations (portrait and landscape) has been added to the passcode and pattern screens.
+
+https://github.com/owncloud/android/pull/4871
diff --git a/changelog/4.8.1_2026-06-09/4872 b/changelog/4.8.1_2026-06-09/4872
new file mode 100644
index 000000000000..841145d26809
--- /dev/null
+++ b/changelog/4.8.1_2026-06-09/4872
@@ -0,0 +1,5 @@
+Change: Remove unicode characters from strings file
+
+Unicode characters in strings.xml file have been replaced by the character itself to prevent problems in translations
+
+https://github.com/owncloud/android/pull/4872
diff --git a/changelog/4.8.1_2026-06-09/4877 b/changelog/4.8.1_2026-06-09/4877
new file mode 100644
index 000000000000..2cd8fb67edcc
--- /dev/null
+++ b/changelog/4.8.1_2026-06-09/4877
@@ -0,0 +1,7 @@
+Enhancement: Spaces icon color brandable
+
+The color of the generic space icon has been replaced with a brandable
+color to provide a consistent design across branded variants.
+
+https://github.com/owncloud/android/issues/4860
+https://github.com/owncloud/android/pull/4877
diff --git a/changelog/4.8.1_2026-06-09/4878 b/changelog/4.8.1_2026-06-09/4878
new file mode 100644
index 000000000000..90d3075dcb7e
--- /dev/null
+++ b/changelog/4.8.1_2026-06-09/4878
@@ -0,0 +1,7 @@
+Bugfix: Crash from Google Play Console in ReceiveExternalFilesActivity
+
+In order to prevent crashes, a nullability check has been added before
+refreshing a not found folder while receiving external files.
+
+https://github.com/owncloud/android/issues/4738
+https://github.com/owncloud/android/pull/4878
diff --git a/changelog/4.8.1_2026-06-09/4879 b/changelog/4.8.1_2026-06-09/4879
new file mode 100644
index 000000000000..04ad4ce15cd8
--- /dev/null
+++ b/changelog/4.8.1_2026-06-09/4879
@@ -0,0 +1,7 @@
+Bugfix: Crash from Google Play Console in FileDetailsFragment
+
+To prevent crashes, the navigation logic when navigating back has been
+improved, especially in cases where the observed file is null.
+
+https://github.com/owncloud/android/issues/4737
+https://github.com/owncloud/android/pull/4879
diff --git a/changelog/4.8.2_2026-07-01/4887 b/changelog/4.8.2_2026-07-01/4887
new file mode 100644
index 000000000000..2a1035694f68
--- /dev/null
+++ b/changelog/4.8.2_2026-07-01/4887
@@ -0,0 +1,7 @@
+Bugfix: Infinite edges
+
+All screens have been adapted to the edge-to-edge feature (starting from
+Android 15) and are no longer cut off by the system and navigation bars
+
+https://github.com/owncloud/android/issues/4576
+https://github.com/owncloud/android/pull/4887
diff --git a/changelog/4.8.2_2026-07-01/4897 b/changelog/4.8.2_2026-07-01/4897
new file mode 100644
index 000000000000..d86f0f79e395
--- /dev/null
+++ b/changelog/4.8.2_2026-07-01/4897
@@ -0,0 +1,6 @@
+Security: Redact cookie headers in logs
+
+Cookie headers have been redacted in the logs depending on the app
+configuration, in order to prevent sensitive information leaks.
+
+https://github.com/owncloud/android/pull/4897
diff --git a/changelog/4.8.2_2026-07-01/4901 b/changelog/4.8.2_2026-07-01/4901
new file mode 100644
index 000000000000..8369b490c5ad
--- /dev/null
+++ b/changelog/4.8.2_2026-07-01/4901
@@ -0,0 +1,5 @@
+Bugfix: Remove useless debug manifest file
+
+Manifest file for debug builds has been removed because it was not used, in order to fix a lint warning
+
+https://github.com/owncloud/android/pull/4901
diff --git a/changelog/4.8.3_2026-07-21/4931 b/changelog/4.8.3_2026-07-21/4931
new file mode 100644
index 000000000000..6efab13cfb3d
--- /dev/null
+++ b/changelog/4.8.3_2026-07-21/4931
@@ -0,0 +1,5 @@
+Bugfix: Help URL incorrect
+
+Help URL has been fixed because it returned "Page not found"
+
+https://github.com/owncloud/android/pull/4931
diff --git a/changelog/4.8.4_2026-08-31/4963 b/changelog/4.8.4_2026-08-31/4963
new file mode 100644
index 000000000000..728de185872e
--- /dev/null
+++ b/changelog/4.8.4_2026-08-31/4963
@@ -0,0 +1,7 @@
+Security: Prevent bypassing passcode, pattern and biometric lock
+
+The back gesture has been prevented from dismissing the lock screen
+without validating the passcode, pattern or biometric lock.
+
+https://github.com/owncloud/android/issues/4958
+https://github.com/owncloud/android/pull/4963
diff --git a/changelog/CHANGELOG.tmpl b/changelog/CHANGELOG.tmpl
new file mode 100644
index 000000000000..352139319b97
--- /dev/null
+++ b/changelog/CHANGELOG.tmpl
@@ -0,0 +1,574 @@
+# Table of Contents
+
+{{ range . -}}
+ * [Changelog for {{ .Version }}](#changelog-for-owncloud-android-client-{{ .Version | replace "." ""}}-{{ .Date | lower -}})
+{{ end -}}
+ * [Changelog for 2.17 versions and below](#changelog-for-217-versions-and-below)
+{{ $allVersions := . }}
+{{- range $index, $changes := . }}{{ with $changes -}}
+# Changelog for ownCloud Android Client [{{ .Version }}] ({{ .Date }})
+
+The following sections list the changes in ownCloud Android Client {{ .Version }} relevant to
+ownCloud admins and users.
+
+{{/* creating version compare links */ -}}
+{{ $next := add1 $index -}}
+{{ if ne (len $allVersions) $next -}}
+{{ $previousVersion := (index $allVersions $next).Version -}}
+{{ if eq .Version "unreleased" -}}
+[{{ .Version }}]: https://github.com/owncloud/android/compare/v{{ $previousVersion }}...master
+
+{{- else -}}
+[{{ .Version }}]: https://github.com/owncloud/android/compare/v{{ $previousVersion }}...v{{ .Version }}
+{{- end -}}
+{{ end -}}
+
+{{- /* last version managed by calens, end of the loop */ -}}
+{{ if eq .Version "2.16" -}}
+[{{ .Version }}]: https://github.com/owncloud/android/compare/v2.16...v{{ .Version }}
+{{- end }}
+
+## Summary
+{{ range $entry := .Entries }}{{ with $entry }}
+* {{ .Type }} - {{ .Title }}: [#{{ .PrimaryID }}]({{ .PrimaryURL }})
+{{- end }}{{ end }}
+
+## Details
+{{ range $entry := .Entries }}{{ with $entry }}
+* {{ .Type }} - {{ .Title }}: [#{{ .PrimaryID }}]({{ .PrimaryURL }})
+{{ range $par := .Paragraphs }}
+ {{ wrapIndent $par 80 3 }}
+{{ end -}}
+{{ range $url := .IssueURLs }}
+ {{ $url -}}
+{{ end -}}
+{{ range $url := .PRURLs }}
+ {{ $url -}}
+{{ end -}}
+{{ range $url := .OtherURLs }}
+ {{ $url -}}
+{{ end }}
+{{ end }}{{ end }}
+{{ end }}{{ end -}}
+
+{{/* Start of old changelog */ -}}
+# Changelog for 2.17 versions and below
+
+## 2.17 (March 2021)
+- Toolbar redesign
+- Show thumbnails for every supported file type
+- Fix 301 redirections
+- Fix a crash related to pictures preview
+- Fix two bugs when sharing files with ownCloud
+- Improvements in OAuth2, including
+ + Fix a crash when migrating from OAuth2 to OIDC
+ + Fix a crash when disabling OAuth2
+ + Fix a bug where token was not refreshed properly
+ + Log authentication requests
+ + Support OIDC Dynamic Client Registration
+
+## 2.17 beta v1 (March 2021)
+- Toolbar redesign
+- Show thumbnails for every supported file type
+- Fix 301 redirections
+- Fix a crash related to pictures preview
+- Fix a bug when sharing files with ownCloud
+- Improvements in OAuth2, including
+ + Fix a crash when migrating from OAuth2 to OIDC
+ + Fix a crash when disabling OAuth2
+ + Fix a bug where token was not refreshed properly
+ + Log authentication requests
+ + Support OIDC Dynamic Client Registration
+
+## 2.16.0 (January 2021)
+- Native Android ShareSheet
+- Option to log HTTP requests and responses
+- Move sort menu from toolbar to files view
+- Update background images
+- Search when sharing with ownCloud
+- Bug fixes, including:
+ + Fix a crash while accessing a WebDAV folder
+ + Fix some crashes when rotating the device
+ + Fix a glitch where image was not refreshed properly
+ + Fix some issues when using OCIS
+
+## 2.15.3 (October 2020)
+- Bug fixes, including:
+ + Fix a crash related to downloads notifications
+ + Potential fix for ANR when retrying camera uploads
+ + Removal of legacy header http.protocol.single-cookie-header
+
+## 2.15.2 (September 2020)
+- Update logcat library
+- Bug fixes, including:
+ + Fixed a crash when browsing up
+ + Fixed a crash when logging camera upload request
+ + Fixed a crash related with available offline files
+ + Fixed a crash related with database migration
+
+## 2.15.1 (July 2020)
+- Android 10: TLS 1.3 supported
+- Update network libraries to more recent versions, OkHttp + dav4jvm (old dav4Android)
+- Rearchitecture of avatar and quota features
+- Bug fixes, including:
+ + Fixed some authentication problems regarding password edition
+ + Fixed available offline bad behaviour when the amount of files is huge
+ + Fixed a crash related with FileDataStorageManager
+ + Fixed problem related with server setting `version.hide` to allow users login if such setting is enabled.
+
+## 2.15 (June 2020)
+- Login rearchitecture
+- Support for OpenId Connect
+- Native biometrical lock
+- UI improvements, including:
+ + New bottom navigation bar
+- Support for usernames with '+' (Available since oC 10.4.1)
+- Chunking adaption to oCIS
+- End of support for Android KitKat (4.4)
+- End of support for servers older than 10 version
+- Bug fixes, including:
+ + Fix crash when changing orientation in some operations
+ + Fix OAuth2 token is not renewed after being revoked
+ + Fix occasional crash when opening share by link
+ + Fix navigation loop in shared by link and Av. Offline options
+
+## 2.15 beta v2 (May 2020)
+- Login rearchitecture
+- Support for OpenId Connect
+- Native biometrical lock
+- UI improvements, including:
+ + New bottom navigation bar
+- Support for usernames with '+' (Available since oC 10.4.1)
+- Chunking adaption to oCIS
+- End of support for Android KitKat (4.4)
+- End of support for servers older than 10 version
+- Bug fixes, including:
+ + Fix crash when changing orientation in some operations
+ + Fix OAuth2 token is not renewed after being revoked
+
+## 2.15 beta v1 (May 2020)
+- Login rearchitecture
+- Support for OpenId Connect
+- Native biometrical lock
+- UI improvements, including:
+ + New bottom navigation bar
+- Support for usernames with '+' (Available since oC 10.4.1)
+- End of support for Android KitKat (4.4)
+- End of support for servers older than 10 version
+- Bug fixes, including:
+ + Fix crash when changing orientation in some operations
+ + Fix OAuth2 token is not renewed after being revoked
+
+## 2.14.2 (January 2020)
+- Fix crash triggered when trying to connect to server secured with self signed certificate
+
+## 2.14.1 (December 2019)
+- Some improvements in wizard
+
+## 2.14 (December 2019)
+- Splash screen
+- Shortcut to shared by link files from side menu (contribution)
+- Use new server parameter to set a minimum number of characters for searching users, groups or federated shares
+- End of support for SAML authentication.
+- UI improvements, including:
+ + Mix files and folders when sorting them by date (contribution) or size
+ + Redesign logs view with new tabs, filters and share options (contribution)
+ + Resize cloud image in side menu to not overlap the new side menu options
+- Bug fixes, including:
+ + Avoid overwritten files with the same name during copy or move operations
+ + Retry camera uploads when recovering wifi connectivity and "Upload with wifi only" option is enabled
+
+## 2.13.1 (October 2019)
+- Improve oAuth user experience flow and wording when token expires or becomes invalid
+
+## 2.13 (September 2019)
+- Copy and move files from other third-party apps or internal storage to an ownCloud account through Downloads or Files app
+- Save files in an ownCloud account from third-party apps
+- Copy and move files within the same ownCloud account through Downloads or Files app
+- Add more logs coverage to gather information about known but difficult to reproduce issues
+- UI improvements, including:
+ + Show date and size for every file in Available Offline option from side menu
+
+## 2.12 (August 2019)
+- Shares rearchitecture
+- UI improvements, including:
+ + Private link accessible when share API is disabled
+- Bug fixes, including:
+ + Fix images not detected in Android 9 gallery after being downloaded
+
+## 2.12 beta v1 (August 2019)
+- Shares rearchitecture
+- UI improvements, including:
+ + Private link accessible when share API is disabled
+- Bug fixes, including:
+ + Fix images not detected in Android 9 gallery after being downloaded
+
+## 2.11.1 (June 2019)
+- Fix crash triggered when notifying upload results
+
+## 2.11 (June 2019)
+- Replace ownCloud file picker with the Android native one when uploading files (contribution)
+- Send logs to support, enable it via new developer menu (contribution)
+- Logs search (contribution)
+- Shortcut to available offline files from side menu
+- Document provider: files and folders rename, edition and deletion.
+- Document provider: folder creation
+- Document provider: multiaccount support
+- UI improvements, including:
+ + Notch support
+ + Batched permission errors when deleting multiple files (contribution)
+- Bug fixes, including:
+ + Fix just created folder disappears when synchronizing parent folder
+ + Fix crash when clearing successful/failed uploads (contribution)
+ + Fix download progress bar still visible after successful download
+ + Fix UI glitch in warning icon when sharing a file publicly (contribution)
+ + Fix crash when sharing files with ownCloud and creating new folder (contribution)
+ + Fix canceling dialog in settings turns on setting (contribution)
+ + Bring back select all and select inverse icons to the app bar (contribution)
+ + Fix folder with brackets [ ] does not show the content
+ + Fix login fails with "§" in password
+
+## 2.11 beta v1 (May 2019)
+- Send logs to support, enable it via new developer menu (contribution)
+- Logs search (contribution)
+- Shortcut to available offline files from side menu
+- Document provider: files and folders rename, edition and deletion.
+- Document provider: folder creation
+- Document provider: multiaccount support
+- UI improvements, including:
+ + Notch support
+- Bug fixes, including:
+ + Fix download progress bar still visible after successful download
+ + Fix UI glitch in warning icon when sharing a file publicly (contribution)
+ + Fix crash when sharing files with ownCloud and creating new folder (contribution)
+ + Fix canceling dialog in settings turns on setting (contribution)
+ + Bring back select all and select inverse icons to the app bar (contribution)
+ + Fix folder with brackets [ ] does not show the content
+ + Fix login fails with "§" in password
+
+## 2.10.1 (April 2019)
+- Content provider improvements
+
+## 2.10.0 (March 2019)
+- Android 9 (P) support (contribution)
+- Allow light filtering apps (optional)
+- Show additional info (user ID, email) when sharing with users with same display name
+- Support more options to enforce password when sharing publicly
+- Select all and inverse when uploading files (contribution)
+- Sorting options in sharing view (contribution)
+- Batched notifications for file deletions (contribution)
+- Commit hash in settings (contribution)
+- UI improvements, including:
+ + Disable log in button when credentials are empty (contribution)
+ + Warning to properly set camera folder in camera uploads
+- Bug fixes, including:
+ + Some camera upload issues in Android 9 (P) (contribution)
+ + Fix eye icon not visible to show/hide password in public shares (contribution)
+ + Fix welcome wizard rotation (contribution)
+
+## 2.10.0 beta v1 (February 2019)
+- Android 9 (P) support (contribution)
+- Select all and inverse when uploading files (contribution)
+- Sorting options in sharing view (contribution)
+- Batched notifications for file deletions (contribution)
+- Commit hash in settings (contribution)
+- UI improvements, including:
+ + Disable log in button when credentials are empty (contribution)
+ + Warning to properly set camera folder in camera uploads
+- Bug fixes, including:
+ + Some camera upload issues in Android 9 (P) (contribution)
+ + Fix eye icon not visible to show/hide password in public shares (contribution)
+ + Fix welcome wizard rotation (contribution)
+
+## 2.9.3 (November 2018)
+- Bug fixes for users with username containing @ character
+
+## 2.9.2 (November 2018)
+- Bug fixes for users with username containing spaces
+
+## 2.9.1 (November 2018)
+- Bug fixes for LDAP users using uid:
+ + Fix login not working
+ + Fix empty list of files
+
+## 2.9.0 (November 2018)
+- Search in current folder
+- Select all/inverse files (contribution)
+- Improve available offline files synchronization and conflict resolution (Android 5 or higher required)
+- Sort files in file picker when uploading (contribution)
+- Access ownCloud files from files apps, even with files not downloaded
+- New login view
+- Show re-shares
+- Switch apache and jackrabbit deprecated network libraries to more modern and active library, OkHttp + Dav4Android
+- UI improvements, including:
+ + Change edit share icon
+ + New gradient in top of the list of files (contribution)
+ + More accurate message when creating folders with the same name (contribution)
+- Bug fixes, including:
+ + Fix some crashes:
+ - When rebooting the device
+ - When copying, moving files or choosing a folder within camera uploads feature
+ - When creating private/public link
+ + Fix some failing downloads
+ + Fix pattern lock being asked very often after disabling fingerprint lock (contribution)
+
+## 2.9.0 beta v2 (October 2018)
+- Bug fixes, including:
+ + Fix some crashes:
+ - When rebooting the device
+ - When copying, moving files or choosing a folder within camera uploads feature
+ + Fix some failing downloads
+ + Fix pattern lock being asked very often after disabling fingerprint lock
+
+## 2.9.0 beta v1 (September 2018)
+- Switch apache and jackrabbit deprecated libraries to more modern and active library, OkHttp
+- Search in current folder
+- Select all/inverse files
+- New login view
+- Show re-shares
+- UI improvements, including:
+ + Change edit share icon
+ + New gradient in top of the list of files
+
+## 2.8.0 (July 2018)
+- Side menu redesign
+- User quota in side menu
+- Descending option when sorting
+- New downloaded/offline icons and pins
+- One panel design for tablets
+- Custom tabs for OAuth
+- Improve public link sharing permissions for folders
+- Redirect to login view when SAML session expires
+- UI improvements, including:
+ + Fab button above snackbar
+ + Toggle to control password visibility when sharing via link
+ + Adaptive icons support (Android 8 required)
+- Bug fixes, including:
+ + Fix block for deleted basic/oauth accounts
+ + Fix available offline when renaming files
+ + Fix camera directory not selectable in root
+ + Fix guest account showing an empty file list
+ + Hide keyboard when going back from select user view
+ + Fix black "downloading screen" message when downloading an image offline
+ + Show proper timestamp in uploads/downloads notification
+ + Fix sharing when disabling files versioning app in server
+
+## 2.8.0 beta v1 (May 2018)
+- Side menu redesign
+- User quota in side menu
+- Descending option when sorting
+- New downloaded/offline icons and pins
+- One panel design for tablets
+- Custom tabs for OAuth
+- UI improvements, including:
+ + Fab button above snackbar
+ + Toggle to control password visibility when sharing via link
+- Bug fixes, including:
+ + Fix block for deleted basic/oauth accounts
+ + Fix available offline when renaming files
+ + Fix camera directory not selectable in root
+ + Fix guest account showing an empty file list
+ + Hide keyboard when going back from select user view
+ + Fix black "downloading screen" message when downloading an image offline.
+
+## 2.7.0 (April 2018)
+- Fingerprint lock
+- Pattern lock (contribution)
+- Upload picture directly from camera (contribution)
+- GIF support
+- New features wizard
+- UI improvements, including:
+ + Display file size during upload (contribution)
+ + Animations when switching folders
+- Bug fixes, including:
+ + Hide always visible notification in Android 8
+
+## 2.7.0 beta v1 (March 2018)
+- Fingerprint lock
+- Pattern lock (contribution)
+- Upload picture directly from camera (contribution)
+- GIF support
+- New features wizard
+- UI improvements, including:
+ + Display file size during upload (contribution)
+- Bug fixes, including:
+ + Hide always visible notification in Android 8
+
+## 2.6.0 (February 2018)
+- Camera uploads, replacing instant uploads (Android 5 or higher required)
+- Android 8 support
+- Notification channels (Android 8 required)
+- Private link (OC X required)
+- Fixed typos in some translations
+
+## 2.5.1 beta v1 (November 2017)
+- Camera uploads (replacing instant uploads)
+- Android O support
+- Notification channels (Android O required)
+- Private link (OC X required)
+- Fixed typos in some translations
+
+## 2.5.0 (October 2017)
+- OAuth2 support
+- Show file listing option (anonymous upload) when sharing a folder (OC X required)
+- First approach to fix instant uploads
+- UI improvements, including:
+ + Hide share icon when resharing is forbidden
+ + Improve feedback when uploading infected files
+- Bug fixes
+
+## 2.4.0 (May 2017)
+- Video streaming
+- Multiple public links per file (OC X required)
+- Share with custom groups (OC X required)
+- Automated retry of failed transfers in Android 6 and 7
+- Save shared text as new file
+- File count per section in uploads view
+- UI improvements, including:
+ + Share view update
+- Bug fixes
+
+## 2.3.0 (March 2017)
+- Included privacy policy.
+- Error messages improvement.
+- Design/UI improvement: snackbars replace toasts.
+- Bugs fixed, including:
+ + Crash when other app uses same account name.
+
+## 2.2.0 (December 2016)
+- Set folders as Available Offline
+- New navigation drawer, with avatar and account switch.
+- New account manager, accessible from navigation drawer.
+- Set edit permissions in federated shares of folders (OC server >= 9.1)
+- Monitor and revoke session from web UI (OC server >= 9.1)
+- Improved look and contents of file menu.
+- Bugs fixed, including:
+ + Keep modification time of uploaded files.
+ + Stop audio when file is deleted.
+ + Upload of big files.
+
+## 2.1.2 (September 2016)
+- Instant uploads fixed in Android 6.
+
+## 2.1.1 (September 2016)
+- Instant uploads work in Android 7.
+- Select your camera folder to upload pictures or videos from any
+ camera app.
+- Multi-Window support for Android 7.
+- Size of folders shown in list of files.
+- Sort by size your list of files.
+
+## 2.1.0 (August 2016)
+- Select and handle multiple files
+- Sync files on tap
+- Access files through Documents Provider
+- "Can share" option for federated shares (server 9.1+)
+- Full name shown instead of user name
+- New icon
+- Style and sorting fixes
+- Bugs fixed, including:
+ + Icon "available offline" shown when set
+ + Trim blanks of username in login view
+ + Protect password field from suggestions
+
+## 2.0.1 (June 2016)
+- Favorite files are now called AVAILABLE OFFLINE
+- New overlay icons
+- Bugs fixed, including:
+ + Upload content from other apps works again
+ + Passwords with non-alphanumeric characters work fine
+ + Sending files from other apps does not duplicate them
+ + Favorite setting is not lost after uploading
+ + Instant uploads waiting for Wi-Fi are not shown as failed
+
+## 2.0.0 (April 2016)
+- Uploads view: track the progress of your uploads and handle failures
+- Federated sharing: share files with users in other ownCloud servers
+- Improvements on the UI following material design lines
+- Set a shared-by-link folder as editable
+- Wifi-only for instant uploads stop on Wifi loss
+- Be warned of server certificate changed in any action
+- Improvements when other apps send files to ownCloud
+- Bug fixing
+
+## 1.9.1 (February 2016)
+- Set and edit permissions on internal shared data
+- Instant uploads: avoid file duplications, set policy in app settings
+- Control duplication of files uploaded via 'Upload' button
+- Select view mode: either list or grid per folder
+- More Material Design: buttons and checkboxes
+- Fixed battery drain in automatic synchronization
+- Security fixes related to passcode
+- Wording fixes
+
+## 1.9.0 (December 2015)
+- Share privately with users or groups in your server
+- Share link with password protection and expiration date
+- Fully sync a folder in two ways (manually)
+- Detect share configuration in server
+- Fingerprints in untrusted certificate dialog
+- Thumbnail in details view
+- OC color in notifications
+- Fixed video preview
+- Fixed sorting with accents
+- Error shown when no app can "open with" a file
+- Fixed relative date in some languages
+- Media scanner triggered after uploads
+
+## 1.8.0 (September 2015)
+- New MATERIAL DESIGN theme
+- Updated FILE TYPE ICONS
+- Preview TXT files within the app
+- COPY files & folders
+- Preview the full file/folder name from the long press menu
+- Set a file as FAVORITE (kept-in-sync) from the CONTEXT MENU
+- Updated CONFLICT RESOLUTION dialog (wording)
+- Updated background for images with TRANSPARENCY in GALLERY
+- Hidden files will not enforce list view instead of GRID VIEW (folders from Picasa & others)
+- Security:
+ + Updated network stack with security fixes (Jackrabbit 2.10.1)
+- Bugs fixed:
+ + Fixed crash when ETag is lost
+ + Passcode creation not restarted on device rotation
+ + Recovered share icon shown on folders 'shared with me'
+ + User name added to subject when sending a share link through e-mail (fixed on SAMLed apps)
+
+## 1.7.2 (July 2015)
+- New navigation drawer
+- Improved Passcode
+- Automatic grid view just for folders full of images
+- More characters allowed in file names
+- Support for servers in same domain, different path
+- Bugs fixed:
+ + Frequent crashes in folder with several images
+ + Sync error in servers with huge quota and external storage enable
+ + Share by link error
+ + Some other crashes and minor bugs
+
+## 1.7.1 (April 2015)
+
+- Share link even with password enforced by server
+- Get the app ready for oc 8.1 servers
+- Added option to create new folder in uploads from external apps
+- Improved management of deleted users
+- Bugs fixed
+ + Fixed crash on Android 2.x devices
+ + Improvements on uploads
+
+## 1.7.0 (February 2015)
+
+- Download full folders
+- Grid view for images
+- Remote thumbnails (OC Server 8.0+)
+- Added number of files and folders at the end of the list
+- "Open with" in contextual menu
+- Downloads added to Media Provider
+- Uploads:
+ + Local thumbnails in section "Files"
+ + Multiple selection in "Content from other apps" (Android 4.3+)
+- Gallery:
+ + proper handling of EXIF
+ + obey sorting in the list of files
+- Settings view updated
+- Improved subjects in e-mails
+- Bugs fixed
diff --git a/changelog/README.md b/changelog/README.md
new file mode 100644
index 000000000000..1e122f7315c0
--- /dev/null
+++ b/changelog/README.md
@@ -0,0 +1,17 @@
+# Changelog
+
+We are using [calens](https://github.com/restic/calens) to properly generate a
+changelog before we are tagging a new release.
+
+## Create Changelog items
+Create a file according to the [template](TEMPLATE.md) for each
+feature, fix, change... in the [unreleased](./unreleased) folder. The file should be named after
+the # of the merged PR it is describing. The following change types are possible: `Bugfix, Change, Enhancement, Security`.
+
+## Automated Changelog build and commit
+- After each merge to master, the CHANGELOG.md file is automatically updated and the new version will be committed to master while skipping CI.
+
+## Create a new Release
+- Copy the files from the [unreleased](./unreleased) folder into a folder matching the schema `0.3.0_2020-01-10`
+
+
diff --git a/changelog/TEMPLATE.md b/changelog/TEMPLATE.md
new file mode 100644
index 000000000000..f010762f4475
--- /dev/null
+++ b/changelog/TEMPLATE.md
@@ -0,0 +1,33 @@
+```
+Bugfix/Change/Enhancement/Security: Title (Present tense - Max length: 80 chars)
+
+Description of the new bugfix, change or feature (Present perfect passive tense)
+
+https://github.com/owncloud/android/issues/
+https://github.com/owncloud/android/pull/
+```
+
+*Note: The last section is a list of issue URLs, PR URLs and other URLs. The first issue ID (or the first PR ID, in case there aren't any issue links) is used as the primary ID.*
+
+## Examples
+
+```
+Bugfix: Side menu collapses info in landscape
+
+Two empty and visual items have been added to prevent the drawer from collapsing in landscape mode.
+
+https://github.com/owncloud/android/issues/4513
+https://github.com/owncloud/android/pull/4580
+```
+
+```
+Enhancement: Create a new space
+
+A new floating action button has been added to the spaces list, visible only to users with right permissions.
+In addition, a new dialog for space creation has been implemented and, it is shown when the floating action button is tapped.
+
+https://github.com/owncloud/android/issues/4606
+https://github.com/owncloud/android/issues/4688
+https://github.com/owncloud/android/pull/4683
+https://github.com/owncloud/android/pull/4694
+```
diff --git a/changelog/unreleased/4865 b/changelog/unreleased/4865
new file mode 100644
index 000000000000..edc93418b323
--- /dev/null
+++ b/changelog/unreleased/4865
@@ -0,0 +1,6 @@
+Change: Bump AGP from 8.7.2 to 9.2.1
+
+Android Gradle Plugin (AGP) has been updated from version 8.7.2 to 9.2.1, along
+with other dependencies required to build the app with this new version.
+
+https://github.com/owncloud/android/pull/4865
diff --git a/changelog/unreleased/4885 b/changelog/unreleased/4885
new file mode 100644
index 000000000000..8fa366f9ccb8
--- /dev/null
+++ b/changelog/unreleased/4885
@@ -0,0 +1,7 @@
+Change: Upgrade minimum SDK version to API 28 (Android 9)
+
+The minimum SDK version has been upgraded to API 28 (Android 9)
+and previous versions will not be supported anymore
+
+https://github.com/owncloud/android/issues/4880
+https://github.com/owncloud/android/pull/4885
diff --git a/changelog/unreleased/4922 b/changelog/unreleased/4922
new file mode 100644
index 000000000000..5cd169e7c8ef
--- /dev/null
+++ b/changelog/unreleased/4922
@@ -0,0 +1,6 @@
+Bugfix: Spaces search filter for multi-personal accounts (Kiteworks accounts)
+
+All matching personal spaces have been included in the spaces search filter for multi-personal accounts.
+
+https://github.com/owncloud/android/issues/4674
+https://github.com/owncloud/android/pull/4922
diff --git a/changelog/unreleased/4935 b/changelog/unreleased/4935
new file mode 100644
index 000000000000..a9c055e30852
--- /dev/null
+++ b/changelog/unreleased/4935
@@ -0,0 +1,7 @@
+Bugfix: Recalculate grid mode after rotation
+
+The grid mode column count has been recalculated after device rotation,
+keeping file layouts aligned with the new orientation.
+
+https://github.com/owncloud/android/issues/4884
+https://github.com/owncloud/android/pull/4935
diff --git a/changelog/unreleased/4936 b/changelog/unreleased/4936
new file mode 100644
index 000000000000..baadb340b72b
--- /dev/null
+++ b/changelog/unreleased/4936
@@ -0,0 +1,7 @@
+Enhancement: New UI for oCIS sharing
+
+A new UI has been implemented for oCIS sharing. The app now distinguishes between the backend in
+use: the new UI is displayed for oCIS, while the existing sharing UI is preserved for oC classic.
+
+https://github.com/owncloud/android/issues/4839
+https://github.com/owncloud/android/pull/4936
diff --git a/changelog/unreleased/4943 b/changelog/unreleased/4943
new file mode 100644
index 000000000000..389d87f74ea8
--- /dev/null
+++ b/changelog/unreleased/4943
@@ -0,0 +1,5 @@
+Enhancement: Back gesture to go up a folder
+
+A new gesture has been added in the list of files to browse back to the parent folder instead of closing the app
+
+https://github.com/owncloud/android/pull/4943
diff --git a/changelog/unreleased/4945 b/changelog/unreleased/4945
new file mode 100644
index 000000000000..1a651433576d
--- /dev/null
+++ b/changelog/unreleased/4945
@@ -0,0 +1,7 @@
+Enhancement: List shares for oCIS accounts
+
+A new UI has been implemented to list and show information about the
+shares of a file or folder on oCIS accounts (using Graph API)
+
+https://github.com/owncloud/android/issues/4876
+https://github.com/owncloud/android/pull/4945
diff --git a/changelog/unreleased/4952 b/changelog/unreleased/4952
new file mode 100644
index 000000000000..b6a01ce81263
--- /dev/null
+++ b/changelog/unreleased/4952
@@ -0,0 +1,8 @@
+Enhancement: Create a share over a file or a folder on an oCIS server
+
+A new option to create a share over a file or a folder on an oCIS has been added.
+It will be only visible for users with proper permissions.
+
+https://github.com/owncloud/android/issues/4875
+https://github.com/owncloud/android/issues/4917
+https://github.com/owncloud/android/pull/4952
diff --git a/changelog/unreleased/4967 b/changelog/unreleased/4967
new file mode 100644
index 000000000000..13df984c35c1
--- /dev/null
+++ b/changelog/unreleased/4967
@@ -0,0 +1,6 @@
+Bugfix: Hide keyboard and clear error when brute-force countdown ends
+
+The number keyboard has been hidden during the brute-force protection countdown so users cannot interact with it. The wrong-passcode error message has been cleared once the countdown ends, leaving a clean screen for the next attempt.
+
+https://github.com/owncloud/android/issues/4874
+https://github.com/owncloud/android/pull/4967
diff --git a/changelog/unreleased/4986 b/changelog/unreleased/4986
new file mode 100644
index 000000000000..d4266e778c20
--- /dev/null
+++ b/changelog/unreleased/4986
@@ -0,0 +1,6 @@
+Bugfix: Crash when returning from external-file receive screen
+
+A null guard has been added to `startSyncFolderOperation` in `ReceiveExternalFilesActivity` to prevent a NullPointerException when `getCurrentDir()` cannot resolve the current folder.
+
+https://github.com/owncloud/android/issues/4738
+https://github.com/owncloud/android/pull/4986
diff --git a/check_code_script.sh b/check_code_script.sh
new file mode 100755
index 000000000000..414cebbc4a25
--- /dev/null
+++ b/check_code_script.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+check_license_in_file() {
+ if ! head -n 20 $FILE | grep -q "This program is free software: you can redistribute it and/or modify"
+ then
+ if ! head -n 20 $FILE | grep -q "Licensed under the Apache License, Version 2.0"
+ then
+ echo "$FILE does not contain a current copyright header"
+ fi
+ fi
+}
+
+
+for DIRS in owncloudApp/src owncloudData/src owncloudDomain/src owncloudTestUtil/src
+do
+ for FILE in $(find $DIRS -name "*.java" -o -name "*.kt")
+ do
+ check_license_in_file
+ done
+done
+
+./gradlew ktlintFormat
diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml
new file mode 100644
index 000000000000..07ec58ebdf48
--- /dev/null
+++ b/config/detekt/detekt.yml
@@ -0,0 +1,926 @@
+build:
+ maxIssues: 0
+ excludeCorrectable: false
+ weights:
+ # complexity: 2
+ # LongParameterList: 1
+ # style: 1
+ # comments: 1
+
+config:
+ validation: true
+ warningsAsErrors: false
+ # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
+ excludes: ''
+
+processors:
+ active: true
+ exclude:
+ - 'DetektProgressListener'
+ # - 'KtFileCountProcessor'
+ # - 'PackageCountProcessor'
+ # - 'ClassCountProcessor'
+ # - 'FunctionCountProcessor'
+ # - 'PropertyCountProcessor'
+ # - 'ProjectComplexityProcessor'
+ # - 'ProjectCognitiveComplexityProcessor'
+ # - 'ProjectLLOCProcessor'
+ # - 'ProjectCLOCProcessor'
+ # - 'ProjectLOCProcessor'
+ # - 'ProjectSLOCProcessor'
+ # - 'LicenseHeaderLoaderExtension'
+
+console-reports:
+ active: true
+ exclude:
+ - 'ProjectStatisticsReport'
+ - 'ComplexityReport'
+ - 'NotificationReport'
+ - 'FindingsReport'
+ - 'FileBasedFindingsReport'
+ # - 'LiteFindingsReport'
+
+output-reports:
+ active: true
+ exclude:
+ # - 'TxtOutputReport'
+ # - 'XmlOutputReport'
+ # - 'HtmlOutputReport'
+
+comments:
+ active: false
+ AbsentOrWrongFileLicense:
+ active: false
+ licenseTemplateFile: 'license.template'
+ licenseTemplateIsRegex: false
+ CommentOverPrivateFunction:
+ active: false
+ CommentOverPrivateProperty:
+ active: false
+ DeprecatedBlockTag:
+ active: false
+ EndOfSentenceFormat:
+ active: false
+ endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
+ KDocReferencesNonPublicProperty:
+ active: false
+ OutdatedDocumentation:
+ active: false
+ matchTypeParameters: true
+ matchDeclarationsOrder: true
+ allowParamOnConstructorProperties: false
+ UndocumentedPublicClass:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchInNestedClass: true
+ searchInInnerClass: true
+ searchInInnerObject: true
+ searchInInnerInterface: true
+ UndocumentedPublicFunction:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ UndocumentedPublicProperty:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+
+complexity:
+ active: true
+ CognitiveComplexMethod:
+ active: false
+ threshold: 20
+ ComplexCondition:
+ active: true
+ threshold: 6
+ ComplexInterface:
+ active: false
+ threshold: 10
+ includeStaticDeclarations: false
+ includePrivateDeclarations: false
+ CyclomaticComplexMethod:
+ active: false
+ threshold: 15
+ ignoreSingleWhenExpression: false
+ ignoreSimpleWhenEntries: false
+ ignoreNestingFunctions: false
+ nestingFunctions:
+ - 'also'
+ - 'apply'
+ - 'forEach'
+ - 'isNotNull'
+ - 'ifNull'
+ - 'let'
+ - 'run'
+ - 'use'
+ - 'with'
+ LabeledExpression:
+ active: false
+ ignoredLabels: []
+ LargeClass:
+ active: false
+ threshold: 600
+ LongMethod:
+ active: true
+ threshold: 100
+ LongParameterList:
+ active: false
+ functionThreshold: 6
+ constructorThreshold: 7
+ ignoreDefaultParameters: false
+ ignoreDataClasses: true
+ ignoreAnnotatedParameter: []
+ MethodOverloading:
+ active: true
+ threshold: 6
+ NamedArguments:
+ active: false
+ threshold: 3
+ ignoreArgumentsMatchingNames: false
+ NestedBlockDepth:
+ active: false
+ threshold: 5
+ NestedScopeFunctions:
+ active: false
+ threshold: 1
+ functions: ['kotlin.apply', 'kotlin.run', 'kotlin.with', 'kotlin.let', 'kotlin.also']
+ ReplaceSafeCallChainWithRun:
+ active: false
+ StringLiteralDuplication:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ threshold: 3
+ ignoreAnnotation: true
+ excludeStringsWithLessThan5Characters: true
+ ignoreStringsRegex: '$^'
+ TooManyFunctions:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ thresholdInFiles: 11
+ thresholdInClasses: 11
+ thresholdInInterfaces: 11
+ thresholdInObjects: 11
+ thresholdInEnums: 11
+ ignoreDeprecated: false
+ ignorePrivate: false
+ ignoreOverridden: false
+
+coroutines:
+ active: true
+ GlobalCoroutineUsage:
+ active: true
+ InjectDispatcher:
+ active: false
+ dispatcherNames:
+ - 'IO'
+ - 'Default'
+ - 'Unconfined'
+ RedundantSuspendModifier:
+ active: true
+ SleepInsteadOfDelay:
+ active: true
+ SuspendFunWithCoroutineScopeReceiver:
+ active: true
+ SuspendFunWithFlowReturnType:
+ active: true
+
+empty-blocks:
+ active: true
+ EmptyCatchBlock:
+ active: true
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ EmptyClassBlock:
+ active: true
+ EmptyDefaultConstructor:
+ active: true
+ EmptyDoWhileBlock:
+ active: true
+ EmptyElseBlock:
+ active: true
+ EmptyFinallyBlock:
+ active: true
+ EmptyForBlock:
+ active: true
+ EmptyFunctionBlock:
+ active: true
+ ignoreOverridden: true
+ EmptyIfBlock:
+ active: true
+ EmptyInitBlock:
+ active: true
+ EmptyKtFile:
+ active: true
+ EmptySecondaryConstructor:
+ active: true
+ EmptyTryBlock:
+ active: true
+ EmptyWhenBlock:
+ active: true
+ EmptyWhileBlock:
+ active: true
+
+exceptions:
+ active: true
+ ExceptionRaisedInUnexpectedLocation:
+ active: false
+ methodNames:
+ - 'equals'
+ - 'finalize'
+ - 'hashCode'
+ - 'toString'
+ InstanceOfCheckForException:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ NotImplementedDeclaration:
+ active: true
+ ObjectExtendsThrowable:
+ active: true
+ PrintStackTrace:
+ active: true
+ RethrowCaughtException:
+ active: true
+ ReturnFromFinally:
+ active: true
+ ignoreLabeled: false
+ SwallowedException:
+ active: true
+ ignoredExceptionTypes:
+ - 'InterruptedException'
+ - 'MalformedURLException'
+ - 'NumberFormatException'
+ - 'ParseException'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ ThrowingExceptionFromFinally:
+ active: true
+ ThrowingExceptionInMain:
+ active: false
+ ThrowingExceptionsWithoutMessageOrCause:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptions:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Exception'
+ - 'IllegalArgumentException'
+ - 'IllegalMonitorStateException'
+ - 'IllegalStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ ThrowingNewInstanceOfSameException:
+ active: true
+ TooGenericExceptionCaught:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptionNames:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Error'
+ - 'Exception'
+ - 'IllegalMonitorStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ TooGenericExceptionThrown:
+ active: false
+ exceptionNames:
+ - 'Error'
+ - 'Exception'
+ - 'RuntimeException'
+ - 'Throwable'
+
+formatting:
+ active: false
+ AnnotationOnSeparateLine:
+ active: true
+ indentSize: 4
+ AnnotationSpacing:
+ active: true
+ ArgumentListWrapping:
+ active: true
+ indentSize: 4
+ maxLineLength: 120
+ BlockCommentInitialStarAlignment:
+ active: true
+ ChainWrapping:
+ active: true
+ indentSize: 4
+ ClassName:
+ active: false
+ CommentSpacing:
+ active: true
+ CommentWrapping:
+ active: true
+ indentSize: 4
+ ContentReceiverMapping:
+ active: false
+ maxLineLength: 120
+ indentSize: 4
+ DiscouragedCommentLocation:
+ active: false
+ EnumEntryNameCase:
+ active: true
+ EnumWrapping:
+ active: false
+ intentSize: 4
+ Filename:
+ active: true
+ FinalNewline:
+ active: true
+ insertFinalNewLine: true
+ FunKeywordSpacing:
+ active: true
+ FunctionName:
+ active: false
+ FunctionReturnTypeSpacing:
+ active: true
+ maxLineLength: 120
+ FunctionSignature:
+ active: false
+ forceMultilineWhenParameterCountGreaterOrEqualThan: 2147483647
+ functionBodyExpressionWrapping: 'default'
+ maxLineLength: 120
+ indentSize: 4
+ FunctionStartOfBodySpacing:
+ active: true
+ FunctionTypeReferenceSpacing:
+ active: true
+ IfElseBracing:
+ active: false
+ IfElseWrapping:
+ active: false
+ indentSize: 4
+ ImportOrdering:
+ active: true
+ layout: '*,java.**,javax.**,kotlin.**,^'
+ Indentation:
+ active: true
+ indentSize: 4
+ KdocWrapping:
+ active: true
+ indentSize: 4
+ MaximumLineLength:
+ active: true
+ maxLineLength: 120
+ ignoreBackTickedIdentifier: false
+ ModifierListSpacing:
+ active: true
+ MultiLineIfElse:
+ active: true
+ indentSize: 4
+ MultilineExpressionWrapping:
+ active: false
+ indentSize: 4
+ NoBlankLineBeforeRbrace:
+ active: true
+ NoBlankLineInList:
+ active: false
+ NoBlankLinesInChainedMethodCalls:
+ active: true
+ NoConsecutiveBlankLines:
+ active: true
+ NoConsecutiveComments:
+ active: false
+ NoEmptyClassBody:
+ active: true
+ NoEmptyFirstLineInClassBody:
+ active: false
+ indentSize: 4
+ NoEmptyFirstLineInMethodBlock:
+ active: true
+ NoLineBreakAfterElse:
+ active: true
+ NoLineBreakBeforeAssignment:
+ active: true
+ NoMultipleSpaces:
+ active: true
+ NoSemicolons:
+ active: true
+ NoSingleLineBlockComment:
+ active: false
+ indentSize: 4
+ NoTrailingSpaces:
+ active: true
+ NoUnitReturn:
+ active: true
+ NoUnusedImports:
+ active: true
+ NoWildcardImports:
+ active: true
+ packagesToUseImportOnDemandProperty: 'java.util.*,kotlinx.android.synthetic.**'
+ NullableTypeSpacing:
+ active: true
+ PackageName:
+ active: true
+ ParameterListSpacing:
+ active: false
+ ParameterListWrapping:
+ active: true
+ maxLineLength: 120
+ indentSize: 4
+ ParameterWrapping:
+ active: true
+ indentSize: 4
+ MaxLineLength: 120
+ PropertyName:
+ active: false
+ PropertyWrapping:
+ active: true
+ indentSize: 4
+ maxLineLength: 120
+ SpacingAroundAngleBrackets:
+ active: true
+ SpacingAroundColon:
+ active: true
+ SpacingAroundComma:
+ active: true
+ SpacingAroundCurly:
+ active: true
+ SpacingAroundDot:
+ active: true
+ SpacingAroundDoubleColon:
+ active: true
+ SpacingAroundKeyword:
+ active: true
+ SpacingAroundOperators:
+ active: true
+ SpacingAroundParens:
+ active: true
+ SpacingAroundRangeOperator:
+ active: true
+ SpacingAroundUnaryOperator:
+ active: true
+ SpacingBetweenDeclarationsWithAnnotations:
+ active: true
+ SpacingBetweenDeclarationsWithComments:
+ active: true
+ SpacingBetweenFunctionNameAndOpeningParenthesis:
+ active: true
+ StringTemplate:
+ active: true
+ StringTemplateIndent:
+ active: false
+ indentSize: 4
+ TrailingCommaOnCallSite:
+ active: false
+ useTrailingCommaOnCallSite: true
+ TrailingCommaOnDeclarationSite:
+ active: false
+ useTrailingCommaOnDeclarationSite: true
+ TryCatchFinallySpacing:
+ active: false
+ indentSize: 4
+ TypeArgumentListSpacing:
+ active: false
+ indentSize: 4
+ TypeParameterListSpacing:
+ active: false
+ indentSize: 4
+ UnnecessaryParenthesesBeforeTrailingLambda:
+ active: true
+ Wrapping:
+ active: true
+ indentSize: 4
+ maxLineLength: 120
+
+libraries:
+ active: false
+ ForbiddenPublicDataClass:
+ active: true
+ ignorePackages: ['*.internal', '*.internal.*']
+ LibraryCodeMustSpecifyReturnType:
+ active: true
+ allowOmitUnit: false
+ LibraryEntitiesShouldNotBePublic:
+ active: true
+
+naming:
+ active: true
+ BooleanPropertyNaming:
+ active: false
+ allowedPattern: '^(is|has|are)'
+ ClassNaming:
+ active: true
+ classPattern: '[A-Z][a-zA-Z0-9]*'
+ ConstructorParameterNaming:
+ active: true
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ privateParameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ EnumNaming:
+ active: true
+ enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
+ ForbiddenClassName:
+ active: false
+ forbiddenName: []
+ FunctionMaxLength:
+ active: false
+ maximumFunctionNameLength: 30
+ FunctionMinLength:
+ active: false
+ minimumFunctionNameLength: 3
+ FunctionNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ functionPattern: '[a-z][a-zA-Z0-9]*'
+ excludeClassPattern: '$^'
+ FunctionParameterNaming:
+ active: true
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ InvalidPackageDeclaration:
+ active: true
+ rootPackage: ''
+ requireRootInDeclaration: false
+ LambdaParameterNaming:
+ active: true
+ parameterPattern: '[a-z][A-Za-z0-9]*|_'
+ MatchingDeclarationName:
+ active: true
+ mustBeFirst: false
+ MemberNameEqualsClassName:
+ active: true
+ ignoreOverridden: true
+ NoNameShadowing:
+ active: true
+ NonBooleanPropertyPrefixedWithIs:
+ active: false
+ ObjectPropertyNaming:
+ active: true
+ constantPattern: '[A-Za-z][_A-Za-z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
+ PackageNaming:
+ active: true
+ packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
+ TopLevelPropertyNaming:
+ active: true
+ constantPattern: '[A-Z][_A-Z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
+ VariableMaxLength:
+ active: false
+ maximumVariableNameLength: 64
+ VariableMinLength:
+ active: false
+ minimumVariableNameLength: 1
+ VariableNaming:
+ active: true
+ variablePattern: '[a-z][A-Za-z0-9]*'
+ privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+
+performance:
+ active: true
+ ArrayPrimitive:
+ active: true
+ CouldBeSequence:
+ active: false
+ threshold: 3
+ ForEachOnRange:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ SpreadOperator:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ UnnecessaryPartOfBinaryExpression:
+ active: true
+ UnnecessaryTemporaryInstantiation:
+ active: true
+
+potential-bugs:
+ active: true
+ AvoidReferentialEquality:
+ active: true
+ forbiddenTypePatterns:
+ - 'kotlin.String'
+ CastNullableToNonNullableType:
+ active: true
+ CastToNullableType:
+ active: false
+ Deprecation:
+ active: false
+ DontDowncastCollectionTypes:
+ active: true
+ DoubleMutabilityForCollection:
+ active: true
+ mutableTypes:
+ - 'kotlin.collections.MutableList'
+ - 'kotlin.collections.MutableMap'
+ - 'kotlin.collections.MutableSet'
+ - 'java.util.ArrayList'
+ - 'java.util.LinkedHashSet'
+ - 'java.util.HashSet'
+ - 'java.util.LinkedHashMap'
+ - 'java.util.HashMap'
+ ElseCaseInsteadOfExhaustiveWhen:
+ active: true
+ ignoredSubjectTypes: []
+ EqualsAlwaysReturnsTrueOrFalse:
+ active: true
+ EqualsWithHashCodeExist:
+ active: true
+ ExitOutsideMain:
+ active: true
+ ExplicitGarbageCollectionCall:
+ active: true
+ HasPlatformType:
+ active: true
+ IgnoredReturnValue:
+ active: false
+ restrictToConfig: true
+ returnValueAnnotations:
+ - '*.CheckResult'
+ - '*.CheckReturnValue'
+ ignoreReturnValueAnnotations:
+ - '*.CanIgnoreReturnValue'
+ ignoreFunctionCall: []
+ ImplicitDefaultLocale:
+ active: true
+ ImplicitUnitReturnType:
+ active: true
+ allowExplicitReturnType: false
+ InvalidRange:
+ active: true
+ IteratorHasNextCallsNextMethod:
+ active: true
+ IteratorNotThrowingNoSuchElementException:
+ active: true
+ LateinitUsage:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ ignoreOnClassesPattern: ''
+ MapGetWithNotNullAssertionOperator:
+ active: true
+ MissingPackageDeclaration:
+ active: true
+ NullCheckOnMutableProperty:
+ active: false
+ NullableToStringCall:
+ active: true
+ PropertyUsedBeforeDeclaration:
+ active: true
+ UnconditionalJumpStatementInLoop:
+ active: true
+ UnnecessaryNotNullCheck:
+ active: true
+ UnnecessaryNotNullOperator:
+ active: true
+ UnnecessarySafeCall:
+ active: true
+ UnreachableCatchBlock:
+ active: true
+ UnreachableCode:
+ active: true
+ UnsafeCallOnNullableType:
+ active: false
+ UnsafeCast:
+ active: true
+ UnusedUnaryOperator:
+ active: true
+ UselessPostfixExpression:
+ active: true
+ WrongEqualsTypeParameter:
+ active: true
+
+style:
+ active: true
+ AlsoCouldBeApply:
+ active: true
+ BracesOnIfStatements:
+ active: true
+ singleLine: 'consistent'
+ multiLine: 'consistent'
+ BracesOnWhenStatements:
+ active: true
+ singleLine: 'consistent'
+ multiLine: 'consistent'
+ CanBeNonNullable:
+ active: true
+ CascadingCallWrapping:
+ active: false
+ includeElvis: true
+ ClassOrdering:
+ active: true
+ CollapsibleIfStatements:
+ active: true
+ DataClassContainsFunctions:
+ active: false
+ allowOperators: false
+ conversionFunctionPrefix: ['to']
+ DataClassShouldBeImmutable:
+ active: false
+ DestructuringDeclarationWithTooManyEntries:
+ active: false
+ maxDestructuringEntries: 3
+ DoubleNegativeLambda:
+ active: true
+ negativeFunctions: ['takeUnless', 'none']
+ negativeFunctionNameParts: ['not', 'non']
+ EqualsNullCall:
+ active: true
+ EqualsOnSignatureLine:
+ active: true
+ ExplicitCollectionElementAccessMethod:
+ active: true
+ ExplicitItLambdaParameter:
+ active: true
+ ExpressionBodySyntax:
+ active: true
+ includeLineWrapping: true
+ ForbiddenAnnotation:
+ active: false
+ annotations: ['java.lang.SuppressWarnings', 'java.lang.Deprecated', 'java.lang.annotation.Documented', 'java.lang.annotation.Target', 'java.lang.annotation.Retention', 'java.lang.annotation.Repeatable', 'java.lang.annotation.Inherited']
+ ForbiddenComment:
+ active: true
+ allowedPatterns: ''
+ comments: ['FIXME:', 'STOPSHIP:', 'TODO:']
+ ForbiddenImport:
+ active: false
+ imports: []
+ forbiddenPatterns: ''
+ ForbiddenMethodCall:
+ active: true
+ methods: ['kotlin.io.print', 'kotlin.io.println']
+ ForbiddenSuppress:
+ active: false
+ rules: []
+ ForbiddenVoid:
+ active: true
+ ignoreOverridden: false
+ ignoreUsageInGenerics: false
+ FunctionOnlyReturningConstant:
+ active: true
+ ignoreOverridableFunction: true
+ ignoreActualFunction: true
+ excludedFunctions: []
+ LoopWithTooManyJumpStatements:
+ active: false
+ maxJumpCount: 1
+ MagicNumber:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ ignoreNumbers:
+ - '-1'
+ - '0'
+ - '1'
+ - '2'
+ ignoreHashCodeFunction: true
+ ignorePropertyDeclaration: false
+ ignoreLocalVariableDeclaration: false
+ ignoreConstantDeclaration: true
+ ignoreCompanionObjectPropertyDeclaration: true
+ ignoreAnnotation: false
+ ignoreNamedArgument: true
+ ignoreEnums: false
+ ignoreRanges: false
+ ignoreExtensionFunctions: true
+ MandatoryBracesLoops:
+ active: true
+ MaxLineLength:
+ active: true
+ maxLineLength: 150
+ excludePackageStatements: true
+ excludeImportStatements: true
+ excludeCommentStatements: false
+ MayBeConst:
+ active: true
+ ModifierOrder:
+ active: true
+ MultilineLambdaItParameter:
+ active: false
+ MultilineRawStringIndentation:
+ active: false
+ indentSize: 4
+ trimmingMethods: ['trimIndent', 'trimMargin']
+ NestedClassesVisibility:
+ active: true
+ NewLineAtEndOfFile:
+ active: true
+ NoTabs:
+ active: false
+ NullableBooleanCheck:
+ active: true
+ ObjectLiteralToLambda:
+ active: true
+ OptionalAbstractKeyword:
+ active: true
+ OptionalUnit:
+ active: true
+ PreferToOverPairSyntax:
+ active: true
+ ProtectedMemberInFinalClass:
+ active: true
+ RedundantExplicitType:
+ active: true
+ RedundantHigherOrderMapUsage:
+ active: true
+ RedundantVisibilityModifierRule:
+ active: true
+ ReturnCount:
+ active: false
+ max: 2
+ excludedFunctions: ['equals']
+ excludeLabeled: false
+ excludeReturnFromLambda: true
+ excludeGuardClauses: false
+ SafeCast:
+ active: true
+ SerialVersionUIDInSerializableClass:
+ active: true
+ SpacingBetweenPackageAndImports:
+ active: true
+ StringShouldBeRawString:
+ active: false
+ maxEscapedCharacterCount: 2
+ ignoredCharacters: []
+ ThrowsCount:
+ active: false
+ max: 2
+ excludeGuardClauses: false
+ TrailingWhitespace:
+ active: true
+ TrimMultilineRawString:
+ active: false
+ trimmingMethods: ['trimIndent', 'trimMargin']
+ UnderscoresInNumericLiterals:
+ active: false
+ acceptableLength: 4
+ allowNonStandardGrouping: false
+ UnnecessaryAbstractClass:
+ active: true
+ UnnecessaryAnnotationUseSiteTarget:
+ active: false
+ UnnecessaryApply:
+ active: true
+ UnnecessaryBackticks:
+ active: false
+ UnnecessaryBracesAroundTrailingLambda:
+ active: true
+ UnnecessaryFilter:
+ active: true
+ UnnecessaryInheritance:
+ active: true
+ UnnecessaryInnerClass:
+ active: true
+ UnnecessaryLet:
+ active: true
+ UnnecessaryParentheses:
+ active: false
+ allowForUnclearPrecedence: true
+ UntilInsteadOfRangeTo:
+ active: true
+ UnusedImports:
+ active: true
+ UnusedParameter:
+ active: true
+ allowedNames: 'ignored|expected'
+ UnusedPrivateClass:
+ active: true
+ UnusedPrivateMember:
+ active: true
+ allowedNames: '(_|ignored|expected|serialVersionUID)'
+ UnusedPrivateProperty:
+ active: true
+ allowedNames: '_|ignored|expected|serialVersionUID'
+ UseAnyOrNoneInsteadOfFind:
+ active: true
+ UseArrayLiteralsInAnnotations:
+ active: true
+ UseCheckNotNull:
+ active: false
+ UseCheckOrError:
+ active: false
+ UseDataClass:
+ active: true
+ allowVars: true
+ UseEmptyCounterpart:
+ active: true
+ UseIfEmptyOrIfBlank:
+ active: true
+ UseIfInsteadOfWhen:
+ active: true
+ ignoreWhenContainingVariableDeclaration: false
+ UseIsNullOrEmpty:
+ active: true
+ UseLet:
+ active: true
+ UseOrEmpty:
+ active: true
+ UseRequire:
+ active: false
+ UseRequireNotNull:
+ active: false
+ UseSumOfInsteadOfFlatMapSize:
+ active: false
+ UselessCallOnNotNull:
+ active: true
+ UtilityClassWithPublicConstructor:
+ active: true
+ VarCouldBeVal:
+ active: true
+ ignoreLateinitVar: false
+ WildcardImport:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ excludeImports:
+ - 'java.util.*'
diff --git a/dependencies.txt b/dependencies.txt
new file mode 100644
index 000000000000..5bdf68c0c55a
--- /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/doc/icon-hires.png b/doc/icon-hires.png
new file mode 100644
index 000000000000..d5f6930e1be4
Binary files /dev/null and b/doc/icon-hires.png differ
diff --git a/doc/icon-playstore.png b/doc/icon-playstore.png
new file mode 100644
index 000000000000..accd7c8b083f
Binary files /dev/null and b/doc/icon-playstore.png differ
diff --git a/doc/icon.svg b/doc/icon.svg
new file mode 100644
index 000000000000..844660a4409b
--- /dev/null
+++ b/doc/icon.svg
@@ -0,0 +1,73 @@
+
+image/svg+xml
\ No newline at end of file
diff --git a/docs_resources/detail_view_device.png b/docs_resources/detail_view_device.png
new file mode 100644
index 000000000000..5f327cf02fb0
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
new file mode 100644
index 000000000000..cb61ca1381b2
Binary files /dev/null and b/docs_resources/filelist_device.png differ
diff --git a/docs_resources/out_of_date_branch.png b/docs_resources/out_of_date_branch.png
new file mode 100644
index 000000000000..297b9edaecd7
Binary files /dev/null and b/docs_resources/out_of_date_branch.png differ
diff --git a/docs_resources/photos_device.png b/docs_resources/photos_device.png
new file mode 100644
index 000000000000..2e5ec6b03d40
Binary files /dev/null and b/docs_resources/photos_device.png differ
diff --git a/docs_resources/share_device.png b/docs_resources/share_device.png
new file mode 100644
index 000000000000..13e43eeeb4cd
Binary files /dev/null and b/docs_resources/share_device.png differ
diff --git a/docs_resources/spaces_device.png b/docs_resources/spaces_device.png
new file mode 100644
index 000000000000..af27002c702b
Binary files /dev/null and b/docs_resources/spaces_device.png differ
diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt
new file mode 100644
index 000000000000..168d0fce0c96
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/full_description.txt
@@ -0,0 +1,13 @@
+Willkommen bei der ownCloud Android App – Zusammen mit einem ownCloud Server haben Sie in wenigen Minuten Ihre private Filesync und –share Cloud.
+
+Sie suchen eine private Filesync und –share Software? Gute Neuigkeiten, die ownCloud Android App erlaubt es Ihnen Android Geräte mit einem privaten ownCloud Server in Ihrem Datenzentrum zu verbinden. ownCloud ist eine Open Source Filesync und –share Software für jedermann, von Privatpersonen mit kostenfreiem ownCloud Server bis hin zu Großunternehmen und Service Providern mit der ownCloud Enterprise Edition. ownCloud bietet eine sichere und flexible Filesync und –share Lösung – auf Servern die Sie kontrollieren.
+
+Mit der ownCloud Android App können Sie alle in ownCloud synchronisierten Dateien durchsuchen, neue Dateien erstellen und bearbeiten, diese Dateien und Ordner mit Kollegen teilen und die Inhalte dieser Ordner über alle Ihre Geräte hinweg synchron halten. Kopieren Sie einfach eine Datei in ein Verzeichnis auf Ihrem Server, ownCloud übernimmt für Sie den Rest.
+
+ownCloud bietet die Möglichkeit die richtigen Dateien zur richtigen Zeit in die richtigen Hände zu geben, gleich ob Sie ein Mobilgerät, den Desktop oder Web Client verwenden. Auf jedem Gerät und in einer leicht bedienbaren, sicheren, privaten und kontrollierten Lösung.
+
+Falls Sie Probleme haben sich mit dem ownCloud Server zu verbinden oder mit diesem zu synchronisieren kontaktieren Sie uns bitte auf https://github.com/owncloud/android/issues (englisch). Oftmals ist auch bereits eine Lösung in unserem Forum zu finden: https://central.owncloud.org (deutsch & englisch)
+
+Besuchen Sie uns unter www.ownCloud.com für weitere Informationen zu ownCloud und den ownCloud Subscriptions. Für mehr Informationen zur freien Open Source Software ownCloud Classic besuchen Sie bitte www.owncloud.org .
+
+***Falls es zu Log-In Problemen nach einem Update kommt und Sie die ownCloud Workaround App auf Ihrem Gerät installiert haben versuchen Sie bitte die Workaround App zu deinstallieren und anschließend neu zu installieren***
diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt
new file mode 100644
index 000000000000..753cf71abb1c
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/short_description.txt
@@ -0,0 +1 @@
+Client zum Synchronisieren von Daten
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
new file mode 100644
index 000000000000..6eaa0ee6fa86
--- /dev/null
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -0,0 +1,13 @@
+Welcome to the ownCloud Android App – Add an ownCloud server, and have your private file sync and share cloud up and running in no time.
+
+Do you need private file sync and share software? Then good news, because the ownCloud Android App enables you to connect Android devices to a private ownCloud Server running in your data center. ownCloud is open source file sync and share software for everyone from individuals operating the free ownCloud server, to large enterprises and service providers operating under the ownCloud Enterprise Subscription. ownCloud provides a safe, secure and compliant file sync and share solution – on servers you control.
+
+With the ownCloud Android App you can browse all of your ownCloud synced files, create and edit new files, share these files and folders with co-workers, and keep the contents of those folders in sync across all of your devices. Simply copy a file into a directory on your server and ownCloud does the rest.
+
+Whether using a mobile device, a desktop, or the web client, ownCloud provides the ability to put the right files in the right hands at the right time on any device in one simple-to-use, secure, private and controlled solution. After all, with ownCloud, it’s Your Cloud, Your Data, Your Way.
+
+Should you have any problem connecting or synchronizing with your ownCloud server, please contact us on https://github.com/owncloud/android/issues or check https://central.owncloud.org.
+
+Visit us at www.ownCloud.com for more information about ownCloud and the ownCloud Subscriptions. For more information on the free and open source ownCloud Classic, visit www.ownCloud.org.
+
+*** If you are experiencing login issues after an update and the ownCloud Workaround App is installed in your device, please, try to uninstall and reinstall the Workaround App ***
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.jpeg b/fastlane/metadata/android/en-US/images/featureGraphic.jpeg
new file mode 100644
index 000000000000..96a9ddf5f5e5
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.jpeg differ
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
new file mode 100644
index 000000000000..d4c8ac67b3e0
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_login.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_login.png
new file mode 100644
index 000000000000..3ee33b8f51f8
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_login.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_list_view.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_list_view.png
new file mode 100644
index 000000000000..cb61ca1381b2
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_list_view.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_grid_view.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_grid_view.png
new file mode 100644
index 000000000000..2e5ec6b03d40
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_grid_view.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_drawer.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_drawer.png
new file mode 100644
index 000000000000..e8174b468bc8
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_drawer.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_spaces.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_spaces.png
new file mode 100644
index 000000000000..af27002c702b
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_spaces.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6_uploads.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_uploads.png
new file mode 100644
index 000000000000..c5c82978ef62
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_uploads.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7_share.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_share.png
new file mode 100644
index 000000000000..13e43eeeb4cd
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_share.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/8_settings.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/8_settings.png
new file mode 100644
index 000000000000..1c6092a1d621
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/8_settings.png differ
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
new file mode 100644
index 000000000000..27aab68fc7cd
--- /dev/null
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -0,0 +1 @@
+File synchronization client
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000000..2516ecb82113
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,4 @@
+android.enableJetifier=true
+android.nonTransitiveRClass=false
+android.useAndroidX=true
+org.gradle.jvmargs=-Xmx1536M
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000000..b2ebc35fc0b5
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,134 @@
+[versions]
+androidGradlePlugin = "9.2.1"
+androidxActivity = "1.6.1"
+androidxAnnotation = "1.6.0"
+androidxAppCompat = "1.8.0"
+androidxArch = "2.2.0"
+androidxBiometric = "1.1.0"
+androidxBrowser = "1.10.0"
+androidxContraintLayout = "2.2.2"
+androidxCore = "1.10.1"
+androidxEmojiPicker = "1.6.0"
+androidxEnterpriseFeedback = "1.1.0"
+androidxEspresso = "3.7.0"
+androidxFragment = "1.9.0"
+androidxLegacy = "1.0.0"
+androidxLifecycle = "2.5.1"
+androidxLifecycleExtensions = "2.2.0"
+androidxRoom = "2.8.4"
+androidxSqlite = "2.5.2"
+androidxTest = "1.7.0"
+androidxTestExt = "1.3.0"
+androidxTestMonitor = "1.6.1"
+androidxTestUiAutomator ="2.4.0"
+androidxWork = "2.8.1"
+coil = "2.7.0"
+cyclonedx = "2.3.1"
+detekt = "1.23.8"
+dexopener = "2.0.5"
+disklrucache = "2.0.2"
+media3 ="1.11.0"
+floatingactionbutton = "1.10.1"
+glide = "4.15.1"
+glideToVectorYou = "v2.0.0"
+junit4 = "4.13.2"
+koin = "4.2.2"
+kotlin = "2.3.10"
+kotlinxCoroutines = "1.11.0"
+ksp = "2.3.10"
+ktlint = "14.2.0"
+markwon = "4.6.2"
+material = "1.14.0"
+mockk = "1.14.11"
+moshi = "1.15.2"
+patternlockview = "a90b0d4bf0"
+photoView = "2.3.0"
+preference = "1.2.1"
+sonarqube = "4.0.0.2929"
+stetho = "1.6.0"
+timber = "5.0.1"
+
+[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-emoji2-emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "androidxEmojiPicker" }
+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" }
+detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
+detekt-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" }
+dexopener = { group = "com.github.tmurakami", name = "dexopener", version.ref = "dexopener" }
+disklrucache = { group = "com.jakewharton", name = "disklrucache", version.ref = "disklrucache" }
+media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
+media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
+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" }
+timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
+
+# 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" }
+detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
+cyclonedx = { id = "org.cyclonedx.bom", version.ref = "cyclonedx" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 13372aef5e24..eddabd2eef8d 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e9ce5369f191..c56b367ba927 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,5 @@
-#Thu Apr 07 22:12:15 CEST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/gradlew b/gradlew
index 91a7e269e19d..249efbb032ce 100755
--- a/gradlew
+++ b/gradlew
@@ -1,79 +1,128 @@
-#!/usr/bin/env bash
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# gradlew start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh gradlew
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
-warn ( ) {
+warn () {
echo "$*"
-}
+} >&2
-die ( ) {
+die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
-# For Cygwin, ensure paths are in UNIX format before anything is touched.
-if $cygwin ; then
- [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
-fi
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >&-
-APP_HOME="`pwd -P`"
-cd "$SAVED" >&-
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -82,83 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=$((i+1))
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index aec99730b4e8..8508ef684d4e 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,90 +1,82 @@
-@if "%DEBUG%" == "" @echo off
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
-@rem Gradle startup script for Windows
+@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
-goto fail
+"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
+if exist "%JAVA_EXE%" goto execute
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
+"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
+@rem Execute gradlew
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
-:omega
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/libs/disklrucache-2.0.2.jar b/libs/disklrucache-2.0.2.jar
deleted file mode 100644
index ca7907d0e220..000000000000
Binary files a/libs/disklrucache-2.0.2.jar and /dev/null differ
diff --git a/libs/touch-image-view.jar b/libs/touch-image-view.jar
deleted file mode 100644
index 539f61510230..000000000000
Binary files a/libs/touch-image-view.jar and /dev/null differ
diff --git a/oc_jb_workaround/.classpath b/oc_jb_workaround/.classpath
deleted file mode 100644
index 0461652ecf98..000000000000
--- a/oc_jb_workaround/.classpath
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/oc_jb_workaround/.gitignore b/oc_jb_workaround/.gitignore
deleted file mode 100644
index 0105cb345b54..000000000000
--- a/oc_jb_workaround/.gitignore
+++ /dev/null
@@ -1,19 +0,0 @@
-# built application files
-*.apk
-*.ap_
-
-# files for the dex VM
-*.dex
-
-# Java class files
-*.class
-
-# generated files
-bin/
-gen/
-
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Mac .DS_Store files
-.DS_Store
diff --git a/oc_jb_workaround/.project b/oc_jb_workaround/.project
deleted file mode 100644
index 54714bb1191d..000000000000
--- a/oc_jb_workaround/.project
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
- owncloud-android-workaround-accounts
-
-
-
-
-
- com.android.ide.eclipse.adt.ResourceManagerBuilder
-
-
-
-
- com.android.ide.eclipse.adt.PreCompilerBuilder
-
-
-
-
- org.eclipse.jdt.core.javabuilder
-
-
-
-
- com.android.ide.eclipse.adt.ApkBuilder
-
-
-
-
-
- com.android.ide.eclipse.adt.AndroidNature
- org.eclipse.jdt.core.javanature
-
-
diff --git a/oc_jb_workaround/.settings/org.eclipse.jdt.core.prefs b/oc_jb_workaround/.settings/org.eclipse.jdt.core.prefs
deleted file mode 100644
index b080d2ddc88f..000000000000
--- a/oc_jb_workaround/.settings/org.eclipse.jdt.core.prefs
+++ /dev/null
@@ -1,4 +0,0 @@
-eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
-org.eclipse.jdt.core.compiler.compliance=1.6
-org.eclipse.jdt.core.compiler.source=1.6
diff --git a/oc_jb_workaround/AndroidManifest.xml b/oc_jb_workaround/AndroidManifest.xml
deleted file mode 100644
index f759681d353d..000000000000
--- a/oc_jb_workaround/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/oc_jb_workaround/README.md b/oc_jb_workaround/README.md
deleted file mode 100644
index cddf3bdae026..000000000000
--- a/oc_jb_workaround/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-ownCloud Jelly Bean Workaround
-==============================
-
-Helper app to work around the problem of lost credentials at reboot time found
-in devices with Android 4.1.x.
-
-Only needed for ownCloud apps installed from the Google Play Store.
-
-See more information about the bug here:
-http://code.google.com/p/android/issues/detail?id=34880
\ No newline at end of file
diff --git a/oc_jb_workaround/build.gradle b/oc_jb_workaround/build.gradle
deleted file mode 100644
index 1dee746d4701..000000000000
--- a/oc_jb_workaround/build.gradle
+++ /dev/null
@@ -1,70 +0,0 @@
-buildscript {
- repositories {
- mavenCentral()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:2.1.0'
- }
-}
-apply plugin: 'com.android.application'
-
-dependencies {
- compile fileTree(dir: 'libs', include: '*.jar')
-}
-
-android {
- compileSdkVersion 23
- buildToolsVersion "23.0.3"
-
- sourceSets {
- main {
- manifest.srcFile 'AndroidManifest.xml'
- java.srcDirs = ['src']
- resources.srcDirs = ['src']
- aidl.srcDirs = ['src']
- renderscript.srcDirs = ['src']
- res.srcDirs = ['res']
- assets.srcDirs = ['assets']
- }
-
- // Move the tests to tests/java, tests/res, etc...
- instrumentTest.setRoot('tests')
-
- // Move the build types to build-types/
- // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...
- // This moves them out of them default location under src//... which would
- // conflict with src/ being used by the main source set.
- // Adding new build types or product flavors should be accompanied
- // by a similar customization.
- debug.setRoot('build-types/debug')
- release.setRoot('build-types/release')
- }
-
- signingConfigs {
- release {
- if (System.env.OC_RELEASE_KEYSTORE) {
- storeFile file(System.env.OC_RELEASE_KEYSTORE) // use an absolute path
- storePassword System.env.OC_RELEASE_KEYSTORE_PASSWORD
- keyAlias System.env.OC_RELEASE_KEY_ALIAS
- keyPassword System.env.OC_RELEASE_KEY_PASSWORD
- }
- }
- }
-
- buildTypes {
- release {
- if (System.env.OC_RELEASE_KEYSTORE) {
- signingConfig signingConfigs.release
- }
- }
- }
-
- applicationVariants.all { variant ->
- def appName = System.env.OC_APP_NAME
- if (appName) {
- appName += "_jb_workaround"
- }
- setOutputFileName(variant, appName, project)
- }
-
-}
diff --git a/oc_jb_workaround/gradle/wrapper/gradle-wrapper.jar b/oc_jb_workaround/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 13372aef5e24..000000000000
Binary files a/oc_jb_workaround/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/oc_jb_workaround/gradle/wrapper/gradle-wrapper.properties b/oc_jb_workaround/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 122a0dca2e80..000000000000
--- a/oc_jb_workaround/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Mon Dec 28 10:00:20 PST 2015
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/oc_jb_workaround/gradlew b/oc_jb_workaround/gradlew
deleted file mode 100755
index 91a7e269e19d..000000000000
--- a/oc_jb_workaround/gradlew
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/usr/bin/env bash
-
-##############################################################################
-##
-## Gradle start up script for UN*X
-##
-##############################################################################
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn ( ) {
- echo "$*"
-}
-
-die ( ) {
- echo
- echo "$*"
- echo
- exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
-esac
-
-# For Cygwin, ensure paths are in UNIX format before anything is touched.
-if $cygwin ; then
- [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
-fi
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >&-
-APP_HOME="`pwd -P`"
-cd "$SAVED" >&-
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
- fi
- i=$((i+1))
- done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
-fi
-
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/oc_jb_workaround/gradlew.bat b/oc_jb_workaround/gradlew.bat
deleted file mode 100644
index aec99730b4e8..000000000000
--- a/oc_jb_workaround/gradlew.bat
+++ /dev/null
@@ -1,90 +0,0 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/oc_jb_workaround/proguard-project.txt b/oc_jb_workaround/proguard-project.txt
deleted file mode 100644
index f2fe1559a217..000000000000
--- a/oc_jb_workaround/proguard-project.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-# To enable ProGuard in your project, edit project.properties
-# to define the proguard.config property as described in that file.
-#
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in ${sdk.dir}/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the ProGuard
-# include property in project.properties.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
diff --git a/oc_jb_workaround/res/drawable-hdpi/main_app_icon.png b/oc_jb_workaround/res/drawable-hdpi/main_app_icon.png
deleted file mode 100644
index 6fe153bb6ed2..000000000000
Binary files a/oc_jb_workaround/res/drawable-hdpi/main_app_icon.png and /dev/null differ
diff --git a/oc_jb_workaround/res/drawable-hdpi/workaround_app_icon.png b/oc_jb_workaround/res/drawable-hdpi/workaround_app_icon.png
deleted file mode 100644
index f28286a74880..000000000000
Binary files a/oc_jb_workaround/res/drawable-hdpi/workaround_app_icon.png and /dev/null differ
diff --git a/oc_jb_workaround/res/drawable-ldpi/main_app_icon.png b/oc_jb_workaround/res/drawable-ldpi/main_app_icon.png
deleted file mode 100644
index 1bc470bee9de..000000000000
Binary files a/oc_jb_workaround/res/drawable-ldpi/main_app_icon.png and /dev/null differ
diff --git a/oc_jb_workaround/res/drawable-ldpi/workaround_app_icon.png b/oc_jb_workaround/res/drawable-ldpi/workaround_app_icon.png
deleted file mode 100644
index cd8eb078a789..000000000000
Binary files a/oc_jb_workaround/res/drawable-ldpi/workaround_app_icon.png and /dev/null differ
diff --git a/oc_jb_workaround/res/drawable-mdpi/main_app_icon.png b/oc_jb_workaround/res/drawable-mdpi/main_app_icon.png
deleted file mode 100644
index 9008b9d332ef..000000000000
Binary files a/oc_jb_workaround/res/drawable-mdpi/main_app_icon.png and /dev/null differ
diff --git a/oc_jb_workaround/res/drawable-mdpi/workaround_app_icon.png b/oc_jb_workaround/res/drawable-mdpi/workaround_app_icon.png
deleted file mode 100644
index f7ca7b00c12c..000000000000
Binary files a/oc_jb_workaround/res/drawable-mdpi/workaround_app_icon.png and /dev/null differ
diff --git a/oc_jb_workaround/res/values/setup.xml b/oc_jb_workaround/res/values/setup.xml
deleted file mode 100644
index d4d347eed0a9..000000000000
--- a/oc_jb_workaround/res/values/setup.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
- ownCloud Jelly Bean Workaround for lost credentials
- ownCloud
- owncloud
-
-
\ No newline at end of file
diff --git a/oc_jb_workaround/res/xml/authenticator.xml b/oc_jb_workaround/res/xml/authenticator.xml
deleted file mode 100644
index f1ec8462c659..000000000000
--- a/oc_jb_workaround/res/xml/authenticator.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/oc_jb_workaround/src/com/owncloud/android/workaround/accounts/AccountAuthenticatorService.java b/oc_jb_workaround/src/com/owncloud/android/workaround/accounts/AccountAuthenticatorService.java
deleted file mode 100644
index 6526f9472f9b..000000000000
--- a/oc_jb_workaround/src/com/owncloud/android/workaround/accounts/AccountAuthenticatorService.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/* ownCloud Jelly Bean Workaround for lost credentials
- * Copyright (C) 2015 ownCloud Inc.
- *
- * 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.workaround.accounts;
-
-import android.accounts.AbstractAccountAuthenticator;
-import android.accounts.Account;
-import android.accounts.AccountAuthenticatorResponse;
-import android.accounts.AccountManager;
-import android.accounts.NetworkErrorException;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.IBinder;
-//import android.util.Log;
-
-public class AccountAuthenticatorService extends Service {
-
- private AccountAuthenticator mAuthenticator;
- //static final public String ACCOUNT_TYPE = "owncloud";
-
- @Override
- public void onCreate() {
- super.onCreate();
- mAuthenticator = new AccountAuthenticator(this);
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return mAuthenticator.getIBinder();
- }
-
-
- public static class AccountAuthenticator extends AbstractAccountAuthenticator {
-
- public static final String KEY_AUTH_TOKEN_TYPE = "authTokenType";
- public static final String KEY_REQUIRED_FEATURES = "requiredFeatures";
- public static final String KEY_LOGIN_OPTIONS = "loginOptions";
-
- public AccountAuthenticator(Context context) {
- super(context);
- }
-
- @Override
- public Bundle addAccount(AccountAuthenticatorResponse response,
- String accountType, String authTokenType,
- String[] requiredFeatures, Bundle options)
- throws NetworkErrorException {
- //Log.e("WORKAROUND", "Yes, WORKAROUND takes the control here");
- final Intent intent = new Intent("com.owncloud.android.workaround.accounts.CREATE");
- intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
- response);
- intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType);
- intent.putExtra(KEY_REQUIRED_FEATURES, requiredFeatures);
- intent.putExtra(KEY_LOGIN_OPTIONS, options);
-
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
- intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- intent.addFlags(Intent.FLAG_FROM_BACKGROUND);
-
- final Bundle bundle = new Bundle();
- bundle.putParcelable(AccountManager.KEY_INTENT, intent);
- return bundle;
- //return getCommonResultBundle();
- }
-
-
- @Override
- public Bundle confirmCredentials(AccountAuthenticatorResponse response,
- Account account, Bundle options) throws NetworkErrorException {
- return getCommonResultBundle();
- }
-
- @Override
- public Bundle editProperties(AccountAuthenticatorResponse response,
- String accountType) {
- return getCommonResultBundle();
- }
-
- @Override
- public Bundle getAuthToken(AccountAuthenticatorResponse response,
- Account account, String authTokenType, Bundle options)
- throws NetworkErrorException {
- return getCommonResultBundle();
- }
-
- @Override
- public String getAuthTokenLabel(String authTokenType) {
- return "";
- }
-
- @Override
- public Bundle hasFeatures(AccountAuthenticatorResponse response,
- Account account, String[] features) throws NetworkErrorException {
- return getCommonResultBundle();
- }
-
- @Override
- public Bundle updateCredentials(AccountAuthenticatorResponse response,
- Account account, String authTokenType, Bundle options)
- throws NetworkErrorException {
- return getCommonResultBundle();
- }
-
- @Override
- public Bundle getAccountRemovalAllowed(
- AccountAuthenticatorResponse response, Account account)
- throws NetworkErrorException {
- return super.getAccountRemovalAllowed(response, account);
- }
-
- private Bundle getCommonResultBundle() {
- Bundle resultBundle = new Bundle();
- resultBundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
- resultBundle.putString(AccountManager.KEY_ERROR_MESSAGE, "This is just a workaround, not a real account authenticator");
- return resultBundle;
- }
-
- }
-}
diff --git a/owncloud-android-library b/owncloud-android-library
deleted file mode 160000
index 0ce18350cfdf..000000000000
--- a/owncloud-android-library
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 0ce18350cfdf246ecad91aab8345c5141c12339c
diff --git a/owncloudApp/build.gradle b/owncloudApp/build.gradle
new file mode 100644
index 000000000000..738431de7bc8
--- /dev/null
+++ b/owncloudApp/build.gradle
@@ -0,0 +1,219 @@
+apply plugin: 'com.android.application'
+apply plugin: 'com.google.devtools.ksp'
+apply plugin: 'kotlin-parcelize'
+
+def commitSHA1 = 'COMMIT_SHA1'
+def gitRemote = 'GIT_REMOTE'
+
+dependencies {
+ // Data and domain modules
+ implementation project(':owncloudDomain')
+ implementation project(':owncloudData')
+
+ // Kotlin
+ implementation libs.kotlin.stdlib
+ implementation libs.kotlinx.coroutines.core
+
+ // Android X
+ 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" }
+ implementation libs.androidx.emoji2.emojipicker
+
+ // Image loading
+ implementation libs.coil
+ implementation libs.glide
+ implementation libs.glide.vector
+
+ // Zooming Android ImageView.
+ implementation libs.photoview
+
+ // Koin dependency injector
+ implementation libs.koin.androidx.workmanager
+ implementation libs.koin.core
+
+ // Miscellaneous
+ implementation libs.disklrucache
+ implementation libs.media3.exoplayer
+ implementation libs.media3.ui
+ implementation libs.floatingactionbutton
+ implementation libs.material
+ implementation libs.patternlockview
+
+ // Markdown Preview
+ implementation libs.bundles.markwon
+
+ // Timber
+ implementation libs.timber
+
+ // Tests
+ testImplementation project(":owncloudTestUtil")
+ testImplementation libs.androidx.arch.core.testing
+ testImplementation libs.junit4
+ testImplementation libs.kotlinx.coroutines.test
+ testImplementation libs.mockk
+
+ // Instrumented tests
+ androidTestImplementation project(":owncloudTestUtil")
+ 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
+
+ // Detekt
+ detektPlugins libs.detekt.formatting
+ detektPlugins libs.detekt.libraries
+}
+
+android {
+ compileSdk sdkCompileVersion
+
+ defaultConfig {
+ minSdk sdkMinVersion
+ targetSdk sdkTargetVersion
+
+ testInstrumentationRunner "com.owncloud.android.utils.OCTestAndroidJUnitRunner"
+
+ versionCode = 48000400
+ versionName = "4.8.4"
+
+ buildConfigField "String", gitRemote, "\"" + getGitOriginRemote() + "\""
+ buildConfigField "String", commitSHA1, "\"" + getLatestGitHash() + "\""
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ sourceSets {
+ androidTest.java.srcDirs += "src/test-common/java"
+ test.java.srcDirs += "src/test-common/java"
+ }
+
+ lint {
+ abortOnError true
+ htmlOutput file('../lint-app-report.html')
+ ignoreWarnings false
+ xmlReport false
+ }
+
+ signingConfigs {
+ release {
+ if (System.env.OC_RELEASE_KEYSTORE) {
+ storeFile file(System.env.OC_RELEASE_KEYSTORE) // use an absolute path
+ storePassword System.env.OC_RELEASE_KEYSTORE_PASSWORD
+ keyAlias System.env.OC_RELEASE_KEY_ALIAS
+ keyPassword System.env.OC_RELEASE_KEY_PASSWORD
+ }
+ }
+ }
+
+ buildTypes {
+
+ release {
+ if (System.env.OC_RELEASE_KEYSTORE) {
+ signingConfig signingConfigs.release
+ }
+ }
+
+ debug {
+ applicationIdSuffix ".debug"
+ }
+ }
+
+ flavorDimensions "management"
+ productFlavors {
+ original {
+ dimension "management"
+ }
+ mdm {
+ dimension "management"
+ }
+ qa {
+ dimension "management"
+ }
+ }
+
+ testOptions {
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging = true
+ }
+ }
+ unitTests.returnDefaultValues = true
+ animationsDisabled = true
+ }
+
+ buildFeatures {
+ viewBinding true
+ buildConfig true
+ }
+
+ packaging {
+ resources.excludes.add("META-INF/*")
+ }
+
+ namespace "com.owncloud.android"
+ testNamespace "com.owncloud.android.test"
+}
+
+// Updates output file names of a given variant to format
+// [appName].[variant.versionName].[OC_BUILD_NUMBER]-[variant.name].apk using the new AGP Variant API (androidComponents)
+androidComponents {
+ onVariants(selector().all()) { variant ->
+ def appName = System.env.OC_APP_NAME ?: "owncloud"
+ def buildNumber = System.env.OC_BUILD_NUMBER
+ variant.outputs.each { output ->
+ if (output.outputType == com.android.build.api.variant.VariantOutputConfiguration.OutputType.SINGLE) {
+ output.outputFileName.set(output.versionName.map { versionName ->
+ def newName = appName + "_" + versionName
+ if (buildNumber) {
+ newName += "_" + buildNumber
+ }
+ newName += "-" + variant.name + ".apk"
+ newName
+ })
+ }
+ }
+ }
+}
+
+static def getLatestGitHash() {
+ def process = "git rev-parse --short HEAD".execute()
+ return process.text.toString().trim()
+}
+
+static def getGitOriginRemote() {
+ def process = "git remote -v".execute()
+ def values = process.text.toString().trim().split("\\r\\n|\\n|\\r")
+
+ def found = values.find { it.startsWith("origin") && it.endsWith("(push)") }
+ return found.replace("origin", "").replace("(push)", "").replace(".git", "").trim()
+}
diff --git a/owncloudApp/lint.xml b/owncloudApp/lint.xml
new file mode 100644
index 000000000000..e483e5db5a14
--- /dev/null
+++ b/owncloudApp/lint.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt
new file mode 100644
index 000000000000..1e4f32d7b86d
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/authentication/LoginActivityTest.kt
@@ -0,0 +1,813 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2024 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.authentication
+
+import android.accounts.AccountManager.KEY_ACCOUNT_NAME
+import android.accounts.AccountManager.KEY_ACCOUNT_TYPE
+import android.app.Activity.RESULT_OK
+import android.app.Instrumentation
+import android.content.Context
+import android.content.Intent
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.closeSoftKeyboard
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.matcher.RootMatchers.isDialog
+import androidx.test.espresso.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.owncloud.android.R
+import com.owncloud.android.domain.exceptions.NoNetworkConnectionException
+import com.owncloud.android.domain.exceptions.OwncloudVersionNotSupportedException
+import com.owncloud.android.domain.exceptions.ServerNotReachableException
+import com.owncloud.android.domain.exceptions.UnauthorizedException
+import com.owncloud.android.domain.server.model.ServerInfo
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.extensions.parseError
+import com.owncloud.android.presentation.authentication.ACTION_UPDATE_EXPIRED_TOKEN
+import com.owncloud.android.presentation.authentication.ACTION_UPDATE_TOKEN
+import com.owncloud.android.presentation.authentication.AuthenticationViewModel
+import com.owncloud.android.presentation.authentication.BASIC_TOKEN_TYPE
+import com.owncloud.android.presentation.authentication.EXTRA_ACCOUNT
+import com.owncloud.android.presentation.authentication.EXTRA_ACTION
+import com.owncloud.android.presentation.authentication.KEY_AUTH_TOKEN_TYPE
+import com.owncloud.android.presentation.authentication.LoginActivity
+import com.owncloud.android.presentation.authentication.OAUTH_TOKEN_TYPE
+import com.owncloud.android.presentation.authentication.oauth.OAuthViewModel
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.presentation.settings.SettingsActivity
+import com.owncloud.android.presentation.settings.SettingsViewModel
+import com.owncloud.android.providers.ContextProvider
+import com.owncloud.android.providers.MdmProvider
+import com.owncloud.android.testutil.OC_ACCOUNT
+import com.owncloud.android.testutil.OC_AUTH_TOKEN_TYPE
+import com.owncloud.android.testutil.OC_BASIC_PASSWORD
+import com.owncloud.android.testutil.OC_BASIC_USERNAME
+import com.owncloud.android.testutil.OC_INSECURE_SERVER_INFO_BASIC_AUTH
+import com.owncloud.android.testutil.OC_SECURE_SERVER_INFO_BASIC_AUTH
+import com.owncloud.android.testutil.OC_SECURE_SERVER_INFO_BEARER_AUTH
+import com.owncloud.android.utils.CONFIGURATION_SERVER_URL
+import com.owncloud.android.utils.CONFIGURATION_SERVER_URL_INPUT_VISIBILITY
+import com.owncloud.android.utils.NO_MDM_RESTRICTION_YET
+import com.owncloud.android.utils.matchers.assertVisibility
+import com.owncloud.android.utils.matchers.isDisplayed
+import com.owncloud.android.utils.matchers.isEnabled
+import com.owncloud.android.utils.matchers.isFocusable
+import com.owncloud.android.utils.matchers.withText
+import com.owncloud.android.utils.mockIntentToComponent
+import com.owncloud.android.utils.replaceText
+import com.owncloud.android.utils.scrollAndClick
+import com.owncloud.android.utils.typeText
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import io.mockk.verify
+import org.hamcrest.Matchers.allOf
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+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 LoginActivityTest {
+
+ private lateinit var activityScenario: ActivityScenario
+
+ private lateinit var authenticationViewModel: AuthenticationViewModel
+ private lateinit var oauthViewModel: OAuthViewModel
+ private lateinit var settingsViewModel: SettingsViewModel
+ private lateinit var ocContextProvider: ContextProvider
+ private lateinit var mdmProvider: MdmProvider
+ private lateinit var context: Context
+
+ private lateinit var loginResultLiveData: MutableLiveData>>
+ private lateinit var serverInfoLiveData: MutableLiveData>>
+ private lateinit var supportsOauth2LiveData: MutableLiveData>>
+ private lateinit var baseUrlLiveData: MutableLiveData>>
+ private lateinit var accountDiscoveryLiveData: MutableLiveData>>
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+
+ authenticationViewModel = mockk(relaxed = true)
+ oauthViewModel = mockk(relaxed = true)
+ settingsViewModel = mockk(relaxUnitFun = true)
+ ocContextProvider = mockk(relaxed = true)
+ mdmProvider = mockk(relaxed = true)
+
+ loginResultLiveData = MutableLiveData()
+ serverInfoLiveData = MutableLiveData()
+ supportsOauth2LiveData = MutableLiveData()
+ baseUrlLiveData = MutableLiveData()
+ accountDiscoveryLiveData = MutableLiveData()
+
+ every { authenticationViewModel.loginResult } returns loginResultLiveData
+ every { authenticationViewModel.serverInfo } returns serverInfoLiveData
+ every { authenticationViewModel.supportsOAuth2 } returns supportsOauth2LiveData
+ every { authenticationViewModel.baseUrl } returns baseUrlLiveData
+ every { authenticationViewModel.accountDiscovery } returns accountDiscoveryLiveData
+ every { settingsViewModel.isThereAttachedAccount() } returns false
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ authenticationViewModel
+ }
+ viewModel {
+ oauthViewModel
+ }
+ viewModel {
+ settingsViewModel
+ }
+ factory {
+ ocContextProvider
+ }
+ factory {
+ mdmProvider
+ }
+ }
+ )
+ }
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ private fun launchTest(
+ showServerUrlInput: Boolean = true,
+ serverUrl: String = "",
+ showLoginBackGroundImage: Boolean = true,
+ showWelcomeLink: Boolean = true,
+ accountType: String = "owncloud",
+ loginWelcomeText: String = "",
+ webfingerLookupServer: String = "",
+ intent: Intent? = null
+ ) {
+ every { mdmProvider.getBrandingBoolean(CONFIGURATION_SERVER_URL_INPUT_VISIBILITY, R.bool.show_server_url_input) } returns showServerUrlInput
+ every { mdmProvider.getBrandingString(CONFIGURATION_SERVER_URL, R.string.server_url) } returns serverUrl
+ every { mdmProvider.getBrandingString(NO_MDM_RESTRICTION_YET, R.string.webfinger_lookup_server) } returns webfingerLookupServer
+ every { ocContextProvider.getBoolean(R.bool.use_login_background_image) } returns showLoginBackGroundImage
+ every { ocContextProvider.getBoolean(R.bool.show_welcome_link) } returns showWelcomeLink
+ every { ocContextProvider.getString(R.string.account_type) } returns accountType
+ every { ocContextProvider.getString(R.string.login_welcome_text) } returns loginWelcomeText
+ every { ocContextProvider.getString(R.string.app_name) } returns BRANDED_APP_NAME
+
+ activityScenario = if (intent == null) {
+ ActivityScenario.launch(LoginActivity::class.java)
+ } else {
+ ActivityScenario.launch(intent)
+ }
+ }
+
+ @Test
+ fun initialViewStatus_notBrandedOptions() {
+ launchTest()
+
+ assertViewsDisplayed()
+ assertWebfingerFlowDisplayed(webfingerEnabled = false)
+ }
+
+ @Test
+ fun initialViewStatus_brandedOptions_webfinger() {
+ launchTest(webfingerLookupServer = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
+
+ assertWebfingerFlowDisplayed(webfingerEnabled = true)
+ }
+
+ @Test
+ fun initialViewStatus_brandedOptions_serverInfoInSetup() {
+ launchTest(showServerUrlInput = false, serverUrl = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
+
+ assertViewsDisplayed(
+ showHostUrlFrame = false,
+ showHostUrlInput = false,
+ showCenteredRefreshButton = true,
+ showEmbeddedCheckServerButton = false
+ )
+ }
+
+ @Test
+ fun initialViewStatus_brandedOptions_serverInfoInSetup_connectionFails() {
+
+ launchTest(showServerUrlInput = false, serverUrl = OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
+
+ serverInfoLiveData.postValue(Event(UIResult.Error(NoNetworkConnectionException())))
+
+ R.id.centeredRefreshButton.isDisplayed(true)
+ R.id.centeredRefreshButton.scrollAndClick()
+
+ 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)
+ }
+
+ @Test
+ fun initialViewStatus_brandedOptions_dontUseLoginBackgroundImage() {
+ launchTest(showLoginBackGroundImage = false)
+
+ assertViewsDisplayed(showLoginBackGroundImage = false)
+ }
+
+ @Test
+ fun initialViewStatus_brandedOptions_dontShowWelcomeLink() {
+ launchTest(showWelcomeLink = false)
+
+ assertViewsDisplayed(showWelcomeLink = false)
+ }
+
+ @Test
+ fun initialViewStatus_brandedOptions_customWelcomeText() {
+ launchTest(showWelcomeLink = true, loginWelcomeText = CUSTOM_WELCOME_TEXT)
+
+ assertViewsDisplayed(showWelcomeLink = true)
+
+ R.id.welcome_link.withText(CUSTOM_WELCOME_TEXT)
+ }
+
+ @Test
+ fun initialViewStatus_brandedOptions_defaultWelcomeText() {
+ launchTest(showWelcomeLink = true, loginWelcomeText = "")
+
+ assertViewsDisplayed(showWelcomeLink = true)
+
+ R.id.welcome_link.withText(String.format(ocContextProvider.getString(R.string.auth_register), BRANDED_APP_NAME))
+ }
+
+ @Test
+ fun checkServerInfo_clickButton_callGetServerInfo() {
+ launchTest()
+
+ R.id.hostUrlInput.typeText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
+
+ R.id.embeddedCheckServerButton.scrollAndClick()
+
+ verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) }
+ }
+
+ @Test
+ fun checkServerInfo_clickLogo_callGetServerInfo() {
+ launchTest()
+ R.id.hostUrlInput.typeText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
+
+ R.id.thumbnail.scrollAndClick()
+
+ verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl, true) }
+ }
+
+ @Test
+ fun checkServerInfo_isLoading_show() {
+ launchTest()
+ serverInfoLiveData.postValue(Event(UIResult.Loading()))
+
+ with(R.id.server_status_text) {
+ isDisplayed(true)
+ withText(R.string.auth_testing_connection)
+ }
+ }
+
+ @Test
+ fun checkServerInfo_isSuccess_updateUrlInput() {
+ launchTest()
+ R.id.hostUrlInput.typeText("demo.owncloud.com")
+
+ serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
+
+ R.id.hostUrlInput.withText(SECURE_SERVER_INFO_BASIC.baseUrl)
+ }
+
+ @Test
+ fun checkServerInfo_isSuccess_Secure() {
+ launchTest()
+ serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
+
+ with(R.id.server_status_text) {
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ withText(R.string.auth_secure_connection)
+ }
+ }
+
+ @Test
+ fun checkServerInfo_isSuccess_NotSecure() {
+ launchTest()
+ serverInfoLiveData.postValue(Event(UIResult.Success(INSECURE_SERVER_INFO_BASIC)))
+
+ onView(withText(R.string.insecure_http_url_title_dialog)).check(matches(isDisplayed()))
+ onView(withText(R.string.insecure_http_url_message_dialog)).check(matches(isDisplayed()))
+ onView(withText(R.string.insecure_http_url_continue_button)).inRoot(isDialog()).check(matches(isDisplayed())).perform(click())
+
+ with(R.id.server_status_text) {
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ withText(R.string.auth_connection_established)
+ }
+ }
+
+ @Test
+ fun checkServerInfo_isSuccess_Basic() {
+ launchTest()
+
+ serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
+
+ checkBasicFieldsVisibility(loginButtonShouldBeVisible = false)
+ }
+
+ @Test
+ fun checkServerInfo_isSuccess_Bearer() {
+ Intents.init()
+ launchTest()
+ avoidOpeningChromeCustomTab()
+
+ serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BEARER)))
+
+ checkBearerFieldsVisibility()
+ Intents.release()
+ }
+
+ @Test
+ fun checkServerInfo_isSuccess_basicModifyUrlInput() {
+ launchTest()
+
+ serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
+
+ checkBasicFieldsVisibility()
+
+ R.id.account_username.typeText(OC_BASIC_USERNAME)
+
+ R.id.account_password.typeText(OC_BASIC_PASSWORD)
+
+ R.id.hostUrlInput.typeText("anything")
+
+ with(R.id.account_username) {
+ withText("")
+ assertVisibility(Visibility.GONE)
+ }
+
+ with(R.id.account_password) {
+ withText("")
+ assertVisibility(Visibility.GONE)
+ }
+
+ R.id.loginButton.assertVisibility(Visibility.GONE)
+ }
+
+ @Test
+ fun checkServerInfo_isSuccess_bearerModifyUrlInput() {
+ Intents.init()
+ launchTest()
+ avoidOpeningChromeCustomTab()
+
+ serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BEARER)))
+
+ checkBearerFieldsVisibility()
+
+ R.id.hostUrlInput.typeText("anything")
+
+ R.id.auth_status_text.assertVisibility(Visibility.GONE)
+ Intents.release()
+ }
+
+ @Test
+ fun checkServerInfo_isError_emptyUrl() {
+ launchTest()
+
+ R.id.hostUrlInput.typeText("")
+
+ R.id.embeddedCheckServerButton.scrollAndClick()
+
+ with(R.id.server_status_text) {
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ withText(R.string.auth_can_not_auth_against_server)
+ }
+
+ verify(exactly = 0) { authenticationViewModel.getServerInfo(any()) }
+ }
+
+ @Test
+ fun checkServerInfo_isError_ownCloudVersionNotSupported() {
+ launchTest()
+
+ serverInfoLiveData.postValue(Event(UIResult.Error(OwncloudVersionNotSupportedException())))
+
+ with(R.id.server_status_text) {
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ withText(R.string.server_not_supported)
+ }
+ }
+
+ @Test
+ fun checkServerInfo_isError_noNetworkConnection() {
+ launchTest()
+
+ serverInfoLiveData.postValue(Event(UIResult.Error(NoNetworkConnectionException())))
+
+ with(R.id.server_status_text) {
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ withText(R.string.error_no_network_connection)
+ }
+ }
+
+ @Test
+ fun checkServerInfo_isError_otherExceptions() {
+ launchTest()
+
+ serverInfoLiveData.postValue(Event(UIResult.Error(ServerNotReachableException())))
+
+ with(R.id.server_status_text) {
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ withText(R.string.network_host_not_available)
+ }
+ }
+
+ @Test
+ fun loginBasic_callLoginBasic() {
+ launchTest()
+
+ serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
+
+ R.id.account_username.typeText(OC_BASIC_USERNAME)
+
+ R.id.account_password.typeText(OC_BASIC_PASSWORD)
+
+ with(R.id.loginButton) {
+ isDisplayed(true)
+ scrollAndClick()
+ }
+
+ verify(exactly = 1) { authenticationViewModel.loginBasic(OC_BASIC_USERNAME, OC_BASIC_PASSWORD, null) }
+ }
+
+ @Test
+ fun loginBasic_callLoginBasic_trimUsername() {
+ launchTest()
+
+ serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
+
+ R.id.account_username.typeText(" $OC_BASIC_USERNAME ")
+
+ R.id.account_password.typeText(OC_BASIC_PASSWORD)
+
+ with(R.id.loginButton) {
+ isDisplayed(true)
+ scrollAndClick()
+ }
+
+ verify(exactly = 1) { authenticationViewModel.loginBasic(OC_BASIC_USERNAME, OC_BASIC_PASSWORD, null) }
+ }
+
+ @Test
+ fun loginBasic_showOrHideFields() {
+ launchTest()
+
+ serverInfoLiveData.postValue(Event(UIResult.Success(SECURE_SERVER_INFO_BASIC)))
+
+ R.id.account_username.typeText(OC_BASIC_USERNAME)
+
+ R.id.loginButton.isDisplayed(false)
+
+ R.id.account_password.typeText(OC_BASIC_PASSWORD)
+
+ R.id.loginButton.isDisplayed(true)
+
+ R.id.account_username.replaceText("")
+
+ R.id.loginButton.isDisplayed(false)
+
+ }
+
+ @Test
+ fun login_isLoading() {
+ launchTest()
+
+ loginResultLiveData.postValue(Event(UIResult.Loading()))
+
+ with(R.id.auth_status_text) {
+ withText(R.string.auth_trying_to_login)
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ }
+ }
+
+ @Test
+ fun login_isSuccess_finishResultCode() {
+ launchTest()
+
+ loginResultLiveData.postValue(Event(UIResult.Success(data = "Account_name")))
+ accountDiscoveryLiveData.postValue(Event(UIResult.Success()))
+
+ assertEquals(activityScenario.result.resultCode, RESULT_OK)
+ val accountName: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_NAME)
+ val accountType: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_TYPE)
+
+ assertNotNull(accountName)
+ assertNotNull(accountType)
+ assertEquals("Account_name", accountName)
+ assertEquals("owncloud", accountType)
+ }
+
+ @Test
+ fun login_isSuccess_finishResultCodeBrandedAccountType() {
+ launchTest(accountType = "notOwnCloud")
+
+ loginResultLiveData.postValue(Event(UIResult.Success(data = "Account_name")))
+ accountDiscoveryLiveData.postValue(Event(UIResult.Success()))
+
+ assertEquals(activityScenario.result.resultCode, RESULT_OK)
+ val accountName: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_NAME)
+ val accountType: String? = activityScenario.result?.resultData?.extras?.getString(KEY_ACCOUNT_TYPE)
+
+ assertNotNull(accountName)
+ assertNotNull(accountType)
+ assertEquals("Account_name", accountName)
+ assertEquals("notOwnCloud", accountType)
+ }
+
+ @Test
+ fun login_isError_NoNetworkConnectionException() {
+ launchTest()
+
+ loginResultLiveData.postValue(Event(UIResult.Error(NoNetworkConnectionException())))
+
+ R.id.server_status_text.withText(R.string.error_no_network_connection)
+
+ checkBasicFieldsVisibility(fieldsShouldBeVisible = false)
+ }
+
+ @Test
+ fun login_isError_ServerNotReachableException() {
+ launchTest()
+
+ loginResultLiveData.postValue(Event(UIResult.Error(ServerNotReachableException())))
+
+ R.id.server_status_text.withText(R.string.error_no_network_connection)
+
+ checkBasicFieldsVisibility(fieldsShouldBeVisible = false)
+ }
+
+ @Test
+ fun login_isError_OtherException() {
+ launchTest()
+
+ val exception = UnauthorizedException()
+
+ loginResultLiveData.postValue(Event(UIResult.Error(exception)))
+
+ R.id.auth_status_text.withText(exception.parseError("", context.resources, true) as String)
+ }
+
+ @Test
+ fun intent_withSavedAccount_viewModelCalls() {
+ val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
+ putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
+ }
+
+ launchTest(intent = intentWithAccount)
+
+ verify(exactly = 1) { authenticationViewModel.supportsOAuth2(OC_ACCOUNT.name) }
+ verify(exactly = 1) { authenticationViewModel.getBaseUrl(OC_ACCOUNT.name) }
+ }
+
+ @Test
+ fun supportsOAuth_isSuccess_actionUpdateExpiredTokenOAuth() {
+ val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
+ putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
+ putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN)
+ putExtra(KEY_AUTH_TOKEN_TYPE, OAUTH_TOKEN_TYPE)
+ }
+
+ launchTest(intent = intentWithAccount)
+
+ supportsOauth2LiveData.postValue(Event(UIResult.Success(true)))
+
+ with(R.id.instructions_message) {
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ withText(context.getString(R.string.auth_expired_oauth_token_toast))
+ }
+ }
+
+ @Test
+ fun supportsOAuth_isSuccess_actionUpdateToken() {
+ val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
+ putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
+ putExtra(EXTRA_ACTION, ACTION_UPDATE_TOKEN)
+ putExtra(KEY_AUTH_TOKEN_TYPE, OC_AUTH_TOKEN_TYPE)
+ }
+
+ launchTest(intent = intentWithAccount)
+
+ supportsOauth2LiveData.postValue(Event(UIResult.Success(false)))
+
+ R.id.instructions_message.assertVisibility(Visibility.GONE)
+ }
+
+ @Test
+ fun supportsOAuth_isSuccess_actionUpdateExpiredTokenBasic() {
+ val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
+ putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
+ putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN)
+ putExtra(KEY_AUTH_TOKEN_TYPE, BASIC_TOKEN_TYPE)
+ }
+
+ launchTest(intent = intentWithAccount)
+
+ supportsOauth2LiveData.postValue(Event(UIResult.Success(false)))
+
+ with(R.id.instructions_message) {
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ withText(context.getString(R.string.auth_expired_basic_auth_toast))
+ }
+ }
+
+ @Test
+ fun getBaseUrl_isSuccess_updatesBaseUrl() {
+ val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
+ putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
+ }
+
+ launchTest(intent = intentWithAccount)
+
+ baseUrlLiveData.postValue(Event(UIResult.Success(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)))
+
+ with(R.id.hostUrlInput) {
+ withText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
+ assertVisibility(Visibility.VISIBLE)
+ isDisplayed(true)
+ isEnabled(false)
+ isFocusable(false)
+ }
+
+ verify(exactly = 0) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl) }
+ }
+
+ @Test
+ fun getBaseUrlAndActionNotCreate_isSuccess_updatesBaseUrl() {
+ val intentWithAccount = Intent(context, LoginActivity::class.java).apply {
+ putExtra(EXTRA_ACCOUNT, OC_ACCOUNT)
+ putExtra(EXTRA_ACTION, ACTION_UPDATE_EXPIRED_TOKEN)
+ }
+
+ launchTest(intent = intentWithAccount)
+
+ baseUrlLiveData.postValue(Event(UIResult.Success(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)))
+
+ with(R.id.hostUrlInput) {
+ withText(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl)
+ assertVisibility(Visibility.VISIBLE)
+ isDisplayed(true)
+ isEnabled(false)
+ isFocusable(false)
+ }
+
+ verify(exactly = 1) { authenticationViewModel.getServerInfo(OC_SECURE_SERVER_INFO_BASIC_AUTH.baseUrl) }
+ }
+
+ @Test
+ fun settingsLink() {
+ Intents.init()
+ launchTest()
+
+ closeSoftKeyboard()
+
+ mockIntentToComponent(RESULT_OK, SettingsActivity::class.java.name)
+ onView(withId(R.id.settings_link)).perform(click())
+ intended(hasComponent(SettingsActivity::class.java.name))
+
+ Intents.release()
+ }
+
+ private fun avoidOpeningChromeCustomTab() {
+ Intents.intending(allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW)))
+ .respondWith(Instrumentation.ActivityResult(RESULT_OK, null))
+ }
+
+ private fun checkBasicFieldsVisibility(
+ fieldsShouldBeVisible: Boolean = true,
+ loginButtonShouldBeVisible: Boolean = false
+ ) {
+ val visibilityMatcherFields = if (fieldsShouldBeVisible) Visibility.VISIBLE else Visibility.GONE
+ val visibilityMatcherLoginButton = if (loginButtonShouldBeVisible) Visibility.VISIBLE else Visibility.GONE
+
+ with(R.id.account_username) {
+ isDisplayed(fieldsShouldBeVisible)
+ assertVisibility(visibilityMatcherFields)
+ withText("")
+ }
+
+ with(R.id.account_password) {
+ isDisplayed(fieldsShouldBeVisible)
+ assertVisibility(visibilityMatcherFields)
+ withText("")
+ }
+
+ R.id.loginButton.assertVisibility(visibilityMatcherLoginButton)
+ R.id.auth_status_text.assertVisibility(visibilityMatcherLoginButton)
+ }
+
+ private fun checkBearerFieldsVisibility() {
+ R.id.account_username.assertVisibility(Visibility.GONE)
+ R.id.account_password.assertVisibility(Visibility.GONE)
+ R.id.auth_status_text.assertVisibility(Visibility.GONE)
+
+ with(R.id.server_status_text) {
+ isDisplayed(true)
+ assertVisibility(Visibility.VISIBLE)
+ }
+ }
+
+ private fun assertWebfingerFlowDisplayed(
+ webfingerEnabled: Boolean,
+ ) {
+ R.id.webfinger_layout.isDisplayed(webfingerEnabled)
+ R.id.webfinger_username.isDisplayed(webfingerEnabled)
+ R.id.webfinger_button.isDisplayed(webfingerEnabled)
+ }
+
+ private fun assertViewsDisplayed(
+ showLoginBackGroundImage: Boolean = true,
+ showThumbnail: Boolean = true,
+ showCenteredRefreshButton: Boolean = false,
+ showInstructionsMessage: Boolean = false,
+ showHostUrlFrame: Boolean = true,
+ showHostUrlInput: Boolean = true,
+ showEmbeddedCheckServerButton: Boolean = true,
+ showEmbeddedRefreshButton: Boolean = false,
+ showServerStatusText: Boolean = false,
+ showAccountUsername: Boolean = false,
+ showAccountPassword: Boolean = false,
+ showAuthStatus: Boolean = false,
+ showLoginButton: Boolean = false,
+ showWelcomeLink: Boolean = true
+ ) {
+ R.id.login_background_image.isDisplayed(displayed = showLoginBackGroundImage)
+ R.id.thumbnail.isDisplayed(displayed = showThumbnail)
+ R.id.centeredRefreshButton.isDisplayed(displayed = showCenteredRefreshButton)
+ R.id.instructions_message.isDisplayed(displayed = showInstructionsMessage)
+ R.id.hostUrlFrame.isDisplayed(displayed = showHostUrlFrame)
+ R.id.hostUrlInput.isDisplayed(displayed = showHostUrlInput)
+ R.id.embeddedCheckServerButton.isDisplayed(displayed = showEmbeddedCheckServerButton)
+ R.id.embeddedRefreshButton.isDisplayed(displayed = showEmbeddedRefreshButton)
+ R.id.server_status_text.isDisplayed(displayed = showServerStatusText)
+ R.id.account_username_container.isDisplayed(displayed = showAccountUsername)
+ R.id.account_username.isDisplayed(displayed = showAccountUsername)
+ R.id.account_password_container.isDisplayed(displayed = showAccountPassword)
+ R.id.account_password.isDisplayed(displayed = showAccountPassword)
+ R.id.auth_status_text.isDisplayed(displayed = showAuthStatus)
+ R.id.loginButton.isDisplayed(displayed = showLoginButton)
+ R.id.welcome_link.isDisplayed(displayed = showWelcomeLink)
+ }
+
+ companion object {
+ val SECURE_SERVER_INFO_BASIC = OC_SECURE_SERVER_INFO_BASIC_AUTH
+ val INSECURE_SERVER_INFO_BASIC = OC_INSECURE_SERVER_INFO_BASIC_AUTH
+ val SECURE_SERVER_INFO_BEARER = OC_SECURE_SERVER_INFO_BEARER_AUTH
+ private const val CUSTOM_WELCOME_TEXT = "Welcome to this test"
+ private const val BRANDED_APP_NAME = "BrandedAppName"
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/files/SortBottomSheetFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/files/SortBottomSheetFragmentTest.kt
new file mode 100644
index 000000000000..cccb370e780d
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/files/SortBottomSheetFragmentTest.kt
@@ -0,0 +1,97 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ *
+ * Copyright (C) 2020 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.os.Bundle
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.fragment.app.testing.launchFragment
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.owncloud.android.R
+import com.owncloud.android.presentation.files.SortBottomSheetFragment
+import com.owncloud.android.presentation.files.SortOrder
+import com.owncloud.android.presentation.files.SortType
+import com.owncloud.android.utils.matchers.bsfItemWithIcon
+import com.owncloud.android.utils.matchers.bsfItemWithTitle
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.junit.Before
+import org.junit.Test
+
+class SortBottomSheetFragmentTest {
+
+ private lateinit var fragmentScenario: FragmentScenario
+ private val fragmentListener = mockk()
+
+ @Before
+ fun setUp() {
+ val fragmentArgs = Bundle().apply {
+ putParcelable(SortBottomSheetFragment.ARG_SORT_TYPE, SortType.SORT_TYPE_BY_NAME)
+ putParcelable(SortBottomSheetFragment.ARG_SORT_ORDER, SortOrder.SORT_ORDER_ASCENDING)
+ }
+ fragmentScenario = launchFragment(fragmentArgs)
+ every { fragmentListener.onSortSelected(any()) } returns Unit
+ fragmentScenario.onFragment { it.sortDialogListener = fragmentListener }
+ }
+
+ @Test
+ fun test_initial_view() {
+ onView(withId(R.id.title))
+ .inRoot(RootMatchers.isDialog())
+ .check(matches(ViewMatchers.withText(R.string.actionbar_sort_title)))
+ .check(matches(ViewMatchers.hasTextColor(R.color.bottom_sheet_fragment_title_color)))
+
+ with(R.id.sort_by_name) {
+ bsfItemWithTitle(R.string.global_name, R.color.primary)
+ bsfItemWithIcon(R.drawable.ic_sort_by_name, R.color.primary)
+ }
+ with(R.id.sort_by_size) {
+ bsfItemWithTitle(R.string.global_size, R.color.bottom_sheet_fragment_item_color)
+ bsfItemWithIcon(R.drawable.ic_sort_by_size, R.color.bottom_sheet_fragment_item_color)
+ }
+ with(R.id.sort_by_date) {
+ bsfItemWithTitle(R.string.global_date, R.color.bottom_sheet_fragment_item_color)
+ bsfItemWithIcon(R.drawable.ic_sort_by_date, R.color.bottom_sheet_fragment_item_color)
+ }
+ }
+
+ @Test
+ fun test_sort_by_name_click() {
+ onView(withId(R.id.sort_by_name)).inRoot(RootMatchers.isDialog()).perform(ViewActions.click())
+ verify { fragmentListener.onSortSelected(SortType.SORT_TYPE_BY_NAME) }
+ }
+
+ @Test
+ fun test_sort_by_date_click() {
+ onView(withId(R.id.sort_by_date)).inRoot(RootMatchers.isDialog()).perform(ViewActions.click())
+ verify { fragmentListener.onSortSelected(SortType.SORT_TYPE_BY_DATE) }
+ }
+
+ @Test
+ fun test_sort_by_size_click() {
+ onView(withId(R.id.sort_by_size)).inRoot(RootMatchers.isDialog()).perform(ViewActions.click())
+ verify { fragmentListener.onSortSelected(SortType.SORT_TYPE_BY_SIZE) }
+ }
+}
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 000000000000..1eb061049dee
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/files/details/FileDetailsFragmentTest.kt
@@ -0,0 +1,215 @@
+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_WITH_SYNC_INFO_AVAILABLE_OFFLINE
+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_WITH_SYNC_INFO_AVAILABLE_OFFLINE)
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ fileDetailsViewModel = mockk(relaxed = true)
+ fileOperationsViewModel = mockk(relaxed = true)
+ every { fileDetailsViewModel.currentFile } returns currentFile
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ fileDetailsViewModel
+ }
+ viewModel {
+ fileOperationsViewModel
+ }
+ }
+ )
+
+ }
+
+ val fileDetailsFragment = FileDetailsFragment.newInstance(
+ OC_FILE,
+ OC_ACCOUNT,
+ syncFileAtOpen = false,
+ isMultiPersonal = 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)
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/logging/LogsListActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/logging/LogsListActivityTest.kt
new file mode 100644
index 000000000000..deec84e0112c
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/logging/LogsListActivityTest.kt
@@ -0,0 +1,106 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Fernando Sanz Velasco
+ * Copyright (C) 2021 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.logging
+
+import androidx.test.core.app.ActivityScenario
+import com.owncloud.android.R
+import com.owncloud.android.presentation.logging.LogsListActivity
+import com.owncloud.android.presentation.logging.LogListViewModel
+import com.owncloud.android.utils.matchers.assertChildCount
+import com.owncloud.android.utils.matchers.isDisplayed
+import com.owncloud.android.utils.matchers.withText
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+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
+import java.io.File
+
+@Ignore
+class LogsListActivityTest {
+
+ private lateinit var activityScenario: ActivityScenario
+
+ private lateinit var logListViewModel: LogListViewModel
+
+ private fun launchTest(logs: List) {
+ every { logListViewModel.getLogsFiles() } returns logs
+ activityScenario = ActivityScenario.launch(LogsListActivity::class.java)
+ }
+
+ @Before
+ fun setUp() {
+ logListViewModel = mockk(relaxed = true)
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ modules(
+ module {
+ viewModel {
+ logListViewModel
+ }
+ }
+ )
+
+ }
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun test_visibility_toolbar() {
+ launchTest(logs = emptyList())
+ R.id.toolbar_activity_logs_list.isDisplayed(true)
+ }
+
+ @Test
+ fun test_isRecyclerViewEmpty_show_label() {
+ launchTest(logs = emptyList())
+ R.id.logs_list_empty.isDisplayed(true)
+ R.id.list_empty_dataset_title.withText(R.string.prefs_log_no_logs_list_view)
+ R.id.list_empty_dataset_sub_title.withText(R.string.prefs_log_empty_subtitle)
+ R.id.recyclerView_activity_logs_list.isDisplayed(false)
+ }
+
+ @Test
+ fun test_isRecyclerViewNotEmpty_hide_label() {
+ launchTest(logs = listOf(File("path")))
+ R.id.logs_list_empty.isDisplayed(false)
+ R.id.recyclerView_activity_logs_list.isDisplayed(true)
+ }
+
+ @Test
+ fun test_childCount() {
+ launchTest(logs = listOf(File("owncloud.2021-01.01.log"), File("owncloud.2021-01-02.log")))
+ R.id.recyclerView_activity_logs_list.assertChildCount(2)
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/SettingsFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/SettingsFragmentTest.kt
new file mode 100644
index 000000000000..b1dc42881464
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/SettingsFragmentTest.kt
@@ -0,0 +1,300 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2021 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.settings
+
+import android.content.ClipboardManager
+import android.content.Context
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.fragment.app.testing.launchFragmentInContainer
+import androidx.preference.Preference
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.matcher.IntentMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isEnabled
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.R
+import com.owncloud.android.presentation.releasenotes.ReleaseNotesActivity
+import com.owncloud.android.presentation.settings.privacypolicy.PrivacyPolicyActivity
+import com.owncloud.android.presentation.settings.SettingsFragment
+import com.owncloud.android.presentation.releasenotes.ReleaseNotesViewModel
+import com.owncloud.android.presentation.settings.more.SettingsMoreViewModel
+import com.owncloud.android.presentation.settings.SettingsViewModel
+import com.owncloud.android.utils.matchers.verifyPreference
+import com.owncloud.android.utils.releaseNotesList
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Ignore
+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 SettingsFragmentTest {
+
+ private lateinit var fragmentScenario: FragmentScenario
+
+ private var subsectionSecurity: Preference? = null
+ private var subsectionLogging: Preference? = null
+ private var subsectionPictureUploads: Preference? = null
+ private var subsectionVideoUploads: Preference? = null
+ private var subsectionMore: Preference? = null
+ private var prefPrivacyPolicy: Preference? = null
+ private var subsectionWhatsNew: Preference? = null
+ private var prefAboutApp: Preference? = null
+
+ private lateinit var settingsViewModel: SettingsViewModel
+ private lateinit var moreViewModel: SettingsMoreViewModel
+ private lateinit var releaseNotesViewModel: ReleaseNotesViewModel
+ private lateinit var context: Context
+
+ private lateinit var version: String
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ settingsViewModel = mockk(relaxed = true)
+ moreViewModel = mockk(relaxed = true)
+ releaseNotesViewModel = mockk(relaxed = true)
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ settingsViewModel
+ }
+ viewModel {
+ moreViewModel
+ }
+ viewModel {
+ releaseNotesViewModel
+ }
+ }
+ )
+ }
+
+ version = String.format(
+ context.getString(R.string.prefs_app_version_summary),
+ context.getString(R.string.app_name),
+ BuildConfig.BUILD_TYPE,
+ BuildConfig.VERSION_NAME,
+ BuildConfig.COMMIT_SHA1
+ )
+
+ Intents.init()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ unmockkAll()
+ }
+
+ private fun launchTest(
+ attachedAccount: Boolean,
+ moreSectionVisible: Boolean = true,
+ privacyPolicyEnabled: Boolean = true,
+ whatsNewSectionVisible: Boolean = true
+ ) {
+ every { settingsViewModel.isThereAttachedAccount() } returns attachedAccount
+ every { moreViewModel.shouldMoreSectionBeVisible() } returns moreSectionVisible
+ every { moreViewModel.isPrivacyPolicyEnabled() } returns privacyPolicyEnabled
+ every { releaseNotesViewModel.shouldWhatsNewSectionBeVisible() } returns whatsNewSectionVisible
+
+ fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_ownCloud)
+ fragmentScenario.onFragment { fragment ->
+ subsectionSecurity = fragment.findPreference(SUBSECTION_SECURITY)
+ subsectionLogging = fragment.findPreference(SUBSECTION_LOGGING)
+ subsectionPictureUploads = fragment.findPreference(SUBSECTION_PICTURE_UPLOADS)
+ subsectionVideoUploads = fragment.findPreference(SUBSECTION_VIDEO_UPLOADS)
+ subsectionMore = fragment.findPreference(SUBSECTION_MORE)
+ prefPrivacyPolicy = fragment.findPreference(PREFERENCE_PRIVACY_POLICY)
+ subsectionWhatsNew = fragment.findPreference(SUBSECTION_WHATSNEW)
+ prefAboutApp = fragment.findPreference(PREFERENCE_ABOUT_APP)
+ }
+ }
+
+ @Test
+ fun settingsViewCommon() {
+ launchTest(attachedAccount = false)
+
+ subsectionSecurity?.verifyPreference(
+ keyPref = SUBSECTION_SECURITY,
+ titlePref = context.getString(R.string.prefs_subsection_security),
+ summaryPref = context.getString(R.string.prefs_subsection_security_summary),
+ visible = true,
+ enabled = true
+ )
+
+ subsectionLogging?.verifyPreference(
+ keyPref = SUBSECTION_LOGGING,
+ titlePref = context.getString(R.string.prefs_subsection_logging),
+ summaryPref = context.getString(R.string.prefs_subsection_logging_summary),
+ visible = true,
+ enabled = true
+ )
+
+ subsectionMore?.verifyPreference(
+ keyPref = SUBSECTION_MORE,
+ titlePref = context.getString(R.string.prefs_subsection_more),
+ summaryPref = context.getString(R.string.prefs_subsection_more_summary),
+ visible = true,
+ enabled = true
+ )
+
+ prefPrivacyPolicy?.verifyPreference(
+ keyPref = PREFERENCE_PRIVACY_POLICY,
+ titlePref = context.getString(R.string.prefs_privacy_policy),
+ visible = true,
+ enabled = true
+ )
+
+ subsectionWhatsNew?.verifyPreference(
+ keyPref = SUBSECTION_WHATSNEW,
+ titlePref = context.getString(R.string.prefs_subsection_whatsnew),
+ visible = true,
+ enabled = true
+ )
+
+ prefAboutApp?.verifyPreference(
+ keyPref = PREFERENCE_ABOUT_APP,
+ titlePref = context.getString(R.string.prefs_app_version),
+ summaryPref = version,
+ visible = true,
+ enabled = true
+ )
+ }
+
+ @Test
+ fun settingsViewNoAccountAttached() {
+ launchTest(attachedAccount = false)
+
+ subsectionPictureUploads?.verifyPreference(
+ keyPref = SUBSECTION_PICTURE_UPLOADS,
+ titlePref = context.getString(R.string.prefs_subsection_picture_uploads),
+ summaryPref = context.getString(R.string.prefs_subsection_picture_uploads_summary),
+ visible = false
+ )
+
+ subsectionVideoUploads?.verifyPreference(
+ keyPref = SUBSECTION_VIDEO_UPLOADS,
+ titlePref = context.getString(R.string.prefs_subsection_video_uploads),
+ summaryPref = context.getString(R.string.prefs_subsection_video_uploads_summary),
+ visible = false
+ )
+ }
+
+ @Test
+ fun settingsViewAccountAttached() {
+ launchTest(attachedAccount = true)
+
+ subsectionPictureUploads?.verifyPreference(
+ keyPref = SUBSECTION_PICTURE_UPLOADS,
+ titlePref = context.getString(R.string.prefs_subsection_picture_uploads),
+ summaryPref = context.getString(R.string.prefs_subsection_picture_uploads_summary),
+ visible = true,
+ enabled = true
+ )
+
+ subsectionVideoUploads?.verifyPreference(
+ keyPref = SUBSECTION_VIDEO_UPLOADS,
+ titlePref = context.getString(R.string.prefs_subsection_video_uploads),
+ summaryPref = context.getString(R.string.prefs_subsection_video_uploads_summary),
+ visible = true,
+ enabled = true
+ )
+ }
+
+ @Test
+ fun settingsMoreSectionHidden() {
+ launchTest(attachedAccount = false, moreSectionVisible = false)
+
+ subsectionMore?.verifyPreference(
+ keyPref = SUBSECTION_MORE,
+ titlePref = context.getString(R.string.prefs_subsection_more),
+ summaryPref = context.getString(R.string.prefs_subsection_more_summary),
+ visible = false
+ )
+ }
+
+ @Test
+ fun settingsWhatsNewSectionHidden() {
+ launchTest(attachedAccount = false, whatsNewSectionVisible = false)
+
+ subsectionWhatsNew?.verifyPreference(
+ keyPref = SUBSECTION_WHATSNEW,
+ titlePref = context.getString(R.string.prefs_subsection_whatsnew),
+ visible = false
+ )
+ }
+
+ @Test
+ fun privacyPolicyOpensPrivacyPolicyActivity() {
+ launchTest(attachedAccount = false)
+
+ onView(withText(R.string.prefs_privacy_policy)).perform(click())
+ Intents.intended(IntentMatchers.hasComponent(PrivacyPolicyActivity::class.java.name))
+ }
+
+ @Ignore("Flaky test")
+ @Test
+ fun whatsnewOpensReleaseNotesActivity() {
+ launchTest(attachedAccount = false)
+ every { releaseNotesViewModel.getReleaseNotes() } returns releaseNotesList
+
+ onView(withText(R.string.prefs_subsection_whatsnew)).perform(click())
+ Intents.intended(IntentMatchers.hasComponent(ReleaseNotesActivity::class.java.name))
+ }
+
+ @Test
+ fun clickOnAppVersion() {
+ launchTest(attachedAccount = false)
+
+ onView(withText(R.string.prefs_app_version)).perform(click())
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
+
+ onView(withText(R.string.clipboard_text_copied)).check(matches(isEnabled()))
+ assertEquals(version, clipboard?.primaryClip?.getItemAt(0)?.coerceToText(context))
+ }
+
+ companion object {
+ private const val SUBSECTION_SECURITY = "security_subsection"
+ private const val SUBSECTION_LOGGING = "logging_subsection"
+ private const val SUBSECTION_PICTURE_UPLOADS = "picture_uploads_subsection"
+ private const val SUBSECTION_VIDEO_UPLOADS = "video_uploads_subsection"
+ private const val SUBSECTION_MORE = "more_subsection"
+ private const val PREFERENCE_PRIVACY_POLICY = "privacyPolicy"
+ private const val PREFERENCE_ABOUT_APP = "about_app"
+ private const val SUBSECTION_WHATSNEW = "whatsNew"
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/advanced/SettingsAdvancedFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/advanced/SettingsAdvancedFragmentTest.kt
new file mode 100644
index 000000000000..e1c5237e2e71
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/advanced/SettingsAdvancedFragmentTest.kt
@@ -0,0 +1,109 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David Crespo Ríos
+ * Copyright (C) 2022 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.settings.advanced
+
+import android.content.Context
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.fragment.app.testing.launchFragmentInContainer
+import androidx.preference.SwitchPreferenceCompat
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.owncloud.android.R
+import com.owncloud.android.presentation.settings.advanced.SettingsAdvancedFragment
+import com.owncloud.android.presentation.settings.advanced.SettingsAdvancedFragment.Companion.PREF_SHOW_HIDDEN_FILES
+import com.owncloud.android.presentation.settings.advanced.SettingsAdvancedViewModel
+import com.owncloud.android.utils.matchers.verifyPreference
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+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 SettingsAdvancedFragmentTest {
+
+ private lateinit var fragmentScenario: FragmentScenario
+
+ private var prefShowHiddenFiles: SwitchPreferenceCompat? = null
+
+ private lateinit var advancedViewModel: SettingsAdvancedViewModel
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ advancedViewModel = mockk(relaxed = true)
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ advancedViewModel
+ }
+ }
+ )
+ }
+
+ every { advancedViewModel.isHiddenFilesShown() } returns true
+
+ fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_ownCloud)
+
+ fragmentScenario.onFragment { fragment ->
+ prefShowHiddenFiles = fragment.findPreference(PREF_SHOW_HIDDEN_FILES)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun advancedView() {
+ assertNotNull(prefShowHiddenFiles)
+ prefShowHiddenFiles?.verifyPreference(
+ keyPref = PREF_SHOW_HIDDEN_FILES,
+ titlePref = context.getString(R.string.prefs_show_hidden_files),
+ visible = true,
+ enabled = true
+ )
+ }
+
+ @Test
+ fun disableShowHiddenFiles() {
+ prefShowHiddenFiles?.isChecked = advancedViewModel.isHiddenFilesShown()
+
+ onView(withText(context.getString(R.string.prefs_show_hidden_files))).perform(click())
+
+ prefShowHiddenFiles?.isChecked?.let { assertFalse(it) }
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/logs/SettingsLogsFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/logs/SettingsLogsFragmentTest.kt
new file mode 100644
index 000000000000..e599329c6a2c
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/logs/SettingsLogsFragmentTest.kt
@@ -0,0 +1,207 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2021 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.settings.logs
+
+import android.content.Context
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.fragment.app.testing.launchFragmentInContainer
+import androidx.preference.CheckBoxPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceManager
+import androidx.preference.SwitchPreferenceCompat
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.owncloud.android.R
+import com.owncloud.android.presentation.logging.LogsListActivity
+import com.owncloud.android.presentation.settings.logging.SettingsLogsFragment
+import com.owncloud.android.presentation.logging.LogListViewModel
+import com.owncloud.android.presentation.settings.logging.SettingsLogsViewModel
+import com.owncloud.android.utils.matchers.verifyPreference
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+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 SettingsLogsFragmentTest {
+
+ private lateinit var fragmentScenario: FragmentScenario
+
+ private lateinit var prefEnableLogging: SwitchPreferenceCompat
+ private lateinit var prefHttpLogs: CheckBoxPreference
+ private lateinit var prefLogsListActivity: Preference
+
+ private lateinit var logsViewModel: SettingsLogsViewModel
+ private lateinit var logListViewModel: LogListViewModel
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ logsViewModel = mockk(relaxed = true)
+ logListViewModel = mockk(relaxed = true)
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ logsViewModel
+ }
+ viewModel {
+ logListViewModel
+ }
+ }
+ )
+ }
+
+ Intents.init()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
+ unmockkAll()
+ }
+
+ private fun launchTest(enabledLogging: Boolean) {
+ every { logsViewModel.isLoggingEnabled() } returns enabledLogging
+
+ fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_ownCloud)
+ fragmentScenario.onFragment { fragment ->
+ prefEnableLogging = fragment.findPreference(SettingsLogsFragment.PREFERENCE_ENABLE_LOGGING)!!
+ prefHttpLogs = fragment.findPreference(SettingsLogsFragment.PREFERENCE_LOG_HTTP)!!
+ prefLogsListActivity = fragment.findPreference(SettingsLogsFragment.PREFERENCE_LOGS_LIST)!!
+ }
+ }
+
+ @Test
+ fun logsViewLoggingDisabled() {
+ launchTest(enabledLogging = false)
+
+ prefEnableLogging.verifyPreference(
+ keyPref = SettingsLogsFragment.PREFERENCE_ENABLE_LOGGING,
+ titlePref = context.getString(R.string.prefs_enable_logging),
+ summaryPref = context.getString(R.string.prefs_enable_logging_summary),
+ visible = true,
+ enabled = true
+ )
+ prefHttpLogs.verifyPreference(
+ keyPref = SettingsLogsFragment.PREFERENCE_LOG_HTTP,
+ titlePref = context.getString(R.string.prefs_http_logs),
+ visible = true,
+ enabled = false
+ )
+
+ prefLogsListActivity.verifyPreference(
+ keyPref = SettingsLogsFragment.PREFERENCE_LOGS_LIST,
+ titlePref = context.getString(R.string.prefs_log_open_logs_list_view),
+ visible = true,
+ enabled = true,
+ )
+
+ }
+
+ @Test
+ fun logsViewLoggingEnabled() {
+ launchTest(enabledLogging = true)
+
+ prefEnableLogging.verifyPreference(
+ keyPref = SettingsLogsFragment.PREFERENCE_ENABLE_LOGGING,
+ titlePref = context.getString(R.string.prefs_enable_logging),
+ summaryPref = context.getString(R.string.prefs_enable_logging_summary),
+ visible = true,
+ enabled = true
+ )
+ prefHttpLogs.verifyPreference(
+ keyPref = SettingsLogsFragment.PREFERENCE_LOG_HTTP,
+ titlePref = context.getString(R.string.prefs_http_logs),
+ visible = true,
+ enabled = true
+ )
+
+ prefLogsListActivity.verifyPreference(
+ keyPref = SettingsLogsFragment.PREFERENCE_LOGS_LIST,
+ titlePref = context.getString(R.string.prefs_log_open_logs_list_view),
+ visible = true,
+ enabled = true,
+ )
+ }
+
+ @Test
+ fun enableLoggingMakesSettingsEnable() {
+ launchTest(enabledLogging = false)
+
+ onView(withText(R.string.prefs_enable_logging)).perform(click())
+ assertTrue(prefHttpLogs.isEnabled)
+ }
+
+ @Test
+ fun disableLoggingMakesSettingsDisable() {
+ launchTest(enabledLogging = false)
+
+ onView(withText(R.string.prefs_enable_logging)).perform(click())
+ onView(withText(R.string.prefs_enable_logging)).perform(click())
+ assertFalse(prefHttpLogs.isEnabled)
+ assertTrue(prefLogsListActivity.isEnabled)
+ }
+
+ @Test
+ fun checkHttpLogs() {
+ launchTest(enabledLogging = true)
+ onView(withText(R.string.prefs_http_logs)).perform(click())
+ assertTrue(prefHttpLogs.isChecked)
+ }
+
+ @Test
+ fun disableLoggingMakesHttpLogsNotChecked() {
+ launchTest(enabledLogging = false)
+
+ onView(withText(R.string.prefs_enable_logging)).perform(click())
+ onView(withText(R.string.prefs_http_logs)).perform(click())
+ onView(withText(R.string.prefs_enable_logging)).perform(click())
+ assertFalse(prefHttpLogs.isChecked)
+ }
+
+ @Test
+ fun loggerOpen() {
+ launchTest(enabledLogging = true)
+
+ onView(withText(R.string.prefs_log_open_logs_list_view)).perform(click())
+ intended(hasComponent(LogsListActivity::class.java.name))
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/more/SettingsMoreFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/more/SettingsMoreFragmentTest.kt
new file mode 100644
index 000000000000..dede18b6454e
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/more/SettingsMoreFragmentTest.kt
@@ -0,0 +1,363 @@
+/**
+ * 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.settings.more
+
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.EXTRA_SUBJECT
+import android.content.Intent.EXTRA_TEXT
+import android.net.Uri
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.fragment.app.testing.launchFragmentInContainer
+import androidx.preference.Preference
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasFlag
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.R
+import com.owncloud.android.presentation.settings.more.SettingsMoreFragment
+import com.owncloud.android.presentation.settings.more.SettingsMoreViewModel
+import com.owncloud.android.utils.matchers.verifyPreference
+import com.owncloud.android.utils.mockIntent
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import org.hamcrest.Matchers.allOf
+import org.junit.After
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+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 SettingsMoreFragmentTest {
+
+ private lateinit var fragmentScenario: FragmentScenario
+
+ private var prefHelp: Preference? = null
+ private var prefSync: Preference? = null
+ private var prefAccessDocProvider: Preference? = null
+ private var prefRecommend: Preference? = null
+ private var prefFeedback: Preference? = null
+ private var prefImprint: Preference? = null
+
+ private lateinit var moreViewModel: SettingsMoreViewModel
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ moreViewModel = mockk(relaxed = true)
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ moreViewModel
+ }
+ }
+ )
+ }
+
+ Intents.init()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ unmockkAll()
+ }
+
+ private fun getPreference(key: String): Preference? {
+ var preference: Preference? = null
+ fragmentScenario.onFragment { fragment ->
+ preference = fragment.findPreference(key)
+ }
+ return preference
+ }
+
+ private fun launchTest(
+ helpEnabled: Boolean = true,
+ syncEnabled: Boolean = true,
+ docProviderAppEnabled: Boolean = true,
+ recommendEnabled: Boolean = true,
+ feedbackEnabled: Boolean = true,
+ imprintEnabled: Boolean = true
+ ) {
+ every { moreViewModel.isHelpEnabled() } returns helpEnabled
+ every { moreViewModel.isSyncEnabled() } returns syncEnabled
+ every { moreViewModel.isDocProviderAppEnabled() } returns docProviderAppEnabled
+ every { moreViewModel.isRecommendEnabled() } returns recommendEnabled
+ every { moreViewModel.isFeedbackEnabled() } returns feedbackEnabled
+ every { moreViewModel.isImprintEnabled() } returns imprintEnabled
+
+ fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_ownCloud)
+ }
+
+ @Test
+ fun moreView() {
+ launchTest()
+
+ prefHelp = getPreference(PREFERENCE_HELP)
+ assertNotNull(prefHelp)
+ prefHelp?.verifyPreference(
+ keyPref = PREFERENCE_HELP,
+ titlePref = context.getString(R.string.prefs_help),
+ visible = true,
+ enabled = true
+ )
+
+ prefSync = getPreference(PREFERENCE_SYNC_CALENDAR_CONTACTS)
+ assertNotNull(prefSync)
+ prefSync?.verifyPreference(
+ keyPref = PREFERENCE_SYNC_CALENDAR_CONTACTS,
+ titlePref = context.getString(R.string.prefs_sync_calendar_contacts),
+ summaryPref = context.getString(R.string.prefs_sync_calendar_contacts_summary),
+ visible = true,
+ enabled = true
+ )
+
+ prefAccessDocProvider = getPreference(PREFERENCE_ACCESS_DOCUMENT_PROVIDER)
+ assertNotNull(prefAccessDocProvider)
+ prefAccessDocProvider?.verifyPreference(
+ keyPref = PREFERENCE_ACCESS_DOCUMENT_PROVIDER,
+ titlePref = context.getString(R.string.prefs_access_document_provider),
+ summaryPref = context.getString(R.string.prefs_access_document_provider_summary),
+ visible = true,
+ enabled = true
+ )
+
+ prefRecommend = getPreference(PREFERENCE_RECOMMEND)
+ assertNotNull(prefRecommend)
+ prefRecommend?.verifyPreference(
+ keyPref = PREFERENCE_RECOMMEND,
+ titlePref = context.getString(R.string.prefs_recommend),
+ visible = true,
+ enabled = true
+ )
+
+ prefFeedback = getPreference(PREFERENCE_FEEDBACK)
+ assertNotNull(prefFeedback)
+ prefFeedback?.verifyPreference(
+ keyPref = PREFERENCE_FEEDBACK,
+ titlePref = context.getString(R.string.prefs_send_feedback),
+ visible = true,
+ enabled = true
+ )
+
+ prefImprint = getPreference(PREFERENCE_IMPRINT)
+ assertNotNull(prefImprint)
+ prefImprint?.verifyPreference(
+ keyPref = PREFERENCE_IMPRINT,
+ titlePref = context.getString(R.string.prefs_imprint),
+ visible = true,
+ enabled = true
+ )
+ }
+
+ @Test
+ fun helpNotEnabledView() {
+ launchTest(helpEnabled = false)
+ prefHelp = getPreference(PREFERENCE_HELP)
+
+ assertNull(prefHelp)
+ }
+
+ @Test
+ fun syncNotEnabledView() {
+ launchTest(syncEnabled = false)
+ prefSync = getPreference(PREFERENCE_SYNC_CALENDAR_CONTACTS)
+
+ assertNull(prefSync)
+ }
+
+ @Test
+ fun accessDocumentProviderNotEnabledView() {
+ launchTest(docProviderAppEnabled = false)
+ prefAccessDocProvider = getPreference(PREFERENCE_ACCESS_DOCUMENT_PROVIDER)
+
+ assertNull(prefAccessDocProvider)
+ }
+
+ @Test
+ fun recommendNotEnabledView() {
+ launchTest(recommendEnabled = false)
+ prefRecommend = getPreference(PREFERENCE_RECOMMEND)
+
+ assertNull(prefRecommend)
+ }
+
+ @Test
+ fun feedbackNotEnabledView() {
+ launchTest(feedbackEnabled = false)
+ prefFeedback = getPreference(PREFERENCE_FEEDBACK)
+
+ assertNull(prefFeedback)
+ }
+
+ @Test
+ fun imprintNotEnabledView() {
+ launchTest(imprintEnabled = false)
+ prefImprint = getPreference(PREFERENCE_IMPRINT)
+
+ assertNull(prefImprint)
+ }
+
+ @Test
+ fun helpOpensNotEmptyUrl() {
+ every { moreViewModel.getHelpUrl() } returns context.getString(R.string.url_help)
+
+ launchTest()
+
+ mockIntent(action = Intent.ACTION_VIEW)
+ onView(withText(R.string.prefs_help)).perform(click())
+
+ intended(hasData(context.getString(R.string.url_help)))
+ }
+
+ @Test
+ fun syncOpensNotEmptyUrl() {
+ every { moreViewModel.getSyncUrl() } returns context.getString(R.string.url_sync_calendar_contacts)
+
+ launchTest()
+
+ mockIntent(action = Intent.ACTION_VIEW)
+ onView(withText(R.string.prefs_sync_calendar_contacts)).perform(click())
+
+ intended(hasData(context.getString(R.string.url_sync_calendar_contacts)))
+ }
+
+ @Test
+ fun accessDocumentProviderOpensNotEmptyUrl() {
+ every { moreViewModel.getDocProviderAppUrl() } returns context.getString(R.string.url_document_provider_app)
+
+ launchTest()
+
+ mockIntent(action = Intent.ACTION_VIEW)
+ onView(withText(R.string.prefs_access_document_provider)).perform(click())
+
+ intended(hasData(context.getString(R.string.url_document_provider_app)))
+ }
+
+ @Test
+ fun recommendOpensSender() {
+ launchTest()
+
+ mockIntent(action = Intent.ACTION_SENDTO)
+
+ onView(withText(R.string.prefs_recommend)).perform(click())
+ // Delay needed since depending on the performance of the device where tests are executed,
+ // sender can interfere with the subsequent tests
+ Thread.sleep(1000)
+ intended(
+ allOf(
+ hasAction(Intent.ACTION_SENDTO), hasExtra(
+ EXTRA_SUBJECT, String.format(
+ context.getString(R.string.recommend_subject),
+ context.getString(R.string.app_name)
+ )
+ ),
+ hasExtra(
+ EXTRA_TEXT,
+ String.format(
+ context.getString(R.string.recommend_text),
+ context.getString(R.string.app_name),
+ context.getString(R.string.url_app_download)
+ )
+ ),
+ hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ )
+ }
+
+ @Test
+ fun feedbackOpensSenderIfFeedbackMailExists() {
+ launchTest()
+ every { moreViewModel.getFeedbackMail() } returns FEEDBACK_MAIL
+ mockIntent(action = Intent.ACTION_SENDTO)
+
+ onView(withText(R.string.prefs_send_feedback)).perform(click())
+ // Delay needed since depending on the performance of the device where tests are executed,
+ // sender can interfere with the subsequent tests
+ Thread.sleep(1000)
+ intended(
+ allOf(
+ hasAction(Intent.ACTION_SENDTO),
+ hasExtra(
+ EXTRA_SUBJECT,
+ "Android v" + BuildConfig.VERSION_NAME + " - " + context.getText(R.string.prefs_feedback)
+ ),
+ hasData(Uri.parse(FEEDBACK_MAIL)),
+ hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ )
+ }
+
+ @Test
+ fun feedbackOpensAlertDialogIfFeedbackMailIsEmpty() {
+ launchTest()
+ every { moreViewModel.getFeedbackMail() } returns ""
+
+ onView(withText(R.string.prefs_send_feedback)).perform(click())
+
+ onView(withText(R.string.drawer_feedback)).check(ViewAssertions.matches(isDisplayed()))
+ }
+
+ @Test
+ fun imprintOpensUrl() {
+ every { moreViewModel.getImprintUrl() } returns "https://owncloud.com/mobile"
+
+ launchTest()
+
+ mockIntent(action = Intent.ACTION_VIEW)
+
+ onView(withText(R.string.prefs_imprint)).perform(click())
+ intended(hasData("https://owncloud.com/mobile"))
+ }
+
+ companion object {
+ private const val PREFERENCE_HELP = "help"
+ private const val PREFERENCE_SYNC_CALENDAR_CONTACTS = "syncCalendarContacts"
+ private const val PREFERENCE_ACCESS_DOCUMENT_PROVIDER = "accessDocumentProvider"
+ private const val PREFERENCE_RECOMMEND = "recommend"
+ private const val PREFERENCE_FEEDBACK = "feedback"
+ private const val PREFERENCE_IMPRINT = "imprint"
+ private const val FEEDBACK_MAIL = "mailto:android-app@owncloud.com"
+
+ }
+
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt
new file mode 100644
index 000000000000..ded6a1bd7e2b
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PassCodeActivityTest.kt
@@ -0,0 +1,357 @@
+/*
+ * ownCloud Android client application
+ *
+ * @author Jesus Recio (@jesmrec)
+ * @author Christian Schabesberger (@theScrabi)
+ * @author Juan Carlos Garrote Gascón (@JuancaG05)
+ * @author David Crespo Ríos (@davcres)
+ *
+ * Copyright (C) 2021 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.settings.security
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.owncloud.android.R
+import com.owncloud.android.db.PreferenceManager
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.presentation.security.passcode.PassCodeActivity
+import com.owncloud.android.presentation.security.passcode.PasscodeAction
+import com.owncloud.android.presentation.security.passcode.PasscodeType
+import com.owncloud.android.presentation.security.passcode.Status
+import com.owncloud.android.presentation.security.biometric.BiometricViewModel
+import com.owncloud.android.presentation.security.passcode.PassCodeViewModel
+import com.owncloud.android.testutil.security.OC_PASSCODE_4_DIGITS
+import com.owncloud.android.utils.matchers.isDisplayed
+import com.owncloud.android.utils.matchers.withChildCountAndId
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.After
+import org.junit.Assert.assertEquals
+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 PassCodeActivityTest {
+
+ private lateinit var activityScenario: ActivityScenario
+
+ private lateinit var context: Context
+
+ private lateinit var timeToUnlockLiveData: MutableLiveData>
+ private lateinit var finishTimeToUnlockLiveData: MutableLiveData>
+ private lateinit var statusLiveData: MutableLiveData>
+ private lateinit var passcodeLiveData: MutableLiveData
+
+ private lateinit var passCodeViewModel: PassCodeViewModel
+ private lateinit var biometricViewModel: BiometricViewModel
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ passCodeViewModel = mockk(relaxed = true)
+ biometricViewModel = mockk(relaxed = true)
+
+ timeToUnlockLiveData = MutableLiveData()
+ finishTimeToUnlockLiveData = MutableLiveData()
+ statusLiveData = MutableLiveData()
+ passcodeLiveData = MutableLiveData()
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ passCodeViewModel
+ }
+ viewModel {
+ biometricViewModel
+ }
+ }
+ )
+ }
+
+ every { passCodeViewModel.getPassCode() } returns OC_PASSCODE_4_DIGITS
+ every { passCodeViewModel.getNumberOfPassCodeDigits() } returns 4
+ every { passCodeViewModel.getNumberOfAttempts() } returns 0
+ every { passCodeViewModel.getTimeToUnlockLiveData } returns timeToUnlockLiveData
+ every { passCodeViewModel.getFinishedTimeToUnlockLiveData } returns finishTimeToUnlockLiveData
+ every { passCodeViewModel.status } returns statusLiveData
+ every { passCodeViewModel.passcode } returns passcodeLiveData
+ }
+
+ @After
+ fun tearDown() {
+ // Clean preferences
+ PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
+ }
+
+ @Test
+ fun passcodeCheckNotLockedView() {
+ // Open Activity in passcode check mode
+ openPasscodeActivity(PassCodeActivity.ACTION_CHECK)
+
+ with(R.id.header) {
+ isDisplayed(true)
+ withText(R.string.pass_code_enter_pass_code)
+ }
+
+ R.id.explanation.isDisplayed(false)
+
+ // Check if required amount of input fields are actually displayed
+ with(R.id.layout_code) {
+ isDisplayed(true)
+ withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText)
+ }
+
+ R.id.lock_time.isDisplayed(false)
+
+ R.id.numberKeyboard.isDisplayed(true)
+ R.id.key0.isDisplayed(true)
+ R.id.key1.isDisplayed(true)
+ R.id.key2.isDisplayed(true)
+ R.id.key3.isDisplayed(true)
+ R.id.key4.isDisplayed(true)
+ R.id.key5.isDisplayed(true)
+ R.id.key6.isDisplayed(true)
+ R.id.key7.isDisplayed(true)
+ R.id.key8.isDisplayed(true)
+ R.id.key9.isDisplayed(true)
+ R.id.backspaceBtn.isDisplayed(true)
+ R.id.biometricBtn.isDisplayed(false)
+ }
+
+ @Test
+ fun passcodeCheckLockedView() {
+ every { passCodeViewModel.getNumberOfAttempts() } returns 3
+ every { passCodeViewModel.getTimeToUnlockLeft() } returns 3000
+
+ timeToUnlockLiveData.postValue(Event("00:03"))
+
+ // Open Activity in passcode check mode
+ openPasscodeActivity(PassCodeActivity.ACTION_CHECK)
+
+ with(R.id.header) {
+ isDisplayed(true)
+ withText(R.string.pass_code_enter_pass_code)
+ }
+
+ R.id.explanation.isDisplayed(false)
+
+ // Check if required amount of input fields are actually displayed
+ with(R.id.layout_code) {
+ isDisplayed(true)
+ withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText)
+ }
+
+ R.id.lock_time.isDisplayed(true)
+ }
+
+ @Test
+ fun passcodeView() {
+ // Open Activity in passcode creation mode
+ openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
+
+ with(R.id.header) {
+ isDisplayed(true)
+ withText(R.string.pass_code_configure_your_pass_code)
+ }
+ with(R.id.explanation) {
+ isDisplayed(true)
+ withText(R.string.pass_code_configure_your_pass_code_explanation)
+ }
+
+ // Check if required amount of input fields are actually displayed
+ with(R.id.layout_code) {
+ isDisplayed(true)
+ withChildCountAndId(passCodeViewModel.getNumberOfPassCodeDigits(), R.id.passCodeEditText)
+ }
+
+ R.id.lock_time.isDisplayed(false)
+
+ R.id.error.isDisplayed(false)
+ }
+
+ @Test
+ fun firstTry() {
+ // Open Activity in passcode creation mode
+ openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
+
+ statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.NO_CONFIRM)))
+
+ with(R.id.header) {
+ isDisplayed(true)
+ withText(R.string.pass_code_reenter_your_pass_code)
+ }
+ onView(withText(R.string.pass_code_configure_your_pass_code)).check(doesNotExist())
+
+ R.id.error.isDisplayed(false)
+ }
+
+ @Test
+ fun secondTryCorrect() {
+ every { biometricViewModel.isBiometricLockAvailable() } returns true
+
+ // Open Activity in passcode creation mode
+ openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
+
+ statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM)))
+
+ // Click dialog's enable option
+ onView(withText(R.string.common_yes)).perform(click())
+
+ // Checking that the result returned is OK
+ assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
+ }
+
+ @Test
+ fun secondTryIncorrect() {
+ // Open Activity in passcode creation mode
+ openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
+
+ statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.ERROR)))
+
+ with(R.id.header) {
+ isDisplayed(true)
+ withText(R.string.pass_code_configure_your_pass_code)
+ }
+ with(R.id.explanation) {
+ isDisplayed(true)
+ withText(R.string.pass_code_configure_your_pass_code_explanation)
+ }
+ with(R.id.error) {
+ isDisplayed(true)
+ withText(R.string.pass_code_mismatch)
+ }
+
+ R.id.lock_time.isDisplayed(false)
+ }
+
+ @Test
+ fun deletePasscodeView() {
+ // Open Activity in passcode deletion mode
+ openPasscodeActivity(PassCodeActivity.ACTION_REMOVE)
+
+ with(R.id.header) {
+ isDisplayed(true)
+ withText(R.string.pass_code_remove_your_pass_code)
+ }
+
+ R.id.explanation.isDisplayed(false)
+
+ R.id.error.isDisplayed(false)
+
+ R.id.lock_time.isDisplayed(false)
+ }
+
+ @Test
+ fun deletePasscodeCorrect() {
+ // Open Activity in passcode deletion mode
+ openPasscodeActivity(PassCodeActivity.ACTION_REMOVE)
+
+ statusLiveData.postValue(Event(Status(PasscodeAction.REMOVE, PasscodeType.OK)))
+
+ assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
+ }
+
+ @Test
+ fun deletePasscodeIncorrect() {
+ // Open Activity in passcode deletion mode
+ openPasscodeActivity(PassCodeActivity.ACTION_REMOVE)
+
+ statusLiveData.postValue(Event(Status(PasscodeAction.REMOVE, PasscodeType.ERROR)))
+
+ with(R.id.header) {
+ isDisplayed(true)
+ withText(R.string.pass_code_enter_pass_code)
+ }
+ with(R.id.error) {
+ isDisplayed(true)
+ withText(R.string.pass_code_wrong)
+ }
+
+ R.id.explanation.isDisplayed(false)
+
+ R.id.lock_time.isDisplayed(false)
+ }
+
+ @Test
+ fun checkEnableBiometricDialogIsVisible() {
+ every { biometricViewModel.isBiometricLockAvailable() } returns true
+
+ // Open Activity in passcode creation mode
+ openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
+
+ statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM)))
+
+ onView(withText(R.string.biometric_dialog_title)).check(matches(isDisplayed()))
+ onView(withText(R.string.common_yes)).check(matches(isDisplayed()))
+ onView(withText(R.string.common_no)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun checkEnableBiometricDialogYesOption() {
+ every { biometricViewModel.isBiometricLockAvailable() } returns true
+
+ // Open Activity in passcode creation mode
+ openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
+
+ statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM)))
+
+ onView(withText(R.string.common_yes)).perform(click())
+
+ // Checking that the result returned is OK
+ assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
+ }
+
+ @Test
+ fun checkEnableBiometricDialogNoOption() {
+ every { biometricViewModel.isBiometricLockAvailable() } returns true
+
+ // Open Activity in passcode creation mode
+ openPasscodeActivity(PassCodeActivity.ACTION_CREATE)
+
+ statusLiveData.postValue(Event(Status(PasscodeAction.CREATE, PasscodeType.CONFIRM)))
+
+ onView(withText(R.string.common_no)).perform(click())
+
+ // Checking that the result returned is OK
+ assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
+ }
+
+ private fun openPasscodeActivity(mode: String) {
+ val intent = Intent(context, PassCodeActivity::class.java).apply {
+ action = mode
+ }
+ activityScenario = ActivityScenario.launch(intent)
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt
new file mode 100644
index 000000000000..78d380f6dbe3
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/PatternActivityTest.kt
@@ -0,0 +1,127 @@
+/*
+ * ownCloud Android client application
+ *
+ * @author Jesus Recio (@jesmrec)
+ * @author Juan Carlos Garrote Gascón (@JuancaG05)
+ *
+ * Copyright (C) 2021 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.settings.security
+
+import android.content.Context
+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.db.PreferenceManager
+import com.owncloud.android.presentation.security.pattern.PatternActivity
+import com.owncloud.android.presentation.security.pattern.PatternViewModel
+import com.owncloud.android.testutil.security.OC_PATTERN
+import com.owncloud.android.utils.matchers.isDisplayed
+import com.owncloud.android.utils.matchers.withText
+import io.mockk.mockk
+import org.junit.After
+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 PatternActivityTest {
+
+ private lateinit var activityScenario: ActivityScenario
+
+ private lateinit var context: Context
+
+ private lateinit var patternViewModel: PatternViewModel
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ patternViewModel = mockk(relaxUnitFun = true)
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ patternViewModel
+ }
+ }
+ )
+ }
+ }
+
+ @After
+ fun tearDown() {
+ // Clean preferences
+ PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
+ }
+
+ @Test
+ fun patternLockView() {
+ // Open Activity in pattern creation mode
+ openPatternActivity(PatternActivity.ACTION_REQUEST_WITH_RESULT)
+
+ with(R.id.header_pattern) {
+ isDisplayed(true)
+ withText(R.string.pattern_configure_pattern)
+ }
+ with(R.id.explanation_pattern) {
+ isDisplayed(true)
+ withText(R.string.pattern_configure_your_pattern_explanation)
+ }
+ R.id.pattern_lock_view.isDisplayed(true)
+ }
+
+ @Test
+ fun removePatternLock() {
+ // Save a pattern in Preferences
+ storePattern()
+
+ // Open Activity in pattern deletion mode
+ openPatternActivity(PatternActivity.ACTION_CHECK_WITH_RESULT)
+
+ with(R.id.header_pattern) {
+ isDisplayed(true)
+ withText(R.string.pattern_remove_pattern)
+ }
+ with(R.id.explanation_pattern) {
+ isDisplayed(true)
+ withText(R.string.pattern_no_longer_required)
+ }
+ }
+
+ private fun storePattern() {
+ val appPrefs = PreferenceManager.getDefaultSharedPreferences(context).edit()
+ appPrefs.apply {
+ putString(PatternActivity.PREFERENCE_PATTERN, OC_PATTERN)
+ putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, true)
+ apply()
+ }
+ }
+
+ private fun openPatternActivity(mode: String) {
+ val intent = Intent(context, PatternActivity::class.java).apply {
+ action = mode
+ }
+ activityScenario = ActivityScenario.launch(intent)
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt
new file mode 100644
index 000000000000..391cf29c0d80
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/settings/security/SettingsSecurityFragmentTest.kt
@@ -0,0 +1,491 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2021 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.settings.security
+
+import android.app.Activity.RESULT_OK
+import android.content.Context
+import androidx.biometric.BiometricViewModel
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.fragment.app.testing.launchFragmentInContainer
+import androidx.preference.CheckBoxPreference
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceManager
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.intent.Intents.intended
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isEnabled
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.owncloud.android.R
+import com.owncloud.android.presentation.security.biometric.BiometricActivity
+import com.owncloud.android.presentation.security.biometric.BiometricManager
+import com.owncloud.android.presentation.security.PREFERENCE_LOCK_TIMEOUT
+import com.owncloud.android.presentation.security.pattern.PatternActivity
+import com.owncloud.android.presentation.security.passcode.PassCodeActivity
+import com.owncloud.android.presentation.settings.security.SettingsSecurityFragment
+import com.owncloud.android.presentation.settings.security.SettingsSecurityFragment.Companion.PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER
+import com.owncloud.android.presentation.settings.security.SettingsSecurityViewModel
+import com.owncloud.android.testutil.security.OC_PATTERN
+import com.owncloud.android.utils.matchers.verifyPreference
+import com.owncloud.android.utils.mockIntent
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import org.hamcrest.Matchers.not
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+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 SettingsSecurityFragmentTest {
+
+ private lateinit var fragmentScenario: FragmentScenario
+
+ private lateinit var prefPasscode: CheckBoxPreference
+ private lateinit var prefPattern: CheckBoxPreference
+ private var prefBiometric: CheckBoxPreference? = null
+ private lateinit var prefLockApplication: ListPreference
+ private lateinit var prefLockAccessDocumentProvider: CheckBoxPreference
+ private lateinit var prefTouchesWithOtherVisibleWindows: CheckBoxPreference
+
+ private lateinit var securityViewModel: SettingsSecurityViewModel
+ private lateinit var biometricViewModel: BiometricViewModel
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ securityViewModel = mockk(relaxed = true)
+ biometricViewModel = mockk(relaxed = true)
+ mockkObject(BiometricManager)
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ securityViewModel
+ }
+ }
+ )
+ }
+ every { securityViewModel.isSecurityEnforcedEnabled() } returns false
+ every { securityViewModel.getBiometricsState() } returns false
+ every { securityViewModel.isLockDelayEnforcedEnabled() } returns false
+
+ Intents.init()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
+ }
+
+ private fun launchTest(withBiometrics: Boolean = true) {
+ every { BiometricManager.isHardwareDetected() } returns withBiometrics
+
+ fragmentScenario = launchFragmentInContainer(themeResId = R.style.Theme_ownCloud)
+ fragmentScenario.onFragment { fragment ->
+ prefPasscode = fragment.findPreference(PassCodeActivity.PREFERENCE_SET_PASSCODE)!!
+ prefPattern = fragment.findPreference(PatternActivity.PREFERENCE_SET_PATTERN)!!
+ prefBiometric = fragment.findPreference(BiometricActivity.PREFERENCE_SET_BIOMETRIC)
+ prefLockApplication = fragment.findPreference(PREFERENCE_LOCK_TIMEOUT)!!
+ prefLockAccessDocumentProvider = fragment.findPreference(PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER)!!
+ prefTouchesWithOtherVisibleWindows =
+ fragment.findPreference(SettingsSecurityFragment.PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS)!!
+ }
+ }
+
+ private fun checkCommonPreferences() {
+ prefPasscode.verifyPreference(
+ keyPref = PassCodeActivity.PREFERENCE_SET_PASSCODE,
+ titlePref = context.getString(R.string.prefs_passcode),
+ visible = true,
+ enabled = true
+ )
+ assertFalse(prefPasscode.isChecked)
+
+ prefPattern.verifyPreference(
+ keyPref = PatternActivity.PREFERENCE_SET_PATTERN,
+ titlePref = context.getString(R.string.prefs_pattern),
+ visible = true,
+ enabled = true
+ )
+ assertFalse(prefPattern.isChecked)
+
+ prefLockApplication.verifyPreference(
+ keyPref = PREFERENCE_LOCK_TIMEOUT,
+ titlePref = context.getString(R.string.prefs_lock_application),
+ visible = true,
+ enabled = false
+ )
+
+ prefLockAccessDocumentProvider.verifyPreference(
+ keyPref = PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER,
+ titlePref = context.getString(R.string.prefs_lock_access_from_document_provider),
+ summaryPref = context.getString(R.string.prefs_lock_access_from_document_provider_summary),
+ visible = true,
+ enabled = true
+ )
+ assertFalse(prefLockAccessDocumentProvider.isChecked)
+
+ prefTouchesWithOtherVisibleWindows.verifyPreference(
+ keyPref = SettingsSecurityFragment.PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS,
+ titlePref = context.getString(R.string.prefs_touches_with_other_visible_windows),
+ summaryPref = context.getString(R.string.prefs_touches_with_other_visible_windows_summary),
+ visible = true,
+ enabled = true
+ )
+ assertFalse(prefTouchesWithOtherVisibleWindows.isChecked)
+ }
+
+ @Test
+ fun securityViewDeviceWithBiometrics() {
+ launchTest()
+
+ checkCommonPreferences()
+
+ assertNotNull(prefBiometric)
+ prefBiometric?.run {
+ verifyPreference(
+ keyPref = BiometricActivity.PREFERENCE_SET_BIOMETRIC,
+ titlePref = context.getString(R.string.prefs_biometric),
+ summaryPref = context.getString(R.string.prefs_biometric_summary),
+ visible = true,
+ enabled = false
+ )
+ assertFalse(isChecked)
+ }
+ }
+
+ @Test
+ fun securityViewDeviceWithNoBiometrics() {
+ launchTest(withBiometrics = false)
+
+ checkCommonPreferences()
+
+ assertNull(prefBiometric)
+ }
+
+ @Test
+ fun passcodeOpen() {
+ every { securityViewModel.isPatternSet() } returns false
+
+ launchTest()
+
+ mockIntent(RESULT_OK, PassCodeActivity.ACTION_CREATE)
+ onView(withText(R.string.prefs_passcode)).perform(click())
+ intended(hasComponent(PassCodeActivity::class.java.name))
+ }
+
+ @Test
+ fun patternOpen() {
+ every { securityViewModel.isPasscodeSet() } returns false
+
+ launchTest()
+
+ onView(withText(R.string.prefs_pattern)).perform(click())
+ intended(hasComponent(PatternActivity::class.java.name))
+ }
+
+ @Test
+ fun passcodeLockEnabledOk() {
+ every { securityViewModel.isPatternSet() } returns false
+
+ launchTest()
+
+ mockIntent(
+ action = PassCodeActivity.ACTION_CREATE
+ )
+ onView(withText(R.string.prefs_passcode)).perform(click())
+ assertTrue(prefPasscode.isChecked)
+ }
+
+ @Test
+ fun patternLockEnabledOk() {
+ every { securityViewModel.isPasscodeSet() } returns false
+
+ launchTest()
+
+ mockIntent(
+ extras = Pair(PatternActivity.PREFERENCE_PATTERN, OC_PATTERN),
+ action = PatternActivity.ACTION_REQUEST_WITH_RESULT
+ )
+ onView(withText(R.string.prefs_pattern)).perform(click())
+ assertTrue(prefPattern.isChecked)
+ }
+
+ @Test
+ fun enablePasscodeEnablesBiometricLockAndLockApplication() {
+ launchTest()
+
+ firstEnablePasscode()
+ onView(withText(R.string.prefs_biometric)).check(matches(isEnabled()))
+ assertTrue(prefBiometric!!.isEnabled)
+ assertFalse(prefBiometric!!.isChecked)
+ assertTrue(prefLockApplication.isEnabled)
+ }
+
+ @Test
+ fun enablePatternEnablesBiometricLockAndLockApplication() {
+ launchTest()
+
+ firstEnablePattern()
+ onView(withText(R.string.prefs_biometric)).check(matches(isEnabled()))
+ assertTrue(prefBiometric!!.isEnabled)
+ assertFalse(prefBiometric!!.isChecked)
+ assertTrue(prefLockApplication.isEnabled)
+ }
+
+ @Test
+ fun onlyOneMethodEnabledPattern() {
+ every { securityViewModel.isPatternSet() } returns true
+
+ launchTest()
+
+ firstEnablePattern()
+ onView(withText(R.string.prefs_passcode)).perform(click())
+ onView(withText(R.string.pattern_already_set)).check(matches(isEnabled()))
+ }
+
+ @Test
+ fun onlyOneMethodEnabledPasscode() {
+ every { securityViewModel.isPasscodeSet() } returns true
+
+ launchTest()
+
+ firstEnablePasscode()
+ onView(withText(R.string.prefs_pattern)).perform(click())
+ onView(withText(R.string.passcode_already_set)).check(matches(isEnabled()))
+ }
+
+ @Test
+ fun disablePasscodeOk() {
+ launchTest()
+
+ firstEnablePasscode()
+ mockIntent(
+ action = PassCodeActivity.ACTION_REMOVE
+ )
+ onView(withText(R.string.prefs_passcode)).perform(click())
+ assertFalse(prefPasscode.isChecked)
+ onView(withText(R.string.prefs_biometric)).check(matches(not(isEnabled())))
+ assertFalse(prefBiometric!!.isEnabled)
+ assertFalse(prefBiometric!!.isChecked)
+ assertFalse(prefLockApplication.isEnabled)
+ }
+
+ @Test
+ fun disablePatternOk() {
+ launchTest()
+
+ firstEnablePattern()
+ mockIntent(
+ action = PatternActivity.ACTION_CHECK_WITH_RESULT
+ )
+ onView(withText(R.string.prefs_pattern)).perform(click())
+ assertFalse(prefPattern.isChecked)
+ onView(withText(R.string.prefs_biometric)).check(matches(not(isEnabled())))
+ assertFalse(prefBiometric!!.isEnabled)
+ assertFalse(prefBiometric!!.isChecked)
+ assertFalse(prefLockApplication.isEnabled)
+ }
+
+ @Test
+ fun enableBiometricLockWithPasscodeEnabled() {
+ every { BiometricManager.hasEnrolledBiometric() } returns true
+
+ launchTest()
+
+ firstEnablePasscode()
+ onView(withText(R.string.prefs_biometric)).perform(click())
+ assertTrue(prefBiometric!!.isChecked)
+ }
+
+ @Test
+ fun enableBiometricLockWithPatternEnabled() {
+ every { BiometricManager.hasEnrolledBiometric() } returns true
+
+ launchTest()
+
+ firstEnablePattern()
+ onView(withText(R.string.prefs_biometric)).perform(click())
+ assertTrue(prefBiometric!!.isChecked)
+ }
+
+ @Test
+ fun enableBiometricLockNoEnrolledBiometric() {
+ every { BiometricManager.hasEnrolledBiometric() } returns false
+
+ launchTest()
+
+ firstEnablePasscode()
+ onView(withText(R.string.prefs_biometric)).perform(click())
+ assertFalse(prefBiometric!!.isChecked)
+ onView(withText(R.string.biometric_not_enrolled)).check(matches(isEnabled()))
+ }
+
+ @Test
+ fun disableBiometricLock() {
+ every { BiometricManager.hasEnrolledBiometric() } returns true
+
+ launchTest()
+
+ firstEnablePasscode()
+ onView(withText(R.string.prefs_biometric)).perform(click())
+ onView(withText(R.string.prefs_biometric)).perform(click())
+ assertFalse(prefBiometric!!.isChecked)
+ }
+
+ @Test
+ fun lockAccessFromDocumentProviderEnable() {
+ launchTest()
+
+ onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
+ assertTrue(prefLockAccessDocumentProvider.isChecked)
+ }
+
+ @Test
+ fun lockAccessFromDocumentProviderDisable() {
+ launchTest()
+
+ onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
+ onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
+ assertFalse(prefLockAccessDocumentProvider.isChecked)
+ }
+
+ @Test
+ fun touchesDialog() {
+ launchTest()
+
+ onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
+ onView(withText(R.string.confirmation_touches_with_other_windows_title)).check(matches(isDisplayed()))
+ onView(withText(R.string.confirmation_touches_with_other_windows_message)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun touchesEnable() {
+ launchTest()
+
+ onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
+ onView(withText(R.string.common_yes)).perform(click())
+ assertTrue(prefTouchesWithOtherVisibleWindows.isChecked)
+ }
+
+ @Test
+ fun touchesRefuse() {
+ launchTest()
+
+ onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
+ onView(withText(R.string.common_no)).perform(click())
+ assertFalse(prefTouchesWithOtherVisibleWindows.isChecked)
+ }
+
+ @Test
+ fun touchesDisable() {
+ launchTest()
+
+ onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
+ onView(withText(R.string.common_yes)).perform(click())
+ onView(withText(R.string.prefs_touches_with_other_visible_windows)).perform(click())
+ assertFalse(prefTouchesWithOtherVisibleWindows.isChecked)
+ }
+
+ @Test
+ fun passcodeLockNotVisible() {
+ every { securityViewModel.isSecurityEnforcedEnabled() } returns true
+ launchTest()
+ assertFalse(prefPasscode.isVisible)
+ }
+
+ @Test
+ fun patternLockNotVisible() {
+ every { securityViewModel.isSecurityEnforcedEnabled() } returns true
+ launchTest()
+ assertFalse(prefPattern.isVisible)
+ }
+
+ @Test
+ fun passcodeLockVisible() {
+ launchTest()
+ assertTrue(prefPasscode.isVisible)
+ }
+
+ @Test
+ fun patternLockVisible() {
+ launchTest()
+ assertTrue(prefPattern.isVisible)
+ }
+
+ @Test
+ fun checkIfUserEnabledBiometricRecommendation() {
+ every { securityViewModel.getBiometricsState() } returns true
+
+ launchTest()
+
+ firstEnablePasscode()
+
+ assertTrue(prefBiometric!!.isChecked)
+ assertTrue(prefBiometric!!.isEnabled)
+ }
+
+ @Test
+ fun checkIfUserNotEnabledBiometricRecommendation() {
+ launchTest()
+
+ firstEnablePasscode()
+
+ assertFalse(prefBiometric!!.isChecked)
+ }
+
+ private fun firstEnablePasscode() {
+ every { securityViewModel.isPatternSet() } returns false
+
+ mockIntent(
+ action = PassCodeActivity.ACTION_CREATE
+ )
+ onView(withText(R.string.prefs_passcode)).perform(click())
+ }
+
+ private fun firstEnablePattern() {
+ every { securityViewModel.isPasscodeSet() } returns false
+
+ mockIntent(
+ action = PatternActivity.ACTION_REQUEST_WITH_RESULT
+ )
+ onView(withText(R.string.prefs_pattern)).perform(click())
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/sharees/ui/SearchShareesFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/sharees/ui/SearchShareesFragmentTest.kt
new file mode 100644
index 000000000000..e59dd49ccdc4
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/sharees/ui/SearchShareesFragmentTest.kt
@@ -0,0 +1,126 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.sharing.sharees.ui
+
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario
+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.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.owncloud.android.R
+import com.owncloud.android.domain.sharing.shares.model.OCShare
+import com.owncloud.android.domain.sharing.shares.model.ShareType
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.presentation.sharing.sharees.SearchShareesFragment
+import com.owncloud.android.presentation.sharing.ShareViewModel
+import com.owncloud.android.sharing.shares.ui.TestShareFileActivity
+import com.owncloud.android.testutil.OC_SHARE
+import io.mockk.every
+import io.mockk.mockkClass
+import org.hamcrest.CoreMatchers
+import org.junit.Before
+import org.junit.Test
+import org.koin.android.ext.koin.androidContext
+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 SearchShareesFragmentTest {
+ private val shareViewModel = mockkClass(ShareViewModel::class, relaxed = true)
+ private val sharesLiveData = MutableLiveData>>>()
+
+ @Before
+ fun setUp() {
+ every { shareViewModel.shares } returns sharesLiveData
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ androidContext(ApplicationProvider.getApplicationContext())
+ modules(
+ module {
+ viewModel {
+ shareViewModel
+ }
+ }
+ )
+ }
+
+ ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
+ val searchShareesFragment = SearchShareesFragment()
+ it.startFragment(searchShareesFragment)
+ }
+ }
+
+ @Test
+ fun showSearchBar() {
+ onView(withId(R.id.search_mag_icon)).check(matches(isDisplayed()))
+ onView(withId(R.id.search_plate)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showUserShares() {
+ sharesLiveData.postValue(
+ Event(
+ UIResult.Success(
+ listOf(
+ OC_SHARE.copy(sharedWithDisplayName = "Sheldon"),
+ OC_SHARE.copy(sharedWithDisplayName = "Penny")
+ )
+ )
+ )
+ )
+
+ onView(withText("Sheldon"))
+ .check(matches(isDisplayed()))
+ .check(matches(hasSibling(withId(R.id.unshareButton))))
+ .check(matches(hasSibling(withId(R.id.editShareButton))))
+ onView(withText("Penny")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showGroupShares() {
+ sharesLiveData.postValue(
+ Event(
+ UIResult.Success(
+ listOf(
+ OC_SHARE.copy(
+ shareType = ShareType.GROUP,
+ sharedWithDisplayName = "Friends"
+ )
+ )
+ )
+ )
+ )
+
+ onView(withText("Friends (group)"))
+ .check(matches(isDisplayed()))
+ .check(matches(hasSibling(withId(R.id.icon))))
+ onView(ViewMatchers.withTagValue(CoreMatchers.equalTo(R.drawable.ic_group))).check(matches(isDisplayed()))
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/EditPrivateShareFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/EditPrivateShareFragmentTest.kt
new file mode 100644
index 000000000000..65f823436b2f
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/EditPrivateShareFragmentTest.kt
@@ -0,0 +1,278 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.sharing.shares.ui
+
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.owncloud.android.R
+import com.owncloud.android.domain.sharing.shares.model.OCShare
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.presentation.sharing.sharees.EditPrivateShareFragment
+import com.owncloud.android.presentation.sharing.ShareViewModel
+import com.owncloud.android.testutil.OC_ACCOUNT
+import com.owncloud.android.testutil.OC_FILE
+import com.owncloud.android.testutil.OC_FOLDER
+import com.owncloud.android.testutil.OC_SHARE
+import com.owncloud.android.utils.Permissions
+import io.mockk.every
+import io.mockk.mockk
+import org.hamcrest.CoreMatchers.not
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.koin.android.ext.koin.androidContext
+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 EditPrivateShareFragmentTest {
+ private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
+ private val defaultSharedWithDisplayName = "user"
+ private val shareViewModel = mockk(relaxed = true)
+ private val privateShareAsLiveData = MutableLiveData>>()
+
+ private lateinit var activityScenario: ActivityScenario
+
+ @Before
+ fun setUp() {
+ every { shareViewModel.privateShare } returns privateShareAsLiveData
+ every { shareViewModel.isResharingAllowed() } returns true
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ androidContext(ApplicationProvider.getApplicationContext())
+ modules(
+ module {
+ viewModel {
+ shareViewModel
+ }
+ }
+ )
+ }
+ }
+
+ @Test
+ fun showDialogTitle() {
+ loadEditPrivateShareFragment()
+ onView(withId(R.id.editShareTitle))
+ .check(
+ matches(
+ withText(
+ targetContext.getString(R.string.share_with_edit_title, defaultSharedWithDisplayName)
+ )
+ )
+ )
+ }
+
+ @Test
+ fun closeDialog() {
+ loadEditPrivateShareFragment()
+ onView(withId(R.id.closeButton)).perform(click())
+ activityScenario.onActivity { Assert.assertNull(it.getTestFragment()) }
+ }
+
+ @Test
+ fun showToggles() {
+ loadEditPrivateShareFragment()
+ onView(withId(R.id.canEditSwitch)).check(matches(isDisplayed()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showFileShareWithNoPermissions() {
+ loadEditPrivateShareFragment()
+ onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+ }
+
+ @Test
+ fun showFileShareWithEditPermissions() {
+ loadEditPrivateShareFragment(permissions = Permissions.EDIT_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+ }
+
+ @Test
+ fun showFileShareWithSharePermissions() {
+ loadEditPrivateShareFragment(permissions = Permissions.SHARE_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isChecked()))
+ }
+
+ @Test
+ fun showFileShareWithAllPermissions() {
+ loadEditPrivateShareFragment(permissions = Permissions.ALL_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isChecked()))
+ }
+
+ @Test
+ fun showFolderShareWithCreatePermissions() {
+ loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(isNotChecked()))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isNotChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+ }
+
+ @Test
+ fun showFolderShareWithCreateChangePermissions() {
+ loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_CHANGE_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isNotChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+ }
+
+ @Test
+ fun showFolderShareWithCreateDeletePermissions() {
+ loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_DELETE_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(isNotChecked()))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+ }
+
+ @Test
+ fun showFolderShareWithCreateChangeDeletePermissions() {
+ loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CREATE_CHANGE_DELETE_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+ }
+
+ @Test
+ fun showFolderShareWithChangePermissions() {
+ loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CHANGE_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(isNotChecked()))
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isNotChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+ }
+
+ @Test
+ fun showFolderShareWithChangeDeletePermissions() {
+ loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_CHANGE_DELETE_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(isNotChecked()))
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+ }
+
+ @Test
+ fun showFolderShareWithDeletePermissions() {
+ loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_DELETE_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(isNotChecked()))
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(isNotChecked()))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+ }
+
+ @Test
+ fun disableEditPermissionWithFile() {
+ loadEditPrivateShareFragment(permissions = Permissions.EDIT_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+
+ onView(withId(R.id.canEditSwitch)).perform(click())
+
+ onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked())) // "Can edit" changes
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(not(isDisplayed()))) // No suboptions
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(not(isDisplayed())))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(not(isDisplayed())))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked())) // "Can share" does not change
+ }
+
+ @Test
+ fun disableEditPermissionWithFolder() {
+ loadEditPrivateShareFragment(true, permissions = Permissions.EDIT_PERMISSIONS.value)
+ onView(withId(R.id.canEditSwitch)).check(matches(isChecked()))
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(isDisplayed())) // Suboptions appear
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(isDisplayed()))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(isDisplayed()))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked()))
+
+ onView(withId(R.id.canEditSwitch)).perform(click())
+
+ onView(withId(R.id.canEditSwitch)).check(matches(isNotChecked())) // "Can edit" changes
+ onView(withId(R.id.canEditCreateCheckBox)).check(matches(not(isDisplayed()))) // Suboptions hidden
+ onView(withId(R.id.canEditChangeCheckBox)).check(matches(not(isDisplayed())))
+ onView(withId(R.id.canEditDeleteCheckBox)).check(matches(not(isDisplayed())))
+ onView(withId(R.id.canShareSwitch)).check(matches(isNotChecked())) // "Can share" does not change
+ }
+
+ private fun loadEditPrivateShareFragment(
+ isFolder: Boolean = false,
+ permissions: Int = Permissions.READ_PERMISSIONS.value
+ ) {
+ val shareToEdit = OC_SHARE.copy(
+ sharedWithDisplayName = defaultSharedWithDisplayName,
+ permissions = permissions
+ )
+
+ val sharedFile = if (isFolder) OC_FOLDER else OC_FILE
+
+ val editPrivateShareFragment = EditPrivateShareFragment.newInstance(
+ shareToEdit,
+ sharedFile,
+ OC_ACCOUNT
+ )
+
+ activityScenario = ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
+ it.startFragment(editPrivateShareFragment)
+ }
+
+ privateShareAsLiveData.postValue(
+ Event(
+ UIResult.Success(
+ OC_SHARE.copy(
+ shareWith = "user",
+ sharedWithDisplayName = "User",
+ path = "/Videos",
+ isFolder = isFolder,
+ permissions = permissions
+ )
+ )
+ )
+ )
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/PublicShareCreationDialogFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/PublicShareCreationDialogFragmentTest.kt
new file mode 100644
index 000000000000..f2a7957fc862
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/PublicShareCreationDialogFragmentTest.kt
@@ -0,0 +1,493 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.sharing.shares.ui
+
+import android.text.InputType.TYPE_CLASS_TEXT
+import android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.scrollTo
+import androidx.test.espresso.action.ViewActions.typeText
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isEnabled
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withInputType
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.owncloud.android.R
+import com.owncloud.android.domain.capabilities.model.CapabilityBooleanType
+import com.owncloud.android.domain.capabilities.model.OCCapability
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.presentation.sharing.shares.PublicShareDialogFragment
+import com.owncloud.android.presentation.capabilities.CapabilityViewModel
+import com.owncloud.android.presentation.sharing.ShareViewModel
+import com.owncloud.android.testutil.OC_ACCOUNT
+import com.owncloud.android.testutil.OC_CAPABILITY
+import com.owncloud.android.testutil.OC_FILE
+import com.owncloud.android.testutil.OC_FOLDER
+import com.owncloud.android.utils.DateUtils
+import io.mockk.every
+import io.mockk.mockk
+import org.hamcrest.CoreMatchers.not
+import org.junit.Before
+import org.junit.Test
+import org.koin.android.ext.koin.androidContext
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.dsl.module
+import java.text.SimpleDateFormat
+import java.util.Date
+
+class PublicShareCreationDialogFragmentTest {
+ private val capabilityViewModel = mockk(relaxed = true)
+ private val capabilitiesLiveData = MutableLiveData>>()
+ private val shareViewModel = mockk(relaxed = true)
+ private val publicShareCreationStatus = MutableLiveData>>()
+
+ @Before
+ fun setUp() {
+ every { capabilityViewModel.capabilities } returns capabilitiesLiveData
+ every { shareViewModel.publicShareCreationStatus } returns publicShareCreationStatus
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ androidContext(ApplicationProvider.getApplicationContext())
+ modules(
+ module {
+ viewModel {
+ capabilityViewModel
+ }
+ viewModel {
+ shareViewModel
+ }
+ }
+ )
+ }
+ }
+
+ @Test
+ fun showDialogTitle() {
+ loadPublicShareDialogFragment()
+ onView(withId(R.id.publicShareDialogTitle)).check(matches(withText(R.string.share_via_link_create_title)))
+ }
+
+ @Test
+ fun showMandatoryFields() {
+ loadPublicShareDialogFragment()
+ onView(withId(R.id.shareViaLinkNameSection)).check(matches(isDisplayed()))
+ onView(withId(R.id.shareViaLinkPasswordSection)).check(matches(isDisplayed()))
+ onView(withId(R.id.shareViaLinkExpirationSection)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showDialogButtons() {
+ loadPublicShareDialogFragment()
+ onView(withId(R.id.cancelButton)).check(matches(isDisplayed()))
+ onView(withId(R.id.saveButton)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showFolderAdditionalFields() {
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.0.1",
+ filesSharingPublicUpload = CapabilityBooleanType.TRUE,
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE
+ )
+ )
+ onView(withId(R.id.shareViaLinkEditPermissionGroup)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showDefaultLinkName() {
+ loadPublicShareDialogFragment()
+ onView(withId(R.id.shareViaLinkNameValue)).check(matches(withText("DOC_12112018.jpg link")))
+ }
+
+ @Test
+ fun enablePasswordSwitch() {
+ loadPublicShareDialogFragment()
+ onView(withId(R.id.shareViaLinkPasswordSwitch)).perform(click())
+ onView(withId(R.id.shareViaLinkPasswordValue)).check(matches(isDisplayed()))
+ onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
+ }
+
+ @Test
+ fun checkPasswordNotVisible() {
+ loadPublicShareDialogFragment()
+ onView(withId(R.id.shareViaLinkPasswordSwitch)).perform(click())
+ onView(withId(R.id.shareViaLinkPasswordValue)).perform(typeText("supersecure"))
+ onView(withId(R.id.shareViaLinkPasswordValue)).check(
+ matches(
+ withInputType(
+ TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_PASSWORD
+ )
+ )
+ )
+ }
+
+ @Test
+ fun checkPasswordEnforced() {
+ loadPublicShareDialogFragment(
+ capabilities = OC_CAPABILITY.copy(
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
+ )
+ )
+ onView(withId(R.id.shareViaLinkPasswordLabel)).check(
+ matches(withText(R.string.share_via_link_password_enforced_label))
+ )
+ onView(withId(R.id.shareViaLinkPasswordSwitch))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+ onView(withId(R.id.shareViaLinkPasswordValue))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
+ }
+
+ @Test
+ fun checkExpireDateEnforced() {
+ loadPublicShareDialogFragment(
+ capabilities = OC_CAPABILITY.copy(
+ filesSharingPublicExpireDateEnforced = CapabilityBooleanType.TRUE
+ )
+ )
+ onView(withId(R.id.shareViaLinkExpirationLabel))
+ .check(matches(withText(R.string.share_via_link_expiration_date_enforced_label)))
+ onView(withId(R.id.shareViaLinkExpirationSwitch))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+ onView(withId(R.id.shareViaLinkExpirationExplanationLabel))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ }
+
+ @Test
+ fun checkExpireDateNotEnforced() {
+ loadPublicShareDialogFragment(
+ capabilities = OC_CAPABILITY.copy(
+ filesSharingPublicExpireDateEnforced = CapabilityBooleanType.FALSE
+ )
+ )
+ onView(withId(R.id.shareViaLinkExpirationLabel))
+ .check(matches(withText(R.string.share_via_link_expiration_date_label)))
+ onView(withId(R.id.shareViaLinkExpirationSwitch))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ onView(withId(R.id.shareViaLinkExpirationExplanationLabel))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+ }
+
+ @Test
+ fun enableExpirationSwitch() {
+ loadPublicShareDialogFragment()
+ onView(withId(R.id.shareViaLinkExpirationSwitch)).perform(click())
+ onView(withId(android.R.id.button1)).perform(click())
+ onView(withId(R.id.shareViaLinkExpirationValue))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
+ //TODO: check the date form the picker
+ }
+
+ @Test
+ fun cancelExpirationSwitch() {
+ loadPublicShareDialogFragment()
+ onView(withId(R.id.shareViaLinkExpirationSwitch)).perform(click())
+ onView(withId(android.R.id.button2)).perform(click())
+ onView(withId(R.id.shareViaLinkExpirationValue))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.INVISIBLE)))
+ }
+
+ @Test
+ fun showError() {
+ loadPublicShareDialogFragment()
+
+ onView(withId(R.id.saveButton)).perform(click())
+
+ publicShareCreationStatus.postValue(
+ Event(
+ UIResult.Error(
+ error = Throwable("It was not possible to share this file or folder")
+ )
+ )
+ )
+
+ onView(withId(R.id.public_link_error_message)).check(matches(isDisplayed()))
+ onView(withId(R.id.public_link_error_message)).check(
+ matches(
+ withText(R.string.share_link_file_error)
+ )
+ )
+ }
+
+ @Test
+ fun uploadPermissionsWithFolderDisplayed() {
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicUpload = CapabilityBooleanType.TRUE
+ )
+ )
+
+ onView(withId(R.id.shareViaLinkEditPermissionGroup)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun uploadPermissionsWithFolderNotDisplayed() {
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicUpload = CapabilityBooleanType.FALSE
+ )
+ )
+
+ onView(withId(R.id.shareViaLinkEditPermissionGroup)).check(matches(not(isDisplayed())))
+ }
+
+ @Test
+ fun expirationDateDays() {
+ val daysToTest = 15
+ loadPublicShareDialogFragment(
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicExpireDateDays = daysToTest
+ )
+ )
+ val formattedDate = SimpleDateFormat.getDateInstance().format(
+ DateUtils.addDaysToDate(
+ Date(),
+ daysToTest
+ )
+ )
+ onView(withId(R.id.shareViaLinkExpirationSwitch))
+ .check(matches(isEnabled()))
+ onView(withId(R.id.shareViaLinkExpirationValue))
+ .check(matches(withText(formattedDate)))
+ }
+
+ @Test
+ fun passwordNotEnforced() {
+ loadPublicShareDialogFragment(
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
+ )
+ )
+ onView(withId(R.id.shareViaLinkPasswordLabel))
+ .check(matches(withText(R.string.share_via_link_password_label)))
+ onView(withId(R.id.saveButton)).check(matches(isEnabled()))
+ }
+
+ @Test
+ fun passwordEnforced() {
+ loadPublicShareDialogFragment(
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
+ )
+ )
+ onView(withId(R.id.shareViaLinkPasswordLabel))
+ .check(matches(withText(R.string.share_via_link_password_enforced_label)))
+ onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
+ }
+
+ @Test
+ fun passwordEnforcedReadOnlyFolders() {
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicUpload = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
+ )
+ )
+
+ onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).perform(scrollTo())
+ onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).check(matches(isDisplayed()))
+ onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).perform(click())
+ onView(withId(R.id.shareViaLinkPasswordLabel))
+ .check(matches(withText(R.string.share_via_link_password_enforced_label)))
+ onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
+ }
+
+ @Test
+ fun passwordNotEnforcedReadOnlyFolders() {
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicUpload = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.FALSE,
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
+ )
+ )
+ onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).check(matches(isDisplayed()))
+ onView(withId(R.id.shareViaLinkEditPermissionReadOnly)).perform(click())
+ onView(withId(R.id.shareViaLinkPasswordLabel))
+ .check(matches(withText(R.string.share_via_link_password_label)))
+ onView(withId(R.id.saveButton)).check(matches(isEnabled()))
+ }
+
+ @Test
+ fun passwordEnforcedReadWriteFolders() {
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicUpload = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforcedReadWrite = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
+ )
+ )
+ onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).check(matches(isDisplayed()))
+ onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).perform(click())
+ onView(withId(R.id.shareViaLinkPasswordLabel))
+ .check(matches(withText(R.string.share_via_link_password_enforced_label)))
+ onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
+ }
+
+ @Test
+ fun passwordNotEnforcedReadWriteFolders() {
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicUpload = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforcedReadWrite = CapabilityBooleanType.FALSE,
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
+ )
+ )
+ onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).check(matches(isDisplayed()))
+ onView(withId(R.id.shareViaLinkEditPermissionReadAndWrite)).perform(click())
+ onView(withId(R.id.shareViaLinkPasswordLabel))
+ .check(matches(withText(R.string.share_via_link_password_label)))
+ onView(withId(R.id.saveButton)).check(matches(isEnabled()))
+ }
+
+ @Test
+ fun passwordEnforcedUploadOnlyFolders() {
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicUpload = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
+ )
+ )
+ onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).check(matches(isDisplayed()))
+ onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).perform(click())
+ onView(withId(R.id.shareViaLinkPasswordLabel))
+ .check(matches(withText(R.string.share_via_link_password_enforced_label)))
+ onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
+ }
+
+ @Test
+ fun passwordNotEnforcedUploadOnlyFolders() {
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicUpload = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.FALSE,
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.FALSE
+ )
+ )
+ onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).check(matches(isDisplayed()))
+ onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).perform(click())
+ onView(withId(R.id.shareViaLinkPasswordLabel))
+ .check(matches(withText(R.string.share_via_link_password_label)))
+ onView(withId(R.id.saveButton)).check(matches(isEnabled()))
+ }
+
+ @Test
+ fun passwordEnforcedClearErrorMessageIfSwitchesToNotEnforced() {
+ val commonError = "Common error"
+
+ //One permission with password enforced. Error is cleaned after switching permission
+ //to a non-forced one
+ loadPublicShareDialogFragment(
+ isFolder = true,
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.TRUE,
+ filesSharingPublicUpload = CapabilityBooleanType.TRUE,
+ filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.FALSE,
+ filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.FALSE,
+ filesSharingPublicPasswordEnforced = CapabilityBooleanType.TRUE
+ )
+ )
+
+ onView(withId(R.id.saveButton)).perform(scrollTo())
+ onView(withId(R.id.saveButton)).perform(click())
+
+ publicShareCreationStatus.postValue(
+ Event(
+ UIResult.Error(
+ error = Throwable(commonError)
+ )
+ )
+ )
+
+ onView(withId(R.id.public_link_error_message)).perform(scrollTo())
+ onView(withText(commonError)).check(matches(isDisplayed()))
+
+ onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).perform(scrollTo(), click())
+
+ onView(withText(commonError)).check(matches(not(isDisplayed())))
+ onView(withId(R.id.saveButton)).check(matches(not(isEnabled())))
+ }
+
+ private fun loadPublicShareDialogFragment(
+ isFolder: Boolean = false,
+ capabilities: OCCapability = OC_CAPABILITY
+ ) {
+ val file = if (isFolder) OC_FOLDER else OC_FILE
+
+ val publicShareDialogFragment = PublicShareDialogFragment.newInstanceToCreate(
+ file,
+ OC_ACCOUNT,
+ "DOC_12112018.jpg link"
+ )
+
+ ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
+ it.startFragment(publicShareDialogFragment)
+ }
+
+ capabilitiesLiveData.postValue(
+ Event(UIResult.Success(capabilities))
+ )
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/PublicShareEditionDialogFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/PublicShareEditionDialogFragmentTest.kt
new file mode 100644
index 000000000000..2e20ab38b3e6
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/PublicShareEditionDialogFragmentTest.kt
@@ -0,0 +1,145 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.sharing.shares.ui
+
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario
+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.Visibility.VISIBLE
+import androidx.test.espresso.matcher.ViewMatchers.isChecked
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withHint
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.owncloud.android.R
+import com.owncloud.android.domain.capabilities.model.OCCapability
+import com.owncloud.android.domain.sharing.shares.model.ShareType
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.lib.resources.shares.RemoteShare
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.presentation.sharing.shares.PublicShareDialogFragment
+import com.owncloud.android.presentation.capabilities.CapabilityViewModel
+import com.owncloud.android.presentation.sharing.ShareViewModel
+import com.owncloud.android.testutil.OC_ACCOUNT
+import com.owncloud.android.testutil.OC_FILE
+import com.owncloud.android.testutil.OC_SHARE
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Before
+import org.junit.Test
+import org.koin.android.ext.koin.androidContext
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.dsl.module
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.GregorianCalendar
+import java.util.TimeZone
+
+class PublicShareEditionDialogFragmentTest {
+ private val capabilityViewModel = mockk(relaxed = true)
+ private val capabilitiesLiveData = MutableLiveData>>()
+ private val shareViewModel = mockk(relaxed = true)
+
+ private val expirationDate = 1556575200000 // GMT: Monday, April 29, 2019 10:00:00 PM
+
+ @Before
+ fun setUp() {
+ every { capabilityViewModel.capabilities } returns capabilitiesLiveData
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ androidContext(ApplicationProvider.getApplicationContext())
+ modules(
+ module {
+ viewModel {
+ capabilityViewModel
+ }
+ viewModel {
+ shareViewModel
+ }
+ }
+ )
+ }
+
+ val publicShareDialogFragment = PublicShareDialogFragment.newInstanceToUpdate(
+ OC_FILE,
+ OC_ACCOUNT,
+ OC_SHARE.copy(
+ shareType = ShareType.PUBLIC_LINK,
+ shareWith = "user",
+ name = "Docs link",
+ permissions = RemoteShare.CREATE_PERMISSION_FLAG,
+ expirationDate = expirationDate,
+ isFolder = true
+ )
+ )
+
+ ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
+ it.startFragment(publicShareDialogFragment)
+ }
+ }
+
+ @Test
+ fun showEditionDialogTitle() {
+ onView(withId(R.id.publicShareDialogTitle)).check(matches(withText(R.string.share_via_link_edit_title)))
+ }
+
+ @Test
+ fun checkLinkNameSet() {
+ onView(withText(R.string.share_via_link_name_label)).check(matches(ViewMatchers.isDisplayed()))
+ onView(withId(R.id.shareViaLinkNameValue)).check(matches(withText("Docs link")))
+ }
+
+ @Test
+ fun checkUploadOnly() {
+ onView(withId(R.id.shareViaLinkEditPermissionUploadFiles)).check(matches(isChecked()))
+ }
+
+ @Test
+ fun checkPasswordSet() {
+ onView(withId(R.id.shareViaLinkPasswordLabel)).check(matches(withText(R.string.share_via_link_password_label)))
+ onView(withId(R.id.shareViaLinkPasswordSwitch)).check(matches(withEffectiveVisibility(VISIBLE)))
+ onView(withId(R.id.shareViaLinkPasswordValue)).check(matches(withEffectiveVisibility(VISIBLE)))
+ onView(withId(R.id.shareViaLinkPasswordValue)).check(matches(withHint(R.string.share_via_link_default_password)))
+ }
+
+ @Test
+ fun checkExpirationDateSet() {
+ val calendar = GregorianCalendar()
+ calendar.timeInMillis = expirationDate
+
+ val formatter: DateFormat = SimpleDateFormat.getDateInstance()
+ formatter.timeZone = TimeZone.getDefault()
+
+ val time = formatter.format(calendar.time)
+
+ onView(withId(R.id.shareViaLinkExpirationLabel)).check(matches(withText(R.string.share_via_link_expiration_date_label)))
+ onView(withId(R.id.shareViaLinkExpirationSwitch)).check(matches(withEffectiveVisibility(VISIBLE)))
+ onView(withId(R.id.shareViaLinkExpirationValue)).check(matches(withEffectiveVisibility(VISIBLE)))
+ onView(withId(R.id.shareViaLinkExpirationValue)).check(matches(withText(time)))
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/ShareFileFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/ShareFileFragmentTest.kt
new file mode 100644
index 000000000000..b06716e91c44
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/ShareFileFragmentTest.kt
@@ -0,0 +1,318 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.sharing.shares.ui
+
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario
+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.hasSibling
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withTagValue
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.owncloud.android.R
+import com.owncloud.android.domain.capabilities.model.CapabilityBooleanType
+import com.owncloud.android.domain.capabilities.model.OCCapability
+import com.owncloud.android.domain.sharing.shares.model.OCShare
+import com.owncloud.android.domain.sharing.shares.model.ShareType
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.presentation.sharing.ShareFileFragment
+import com.owncloud.android.presentation.capabilities.CapabilityViewModel
+import com.owncloud.android.presentation.sharing.ShareViewModel
+import com.owncloud.android.testutil.OC_ACCOUNT
+import com.owncloud.android.testutil.OC_CAPABILITY
+import com.owncloud.android.testutil.OC_FILE
+import com.owncloud.android.testutil.OC_SHARE
+import com.owncloud.android.utils.matchers.assertVisibility
+import com.owncloud.android.utils.matchers.isDisplayed
+import com.owncloud.android.utils.matchers.withText
+import io.mockk.every
+import io.mockk.mockk
+import org.hamcrest.CoreMatchers
+import org.junit.Before
+import org.junit.Test
+import org.koin.android.ext.koin.androidContext
+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 ShareFileFragmentTest {
+ private val capabilityViewModel = mockk(relaxed = true)
+ private val capabilitiesLiveData = MutableLiveData>>()
+ private val shareViewModel = mockk(relaxed = true)
+ private val sharesLiveData = MutableLiveData>>>()
+
+ @Before
+ fun setUp() {
+ every { capabilityViewModel.capabilities } returns capabilitiesLiveData
+ every { shareViewModel.shares } returns sharesLiveData
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ androidContext(ApplicationProvider.getApplicationContext())
+ modules(
+ module {
+ viewModel {
+ capabilityViewModel
+ }
+ viewModel {
+ shareViewModel
+ }
+ }
+ )
+ }
+ }
+
+ @Test
+ fun showHeader() {
+ loadShareFileFragment()
+ onView(withId(R.id.shareFileName)).check(matches(withText(OC_FILE.fileName)))
+ }
+
+ @Test
+ fun fileSizeVisible() {
+ loadShareFileFragment()
+ R.id.shareFileSize.isDisplayed(displayed = true)
+ }
+
+ @Test
+ fun showPrivateLink() {
+ loadShareFileFragment()
+ R.id.getPrivateLinkButton.isDisplayed(displayed = true)
+ }
+
+ @Test
+ fun hidePrivateLink() {
+ loadShareFileFragment(capabilities = OC_CAPABILITY.copy(filesPrivateLinks = CapabilityBooleanType.FALSE))
+ R.id.getPrivateLinkButton.isDisplayed(displayed = false)
+ }
+
+ /******************************************************************************************************
+ ******************************************* PRIVATE SHARES *******************************************
+ ******************************************************************************************************/
+
+ private var userSharesList = listOf(
+ OC_SHARE.copy(sharedWithDisplayName = "Batman"),
+ OC_SHARE.copy(sharedWithDisplayName = "Joker")
+ )
+
+ private var groupSharesList = listOf(
+ OC_SHARE.copy(
+ shareType = ShareType.GROUP,
+ sharedWithDisplayName = "Suicide Squad"
+ ),
+ OC_SHARE.copy(
+ shareType = ShareType.GROUP,
+ sharedWithDisplayName = "Avengers"
+ )
+ )
+
+ @Test
+ fun showUsersAndGroupsSectionTitle() {
+ loadShareFileFragment(shares = userSharesList)
+ onView(withText(R.string.share_with_user_section_title)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showNoPrivateShares() {
+ loadShareFileFragment(shares = listOf())
+ onView(withText(R.string.share_no_users)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showUserShares() {
+ loadShareFileFragment(shares = userSharesList)
+ onView(withText("Batman")).check(matches(isDisplayed()))
+ onView(withText("Batman")).check(matches(hasSibling(withId(R.id.unshareButton))))
+ .check(matches(isDisplayed()))
+ onView(withText("Batman")).check(matches(hasSibling(withId(R.id.editShareButton))))
+ .check(matches(isDisplayed()))
+ onView(withText("Joker")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showGroupShares() {
+ loadShareFileFragment(shares = listOf(groupSharesList.first()))
+ onView(withText("Suicide Squad (group)")).check(matches(isDisplayed()))
+ onView(withText("Suicide Squad (group)")).check(matches(hasSibling(withId(R.id.icon))))
+ .check(matches(isDisplayed()))
+ onView(withTagValue(CoreMatchers.equalTo(R.drawable.ic_group))).check(matches(isDisplayed()))
+ }
+
+ /******************************************************************************************************
+ ******************************************* PUBLIC SHARES ********************************************
+ ******************************************************************************************************/
+
+ private var publicShareList = listOf(
+ OC_SHARE.copy(
+ shareType = ShareType.PUBLIC_LINK,
+ path = "/Photos/image.jpg",
+ isFolder = false,
+ name = "Image link",
+ shareLink = "http://server:port/s/1"
+ ),
+ OC_SHARE.copy(
+ shareType = ShareType.PUBLIC_LINK,
+ path = "/Photos/image.jpg",
+ isFolder = false,
+ name = "Image link 2",
+ shareLink = "http://server:port/s/2"
+ ),
+ OC_SHARE.copy(
+ shareType = ShareType.PUBLIC_LINK,
+ path = "/Photos/image.jpg",
+ isFolder = false,
+ name = "Image link 3",
+ shareLink = "http://server:port/s/3"
+ )
+ )
+
+ @Test
+ fun showNoPublicShares() {
+ loadShareFileFragment(shares = listOf())
+ onView(withText(R.string.share_no_public_links)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showPublicShares() {
+ loadShareFileFragment(shares = publicShareList)
+ onView(withText("Image link")).check(matches(isDisplayed()))
+ onView(withText("Image link")).check(matches(hasSibling(withId(R.id.getPublicLinkButton))))
+ .check(matches(isDisplayed()))
+ onView(withText("Image link")).check(matches(hasSibling(withId(R.id.deletePublicLinkButton))))
+ .check(matches(isDisplayed()))
+ onView(withText("Image link")).check(matches(hasSibling(withId(R.id.editPublicLinkButton))))
+ .check(matches(isDisplayed()))
+ onView(withText("Image link 2")).check(matches(isDisplayed()))
+ onView(withText("Image link 3")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun showPublicSharesSharingEnabled() {
+ loadShareFileFragment(
+ capabilities = OC_CAPABILITY.copy(filesSharingPublicEnabled = CapabilityBooleanType.TRUE),
+ shares = publicShareList
+ )
+
+ onView(withText("Image link")).check(matches(isDisplayed()))
+ onView(withText("Image link 2")).check(matches(isDisplayed()))
+ onView(withText("Image link 3")).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun hidePublicSharesSharingDisabled() {
+ loadShareFileFragment(
+ capabilities = OC_CAPABILITY.copy(filesSharingPublicEnabled = CapabilityBooleanType.FALSE),
+ shares = publicShareList
+ )
+
+ R.id.shareViaLinkSection.assertVisibility(ViewMatchers.Visibility.GONE)
+ }
+
+ @Test
+ fun createPublicShareMultipleCapability() {
+ loadShareFileFragment(
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicMultiple = CapabilityBooleanType.TRUE
+ ),
+ shares = listOf(publicShareList[0])
+ )
+
+ R.id.addPublicLinkButton.assertVisibility(ViewMatchers.Visibility.VISIBLE)
+ }
+
+ @Test
+ fun cannotCreatePublicShareMultipleCapability() {
+ loadShareFileFragment(
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "10.1.1",
+ filesSharingPublicMultiple = CapabilityBooleanType.FALSE
+ ),
+ shares = listOf(publicShareList[0])
+ )
+
+ R.id.addPublicLinkButton.assertVisibility(ViewMatchers.Visibility.INVISIBLE)
+ }
+
+ @Test
+ fun cannotCreatePublicShareServerCapability() {
+ loadShareFileFragment(
+ capabilities = OC_CAPABILITY.copy(
+ versionString = "9.3.1"
+ ),
+ shares = listOf(publicShareList[0])
+ )
+
+ R.id.addPublicLinkButton.assertVisibility(ViewMatchers.Visibility.INVISIBLE)
+ }
+
+ /******************************************************************************************************
+ *********************************************** COMMON ***********************************************
+ ******************************************************************************************************/
+
+ @Test
+ fun hideSharesSharingApiDisabled() {
+ loadShareFileFragment(
+ capabilities = OC_CAPABILITY.copy(
+ filesSharingApiEnabled = CapabilityBooleanType.FALSE
+ )
+ )
+ R.id.shareWithUsersSection.assertVisibility(ViewMatchers.Visibility.GONE)
+
+ R.id.shareViaLinkSection.assertVisibility(ViewMatchers.Visibility.GONE)
+ }
+
+ @Test
+ fun showError() {
+ loadShareFileFragment(
+ sharesUIResult = UIResult.Error(
+ error = Throwable("It was not possible to retrieve the shares from the server")
+ )
+ )
+ com.google.android.material.R.id.snackbar_text.withText(R.string.get_shares_error)
+ }
+
+ private fun loadShareFileFragment(
+ capabilities: OCCapability = OC_CAPABILITY,
+ capabilitiesEvent: Event> = Event(UIResult.Success(capabilities)),
+ shares: List = listOf(OC_SHARE),
+ sharesUIResult: UIResult> = UIResult.Success(shares)
+ ) {
+ val shareFileFragment = ShareFileFragment.newInstance(
+ OC_FILE,
+ OC_ACCOUNT
+ )
+
+ ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
+ it.startFragment(shareFileFragment)
+ }
+
+ capabilitiesLiveData.postValue(capabilitiesEvent)
+ sharesLiveData.postValue(Event(sharesUIResult))
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/ShareFolderFragmentTest.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/ShareFolderFragmentTest.kt
new file mode 100644
index 000000000000..414f52814c26
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/ShareFolderFragmentTest.kt
@@ -0,0 +1,104 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Jesus Recio Rincon
+ * Copyright (C) 2020 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.sharing.shares.ui
+
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ActivityScenario
+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.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import com.owncloud.android.R
+import com.owncloud.android.domain.capabilities.model.OCCapability
+import com.owncloud.android.domain.sharing.shares.model.OCShare
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.presentation.sharing.ShareFileFragment
+import com.owncloud.android.presentation.capabilities.CapabilityViewModel
+import com.owncloud.android.presentation.sharing.ShareViewModel
+import com.owncloud.android.testutil.OC_ACCOUNT
+import com.owncloud.android.testutil.OC_CAPABILITY
+import com.owncloud.android.testutil.OC_FOLDER
+import com.owncloud.android.testutil.OC_SHARE
+import io.mockk.every
+import io.mockk.mockk
+import org.hamcrest.CoreMatchers.not
+import org.junit.Before
+import org.junit.Test
+import org.koin.android.ext.koin.androidContext
+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 ShareFolderFragmentTest {
+ private val capabilityViewModel = mockk(relaxed = true)
+ private val capabilitiesLiveData = MutableLiveData>>()
+ private val shareViewModel = mockk(relaxed = true)
+ private val sharesLiveData = MutableLiveData>>>()
+
+ @Before
+ fun setUp() {
+ every { capabilityViewModel.capabilities } returns capabilitiesLiveData
+ every { shareViewModel.shares } returns sharesLiveData
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ androidContext(ApplicationProvider.getApplicationContext())
+ modules(
+ module {
+ viewModel {
+ capabilityViewModel
+ }
+ viewModel {
+ shareViewModel
+ }
+ }
+ )
+ }
+
+ val shareFileFragment = ShareFileFragment.newInstance(
+ OC_FOLDER.copy(privateLink = null),
+ OC_ACCOUNT
+ )
+
+ ActivityScenario.launch(TestShareFileActivity::class.java).onActivity {
+ it.startFragment(shareFileFragment)
+ }
+
+ capabilitiesLiveData.postValue(Event(UIResult.Success(OC_CAPABILITY)))
+
+ sharesLiveData.postValue(Event(UIResult.Success(listOf(OC_SHARE))))
+ }
+
+ @Test
+ fun folderSizeVisible() {
+ onView(withId(R.id.shareFileSize)).check(matches(not(isDisplayed())))
+ }
+
+ @Test
+ fun hidePrivateLink() {
+ onView(withId(R.id.getPrivateLinkButton)).check(matches(not(isDisplayed())))
+ }
+}
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
new file mode 100644
index 000000000000..f4c4ae95864b
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/sharing/shares/ui/TestShareFileActivity.kt
@@ -0,0 +1,87 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.sharing.shares.ui
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.commit
+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, ContainerActivity {
+ fun startFragment(fragment: Fragment) {
+ supportFragmentManager.commit(allowStateLoss = true) {
+ add(R.id.container, fragment, TEST_FRAGMENT_TAG)
+ }
+ }
+
+ fun getTestFragment(): Fragment? = supportFragmentManager.findFragmentByTag(TEST_FRAGMENT_TAG)
+
+ override fun copyOrSendPrivateLink(file: OCFile) {
+ }
+
+ override fun deleteShare(remoteId: String) {
+ }
+
+ override fun showLoading() {
+ }
+
+ override fun dismissLoading() {
+ }
+
+ override fun showAddPublicShare(defaultLinkName: String) {
+ }
+
+ override fun showEditPublicShare(share: OCShare) {
+ }
+
+ override fun showRemoveShare(share: OCShare) {
+ }
+
+ override fun copyOrSendPublicLink(share: OCShare) {
+ }
+
+ override fun showSearchUsersAndGroups() {
+ }
+
+ override fun showEditPrivateShare(share: OCShare) {
+ }
+
+ 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
new file mode 100644
index 000000000000..746e89ab3d1a
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/ui/activity/ReleaseNotesActivityTest.kt
@@ -0,0 +1,114 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David Crespo Ríos
+ * Copyright (C) 2022 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.activity
+
+import android.app.Activity
+import android.content.Context
+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.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
+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 ReleaseNotesActivityTest {
+ private lateinit var activityScenario: ActivityScenario
+ private lateinit var context: Context
+
+ private lateinit var releaseNotesViewModel: ReleaseNotesViewModel
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ releaseNotesViewModel = mockk(relaxed = true)
+
+ stopKoin()
+
+ startKoin {
+ allowOverride(true)
+ context
+ modules(
+ module {
+ viewModel {
+ releaseNotesViewModel
+ }
+ }
+ )
+ }
+
+ every { releaseNotesViewModel.getReleaseNotes() } returns releaseNotesList
+
+ val intent = Intent(context, ReleaseNotesActivity::class.java)
+ activityScenario = ActivityScenario.launch(intent)
+ }
+
+ @Test
+ fun releaseNotesView() {
+ val header = String.format(
+ context.getString(R.string.release_notes_header),
+ context.getString(R.string.app_name)
+ )
+
+ val footer = String.format(
+ context.getString(R.string.release_notes_footer),
+ context.getString(R.string.app_name)
+ )
+
+ with(R.id.txtHeader) {
+ isDisplayed(true)
+ withText(header)
+ }
+
+ R.id.releaseNotes.isDisplayed(true)
+
+ with(R.id.txtFooter) {
+ isDisplayed(true)
+ withText(footer)
+ }
+
+ R.id.btnProceed.isDisplayed(true)
+ }
+
+ @Test
+ fun releaseNotesProceedButton() {
+ R.id.btnProceed.click()
+
+ assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
+ }
+
+ @Test
+ fun test_childCount() {
+ R.id.releaseNotes.assertChildCount(3)
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ActionsExt.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ActionsExt.kt
new file mode 100644
index 000000000000..1ccaeeb8b231
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ActionsExt.kt
@@ -0,0 +1,42 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ *
+ * Copyright (C) 2020 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 androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.scrollTo
+import androidx.test.espresso.matcher.ViewMatchers.withId
+
+fun Int.typeText(text: String) {
+ onView(withId(this)).perform(scrollTo(), ViewActions.typeText(text))
+}
+
+fun Int.replaceText(text: String) {
+ onView(withId(this)).perform(scrollTo(), ViewActions.replaceText(text))
+}
+
+fun Int.scrollAndClick() {
+ onView(withId(this)).perform(scrollTo(), ViewActions.click())
+}
+
+fun Int.click() {
+ onView(withId(this)).perform(ViewActions.click())
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/IntentsUtil.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/IntentsUtil.kt
new file mode 100644
index 000000000000..e7262d9490f0
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/IntentsUtil.kt
@@ -0,0 +1,58 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2021 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.app.Activity
+import android.app.Instrumentation
+import android.content.Intent
+import androidx.test.espresso.intent.Intents.intending
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
+
+fun mockIntent(
+ extras: Pair,
+ resultCode: Int = Activity.RESULT_OK,
+ action: String
+) {
+ val result = Intent()
+ result.putExtra(extras.first, extras.second)
+ val intentResult = Instrumentation.ActivityResult(resultCode, result)
+ intending(hasAction(action)).respondWith(intentResult)
+}
+
+@JvmName("mockIntentNoExtras")
+fun mockIntent(
+ resultCode: Int = Activity.RESULT_OK,
+ action: String
+) {
+ val result = Intent()
+ val intentResult = Instrumentation.ActivityResult(resultCode, result)
+ intending(hasAction(action)).respondWith(intentResult)
+}
+
+fun mockIntentToComponent(
+ resultCode: Int = Activity.RESULT_OK,
+ packageName: String
+) {
+ val result = Intent()
+ val intentResult = Instrumentation.ActivityResult(resultCode, result)
+ intending(hasComponent(packageName)).respondWith(intentResult)
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/OCTestAndroidJUnitRunner.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/OCTestAndroidJUnitRunner.kt
new file mode 100644
index 000000000000..d13700fe7d87
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/OCTestAndroidJUnitRunner.kt
@@ -0,0 +1,36 @@
+/**
+ * ownCloud Android client application
+ *
+ * Copyright (C) 2026 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.app.Application
+import android.content.Context
+import android.os.Build
+import androidx.test.runner.AndroidJUnitRunner
+import com.github.tmurakami.dexopener.DexOpener
+
+/**
+ * We need to use DexOpener for executing instrumented tests on
.
+ */
+
+package com.owncloud.android.utils
+
+enum class Permissions(val value: Int) {
+ READ_PERMISSIONS(1),
+ EDIT_PERMISSIONS(3),
+ SHARE_PERMISSIONS(17),
+ ALL_PERMISSIONS(19),
+
+ // FOLDERS
+ EDIT_CREATE_PERMISSIONS(5),
+ EDIT_CREATE_CHANGE_PERMISSIONS(7),
+ EDIT_CREATE_DELETE_PERMISSIONS(13),
+ EDIT_CREATE_CHANGE_DELETE_PERMISSIONS(15),
+ EDIT_CHANGE_PERMISSIONS(3),
+ EDIT_CHANGE_DELETE_PERMISSIONS(11),
+ EDIT_DELETE_PERMISSIONS(9),
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ReleaseNotesList.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ReleaseNotesList.kt
new file mode 100644
index 000000000000..16a2cde32a7a
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/ReleaseNotesList.kt
@@ -0,0 +1,42 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David Crespo Ríos
+ * Copyright (C) 2022 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 com.owncloud.android.R
+import com.owncloud.android.presentation.releasenotes.ReleaseNote
+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
+ ),
+ ReleaseNote(
+ title = R.string.release_notes_header,
+ subtitle = R.string.release_notes_footer,
+ type = ReleaseNoteType.BUGFIX
+ ),
+ ReleaseNote(
+ title = R.string.release_notes_header,
+ subtitle = R.string.release_notes_footer,
+ type = ReleaseNoteType.ENHANCEMENT
+ )
+)
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/BottomSheetFragmentItemViewMatchers.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/BottomSheetFragmentItemViewMatchers.kt
new file mode 100644
index 000000000000..eee9b2ecb796
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/BottomSheetFragmentItemViewMatchers.kt
@@ -0,0 +1,77 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ *
+ * Copyright (C) 2020 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.matchers
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.BoundedMatcher
+import androidx.test.espresso.matcher.RootMatchers
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.owncloud.android.R
+import com.owncloud.android.presentation.common.BottomSheetFragmentItemView
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+
+fun Int.bsfItemWithTitle(@StringRes title: Int, @ColorRes tintColor: Int?) {
+ Espresso.onView(ViewMatchers.withId(this)).inRoot(RootMatchers.isDialog())
+ .check(ViewAssertions.matches(withTitle(title, tintColor)))
+}
+
+fun Int.bsfItemWithIcon(@DrawableRes drawable: Int, @ColorRes tintColor: Int?) {
+ Espresso.onView(ViewMatchers.withId(this)).inRoot(RootMatchers.isDialog())
+ .check(ViewAssertions.matches(withIcon(drawable, tintColor)))
+}
+
+private fun withTitle(@StringRes title: Int, @ColorRes tintColor: Int?): Matcher =
+ object : BoundedMatcher(BottomSheetFragmentItemView::class.java) {
+
+ override fun describeTo(description: Description) {
+ description.appendText("BottomSheetFragmentItemView with text: $title")
+ tintColor?.let { description.appendText(" and tint color id: $tintColor") }
+ }
+
+ override fun matchesSafely(item: BottomSheetFragmentItemView): Boolean {
+ val itemTitleView = item.findViewById(R.id.item_title)
+ val textMatches = withText(title).matches(itemTitleView)
+ val textColorMatches = tintColor?.let { withTextColor(tintColor).matches(itemTitleView) } ?: true
+ return textMatches && textColorMatches
+ }
+ }
+
+private fun withIcon(@DrawableRes drawable: Int, @ColorRes tintColor: Int?): Matcher =
+ object : BoundedMatcher(BottomSheetFragmentItemView::class.java) {
+
+ override fun describeTo(description: Description) {
+ description.appendText("BottomSheetFragmentItemView with icon: $drawable")
+ tintColor?.let { description.appendText(" and tint color id: $tintColor") }
+ }
+
+ override fun matchesSafely(item: BottomSheetFragmentItemView): Boolean {
+ val itemIconView = item.findViewById(R.id.item_icon)
+ return withDrawable(drawable, tintColor).matches(itemIconView)
+ }
+ }
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/CountViewMatchers.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/CountViewMatchers.kt
new file mode 100644
index 000000000000..84b60d4b5e33
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/CountViewMatchers.kt
@@ -0,0 +1,62 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Christian Schabesberger
+ * Copyright (C) 2021 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.matchers
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.espresso.matcher.BoundedMatcher
+import org.hamcrest.Matcher
+import org.hamcrest.TypeSafeMatcher
+import org.hamcrest.Description
+
+fun withChildViewCount(count: Int, childMatcher: Matcher): Matcher {
+ return object : BoundedMatcher(ViewGroup::class.java) {
+ override fun matchesSafely(viewGroup: ViewGroup): Boolean {
+ var matchCount = 0
+ for (i in 0 until viewGroup.childCount) {
+ if (childMatcher.matches(viewGroup.getChildAt(i))) {
+ matchCount++
+ }
+ }
+ return matchCount == count
+ }
+
+ override fun describeTo(description: Description?) {
+ description?.appendText("ViewGroup with child-count=$count and")
+ childMatcher.describeTo(description)
+ }
+ }
+}
+
+fun nthChildOf(parentMatcher: Matcher, childPosition: Int): Matcher {
+ return object : TypeSafeMatcher() {
+ override fun matchesSafely(view: View): Boolean {
+ if (view.parent !is ViewGroup) {
+ return parentMatcher.matches(view.parent)
+ }
+ val group = view.parent as ViewGroup
+ return parentMatcher.matches(view.parent) && group.getChildAt(childPosition) == view
+ }
+
+ override fun describeTo(description: Description) {
+ description.appendText("with $childPosition child view of type parentMatcher")
+ }
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/ImageViewMatcher.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/ImageViewMatcher.kt
new file mode 100644
index 000000000000..f64da637ab6e
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/ImageViewMatcher.kt
@@ -0,0 +1,63 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ *
+ * Copyright (C) 2020 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.matchers
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.PorterDuff
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.toBitmap
+import org.hamcrest.Description
+import org.hamcrest.TypeSafeMatcher
+
+fun withDrawable(
+ @DrawableRes id: Int,
+ @ColorRes tint: Int? = null,
+ tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_IN
+) = object : TypeSafeMatcher() {
+ override fun describeTo(description: Description) {
+ description.appendText("ImageView with drawable same as drawable with id $id")
+ tint?.let { description.appendText(", tint color id: $tint, mode: $tintMode") }
+ }
+
+ override fun matchesSafely(view: View): Boolean {
+ val context = view.context
+ val tintColor = tint?.toColor(context)
+ val expectedBitmap = context.getDrawable(id)?.tinted(tintColor, tintMode)?.toBitmap()
+
+ return view is ImageView && view.drawable.toBitmap().sameAs(expectedBitmap)
+ }
+}
+
+private fun Int.toColor(context: Context) = ContextCompat.getColor(context, this)
+
+private fun Drawable.tinted(@ColorInt tintColor: Int? = null, tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_IN) =
+ apply {
+ setTintList(tintColor?.toColorStateList())
+ setTintMode(tintMode)
+ }
+
+private fun Int.toColorStateList() = ColorStateList.valueOf(this)
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/MatcherExt.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/MatcherExt.kt
new file mode 100644
index 000000000000..51f4feec185c
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/MatcherExt.kt
@@ -0,0 +1,74 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ *
+ * Copyright (C) 2020 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.matchers
+
+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.hasChildCount
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import org.hamcrest.CoreMatchers
+
+fun Int.isDisplayed(displayed: Boolean) {
+ val displayMatcher = if (displayed) ViewMatchers.isDisplayed() else CoreMatchers.not(ViewMatchers.isDisplayed())
+
+ onView(withId(this))
+ .check(matches(displayMatcher))
+}
+
+fun Int.isEnabled(enabled: Boolean) {
+ val enableMatcher = if (enabled) ViewMatchers.isEnabled() else CoreMatchers.not(ViewMatchers.isEnabled())
+
+ onView(withId(this))
+ .check(matches(enableMatcher))
+}
+
+fun Int.isFocusable(focusable: Boolean) {
+ val focusableMatcher = if (focusable) ViewMatchers.isFocusable() else CoreMatchers.not(ViewMatchers.isFocusable())
+
+ onView(withId(this))
+ .check(matches(focusableMatcher))
+}
+
+fun Int.withText(text: String) {
+ onView(withId(this))
+ .check(matches(ViewMatchers.withText(text)))
+}
+
+fun Int.withText(resourceId: Int) {
+ onView(withId(this))
+ .check(matches(ViewMatchers.withText(resourceId)))
+}
+
+fun Int.withChildCountAndId(count: Int, resourceId: Int) {
+ onView(withId(this))
+ .check(matches(withChildViewCount(count, withId(resourceId))))
+}
+
+fun Int.assertVisibility(visibility: ViewMatchers.Visibility) {
+ onView(withId(this))
+ .check(matches(ViewMatchers.withEffectiveVisibility(visibility)))
+}
+
+fun Int.assertChildCount(childs: Int) {
+ onView(withId(this))
+ .check(matches(hasChildCount(childs)))
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/PreferenceExt.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/PreferenceExt.kt
new file mode 100644
index 000000000000..6ef6b5c41de4
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/PreferenceExt.kt
@@ -0,0 +1,48 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2021 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.matchers
+
+import androidx.preference.Preference
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import org.junit.Assert.assertEquals
+
+fun Preference.verifyPreference(
+ keyPref: String,
+ titlePref: String,
+ summaryPref: String? = null,
+ visible: Boolean,
+ enabled: Boolean? = null
+) {
+ if (visible) onView(withText(titlePref)).check(matches(isDisplayed()))
+ summaryPref?.let {
+ if (visible) onView(withText(it)).check(matches(isDisplayed()))
+ assertEquals(it, summary)
+ }
+ assertEquals(keyPref, key)
+ assertEquals(titlePref, title)
+ assertEquals(visible, isVisible)
+ enabled?.let {
+ assertEquals(enabled, isEnabled)
+ }
+}
diff --git a/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/TextViewMatcher.kt b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/TextViewMatcher.kt
new file mode 100644
index 000000000000..bfcbb38fce73
--- /dev/null
+++ b/owncloudApp/src/androidTest/java/com/owncloud/android/utils/matchers/TextViewMatcher.kt
@@ -0,0 +1,43 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ *
+ * Copyright (C) 2020 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.matchers
+
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorRes
+import androidx.core.content.ContextCompat
+import androidx.test.espresso.matcher.BoundedMatcher
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+
+fun withTextColor(
+ @ColorRes textColor: Int
+): Matcher =
+ object : BoundedMatcher(TextView::class.java) {
+ override fun describeTo(description: Description) {
+ description.appendText("TextView with text color: $textColor")
+ }
+
+ override fun matchesSafely(view: TextView): Boolean {
+ val expectedColor = ContextCompat.getColor(view.context, textColor)
+ val actualColor = view.currentTextColor
+ return actualColor == expectedColor
+ }
+ }
diff --git a/owncloudApp/src/debug/java/com/owncloud/android/testing/SingleFragmentActivity.kt b/owncloudApp/src/debug/java/com/owncloud/android/testing/SingleFragmentActivity.kt
new file mode 100644
index 000000000000..9b8e947e8b95
--- /dev/null
+++ b/owncloudApp/src/debug/java/com/owncloud/android/testing/SingleFragmentActivity.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.owncloud.android.testing
+
+import android.os.Bundle
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import com.owncloud.android.R
+import com.owncloud.android.ui.activity.BaseActivity
+
+/**
+ * Used for testing fragments inside a fake activity.
+ */
+open class SingleFragmentActivity : BaseActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val content = FrameLayout(this).apply {
+ layoutParams = FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ id = R.id.container
+ }
+ setContentView(content)
+ }
+}
diff --git a/owncloudApp/src/debug/java/com/owncloud/android/utils/DebugInjector.kt b/owncloudApp/src/debug/java/com/owncloud/android/utils/DebugInjector.kt
new file mode 100644
index 000000000000..1933ce09d19b
--- /dev/null
+++ b/owncloudApp/src/debug/java/com/owncloud/android/utils/DebugInjector.kt
@@ -0,0 +1,41 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Christian Schabesberger
+ * Copyright (C) 2020 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.StrictMode
+import com.facebook.stetho.Stetho
+
+object DebugInjector {
+ open fun injectDebugTools(context: Context) {
+ Stetho.initializeWithDefaults(context)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ StrictMode.setVmPolicy(
+ StrictMode.VmPolicy.Builder()
+ .penaltyLog()
+ .detectNonSdkApiUsage()
+ .detectUnsafeIntentLaunch()
+ .build()
+ )
+ }
+ }
+}
diff --git a/owncloudApp/src/main/AndroidManifest.xml b/owncloudApp/src/main/AndroidManifest.xml
new file mode 100644
index 000000000000..c5862b79cab1
--- /dev/null
+++ b/owncloudApp/src/main/AndroidManifest.xml
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/owncloudApp/src/main/java/com/owncloud/android/AppRater.java b/owncloudApp/src/main/java/com/owncloud/android/AppRater.java
new file mode 100644
index 000000000000..46c0fc27c0a7
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/AppRater.java
@@ -0,0 +1,97 @@
+/**
+ * ownCloud Android client application
+ *
+ * Copyright (C) 2020 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;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import com.owncloud.android.ui.dialog.RateMeDialog;
+import timber.log.Timber;
+
+public class AppRater {
+ private static final String DIALOG_RATE_ME_TAG = "DIALOG_RATE_ME";
+
+ private final static int DAYS_UNTIL_PROMPT = 7;
+ private final static int LAUNCHES_UNTIL_PROMPT = 2;
+ private final static int DAYS_UNTIL_NEUTRAL_CLICK = 7;
+
+ public static final String APP_RATER_PREF_TITLE = "app_rater";
+ public static final String APP_RATER_PREF_DONT_SHOW = "don't_show_again";
+ private static final String APP_RATER_PREF_LAUNCH_COUNT = "launch_count";
+ private static final String APP_RATER_PREF_DATE_FIRST_LAUNCH = "date_first_launch";
+ public static final String APP_RATER_PREF_DATE_NEUTRAL = "date_neutral";
+
+ public static void appLaunched(Context mContext, String packageName) {
+ SharedPreferences prefs = mContext.getSharedPreferences(APP_RATER_PREF_TITLE, 0);
+ if (prefs.getBoolean(APP_RATER_PREF_DONT_SHOW, false)) {
+ Timber.d("Do not show the rate dialog again as the user decided");
+ return;
+ }
+
+ SharedPreferences.Editor editor = prefs.edit();
+
+ /// Increment launch counter
+ long launchCount = prefs.getLong(APP_RATER_PREF_LAUNCH_COUNT, 0) + 1;
+ Timber.d("The app has been launched " + launchCount + " times");
+ editor.putLong(APP_RATER_PREF_LAUNCH_COUNT, launchCount);
+
+ /// Get date of first launch
+ long dateFirstLaunch = prefs.getLong(APP_RATER_PREF_DATE_FIRST_LAUNCH, 0);
+ if (dateFirstLaunch == 0) {
+ dateFirstLaunch = System.currentTimeMillis();
+ Timber.d("The app has been launched in " + dateFirstLaunch + " for the first time");
+ editor.putLong(APP_RATER_PREF_DATE_FIRST_LAUNCH, dateFirstLaunch);
+ }
+
+ /// Get date of neutral click
+ long dateNeutralClick = prefs.getLong(APP_RATER_PREF_DATE_NEUTRAL, 0);
+
+ /// Wait at least n days before opening
+ if (launchCount >= LAUNCHES_UNTIL_PROMPT) {
+ Timber.d("The number of launches already exceed " + LAUNCHES_UNTIL_PROMPT +
+ ", the default number of launches, so let's check some dates");
+ Timber.d("Current moment is %s", System.currentTimeMillis());
+ Timber.d("The date of the first launch + days until prompt is " + dateFirstLaunch +
+ daysToMilliseconds(DAYS_UNTIL_PROMPT));
+ Timber.d("The date of the neutral click + days until neutral click is " + dateNeutralClick +
+ daysToMilliseconds(DAYS_UNTIL_NEUTRAL_CLICK));
+ if (System.currentTimeMillis() >= Math.max(dateFirstLaunch
+ + daysToMilliseconds(DAYS_UNTIL_PROMPT), dateNeutralClick
+ + daysToMilliseconds(DAYS_UNTIL_NEUTRAL_CLICK))) {
+ Timber.d("The current moment is later than any of the days set, so let's show the rate dialog");
+ showRateDialog(mContext, packageName);
+ }
+ }
+
+ editor.apply();
+ }
+
+ private static int daysToMilliseconds(int days) {
+ return days * 24 * 60 * 60 * 1000;
+ }
+
+ private static void showRateDialog(Context mContext, String packageName) {
+ RateMeDialog rateMeDialog = RateMeDialog.newInstance(packageName, false);
+ FragmentManager fm = ((FragmentActivity) mContext).getSupportFragmentManager();
+ FragmentTransaction ft = fm.beginTransaction();
+ rateMeDialog.show(ft, DIALOG_RATE_ME_TAG);
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt b/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt
new file mode 100644
index 000000000000..5380e2078401
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/MainApp.kt
@@ -0,0 +1,409 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author masensio
+ * @author David A. Velasco
+ * @author David González Verdugo
+ * @author Christian Schabesberger
+ * @author David Crespo Ríos
+ * @author Juan Carlos Garrote Gascón
+ * @author Aitor Ballesteros Pavón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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
+
+import android.app.Activity
+import android.app.Application
+import android.app.NotificationManager.IMPORTANCE_LOW
+import android.content.Context
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+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.data.providers.implementation.OCSharedPreferencesProvider
+import com.owncloud.android.datamodel.ThumbnailsCacheManager
+import com.owncloud.android.db.PreferenceManager
+import com.owncloud.android.dependecyinjection.commonModule
+import com.owncloud.android.dependecyinjection.localDataSourceModule
+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.domain.user.usecases.GetStoredQuotaUseCase
+import com.owncloud.android.extensions.createNotificationChannel
+import com.owncloud.android.lib.common.SingleSessionManager
+import com.owncloud.android.presentation.authentication.AccountUtils
+import com.owncloud.android.presentation.migration.StorageMigrationActivity
+import com.owncloud.android.presentation.releasenotes.ReleaseNotesActivity
+import com.owncloud.android.presentation.security.biometric.BiometricActivity
+import com.owncloud.android.presentation.security.biometric.BiometricManager
+import com.owncloud.android.presentation.security.passcode.PassCodeActivity
+import com.owncloud.android.presentation.security.passcode.PassCodeManager
+import com.owncloud.android.presentation.security.pattern.PatternActivity
+import com.owncloud.android.presentation.security.pattern.PatternManager
+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.FileDisplayActivity.Companion.PREFERENCE_CLEAR_DATA_ALREADY_TRIGGERED
+import com.owncloud.android.ui.activity.WhatsNewActivity
+import com.owncloud.android.utils.CONFIGURATION_ALLOW_SCREENSHOTS
+import com.owncloud.android.utils.DOWNLOAD_NOTIFICATION_CHANNEL_ID
+import com.owncloud.android.utils.DebugInjector
+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
+import timber.log.Timber
+
+/**
+ * Main Application of the project
+ *
+ *
+ * Contains methods to build the "static" strings. These strings were before constants in different
+ * classes
+ */
+class MainApp : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+
+ appContext = applicationContext
+
+ startLogsIfEnabled()
+
+ DebugInjector.injectDebugTools(appContext)
+
+ createNotificationChannels()
+
+ SingleSessionManager.setUserAgent(userAgent)
+
+ // initialise thumbnails cache on background thread
+ ThumbnailsCacheManager.InitDiskCacheTask().execute()
+
+ initDependencyInjection()
+
+ // register global protection with pass code, pattern lock and biometric lock
+ registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ Timber.d("${activity.javaClass.simpleName} onCreate(Bundle) starting")
+
+ // To prevent taking screenshots in MDM
+ if (!areScreenshotsAllowed()) {
+ activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ }
+
+ // If there's any lock protection, don't show wizard at this point, show it when lock activities
+ // have finished
+ if (activity !is PassCodeActivity &&
+ activity !is PatternActivity &&
+ activity !is BiometricActivity
+ ) {
+ StorageMigrationActivity.runIfNeeded(activity)
+ if (isFirstRun()) {
+ WhatsNewActivity.runIfNeeded(activity)
+
+ } else {
+ ReleaseNotesActivity.runIfNeeded(activity)
+
+ val pref = PreferenceManager.getDefaultSharedPreferences(appContext)
+ val clearDataAlreadyTriggered = pref.contains(PREFERENCE_CLEAR_DATA_ALREADY_TRIGGERED)
+ if (clearDataAlreadyTriggered || isNewVersionCode()) {
+ 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()
+ }
+ } else { // "Clear data" button is pressed from the app settings in the device settings.
+ AccountUtils.deleteAccounts(appContext)
+ WhatsNewActivity.runIfNeeded(activity)
+ }
+ }
+ }
+
+ PreferenceManager.migrateFingerprintToBiometricKey(applicationContext)
+ 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(
+ 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(
+ GetPersonalSpaceForAccountUseCase.Params(
+ accountName = account.name
+ )
+ )
+ }
+ }
+
+ val getStoredQuotaUseCase: GetStoredQuotaUseCase by inject()
+ val quota = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
+ getStoredQuotaUseCase(
+ GetStoredQuotaUseCase.Params(
+ accountName = account.name
+ )
+ )
+ }
+ val isLightUser = quota.getDataOrNull()?.available == -4L
+
+ spacesAllowed && personalSpace == null && !isLightUser
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+ Timber.v("${activity.javaClass.simpleName} onStart() starting")
+ PassCodeManager.onActivityStarted(activity)
+ PatternManager.onActivityStarted(activity)
+ BiometricManager.onActivityStarted(activity)
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ Timber.v("${activity.javaClass.simpleName} onResume() starting")
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+ Timber.v("${activity.javaClass.simpleName} onPause() ending")
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+ Timber.v("${activity.javaClass.simpleName} onStop() ending")
+ PassCodeManager.onActivityStopped(activity)
+ PatternManager.onActivityStopped(activity)
+ BiometricManager.onActivityStopped(activity)
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
+ Timber.v("${activity.javaClass.simpleName} onSaveInstanceState(Bundle) starting")
+ }
+
+ override fun onActivityDestroyed(activity: Activity) {
+ Timber.v("${activity.javaClass.simpleName} onDestroy() ending")
+ }
+ })
+
+ }
+
+ private fun startLogsIfEnabled() {
+ val preferenceProvider = OCSharedPreferencesProvider(applicationContext)
+
+ if (BuildConfig.DEBUG) {
+ val alreadySet = preferenceProvider.containsPreference(PREFERENCE_ENABLE_LOGGING)
+ if (!alreadySet) {
+ preferenceProvider.putBoolean(PREFERENCE_ENABLE_LOGGING, true)
+ }
+ }
+
+ enabledLogging = preferenceProvider.getBoolean(PREFERENCE_ENABLE_LOGGING, false)
+
+ if (enabledLogging) {
+ val mdmProvider = MdmProvider(applicationContext)
+ LogsProvider(applicationContext, mdmProvider).startLogging()
+ }
+ }
+
+ /**
+ * Screenshots allowed in debug or QA mode. Devs and tests <3
+ * Otherwise, depends on branding.
+ */
+ private fun areScreenshotsAllowed(): Boolean {
+ if (BuildConfig.DEBUG || BuildConfig.FLAVOR == QA_FLAVOR) return true
+
+ val mdmProvider = MdmProvider(applicationContext)
+ return mdmProvider.getBrandingBoolean(CONFIGURATION_ALLOW_SCREENSHOTS, R.bool.allow_screenshots)
+ }
+
+ private fun createNotificationChannels() {
+ createNotificationChannel(
+ id = DOWNLOAD_NOTIFICATION_CHANNEL_ID,
+ name = getString(R.string.download_notification_channel_name),
+ description = getString(R.string.download_notification_channel_description),
+ importance = IMPORTANCE_LOW
+ )
+
+ createNotificationChannel(
+ id = UPLOAD_NOTIFICATION_CHANNEL_ID,
+ name = getString(R.string.upload_notification_channel_name),
+ description = getString(R.string.upload_notification_channel_description),
+ importance = IMPORTANCE_LOW
+ )
+
+ createNotificationChannel(
+ id = MEDIA_SERVICE_NOTIFICATION_CHANNEL_ID,
+ name = getString(R.string.media_service_notification_channel_name),
+ description = getString(R.string.media_service_notification_channel_description),
+ importance = IMPORTANCE_LOW
+ )
+
+ createNotificationChannel(
+ id = FILE_SYNC_CONFLICT_NOTIFICATION_CHANNEL_ID,
+ name = getString(R.string.file_sync_conflict_notification_channel_name),
+ description = getString(R.string.file_sync_conflict_notification_channel_description),
+ importance = IMPORTANCE_LOW
+ )
+
+ createNotificationChannel(
+ id = FILE_SYNC_NOTIFICATION_CHANNEL_ID,
+ name = getString(R.string.file_sync_notification_channel_name),
+ description = getString(R.string.file_sync_notification_channel_description),
+ importance = IMPORTANCE_LOW
+ )
+ }
+
+ private fun isFirstRun(): Boolean {
+ if (getLastSeenVersionCode() != 0) {
+ return false
+ }
+ return AccountUtils.getCurrentOwnCloudAccount(appContext) == null
+ }
+
+ companion object {
+ const val MDM_FLAVOR = "mdm"
+ const val QA_FLAVOR = "qa"
+
+ lateinit var appContext: Context
+ private set
+ var enabledLogging: Boolean = false
+ private set
+
+ 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.
+ */
+
+ val accountType: String
+ get() = appContext.resources.getString(R.string.account_type)
+
+ val versionCode: Int
+ get() =
+ try {
+ val pInfo: PackageInfo = appContext.packageManager.getPackageInfo(appContext.packageName, 0)
+ val longVersionCode: Long = PackageInfoCompat.getLongVersionCode(pInfo)
+ longVersionCode.toInt()
+ } catch (e: PackageManager.NameNotFoundException) {
+ Timber.w(e, "Version code not found, using 0 as fallback")
+ 0
+ }
+
+ val authority: String
+ get() = appContext.resources.getString(R.string.authority)
+
+ val authTokenType: String
+ get() = appContext.resources.getString(R.string.authority)
+
+ val dataFolder: String
+ get() = appContext.resources.getString(R.string.data_folder)
+
+ // user agent
+ // Mozilla/5.0 (Android) ownCloud-android/1.7.0
+ val userAgent: String
+ get() {
+ val appString = appContext.resources.getString(R.string.user_agent)
+ val packageName = appContext.packageName
+ var version: String? = ""
+
+ val pInfo: PackageInfo?
+ try {
+ pInfo = appContext.packageManager.getPackageInfo(packageName, 0)
+ if (pInfo != null) {
+ version = pInfo.versionName
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ Timber.e(e, "Trying to get packageName")
+ }
+
+ return String.format(appString, version)
+ }
+
+ fun initDependencyInjection() {
+ stopKoin()
+ startKoin {
+ androidContext(appContext)
+ modules(
+ listOf(
+ commonModule,
+ viewModelModule,
+ useCaseModule,
+ repositoryModule,
+ localDataSourceModule,
+ remoteDataSourceModule
+ )
+ )
+ }
+ }
+
+ fun getLastSeenVersionCode(): Int {
+ val pref = PreferenceManager.getDefaultSharedPreferences(appContext)
+ return pref.getInt(PREFERENCE_KEY_LAST_SEEN_VERSION_CODE, 0)
+ }
+
+ private fun isNewVersionCode(): Boolean {
+ val lastSeenVersionCode = getLastSeenVersionCode()
+ if (lastSeenVersionCode == 0) { // The preferences have been deleted, so we can delete the accounts and navigate to login
+ return false
+ }
+ return lastSeenVersionCode != versionCode // The version has changed and the accounts must not be deleted
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt b/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt
new file mode 100644
index 000000000000..a81faa9757c7
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt
@@ -0,0 +1,123 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Bartek Przybylski
+ * @author Christian Schabesberger
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2012 Bartek Przybylski
+ * 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.datamodel
+
+import android.accounts.Account
+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
+import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase
+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.providers.CoroutinesDispatcherProvider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class FileDataStorageManager(
+ val account: Account,
+) : KoinComponent {
+
+ fun getFileByPath(remotePath: String, spaceId: String? = null): OCFile? =
+ if (remotePath == ROOT_PATH && spaceId == null) {
+ getRootPersonalFolder()
+ } else {
+ getFileByPathAndAccount(remotePath, account.name, spaceId)
+ }
+
+ private fun getFileByPathAndAccount(remotePath: String, accountName: String, spaceId: String? = null): OCFile? =
+ runBlocking(CoroutinesDispatcherProvider().io) {
+ val getFileByRemotePathUseCase: GetFileByRemotePathUseCase by inject()
+
+ val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
+ getFileByRemotePathUseCase(GetFileByRemotePathUseCase.Params(accountName, remotePath, spaceId))
+ }.getDataOrNull()
+ result
+ }
+
+ fun getRootPersonalFolder() = runBlocking(CoroutinesDispatcherProvider().io) {
+ val getPersonalRootFolderForAccountUseCase: GetPersonalRootFolderForAccountUseCase by inject()
+
+ val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
+ getPersonalRootFolderForAccountUseCase(GetPersonalRootFolderForAccountUseCase.Params(account.name))
+ }
+ result
+ }
+
+ fun getRootSharesFolder() = runBlocking(CoroutinesDispatcherProvider().io) {
+ val getSharesRootFolderForAccount: GetSharesRootFolderForAccount by inject()
+
+ val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
+ getSharesRootFolderForAccount(GetSharesRootFolderForAccount.Params(account.name))
+ }
+ result
+ }
+
+ // To do: New_arch. Remove this and call usecase inside FilesViewModel
+ fun getFileById(id: Long): OCFile? = runBlocking(CoroutinesDispatcherProvider().io) {
+ val getFileByIdUseCase: GetFileByIdUseCase by inject()
+
+ val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
+ getFileByIdUseCase(GetFileByIdUseCase.Params(id))
+ }.getDataOrNull()
+ result
+ }
+
+ fun fileExists(path: String): Boolean = getFileByPath(path) != null
+
+ fun getFolderContent(f: OCFile?): List =
+ if (f != null && f.isFolder && f.id != -1L) {
+ // To do: Remove !!
+ getFolderContent(f.id!!)
+ } else {
+ listOf()
+ }
+
+ // To do: New_arch. Remove this and call usecase inside FilesViewModel
+ fun getFolderImages(folder: OCFile?): List = runBlocking(CoroutinesDispatcherProvider().io) {
+ val getFolderImagesUseCase: GetFolderImagesUseCase by inject()
+
+ val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
+ // To do: Remove !!
+ getFolderImagesUseCase(GetFolderImagesUseCase.Params(folderId = folder!!.id!!))
+ }.getDataOrNull()
+ result ?: listOf()
+ }
+
+ // To do: New_arch. Remove this and call usecase inside FilesViewModel
+ private fun getFolderContent(parentId: Long): List = runBlocking(CoroutinesDispatcherProvider().io) {
+ val getFolderContentUseCase: GetFolderContentUseCase by inject()
+
+ val result = withContext(CoroutineScope(CoroutinesDispatcherProvider().io).coroutineContext) {
+ getFolderContentUseCase(GetFolderContentUseCase.Params(parentId))
+ }.getDataOrNull()
+ result ?: listOf()
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
new file mode 100644
index 000000000000..7899af41e9d3
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
@@ -0,0 +1,473 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * @author David A. Velasco
+ * @author Christian Schabesberger
+ * Copyright (C) 2020 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.datamodel;
+
+import android.accounts.Account;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.ThumbnailUtils;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.widget.ImageView;
+
+import androidx.core.content.ContextCompat;
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.domain.files.model.OCFile;
+import com.owncloud.android.domain.files.usecases.DisableThumbnailsForFileUseCase;
+import com.owncloud.android.domain.files.usecases.GetWebDavUrlForSpaceUseCase;
+import com.owncloud.android.domain.spaces.model.SpaceSpecial;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.SingleSessionManager;
+import com.owncloud.android.lib.common.accounts.AccountUtils;
+import com.owncloud.android.lib.common.http.HttpConstants;
+import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod;
+import com.owncloud.android.ui.adapter.DiskLruImageCache;
+import com.owncloud.android.utils.BitmapUtils;
+import kotlin.Lazy;
+import org.jetbrains.annotations.NotNull;
+import timber.log.Timber;
+
+import java.io.File;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+import java.util.Locale;
+
+import static org.koin.java.KoinJavaComponent.inject;
+
+/**
+ * Manager for concurrent access to thumbnails cache.
+ */
+public class ThumbnailsCacheManager {
+
+ private static final String CACHE_FOLDER = "thumbnailCache";
+
+ private static final Object mThumbnailsDiskCacheLock = new Object();
+ private static DiskLruImageCache mThumbnailCache = null;
+ private static boolean mThumbnailCacheStarting = true;
+
+ private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
+ private static final CompressFormat mCompressFormat = CompressFormat.JPEG;
+ private static final int mCompressQuality = 70;
+ private static OwnCloudClient mClient = null;
+
+ private static final String PREVIEW_URI = "%s%s?x=%d&y=%d&c=%s&preview=1";
+ private static final String SPACE_SPECIAL_URI = "%s?scalingup=0&a=1&x=%d&y=%d&c=%s&preview=1";
+
+ public static Bitmap mDefaultImg =
+ BitmapFactory.decodeResource(
+ MainApp.Companion.getAppContext().getResources(),
+ R.drawable.file_image
+ );
+
+ public static class InitDiskCacheTask extends AsyncTask {
+
+ @Override
+ protected Void doInBackground(File... params) {
+ synchronized (mThumbnailsDiskCacheLock) {
+ mThumbnailCacheStarting = true;
+
+ if (mThumbnailCache == null) {
+ try {
+ // Check if media is mounted or storage is built-in, if so,
+ // try and use external cache dir; otherwise use internal cache dir
+ final String cachePath =
+ MainApp.Companion.getAppContext().getExternalCacheDir().getPath() +
+ File.separator + CACHE_FOLDER;
+ Timber.d("create dir: %s", cachePath);
+ final File diskCacheDir = new File(cachePath);
+ mThumbnailCache = new DiskLruImageCache(
+ diskCacheDir,
+ DISK_CACHE_SIZE,
+ mCompressFormat,
+ mCompressQuality
+ );
+ } catch (Exception e) {
+ Timber.e(e, "Thumbnail cache could not be opened ");
+ mThumbnailCache = null;
+ }
+ }
+ mThumbnailCacheStarting = false; // Finished initialization
+ mThumbnailsDiskCacheLock.notifyAll(); // Wake any waiting threads
+ }
+ return null;
+ }
+ }
+
+ public static void addBitmapToCache(String key, Bitmap bitmap) {
+ synchronized (mThumbnailsDiskCacheLock) {
+ if (mThumbnailCache != null) {
+ mThumbnailCache.put(key, bitmap);
+ }
+ }
+ }
+
+ public static void removeBitmapFromCache(String key) {
+ synchronized (mThumbnailsDiskCacheLock) {
+ if (mThumbnailCache != null) {
+ mThumbnailCache.removeKey(key);
+ }
+ }
+ }
+
+ public static Bitmap getBitmapFromDiskCache(String key) {
+ synchronized (mThumbnailsDiskCacheLock) {
+ // Wait while disk cache is started from background thread
+ while (mThumbnailCacheStarting) {
+ try {
+ mThumbnailsDiskCacheLock.wait();
+ } catch (InterruptedException e) {
+ Timber.e(e, "Wait in mThumbnailsDiskCacheLock was interrupted");
+ }
+ }
+ if (mThumbnailCache != null) {
+ return mThumbnailCache.getBitmap(key);
+ }
+ }
+ return null;
+ }
+
+ public static class ThumbnailGenerationTask extends AsyncTask {
+ private final WeakReference mImageViewReference;
+ private static Account mAccount;
+ private Object mFile;
+ private FileDataStorageManager mStorageManager;
+
+ public ThumbnailGenerationTask(ImageView imageView, Account account) {
+ // Use a WeakReference to ensure the ImageView can be garbage collected
+ mImageViewReference = new WeakReference<>(imageView);
+ mAccount = account;
+ }
+
+ public ThumbnailGenerationTask(ImageView imageView) {
+ // Use a WeakReference to ensure the ImageView can be garbage collected
+ mImageViewReference = new WeakReference<>(imageView);
+ }
+
+ @Override
+ protected Bitmap doInBackground(Object... params) {
+ Bitmap thumbnail = null;
+
+ try {
+ if (mAccount != null) {
+ OwnCloudAccount ocAccount = new OwnCloudAccount(
+ mAccount,
+ MainApp.Companion.getAppContext()
+ );
+ mClient = SingleSessionManager.getDefaultSingleton().
+ getClientFor(ocAccount, MainApp.Companion.getAppContext());
+ }
+
+ mFile = params[0];
+
+ if (mFile instanceof OCFile) {
+ thumbnail = doOCFileInBackground();
+ } else if (mFile instanceof File) {
+ thumbnail = doFileInBackground();
+ } else if (mFile instanceof SpaceSpecial) {
+ thumbnail = doSpaceImageInBackground();
+ //} else { do nothing
+ }
+
+ } catch (Throwable t) {
+ // the app should never break due to a problem with thumbnails
+ Timber.e(t, "Generation of thumbnail for " + mFile + " failed");
+ if (t instanceof OutOfMemoryError) {
+ System.gc();
+ }
+ }
+
+ return thumbnail;
+ }
+
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap != null) {
+ final ImageView imageView = mImageViewReference.get();
+ final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+ if (this == bitmapWorkerTask) {
+ String tagId = "";
+ if (mFile instanceof OCFile) {
+ tagId = String.valueOf(((OCFile) mFile).getId());
+ } else if (mFile instanceof File) {
+ tagId = String.valueOf(mFile.hashCode());
+ } else if (mFile instanceof SpaceSpecial) {
+ tagId = ((SpaceSpecial) mFile).getId();
+ }
+ if (String.valueOf(imageView.getTag()).equals(tagId)) {
+ imageView.setImageBitmap(bitmap);
+ }
+ }
+ }
+ }
+
+ /**
+ * Add thumbnail to cache
+ *
+ * @param imageKey: thumb key
+ * @param bitmap: image for extracting thumbnail
+ * @param path: image path
+ * @param px: thumbnail dp
+ * @return Bitmap
+ */
+ private Bitmap addThumbnailToCache(String imageKey, Bitmap bitmap, String path, int px) {
+
+ Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);
+
+ // Rotate image, obeying exif tag
+ thumbnail = BitmapUtils.rotateImage(thumbnail, path);
+
+ // Add thumbnail to cache
+ addBitmapToCache(imageKey, thumbnail);
+
+ return thumbnail;
+ }
+
+ /**
+ * Converts size of file icon from dp to pixel
+ *
+ * @return int
+ */
+ private int getThumbnailDimension() {
+ // Converts dp to pixel
+ Resources r = MainApp.Companion.getAppContext().getResources();
+ return Math.round(r.getDimension(R.dimen.file_icon_size_grid));
+ }
+
+ private String getPreviewUrl(OCFile ocFile, Account account) {
+ String baseUrl = mClient.getBaseUri() + "/remote.php/dav/files/" + AccountUtils.getUserId(account, MainApp.Companion.getAppContext());
+
+ if (ocFile.getSpaceId() != null) {
+ Lazy getWebDavUrlForSpaceUseCaseLazy = inject(GetWebDavUrlForSpaceUseCase.class);
+ baseUrl = getWebDavUrlForSpaceUseCaseLazy.getValue().invoke(
+ new GetWebDavUrlForSpaceUseCase.Params(ocFile.getOwner(), ocFile.getSpaceId())
+ );
+
+ }
+ return String.format(Locale.ROOT,
+ PREVIEW_URI,
+ baseUrl,
+ Uri.encode(ocFile.getRemotePath(), "/"),
+ getThumbnailDimension(),
+ getThumbnailDimension(),
+ ocFile.getEtag());
+ }
+
+ private Bitmap doOCFileInBackground() {
+ OCFile file = (OCFile) mFile;
+
+ final String imageKey = String.valueOf(file.getRemoteId());
+
+ // Check disk cache in background thread
+ Bitmap thumbnail = getBitmapFromDiskCache(imageKey);
+
+ // Not found in disk cache
+ if (thumbnail == null || file.getNeedsToUpdateThumbnail()) {
+
+ int px = getThumbnailDimension();
+
+ // Download thumbnail from server
+ if (mClient != null) {
+ GetMethod get;
+ try {
+ String uri = getPreviewUrl(file, mAccount);
+ Timber.d("URI: %s", uri);
+ get = new GetMethod(new URL(uri));
+ int status = mClient.executeHttpMethod(get);
+ if (status == HttpConstants.HTTP_OK) {
+ InputStream inputStream = get.getResponseBodyAsStream();
+ Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+ thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);
+
+ // Handle PNG
+ if (file.getMimeType().equalsIgnoreCase("image/png")) {
+ thumbnail = handlePNG(thumbnail, px);
+ }
+
+ // Add thumbnail to cache
+ if (thumbnail != null) {
+ addBitmapToCache(imageKey, thumbnail);
+ }
+ } else {
+ mClient.exhaustResponse(get.getResponseBodyAsStream());
+ }
+ if (status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_NOT_FOUND) {
+ @NotNull Lazy disableThumbnailsForFileUseCaseLazy = inject(DisableThumbnailsForFileUseCase.class);
+ disableThumbnailsForFileUseCaseLazy.getValue().invoke(new DisableThumbnailsForFileUseCase.Params(file.getId()));
+ }
+ } catch (Exception e) {
+ Timber.e(e);
+ }
+ }
+ }
+
+ return thumbnail;
+
+ }
+
+ private Bitmap handlePNG(Bitmap bitmap, int px) {
+ Bitmap resultBitmap = Bitmap.createBitmap(px,
+ px,
+ Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(resultBitmap);
+
+ c.drawColor(ContextCompat.getColor(MainApp.Companion.getAppContext(), R.color.background_color));
+ c.drawBitmap(bitmap, 0, 0, null);
+
+ return resultBitmap;
+ }
+
+ private Bitmap doFileInBackground() {
+ File file = (File) mFile;
+
+ final String imageKey = String.valueOf(file.hashCode());
+
+ // Check disk cache in background thread
+ Bitmap thumbnail = getBitmapFromDiskCache(imageKey);
+
+ // Not found in disk cache
+ if (thumbnail == null) {
+
+ int px = getThumbnailDimension();
+
+ Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(
+ file.getAbsolutePath(), px, px);
+
+ if (bitmap != null) {
+ thumbnail = addThumbnailToCache(imageKey, bitmap, file.getPath(), px);
+ }
+ }
+ return thumbnail;
+ }
+
+ private String getSpaceSpecialUri(SpaceSpecial spaceSpecial) {
+ // Converts dp to pixel
+ Resources r = MainApp.Companion.getAppContext().getResources();
+ Integer spacesThumbnailSize = Math.round(r.getDimension(R.dimen.spaces_thumbnail_height)) * 2;
+ return String.format(Locale.ROOT,
+ SPACE_SPECIAL_URI,
+ spaceSpecial.getWebDavUrl(),
+ spacesThumbnailSize,
+ spacesThumbnailSize,
+ spaceSpecial.getETag());
+ }
+
+ private Bitmap doSpaceImageInBackground() {
+ SpaceSpecial spaceSpecial = (SpaceSpecial) mFile;
+
+ final String imageKey = spaceSpecial.getId();
+
+ // Check disk cache in background thread
+ Bitmap thumbnail = getBitmapFromDiskCache(imageKey);
+
+ // Not found in disk cache
+ if (thumbnail == null) {
+ int px = getThumbnailDimension();
+
+ // Download thumbnail from server
+ if (mClient != null) {
+ GetMethod get;
+ try {
+ String uri = getSpaceSpecialUri(spaceSpecial);
+ Timber.d("URI: %s", uri);
+ get = new GetMethod(new URL(uri));
+ int status = mClient.executeHttpMethod(get);
+ if (status == HttpConstants.HTTP_OK) {
+ InputStream inputStream = get.getResponseBodyAsStream();
+ Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+ thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px);
+
+ // Handle PNG
+ if (spaceSpecial.getFile().getMimeType().equalsIgnoreCase("image/png")) {
+ thumbnail = handlePNG(thumbnail, px);
+ }
+
+ // Add thumbnail to cache
+ if (thumbnail != null) {
+ addBitmapToCache(imageKey, thumbnail);
+ }
+ } else {
+ mClient.exhaustResponse(get.getResponseBodyAsStream());
+ }
+ } catch (Exception e) {
+ Timber.e(e);
+ }
+ }
+ }
+
+ return thumbnail;
+
+ }
+ }
+
+ public static boolean cancelPotentialThumbnailWork(Object file, ImageView imageView) {
+ final ThumbnailGenerationTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final Object bitmapData = bitmapWorkerTask.mFile;
+ // If bitmapData is not yet set or it differs from the new data
+ if (bitmapData == null || bitmapData != file) {
+ // Cancel previous task
+ bitmapWorkerTask.cancel(true);
+ Timber.v("Cancelled generation of thumbnail for a reused imageView");
+ } else {
+ // The same work is already in progress
+ return false;
+ }
+ }
+ // No task associated with the ImageView, or an existing task was cancelled
+ return true;
+ }
+
+ private static ThumbnailGenerationTask getBitmapWorkerTask(ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncThumbnailDrawable) {
+ final AsyncThumbnailDrawable asyncDrawable = (AsyncThumbnailDrawable) drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+
+ public static class AsyncThumbnailDrawable extends BitmapDrawable {
+ private final WeakReference bitmapWorkerTaskReference;
+
+ public AsyncThumbnailDrawable(
+ Resources res, Bitmap bitmap, ThumbnailGenerationTask bitmapWorkerTask
+ ) {
+
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+ }
+
+ ThumbnailGenerationTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/db/PreferenceManager.java b/owncloudApp/src/main/java/com/owncloud/android/db/PreferenceManager.java
new file mode 100644
index 000000000000..eb5481edc4ac
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/db/PreferenceManager.java
@@ -0,0 +1,226 @@
+/*
+ * ownCloud Android client application
+ *
+ * @author David A. Velasco
+ * @author David González Verdugo
+ * @author Shashvat Kedia
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2020 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.db;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.owncloud.android.presentation.security.biometric.BiometricActivity;
+import com.owncloud.android.utils.FileStorageUtils;
+
+/**
+ * Helper to simplify reading of Preferences all around the app
+ */
+public abstract class PreferenceManager {
+ // Legacy preferences - done in version 2.18
+ public static final String PREF__LEGACY_CLICK_DEV_MENU = "clickDeveloperMenu";
+ public static final String PREF__LEGACY_CAMERA_PICTURE_UPLOADS_ENABLED = "camera_picture_uploads";
+ public static final String PREF__LEGACY_CAMERA_VIDEO_UPLOADS_ENABLED = "camera_video_uploads";
+ public static final String PREF__LEGACY_CAMERA_PICTURE_UPLOADS_WIFI_ONLY = "camera_picture_uploads_on_wifi";
+ public static final String PREF__LEGACY_CAMERA_VIDEO_UPLOADS_WIFI_ONLY = "camera_video_uploads_on_wifi";
+ public static final String PREF__LEGACY_CAMERA_PICTURE_UPLOADS_PATH = "camera_picture_uploads_path";
+ public static final String PREF__LEGACY_CAMERA_VIDEO_UPLOADS_PATH = "camera_video_uploads_path";
+ public static final String PREF__LEGACY_CAMERA_UPLOADS_BEHAVIOUR = "camera_uploads_behaviour";
+ public static final String PREF__LEGACY_CAMERA_UPLOADS_SOURCE = "camera_uploads_source_path";
+ public static final String PREF__LEGACY_CAMERA_UPLOADS_ACCOUNT_NAME = "camera_uploads_account_name";
+ public static final String PREF__CAMERA_PICTURE_UPLOADS_ENABLED = "enable_picture_uploads";
+ public static final String PREF__CAMERA_VIDEO_UPLOADS_ENABLED = "enable_video_uploads";
+ public static final String PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY = "picture_uploads_on_wifi";
+ public static final String PREF__CAMERA_PICTURE_UPLOADS_CHARGING_ONLY = "picture_uploads_on_charging";
+ public static final String PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY = "video_uploads_on_wifi";
+ public static final String PREF__CAMERA_VIDEO_UPLOADS_CHARGING_ONLY = "video_uploads_on_charging";
+ public static final String PREF__CAMERA_PICTURE_UPLOADS_PATH = "picture_uploads_path";
+ public static final String PREF__CAMERA_VIDEO_UPLOADS_PATH = "video_uploads_path";
+ public static final String PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR = "picture_uploads_behaviour";
+ public static final String PREF__CAMERA_PICTURE_UPLOADS_SOURCE = "picture_uploads_source_path";
+ public static final String PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR = "video_uploads_behaviour";
+ public static final String PREF__CAMERA_VIDEO_UPLOADS_SOURCE = "video_uploads_source_path";
+ public static final String PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME = "picture_uploads_account_name";
+ public static final String PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME = "video_uploads_account_name";
+ public static final String PREF__CAMERA_PICTURE_UPLOADS_LAST_SYNC = "picture_uploads_last_sync";
+ public static final String PREF__CAMERA_VIDEO_UPLOADS_LAST_SYNC = "video_uploads_last_sync";
+ public static final String PREF__CAMERA_UPLOADS_DEFAULT_PATH = "/CameraUpload";
+ public static final String PREF__LEGACY_FINGERPRINT = "set_fingerprint";
+ /**
+ * Constant to access value of last path selected by the user to upload a file shared from other app.
+ * Value handled by the app without direct access in the UI.
+ */
+ private static final String AUTO_PREF__LAST_UPLOAD_PATH = "last_upload_path";
+ private static final String AUTO_PREF__SORT_ORDER_FILE_DISP = "sortOrderFileDisp";
+ private static final String AUTO_PREF__SORT_ASCENDING_FILE_DISP = "sortAscendingFileDisp";
+ private static final String AUTO_PREF__SORT_ORDER_UPLOAD = "sortOrderUpload";
+ private static final String AUTO_PREF__SORT_ASCENDING_UPLOAD = "sortAscendingUpload";
+
+ public static void migrateFingerprintToBiometricKey(Context context) {
+ SharedPreferences sharedPref = getDefaultSharedPreferences(context);
+
+ // Check if legacy fingerprint key exists, delete it and migrate its value to the new key
+ if (sharedPref.contains(PREF__LEGACY_FINGERPRINT)) {
+ boolean currentFingerprintValue = sharedPref.getBoolean(PREF__LEGACY_FINGERPRINT, false);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.remove(PREF__LEGACY_FINGERPRINT);
+ editor.putBoolean(BiometricActivity.PREFERENCE_SET_BIOMETRIC, currentFingerprintValue);
+ editor.apply();
+ }
+ }
+
+ public static void deleteOldSettingsPreferences(Context context) {
+ SharedPreferences sharedPref = getDefaultSharedPreferences(context);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ if (sharedPref.contains(PREF__LEGACY_CLICK_DEV_MENU)) {
+ editor.remove(PREF__LEGACY_CLICK_DEV_MENU);
+ }
+ if (sharedPref.contains(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_ENABLED)) {
+ editor.remove(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_ENABLED);
+ }
+ if (sharedPref.contains(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_ENABLED)) {
+ editor.remove(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_ENABLED);
+ }
+ if (sharedPref.contains(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_WIFI_ONLY)) {
+ editor.remove(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_WIFI_ONLY);
+ }
+ if (sharedPref.contains(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_WIFI_ONLY)) {
+ editor.remove(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_WIFI_ONLY);
+ }
+ if (sharedPref.contains(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_PATH)) {
+ editor.remove(PREF__LEGACY_CAMERA_PICTURE_UPLOADS_PATH);
+ }
+ if (sharedPref.contains(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_PATH)) {
+ editor.remove(PREF__LEGACY_CAMERA_VIDEO_UPLOADS_PATH);
+ }
+ if (sharedPref.contains(PREF__LEGACY_CAMERA_UPLOADS_BEHAVIOUR)) {
+ editor.remove(PREF__LEGACY_CAMERA_UPLOADS_BEHAVIOUR);
+ }
+ if (sharedPref.contains(PREF__LEGACY_CAMERA_UPLOADS_SOURCE)) {
+ editor.remove(PREF__LEGACY_CAMERA_UPLOADS_SOURCE);
+ }
+ if (sharedPref.contains(PREF__LEGACY_CAMERA_UPLOADS_ACCOUNT_NAME)) {
+ editor.remove(PREF__LEGACY_CAMERA_UPLOADS_ACCOUNT_NAME);
+ }
+ editor.apply();
+ }
+
+ /**
+ * Gets the path where the user selected to do the last upload of a file shared from other app.
+ *
+ * @param context Caller {@link Context}, used to access to shared preferences manager.
+ * @return path Absolute path to a folder, as previously stored by {@link #setLastUploadPath(String, Context)},
+ * or empty String if never saved before.
+ */
+ public static String getLastUploadPath(Context context) {
+ return getDefaultSharedPreferences(context).getString(AUTO_PREF__LAST_UPLOAD_PATH, "");
+ }
+
+ /**
+ * Saves the path where the user selected to do the last upload of a file shared from other app.
+ *
+ * @param path Absolute path to a folder.
+ * @param context Caller {@link Context}, used to access to shared preferences manager.
+ */
+ public static void setLastUploadPath(String path, Context context) {
+ saveStringPreference(AUTO_PREF__LAST_UPLOAD_PATH, path, context);
+ }
+
+ /**
+ * Gets the sort order which the user has set last.
+ *
+ * @param context Caller {@link Context}, used to access to shared preferences manager.
+ * @return sort order the sort order, default is {@link FileStorageUtils#SORT_NAME} (sort by name)
+ */
+ public static int getSortOrder(Context context, int flag) {
+ if (flag == FileStorageUtils.FILE_DISPLAY_SORT) {
+ return getDefaultSharedPreferences(context)
+ .getInt(AUTO_PREF__SORT_ORDER_FILE_DISP, FileStorageUtils.SORT_NAME);
+ } else {
+ return getDefaultSharedPreferences(context)
+ .getInt(AUTO_PREF__SORT_ORDER_UPLOAD, FileStorageUtils.SORT_DATE);
+ }
+ }
+
+ /**
+ * Save the sort order which the user has set last.
+ *
+ * @param order the sort order
+ * @param context Caller {@link Context}, used to access to shared preferences manager.
+ */
+ public static void setSortOrder(int order, Context context, int flag) {
+ if (flag == FileStorageUtils.FILE_DISPLAY_SORT) {
+ saveIntPreference(AUTO_PREF__SORT_ORDER_FILE_DISP, order, context);
+ } else {
+ saveIntPreference(AUTO_PREF__SORT_ORDER_UPLOAD, order, context);
+ }
+ }
+
+ /**
+ * Gets the ascending order flag which the user has set last.
+ *
+ * @param context Caller {@link Context}, used to access to shared preferences manager.
+ * @return ascending order the ascending order, default is true
+ */
+ public static boolean getSortAscending(Context context, int flag) {
+ if (flag == FileStorageUtils.FILE_DISPLAY_SORT) {
+ return getDefaultSharedPreferences(context)
+ .getBoolean(AUTO_PREF__SORT_ASCENDING_FILE_DISP, true);
+ } else {
+ return getDefaultSharedPreferences(context)
+ .getBoolean(AUTO_PREF__SORT_ASCENDING_UPLOAD, true);
+ }
+ }
+
+ /**
+ * Saves the ascending order flag which the user has set last.
+ *
+ * @param ascending flag if sorting is ascending or descending
+ * @param context Caller {@link Context}, used to access to shared preferences manager.
+ */
+ public static void setSortAscending(boolean ascending, Context context, int flag) {
+ if (flag == FileStorageUtils.FILE_DISPLAY_SORT) {
+ saveBooleanPreference(AUTO_PREF__SORT_ASCENDING_FILE_DISP, ascending, context);
+ } else {
+ saveBooleanPreference(AUTO_PREF__SORT_ASCENDING_UPLOAD, ascending, context);
+ }
+ }
+
+ private static void saveBooleanPreference(String key, boolean value, Context context) {
+ SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
+ appPreferences.putBoolean(key, value);
+ appPreferences.apply();
+ }
+
+ private static void saveStringPreference(String key, String value, Context context) {
+ SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
+ appPreferences.putString(key, value);
+ appPreferences.apply();
+ }
+
+ private static void saveIntPreference(String key, int value, Context context) {
+ SharedPreferences.Editor appPreferences = getDefaultSharedPreferences(context.getApplicationContext()).edit();
+ appPreferences.putInt(key, value);
+ appPreferences.apply();
+ }
+
+ public static SharedPreferences getDefaultSharedPreferences(Context context) {
+ return android.preference.PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/db/ProviderMeta.java b/owncloudApp/src/main/java/com/owncloud/android/db/ProviderMeta.java
new file mode 100644
index 000000000000..017956186e71
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/db/ProviderMeta.java
@@ -0,0 +1,198 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Bartek Przybylski
+ * @author David A. Velasco
+ * @author masensio
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * Copyright (C) 2011 Bartek Przybylski
+ * Copyright (C) 2020 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.db;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import com.owncloud.android.MainApp;
+
+/**
+ * Meta-Class that holds various static field information
+ * This is only used in FileContentProvider for legacy DB
+ */
+@Deprecated
+public class ProviderMeta {
+
+ private ProviderMeta() {
+ }
+
+ static public class ProviderTableMeta implements BaseColumns {
+ public static final String FILE_TABLE_NAME = "filelist";
+ public static final String OCSHARES_TABLE_NAME = "ocshares";
+ public static final String CAPABILITIES_TABLE_NAME = "capabilities";
+ public static final String UPLOADS_TABLE_NAME = "list_of_uploads";
+ public static final String USER_AVATARS__TABLE_NAME = "user_avatars";
+ public static final String CAMERA_UPLOADS_SYNC_TABLE_NAME = "camera_uploads_sync";
+ public static final String USER_QUOTAS_TABLE_NAME = "user_quotas";
+
+ public static final Uri CONTENT_URI = Uri.parse("content://"
+ + MainApp.Companion.getAuthority() + "/");
+ public static final Uri CONTENT_URI_FILE = Uri.parse("content://"
+ + MainApp.Companion.getAuthority() + "/file");
+ public static final Uri CONTENT_URI_DIR = Uri.parse("content://"
+ + MainApp.Companion.getAuthority() + "/dir");
+ public static final Uri CONTENT_URI_SHARE = Uri.parse("content://"
+ + MainApp.Companion.getAuthority() + "/shares");
+ public static final Uri CONTENT_URI_CAPABILITIES = Uri.parse("content://"
+ + MainApp.Companion.getAuthority() + "/capabilities");
+ public static final Uri CONTENT_URI_UPLOADS = Uri.parse("content://"
+ + MainApp.Companion.getAuthority() + "/uploads");
+ public static final Uri CONTENT_URI_CAMERA_UPLOADS_SYNC = Uri.parse("content://"
+ + MainApp.Companion.getAuthority() + "/cameraUploadsSync");
+ public static final Uri CONTENT_URI_QUOTAS = Uri.parse("content://"
+ + MainApp.Companion.getAuthority() + "/quotas");
+
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.owncloud.file";
+ public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.item/vnd.owncloud.file";
+
+ public static final String ID = "id";
+
+ // Columns of filelist table
+ public static final String FILE_PARENT = "parent";
+ public static final String FILE_NAME = "filename";
+ public static final String FILE_CREATION = "created";
+ public static final String FILE_MODIFIED = "modified";
+ public static final String FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA = "modified_at_last_sync_for_data";
+ public static final String FILE_CONTENT_LENGTH = "content_length";
+ public static final String FILE_CONTENT_TYPE = "content_type";
+ public static final String FILE_STORAGE_PATH = "media_path";
+ public static final String FILE_PATH = "path";
+ public static final String FILE_ACCOUNT_OWNER = "file_owner";
+ public static final String FILE_LAST_SYNC_DATE = "last_sync_date";// _for_properties, but let's keep it as it is
+ public static final String FILE_LAST_SYNC_DATE_FOR_DATA = "last_sync_date_for_data";
+ public static final String FILE_KEEP_IN_SYNC = "keep_in_sync";
+ public static final String FILE_ETAG = "etag";
+ public static final String FILE_TREE_ETAG = "tree_etag";
+ public static final String FILE_SHARED_VIA_LINK = "share_by_link";
+ public static final String FILE_SHARED_WITH_SHAREE = "shared_via_users";
+ public static final String FILE_PERMISSIONS = "permissions";
+ public static final String FILE_REMOTE_ID = "remote_id";
+ public static final String FILE_UPDATE_THUMBNAIL = "update_thumbnail";
+ public static final String FILE_IS_DOWNLOADING = "is_downloading";
+ public static final String FILE_ETAG_IN_CONFLICT = "etag_in_conflict";
+ public static final String FILE_PRIVATE_LINK = "private_link";
+
+ public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME
+ + " collate nocase asc";
+
+ // @deprecated
+ public static final String FILE_PUBLIC_LINK = "public_link";
+
+ // Columns of ocshares table
+ public static final String OCSHARES_SHARE_TYPE = "share_type";
+ public static final String OCSHARES_SHARE_WITH = "share_with";
+ public static final String OCSHARES_PATH = "path";
+ public static final String OCSHARES_PERMISSIONS = "permissions";
+ public static final String OCSHARES_SHARED_DATE = "shared_date";
+ public static final String OCSHARES_EXPIRATION_DATE = "expiration_date";
+ public static final String OCSHARES_TOKEN = "token";
+ public static final String OCSHARES_SHARE_WITH_DISPLAY_NAME = "shared_with_display_name";
+ public static final String OCSHARES_SHARE_WITH_ADDITIONAL_INFO = "share_with_additional_info";
+ public static final String OCSHARES_IS_DIRECTORY = "is_directory";
+ public static final String OCSHARES_ID_REMOTE_SHARED = "id_remote_shared";
+ public static final String OCSHARES_ACCOUNT_OWNER = "owner_share";
+ public static final String OCSHARES_NAME = "name";
+ public static final String OCSHARES_URL = "url";
+
+ public static final String OCSHARES_DEFAULT_SORT_ORDER = OCSHARES_ID_REMOTE_SHARED
+ + " collate nocase asc";
+
+ // Columns of capabilities table
+ public static final String CAPABILITIES_ACCOUNT_NAME = "account";
+ public static final String CAPABILITIES_VERSION_MAYOR = "version_mayor";
+ public static final String CAPABILITIES_VERSION_MINOR = "version_minor";
+ public static final String CAPABILITIES_VERSION_MICRO = "version_micro";
+ public static final String CAPABILITIES_VERSION_STRING = "version_string";
+ public static final String CAPABILITIES_VERSION_EDITION = "version_edition";
+ public static final String CAPABILITIES_CORE_POLLINTERVAL = "core_pollinterval";
+ public static final String CAPABILITIES_DAV_CHUNKING_VERSION = "dav_chunking_version";
+ public static final String CAPABILITIES_SHARING_API_ENABLED = "sharing_api_enabled";
+ public static final String CAPABILITIES_SHARING_PUBLIC_ENABLED = "sharing_public_enabled";
+ public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED = "sharing_public_password_enforced";
+ public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY =
+ "sharing_public_password_enforced_read_only";
+ public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE =
+ "sharing_public_password_enforced_read_write";
+ public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY =
+ "sharing_public_password_enforced_public_only";
+ public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED =
+ "sharing_public_expire_date_enabled";
+ public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS =
+ "sharing_public_expire_date_days";
+ public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED =
+ "sharing_public_expire_date_enforced";
+ public static final String CAPABILITIES_SHARING_PUBLIC_UPLOAD = "sharing_public_upload";
+ public static final String CAPABILITIES_SHARING_PUBLIC_MULTIPLE = "sharing_public_multiple";
+ public static final String CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY = "supports_upload_only";
+ public static final String CAPABILITIES_SHARING_RESHARING = "sharing_resharing";
+ public static final String CAPABILITIES_SHARING_FEDERATION_OUTGOING = "sharing_federation_outgoing";
+ public static final String CAPABILITIES_SHARING_FEDERATION_INCOMING = "sharing_federation_incoming";
+ public static final String CAPABILITIES_FILES_BIGFILECHUNKING = "files_bigfilechunking";
+ public static final String CAPABILITIES_FILES_UNDELETE = "files_undelete";
+ public static final String CAPABILITIES_FILES_VERSIONING = "files_versioning";
+
+ public static final String CAPABILITIES_DEFAULT_SORT_ORDER = CAPABILITIES_ACCOUNT_NAME
+ + " collate nocase asc";
+
+ //Columns of Uploads table
+ public static final String UPLOADS_LOCAL_PATH = "local_path";
+ public static final String UPLOADS_REMOTE_PATH = "remote_path";
+ public static final String UPLOADS_ACCOUNT_NAME = "account_name";
+ public static final String UPLOADS_FILE_SIZE = "file_size";
+ public static final String UPLOADS_STATUS = "status";
+ public static final String UPLOADS_LOCAL_BEHAVIOUR = "local_behaviour";
+ public static final String UPLOADS_UPLOAD_TIME = "upload_time";
+ public static final String UPLOADS_FORCE_OVERWRITE = "force_overwrite";
+ public static final String UPLOADS_IS_CREATE_REMOTE_FOLDER = "is_create_remote_folder";
+ public static final String UPLOADS_UPLOAD_END_TIMESTAMP = "upload_end_timestamp";
+ public static final String UPLOADS_LAST_RESULT = "last_result";
+ public static final String UPLOADS_CREATED_BY = "created_by";
+ public static final String UPLOADS_TRANSFER_ID = "transfer_id";
+
+ public static final String UPLOADS_DEFAULT_SORT_ORDER =
+ ProviderTableMeta._ID + " collate nocase desc";
+
+ // Columns of user_avatars table
+ public static final String USER_AVATARS__ACCOUNT_NAME = "account_name";
+ public static final String USER_AVATARS__CACHE_KEY = "cache_key";
+ public static final String USER_AVATARS__ETAG = "etag";
+ public static final String USER_AVATARS__MIME_TYPE = "mime_type";
+
+ // Columns of camera upload synchronization table
+ public static final String PICTURES_LAST_SYNC_TIMESTAMP = "pictures_last_sync_date";
+ public static final String VIDEOS_LAST_SYNC_TIMESTAMP = "videos_last_sync_date";
+ public static final String CAMERA_UPLOADS_SYNC_DEFAULT_SORT_ORDER =
+ ProviderTableMeta._ID + " collate nocase asc";
+
+ // Columns of user_quotas table
+ public static final String USER_QUOTAS__ACCOUNT_NAME = "account_name";
+ public static final String USER_QUOTAS__FREE = "free";
+ public static final String USER_QUOTAS__RELATIVE = "relative";
+ public static final String USER_QUOTAS__TOTAL = "total";
+ public static final String USER_QUOTAS__USED = "used";
+ public static final String USER_QUOTAS_DEFAULT_SORT_ORDER =
+ ProviderTableMeta._ID + " collate nocase asc";
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/CommonModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/CommonModule.kt
new file mode 100644
index 000000000000..ff92468c301f
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/CommonModule.kt
@@ -0,0 +1,46 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * Copyright (C) 2021 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.dependecyinjection
+
+import androidx.work.WorkManager
+import com.owncloud.android.presentation.avatar.AvatarManager
+import com.owncloud.android.providers.AccountProvider
+import com.owncloud.android.providers.ContextProvider
+import com.owncloud.android.providers.CoroutinesDispatcherProvider
+import com.owncloud.android.providers.LogsProvider
+import com.owncloud.android.providers.MdmProvider
+import com.owncloud.android.providers.WorkManagerProvider
+import com.owncloud.android.providers.implementation.OCContextProvider
+import org.koin.android.ext.koin.androidApplication
+import org.koin.android.ext.koin.androidContext
+import org.koin.dsl.module
+
+val commonModule = module {
+
+ single { AvatarManager() }
+ single { CoroutinesDispatcherProvider() }
+ factory { OCContextProvider(androidContext()) }
+ single { LogsProvider(get(), get()) }
+ single { MdmProvider(androidContext()) }
+ single { WorkManagerProvider(androidContext()) }
+ single { AccountProvider(androidContext()) }
+ single { WorkManager.getInstance(androidApplication()) }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt
new file mode 100644
index 000000000000..c66f89641765
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt
@@ -0,0 +1,91 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2025 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.dependecyinjection
+
+import android.accounts.AccountManager
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.MainApp
+import com.owncloud.android.MainApp.Companion.accountType
+import com.owncloud.android.MainApp.Companion.dataFolder
+import com.owncloud.android.data.OwncloudDatabase
+import com.owncloud.android.data.appregistry.datasources.LocalAppRegistryDataSource
+import com.owncloud.android.data.appregistry.datasources.implementation.OCLocalAppRegistryDataSource
+import com.owncloud.android.data.authentication.datasources.LocalAuthenticationDataSource
+import com.owncloud.android.data.authentication.datasources.implementation.OCLocalAuthenticationDataSource
+import com.owncloud.android.data.capabilities.datasources.LocalCapabilitiesDataSource
+import com.owncloud.android.data.capabilities.datasources.implementation.OCLocalCapabilitiesDataSource
+import com.owncloud.android.data.files.datasources.LocalFileDataSource
+import com.owncloud.android.data.files.datasources.implementation.OCLocalFileDataSource
+import com.owncloud.android.data.folderbackup.datasources.LocalFolderBackupDataSource
+import com.owncloud.android.data.folderbackup.datasources.implementation.OCLocalFolderBackupDataSource
+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.providers.LocalStorageProvider
+import com.owncloud.android.data.providers.QaStorageProvider
+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
+import com.owncloud.android.data.user.datasources.implementation.OCLocalUserDataSource
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.module.dsl.factoryOf
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.bind
+import org.koin.dsl.module
+
+val localDataSourceModule = module {
+ single { AccountManager.get(androidContext()) }
+
+ single { OwncloudDatabase.getDatabase(androidContext()).appRegistryDao() }
+ single { OwncloudDatabase.getDatabase(androidContext()).capabilityDao() }
+ single { OwncloudDatabase.getDatabase(androidContext()).fileDao() }
+ single { OwncloudDatabase.getDatabase(androidContext()).folderBackUpDao() }
+ single { OwncloudDatabase.getDatabase(androidContext()).shareDao() }
+ single { OwncloudDatabase.getDatabase(androidContext()).spacesDao() }
+ single { OwncloudDatabase.getDatabase(androidContext()).transferDao() }
+ single { OwncloudDatabase.getDatabase(androidContext()).userDao() }
+
+ singleOf(::OCSharedPreferencesProvider) bind SharedPreferencesProvider::class
+ single {
+ if (BuildConfig.FLAVOR == MainApp.QA_FLAVOR) {
+ QaStorageProvider(dataFolder)
+ } else {
+ ScopedStorageProvider(dataFolder, androidContext())
+ }
+ }
+
+ factory { OCLocalAuthenticationDataSource(androidContext(), get(), get(), accountType) }
+ factoryOf(::OCLocalFolderBackupDataSource) bind LocalFolderBackupDataSource::class
+ factoryOf(::OCLocalAppRegistryDataSource) bind LocalAppRegistryDataSource::class
+ factoryOf(::OCLocalCapabilitiesDataSource) bind LocalCapabilitiesDataSource::class
+ factoryOf(::OCLocalFileDataSource) bind LocalFileDataSource::class
+ factoryOf(::OCLocalShareDataSource) bind LocalShareDataSource::class
+ factoryOf(::OCLocalSpacesDataSource) bind LocalSpacesDataSource::class
+ factoryOf(::OCLocalTransferDataSource) bind LocalTransferDataSource::class
+ factoryOf(::OCLocalUserDataSource) bind LocalUserDataSource::class
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt
new file mode 100644
index 000000000000..483b90e6e11d
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt
@@ -0,0 +1,97 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.dependecyinjection
+
+import com.owncloud.android.MainApp
+import com.owncloud.android.R
+import com.owncloud.android.data.ClientManager
+import com.owncloud.android.data.appregistry.datasources.RemoteAppRegistryDataSource
+import com.owncloud.android.data.appregistry.datasources.implementation.OCRemoteAppRegistryDataSource
+import com.owncloud.android.data.authentication.datasources.RemoteAuthenticationDataSource
+import com.owncloud.android.data.authentication.datasources.implementation.OCRemoteAuthenticationDataSource
+import com.owncloud.android.data.capabilities.datasources.RemoteCapabilitiesDataSource
+import com.owncloud.android.data.capabilities.datasources.implementation.OCRemoteCapabilitiesDataSource
+import com.owncloud.android.data.capabilities.datasources.mapper.RemoteCapabilityMapper
+import com.owncloud.android.data.files.datasources.RemoteFileDataSource
+import com.owncloud.android.data.files.datasources.implementation.OCRemoteFileDataSource
+import com.owncloud.android.data.links.datasources.RemoteLinksDataSource
+import com.owncloud.android.data.links.datasources.implementation.OCRemoteLinksDataSource
+import com.owncloud.android.data.members.datasources.RemoteMembersDataSource
+import com.owncloud.android.data.members.datasources.implementation.OCRemoteMembersDataSource
+import com.owncloud.android.data.oauth.datasources.RemoteOAuthDataSource
+import com.owncloud.android.data.oauth.datasources.implementation.OCRemoteOAuthDataSource
+import com.owncloud.android.data.roles.datasources.RemoteRolesDataSource
+import com.owncloud.android.data.roles.datasources.implementation.OCRemoteRolesDataSource
+import com.owncloud.android.data.server.datasources.RemoteServerInfoDataSource
+import com.owncloud.android.data.server.datasources.implementation.OCRemoteServerInfoDataSource
+import com.owncloud.android.data.sharing.sharees.datasources.RemoteShareeDataSource
+import com.owncloud.android.data.sharing.sharees.datasources.implementation.OCRemoteShareeDataSource
+import com.owncloud.android.data.sharing.sharees.datasources.mapper.RemoteShareeMapper
+import com.owncloud.android.data.sharing.shares.datasources.RemoteShareDataSource
+import com.owncloud.android.data.sharing.shares.datasources.implementation.OCRemoteShareDataSource
+import com.owncloud.android.data.sharing.shares.datasources.mapper.RemoteShareMapper
+import com.owncloud.android.data.spaces.datasources.RemoteSpacesDataSource
+import com.owncloud.android.data.spaces.datasources.implementation.OCRemoteSpacesDataSource
+import com.owncloud.android.data.user.datasources.RemoteUserDataSource
+import com.owncloud.android.data.user.datasources.implementation.OCRemoteUserDataSource
+import com.owncloud.android.data.webfinger.datasources.RemoteWebFingerDataSource
+import com.owncloud.android.data.webfinger.datasources.implementation.OCRemoteWebFingerDataSource
+import com.owncloud.android.lib.common.ConnectionValidator
+import com.owncloud.android.lib.resources.oauth.services.OIDCService
+import com.owncloud.android.lib.resources.oauth.services.implementation.OCOIDCService
+import com.owncloud.android.lib.resources.status.services.ServerInfoService
+import com.owncloud.android.lib.resources.status.services.implementation.OCServerInfoService
+import com.owncloud.android.lib.resources.webfinger.services.WebFingerService
+import com.owncloud.android.lib.resources.webfinger.services.implementation.OCWebFingerService
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.module.dsl.factoryOf
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.bind
+import org.koin.dsl.module
+
+val remoteDataSourceModule = module {
+ single { ConnectionValidator(androidContext(), androidContext().resources.getBoolean(R.bool.clear_cookies_on_validation)) }
+ single { ClientManager(get(), get(), androidContext(), MainApp.accountType, get()) }
+
+ singleOf(::OCServerInfoService) bind ServerInfoService::class
+ singleOf(::OCOIDCService) bind OIDCService::class
+ singleOf(::OCWebFingerService) bind WebFingerService::class
+
+ singleOf(::OCRemoteAppRegistryDataSource) bind RemoteAppRegistryDataSource::class
+ singleOf(::OCRemoteAuthenticationDataSource) bind RemoteAuthenticationDataSource::class
+ singleOf(::OCRemoteCapabilitiesDataSource) bind RemoteCapabilitiesDataSource::class
+ singleOf(::OCRemoteFileDataSource) bind RemoteFileDataSource::class
+ singleOf(::OCRemoteLinksDataSource) bind RemoteLinksDataSource::class
+ singleOf(::OCRemoteMembersDataSource) bind RemoteMembersDataSource::class
+ singleOf(::OCRemoteOAuthDataSource) bind RemoteOAuthDataSource::class
+ singleOf(::OCRemoteRolesDataSource) bind RemoteRolesDataSource::class
+ singleOf(::OCRemoteServerInfoDataSource) bind RemoteServerInfoDataSource::class
+ singleOf(::OCRemoteShareDataSource) bind RemoteShareDataSource::class
+ singleOf(::OCRemoteShareeDataSource) bind RemoteShareeDataSource::class
+ singleOf(::OCRemoteSpacesDataSource) bind RemoteSpacesDataSource::class
+ singleOf(::OCRemoteWebFingerDataSource) bind RemoteWebFingerDataSource::class
+ single { OCRemoteUserDataSource(get(), androidContext().resources.getDimension(R.dimen.file_avatar_size).toInt()) }
+
+ factoryOf(::RemoteCapabilityMapper)
+ factoryOf(::RemoteShareMapper)
+ factoryOf(::RemoteShareeMapper)
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt
new file mode 100644
index 000000000000..3135cde74e61
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt
@@ -0,0 +1,79 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.dependecyinjection
+
+import com.owncloud.android.data.appregistry.repository.OCAppRegistryRepository
+import com.owncloud.android.data.authentication.repository.OCAuthenticationRepository
+import com.owncloud.android.data.capabilities.repository.OCCapabilityRepository
+import com.owncloud.android.data.files.repository.OCFileRepository
+import com.owncloud.android.data.folderbackup.repository.OCFolderBackupRepository
+import com.owncloud.android.data.links.repository.OCLinksRepository
+import com.owncloud.android.data.members.repository.OCMembersRepository
+import com.owncloud.android.data.oauth.repository.OCOAuthRepository
+import com.owncloud.android.data.roles.repository.OCRolesRepository
+import com.owncloud.android.data.server.repository.OCServerInfoRepository
+import com.owncloud.android.data.sharing.sharees.repository.OCShareeRepository
+import com.owncloud.android.data.sharing.shares.repository.OCShareRepository
+import com.owncloud.android.data.spaces.repository.OCSpacesRepository
+import com.owncloud.android.data.transfers.repository.OCTransferRepository
+import com.owncloud.android.data.user.repository.OCUserRepository
+import com.owncloud.android.data.webfinger.repository.OCWebFingerRepository
+import com.owncloud.android.domain.appregistry.AppRegistryRepository
+import com.owncloud.android.domain.authentication.AuthenticationRepository
+import com.owncloud.android.domain.authentication.oauth.OAuthRepository
+import com.owncloud.android.domain.automaticuploads.FolderBackupRepository
+import com.owncloud.android.domain.capabilities.CapabilityRepository
+import com.owncloud.android.domain.files.FileRepository
+import com.owncloud.android.domain.links.LinksRepository
+import com.owncloud.android.domain.members.MembersRepository
+import com.owncloud.android.domain.roles.RolesRepository
+import com.owncloud.android.domain.server.ServerInfoRepository
+import com.owncloud.android.domain.sharing.sharees.ShareeRepository
+import com.owncloud.android.domain.sharing.shares.ShareRepository
+import com.owncloud.android.domain.spaces.SpacesRepository
+import com.owncloud.android.domain.transfers.TransferRepository
+import com.owncloud.android.domain.user.UserRepository
+import com.owncloud.android.domain.webfinger.WebFingerRepository
+import org.koin.core.module.dsl.factoryOf
+import org.koin.dsl.bind
+import org.koin.dsl.module
+
+val repositoryModule = module {
+ factoryOf(::OCAppRegistryRepository) bind AppRegistryRepository::class
+ factoryOf(::OCAuthenticationRepository) bind AuthenticationRepository::class
+ factoryOf(::OCCapabilityRepository) bind CapabilityRepository::class
+ factoryOf(::OCFileRepository) bind FileRepository::class
+ factoryOf(::OCFolderBackupRepository) bind FolderBackupRepository::class
+ factoryOf(::OCLinksRepository) bind LinksRepository::class
+ factoryOf(::OCMembersRepository) bind MembersRepository::class
+ factoryOf(::OCOAuthRepository) bind OAuthRepository::class
+ factoryOf(::OCRolesRepository) bind RolesRepository::class
+ factoryOf(::OCServerInfoRepository) bind ServerInfoRepository::class
+ factoryOf(::OCShareRepository) bind ShareRepository::class
+ factoryOf(::OCShareeRepository) bind ShareeRepository::class
+ factoryOf(::OCSpacesRepository) bind SpacesRepository::class
+ factoryOf(::OCTransferRepository) bind TransferRepository::class
+ factoryOf(::OCUserRepository) bind UserRepository::class
+ factoryOf(::OCWebFingerRepository) bind WebFingerRepository::class
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt
new file mode 100644
index 000000000000..94e509739fc1
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt
@@ -0,0 +1,333 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ * @author Aitor Ballesteros Pavón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.dependecyinjection
+
+import com.owncloud.android.domain.appregistry.usecases.CreateFileWithAppProviderUseCase
+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.authentication.oauth.OIDCDiscoveryUseCase
+import com.owncloud.android.domain.authentication.oauth.RegisterClientUseCase
+import com.owncloud.android.domain.authentication.oauth.RequestTokenUseCase
+import com.owncloud.android.domain.authentication.usecases.GetBaseUrlUseCase
+import com.owncloud.android.domain.authentication.usecases.LoginBasicAsyncUseCase
+import com.owncloud.android.domain.authentication.usecases.LoginOAuthAsyncUseCase
+import com.owncloud.android.domain.authentication.usecases.SupportsOAuth2UseCase
+import com.owncloud.android.domain.availableoffline.usecases.GetFilesAvailableOfflineFromAccountAsStreamUseCase
+import com.owncloud.android.domain.availableoffline.usecases.GetFilesAvailableOfflineFromAccountUseCase
+import com.owncloud.android.domain.availableoffline.usecases.GetFilesAvailableOfflineFromEveryAccountUseCase
+import com.owncloud.android.domain.availableoffline.usecases.SetFilesAsAvailableOfflineUseCase
+import com.owncloud.android.domain.availableoffline.usecases.UnsetFilesAsAvailableOfflineUseCase
+import com.owncloud.android.domain.automaticuploads.usecases.GetAutomaticUploadsConfigurationUseCase
+import com.owncloud.android.domain.automaticuploads.usecases.GetPictureUploadsConfigurationStreamUseCase
+import com.owncloud.android.domain.automaticuploads.usecases.GetVideoUploadsConfigurationStreamUseCase
+import com.owncloud.android.domain.automaticuploads.usecases.ResetPictureUploadsUseCase
+import com.owncloud.android.domain.automaticuploads.usecases.ResetVideoUploadsUseCase
+import com.owncloud.android.domain.automaticuploads.usecases.SavePictureUploadsConfigurationUseCase
+import com.owncloud.android.domain.automaticuploads.usecases.SaveVideoUploadsConfigurationUseCase
+import com.owncloud.android.domain.capabilities.usecases.GetCapabilitiesAsLiveDataUseCase
+import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
+import com.owncloud.android.domain.capabilities.usecases.RefreshCapabilitiesFromServerAsyncUseCase
+import com.owncloud.android.domain.files.usecases.IsAnyFileAvailableLocallyAndNotAvailableOfflineUseCase
+import com.owncloud.android.domain.files.usecases.CleanConflictUseCase
+import com.owncloud.android.domain.files.usecases.CleanWorkersUUIDUseCase
+import com.owncloud.android.domain.files.usecases.CopyFileUseCase
+import com.owncloud.android.domain.files.usecases.CreateFolderAsyncUseCase
+import com.owncloud.android.domain.files.usecases.DisableThumbnailsForFileUseCase
+import com.owncloud.android.domain.files.usecases.GetFileByIdAsStreamUseCase
+import com.owncloud.android.domain.files.usecases.GetFileByIdUseCase
+import com.owncloud.android.domain.files.usecases.GetFileByRemotePathUseCase
+import com.owncloud.android.domain.files.usecases.GetFileWithSyncInfoByIdUseCase
+import com.owncloud.android.domain.files.usecases.GetFolderContentAsStreamUseCase
+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.GetSearchFolderContentUseCase
+import com.owncloud.android.domain.files.usecases.GetSharedByLinkForAccountAsStreamUseCase
+import com.owncloud.android.domain.files.usecases.GetSharesRootFolderForAccount
+import com.owncloud.android.domain.files.usecases.GetWebDavUrlForSpaceUseCase
+import com.owncloud.android.domain.files.usecases.ManageDeepLinkUseCase
+import com.owncloud.android.domain.files.usecases.MoveFileUseCase
+import com.owncloud.android.domain.files.usecases.RemoveFileUseCase
+import com.owncloud.android.domain.files.usecases.RenameFileUseCase
+import com.owncloud.android.domain.files.usecases.SaveConflictUseCase
+import com.owncloud.android.domain.files.usecases.SaveDownloadWorkerUUIDUseCase
+import com.owncloud.android.domain.files.usecases.SaveFileOrFolderUseCase
+import com.owncloud.android.domain.files.usecases.SetLastUsageFileUseCase
+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.links.usecases.AddLinkUseCase
+import com.owncloud.android.domain.links.usecases.EditLinkUseCase
+import com.owncloud.android.domain.links.usecases.EditPasswordLinkUseCase
+import com.owncloud.android.domain.links.usecases.RemoveLinkUseCase
+import com.owncloud.android.domain.members.usecases.AddMemberUseCase
+import com.owncloud.android.domain.members.usecases.EditMemberUseCase
+import com.owncloud.android.domain.members.usecases.RemoveMemberUseCase
+import com.owncloud.android.domain.members.usecases.SearchMembersUseCase
+import com.owncloud.android.domain.roles.usecases.GetRolesAsyncUseCase
+import com.owncloud.android.domain.server.usecases.GetServerInfoAsyncUseCase
+import com.owncloud.android.domain.sharing.sharees.GetShareesAsyncUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.AddGraphShareAsyncUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.CreatePrivateShareAsyncUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.CreatePublicShareAsyncUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.DeleteShareAsyncUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.EditPrivateShareAsyncUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.EditPublicShareAsyncUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.GetGraphSharesAsyncUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.GetShareAsLiveDataUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUseCase
+import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase
+import com.owncloud.android.domain.spaces.usecases.CreateSpaceUseCase
+import com.owncloud.android.domain.spaces.usecases.DisableSpaceUseCase
+import com.owncloud.android.domain.spaces.usecases.EditSpaceImageUseCase
+import com.owncloud.android.domain.spaces.usecases.EditSpaceUseCase
+import com.owncloud.android.domain.spaces.usecases.EnableSpaceUseCase
+import com.owncloud.android.domain.spaces.usecases.FilterSpaceMenuOptionsUseCase
+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.GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase
+import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase
+import com.owncloud.android.domain.spaces.usecases.GetSpaceByIdForAccountUseCase
+import com.owncloud.android.domain.spaces.usecases.GetSpacePermissionsAsyncUseCase
+import com.owncloud.android.domain.spaces.usecases.GetSpaceWithSpecialsByIdForAccountUseCase
+import com.owncloud.android.domain.spaces.usecases.GetSpacesFromEveryAccountUseCaseAsStream
+import com.owncloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase
+import com.owncloud.android.domain.spaces.usecases.GetSpaceMembersUseCase
+import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransferByIdUseCase
+import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransfersUseCase
+import com.owncloud.android.domain.transfers.usecases.GetAllTransfersAsStreamUseCase
+import com.owncloud.android.domain.transfers.usecases.GetAllTransfersUseCase
+import com.owncloud.android.domain.transfers.usecases.UpdatePendingUploadsPathUseCase
+import com.owncloud.android.domain.user.usecases.GetStoredQuotaUseCase
+import com.owncloud.android.domain.user.usecases.GetStoredQuotaAsStreamUseCase
+import com.owncloud.android.domain.user.usecases.GetUserAvatarAsyncUseCase
+import com.owncloud.android.domain.user.usecases.GetUserGroupsAsyncUseCase
+import com.owncloud.android.domain.user.usecases.GetUserInfoAsyncUseCase
+import com.owncloud.android.domain.user.usecases.GetUserQuotasUseCase
+import com.owncloud.android.domain.user.usecases.GetUserQuotasAsStreamUseCase
+import com.owncloud.android.domain.user.usecases.RefreshUserQuotaFromServerAsyncUseCase
+import com.owncloud.android.domain.user.usecases.GetUserIdAsyncUseCase
+import com.owncloud.android.domain.user.usecases.GetUserPermissionsAsyncUseCase
+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.files.RemoveLocalFilesForAccountUseCase
+import com.owncloud.android.usecases.files.RemoveLocallyFilesWithLastUsageOlderThanGivenTimeUseCase
+import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase
+import com.owncloud.android.usecases.synchronization.SynchronizeFolderUseCase
+import com.owncloud.android.usecases.transfers.downloads.CancelDownloadForFileUseCase
+import com.owncloud.android.usecases.transfers.downloads.CancelDownloadsRecursivelyUseCase
+import com.owncloud.android.usecases.transfers.downloads.DownloadFileUseCase
+import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForDownloadingFileUseCase
+import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForFinishedDownloadsFromAccountUseCase
+import com.owncloud.android.usecases.transfers.uploads.CancelTransfersFromAccountUseCase
+import com.owncloud.android.usecases.transfers.uploads.CancelUploadForFileUseCase
+import com.owncloud.android.usecases.transfers.uploads.CancelUploadUseCase
+import com.owncloud.android.usecases.transfers.uploads.CancelUploadsRecursivelyUseCase
+import com.owncloud.android.usecases.transfers.uploads.ClearFailedTransfersUseCase
+import com.owncloud.android.usecases.transfers.uploads.RetryFailedUploadsForAccountUseCase
+import com.owncloud.android.usecases.transfers.uploads.RetryFailedUploadsUseCase
+import com.owncloud.android.usecases.transfers.uploads.RetryUploadFromContentUriUseCase
+import com.owncloud.android.usecases.transfers.uploads.RetryUploadFromSystemUseCase
+import com.owncloud.android.usecases.transfers.uploads.UploadFileFromContentUriUseCase
+import com.owncloud.android.usecases.transfers.uploads.UploadFileFromSystemUseCase
+import com.owncloud.android.usecases.transfers.uploads.UploadFileInConflictUseCase
+import com.owncloud.android.usecases.transfers.uploads.UploadFilesFromContentUriUseCase
+import com.owncloud.android.usecases.transfers.uploads.UploadFilesFromSystemUseCase
+import org.koin.core.module.dsl.factoryOf
+import org.koin.dsl.module
+
+val useCaseModule = module {
+ // Authentication
+ factoryOf(::GetBaseUrlUseCase)
+ factoryOf(::GetOwnCloudInstanceFromWebFingerUseCase)
+ factoryOf(::GetOwnCloudInstancesFromAuthenticatedWebFingerUseCase)
+ factoryOf(::LoginBasicAsyncUseCase)
+ factoryOf(::LoginOAuthAsyncUseCase)
+ factoryOf(::SupportsOAuth2UseCase)
+
+ // OAuth
+ factoryOf(::OIDCDiscoveryUseCase)
+ factoryOf(::RegisterClientUseCase)
+ factoryOf(::RequestTokenUseCase)
+
+ // Capabilities
+ factoryOf(::GetCapabilitiesAsLiveDataUseCase)
+ factoryOf(::GetStoredCapabilitiesUseCase)
+ factoryOf(::RefreshCapabilitiesFromServerAsyncUseCase)
+
+ // Files
+ factoryOf(::CleanConflictUseCase)
+ factoryOf(::CleanWorkersUUIDUseCase)
+ factoryOf(::CopyFileUseCase)
+ factoryOf(::CreateFolderAsyncUseCase)
+ factoryOf(::DisableThumbnailsForFileUseCase)
+ factoryOf(::FilterFileMenuOptionsUseCase)
+ factoryOf(::GetFileByIdAsStreamUseCase)
+ factoryOf(::GetFileByIdUseCase)
+ factoryOf(::GetFileByRemotePathUseCase)
+ factoryOf(::GetFileWithSyncInfoByIdUseCase)
+ factoryOf(::GetFolderContentAsStreamUseCase)
+ factoryOf(::GetFolderContentUseCase)
+ factoryOf(::GetFolderImagesUseCase)
+ factoryOf(::IsAnyFileAvailableLocallyAndNotAvailableOfflineUseCase)
+ factoryOf(::GetPersonalRootFolderForAccountUseCase)
+ factoryOf(::GetSearchFolderContentUseCase)
+ factoryOf(::GetSharedByLinkForAccountAsStreamUseCase)
+ factoryOf(::GetSharesRootFolderForAccount)
+ factoryOf(::GetUrlToOpenInWebUseCase)
+ factoryOf(::ManageDeepLinkUseCase)
+ factoryOf(::MoveFileUseCase)
+ factoryOf(::RemoveFileUseCase)
+ factoryOf(::RemoveLocalFilesForAccountUseCase)
+ factoryOf(::RemoveLocallyFilesWithLastUsageOlderThanGivenTimeUseCase)
+ factoryOf(::RenameFileUseCase)
+ factoryOf(::SaveConflictUseCase)
+ factoryOf(::SaveDownloadWorkerUUIDUseCase)
+ factoryOf(::SaveFileOrFolderUseCase)
+ factoryOf(::SetLastUsageFileUseCase)
+ factoryOf(::SortFilesUseCase)
+ factoryOf(::SortFilesWithSyncInfoUseCase)
+ factoryOf(::SynchronizeFileUseCase)
+ factoryOf(::SynchronizeFolderUseCase)
+
+ // Open in web
+ factoryOf(::CreateFileWithAppProviderUseCase)
+ factoryOf(::GetAppRegistryForMimeTypeAsStreamUseCase)
+ factoryOf(::GetAppRegistryWhichAllowCreationAsStreamUseCase)
+ factoryOf(::GetUrlToOpenInWebUseCase)
+
+ // Av Offline
+ factoryOf(::GetFilesAvailableOfflineFromAccountAsStreamUseCase)
+ factoryOf(::GetFilesAvailableOfflineFromAccountUseCase)
+ factoryOf(::GetFilesAvailableOfflineFromEveryAccountUseCase)
+ factoryOf(::SetFilesAsAvailableOfflineUseCase)
+ factoryOf(::UnsetFilesAsAvailableOfflineUseCase)
+
+ // Sharing
+ factoryOf(::AddGraphShareAsyncUseCase)
+ factoryOf(::CreatePrivateShareAsyncUseCase)
+ factoryOf(::CreatePublicShareAsyncUseCase)
+ factoryOf(::DeleteShareAsyncUseCase)
+ factoryOf(::EditPrivateShareAsyncUseCase)
+ factoryOf(::EditPublicShareAsyncUseCase)
+ factoryOf(::GetGraphSharesAsyncUseCase)
+ factoryOf(::GetShareAsLiveDataUseCase)
+ factoryOf(::GetShareesAsyncUseCase)
+ factoryOf(::GetSharesAsLiveDataUseCase)
+ factoryOf(::RefreshSharesFromServerAsyncUseCase)
+
+ // Spaces
+ factoryOf(::CreateSpaceUseCase)
+ factoryOf(::DisableSpaceUseCase)
+ factoryOf(::EditSpaceImageUseCase)
+ factoryOf(::EditSpaceUseCase)
+ factoryOf(::EnableSpaceUseCase)
+ factoryOf(::FilterSpaceMenuOptionsUseCase)
+ factoryOf(::GetPersonalAndProjectSpacesForAccountUseCase)
+ factoryOf(::GetPersonalAndProjectSpacesWithSpecialsForAccountAsStreamUseCase)
+ factoryOf(::GetPersonalSpaceForAccountUseCase)
+ factoryOf(::GetPersonalSpacesWithSpecialsForAccountAsStreamUseCase)
+ factoryOf(::GetProjectSpacesWithSpecialsForAccountAsStreamUseCase)
+ factoryOf(::GetSpaceByIdForAccountUseCase)
+ factoryOf(::GetSpaceMembersUseCase)
+ factoryOf(::GetSpacePermissionsAsyncUseCase)
+ factoryOf(::GetSpaceWithSpecialsByIdForAccountUseCase)
+ factoryOf(::GetSpacesFromEveryAccountUseCaseAsStream)
+ factoryOf(::GetWebDavUrlForSpaceUseCase)
+ factoryOf(::RefreshSpacesFromServerAsyncUseCase)
+
+ // Transfers
+ factoryOf(::CancelDownloadForFileUseCase)
+ factoryOf(::CancelDownloadsRecursivelyUseCase)
+ factoryOf(::CancelTransfersFromAccountUseCase)
+ factoryOf(::CancelUploadForFileUseCase)
+ factoryOf(::CancelUploadUseCase)
+ factoryOf(::CancelUploadsRecursivelyUseCase)
+ factoryOf(::ClearFailedTransfersUseCase)
+ factoryOf(::ClearSuccessfulTransferByIdUseCase)
+ factoryOf(::ClearSuccessfulTransfersUseCase)
+ factoryOf(::DownloadFileUseCase)
+ factoryOf(::GetAllTransfersAsStreamUseCase)
+ factoryOf(::GetAllTransfersUseCase)
+ factoryOf(::GetLiveDataForDownloadingFileUseCase)
+ factoryOf(::GetLiveDataForFinishedDownloadsFromAccountUseCase)
+ factoryOf(::RetryFailedUploadsForAccountUseCase)
+ factoryOf(::RetryFailedUploadsUseCase)
+ factoryOf(::RetryUploadFromContentUriUseCase)
+ factoryOf(::RetryUploadFromSystemUseCase)
+ factoryOf(::UpdateAlreadyDownloadedFilesPathUseCase)
+ factoryOf(::UpdatePendingUploadsPathUseCase)
+ factoryOf(::UploadFileFromContentUriUseCase)
+ factoryOf(::UploadFileFromSystemUseCase)
+ factoryOf(::UploadFileInConflictUseCase)
+ factoryOf(::UploadFilesFromContentUriUseCase)
+ factoryOf(::UploadFilesFromSystemUseCase)
+
+ // User
+ factoryOf(::GetStoredQuotaAsStreamUseCase)
+ factoryOf(::GetStoredQuotaUseCase)
+ factoryOf(::GetUserAvatarAsyncUseCase)
+ factoryOf(::GetUserGroupsAsyncUseCase)
+ factoryOf(::GetUserIdAsyncUseCase)
+ factoryOf(::GetUserInfoAsyncUseCase)
+ factoryOf(::GetUserPermissionsAsyncUseCase)
+ factoryOf(::GetUserQuotasAsStreamUseCase)
+ factoryOf(::GetUserQuotasUseCase)
+ factoryOf(::RefreshUserQuotaFromServerAsyncUseCase)
+
+ // Server
+ factoryOf(::GetServerInfoAsyncUseCase)
+
+ // Camera Uploads
+ factoryOf(::GetAutomaticUploadsConfigurationUseCase)
+ factoryOf(::GetPictureUploadsConfigurationStreamUseCase)
+ factoryOf(::GetVideoUploadsConfigurationStreamUseCase)
+ factoryOf(::ResetPictureUploadsUseCase)
+ factoryOf(::ResetVideoUploadsUseCase)
+ factoryOf(::SavePictureUploadsConfigurationUseCase)
+ factoryOf(::SaveVideoUploadsConfigurationUseCase)
+
+ // Accounts
+ factoryOf(::RemoveAccountUseCase)
+
+ // Roles
+ factoryOf(::GetRolesAsyncUseCase)
+
+ // Members
+ factoryOf(::AddMemberUseCase)
+ factoryOf(::EditMemberUseCase)
+ factoryOf(::RemoveMemberUseCase)
+ factoryOf(::SearchMembersUseCase)
+
+ // Links
+ factoryOf(::AddLinkUseCase)
+ factoryOf(::EditLinkUseCase)
+ factoryOf(::EditPasswordLinkUseCase)
+ factoryOf(::RemoveLinkUseCase)
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt
new file mode 100644
index 000000000000..c22b569ea3dd
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt
@@ -0,0 +1,115 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ * @author David Crespo Ríos
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.dependecyinjection
+
+import com.owncloud.android.MainApp
+import com.owncloud.android.domain.files.model.FileListOption
+import com.owncloud.android.domain.files.model.OCFile
+import com.owncloud.android.presentation.accounts.ManageAccountsViewModel
+import com.owncloud.android.presentation.authentication.AuthenticationViewModel
+import com.owncloud.android.presentation.authentication.oauth.OAuthViewModel
+import com.owncloud.android.presentation.capabilities.CapabilityViewModel
+import com.owncloud.android.presentation.common.DrawerViewModel
+import com.owncloud.android.presentation.conflicts.ConflictsResolveViewModel
+import com.owncloud.android.presentation.files.details.FileDetailsViewModel
+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
+import com.owncloud.android.presentation.security.passcode.PasscodeAction
+import com.owncloud.android.presentation.security.pattern.PatternViewModel
+import com.owncloud.android.presentation.settings.SettingsViewModel
+import com.owncloud.android.presentation.settings.advanced.SettingsAdvancedViewModel
+import com.owncloud.android.presentation.settings.automaticuploads.SettingsPictureUploadsViewModel
+import com.owncloud.android.presentation.settings.automaticuploads.SettingsVideoUploadsViewModel
+import com.owncloud.android.presentation.settings.logging.SettingsLogsViewModel
+import com.owncloud.android.presentation.settings.more.SettingsMoreViewModel
+import com.owncloud.android.presentation.settings.security.SettingsSecurityViewModel
+import com.owncloud.android.presentation.sharing.GraphShareViewModel
+import com.owncloud.android.presentation.sharing.ShareViewModel
+import com.owncloud.android.presentation.spaces.SpacesListViewModel
+import com.owncloud.android.presentation.spaces.links.SpaceLinksViewModel
+import com.owncloud.android.presentation.spaces.members.SpaceMembersViewModel
+import com.owncloud.android.presentation.transfers.TransfersViewModel
+import com.owncloud.android.ui.ReceiveExternalFilesViewModel
+import com.owncloud.android.ui.preview.PreviewImageViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.androidx.viewmodel.dsl.viewModelOf
+import org.koin.dsl.module
+
+val viewModelModule = module {
+ viewModelOf(::ManageAccountsViewModel)
+ viewModelOf(::BiometricViewModel)
+ viewModelOf(::DrawerViewModel)
+ viewModelOf(::FileDetailsViewModel)
+ viewModelOf(::FileOperationsViewModel)
+ viewModelOf(::GraphShareViewModel)
+ viewModelOf(::LogListViewModel)
+ viewModelOf(::OAuthViewModel)
+ viewModelOf(::PatternViewModel)
+ viewModelOf(::PreviewAudioViewModel)
+ viewModelOf(::PreviewImageViewModel)
+ viewModelOf(::PreviewTextViewModel)
+ viewModelOf(::PreviewVideoViewModel)
+ viewModelOf(::ReceiveExternalFilesViewModel)
+ viewModelOf(::ReleaseNotesViewModel)
+ viewModelOf(::SettingsAdvancedViewModel)
+ viewModelOf(::SettingsLogsViewModel)
+ viewModelOf(::SettingsMoreViewModel)
+ viewModelOf(::SettingsPictureUploadsViewModel)
+ viewModelOf(::SettingsSecurityViewModel)
+ viewModelOf(::SettingsVideoUploadsViewModel)
+ viewModelOf(::SettingsViewModel)
+ viewModelOf(::SpaceLinksViewModel)
+ viewModelOf(::SpaceMembersViewModel)
+ viewModelOf(::FileOperationsViewModel)
+
+ viewModel { (accountName: String) -> CapabilityViewModel(accountName, get(), get(), get(), get()) }
+ viewModel { (action: PasscodeAction) -> PassCodeViewModel(get(), get(), action) }
+ viewModel { (filePath: String, accountName: String) ->
+ ShareViewModel(filePath, accountName, get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
+ }
+ viewModel { (initialFolderToDisplay: OCFile, fileListOption: FileListOption) ->
+ MainFileListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
+ initialFolderToDisplay, fileListOption)
+ }
+ viewModel { (ocFile: OCFile) -> ConflictsResolveViewModel(get(), get(), get(), get(), get(), ocFile) }
+ viewModel { AuthenticationViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
+ viewModel { MigrationViewModel(MainApp.dataFolder, get(), get(), get(), get(), get(), get(), get()) }
+ viewModel { TransfersViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
+ get(), get()) }
+ viewModel { ReceiveExternalFilesViewModel(get(), get(), get(), get()) }
+ viewModel { (accountName: String, showPersonalSpace: Boolean) ->
+ SpacesListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
+ accountName,
+ showPersonalSpace)
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt
new file mode 100644
index 000000000000..c4027a9bb596
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt
@@ -0,0 +1,548 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Aitor Ballesteros Pavón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.app.Activity
+import android.app.AlertDialog
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NO_HISTORY
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.net.Uri
+import android.text.method.LinkMovementMethod
+import android.util.TypedValue
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import android.webkit.MimeTypeMap
+import android.widget.LinearLayout
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.core.content.FileProvider
+import androidx.core.text.HtmlCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import androidx.core.view.updateMargins
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.snackbar.Snackbar
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.R
+import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider
+import com.owncloud.android.domain.files.model.OCFile
+import com.owncloud.android.presentation.common.ShareSheetHelper
+import com.owncloud.android.presentation.security.LockEnforcedType
+import com.owncloud.android.presentation.security.LockEnforcedType.Companion.parseFromInteger
+import com.owncloud.android.presentation.security.LockType
+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.DrawerActivity
+import com.owncloud.android.ui.activity.FileDisplayActivity.Companion.ALL_FILES_SAF_REGEX
+import com.owncloud.android.utils.CONFIGURATION_DEVICE_PROTECTION
+import com.owncloud.android.utils.MimetypeIconUtil
+import com.owncloud.android.utils.UriUtilsKt.getExposedFileUriForOCFile
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.inject
+import timber.log.Timber
+import java.io.File
+import kotlin.math.min
+
+fun Activity.showErrorInSnackbar(genericErrorMessageId: Int, throwable: Throwable?) =
+ throwable?.let {
+ showMessageInSnackbar(
+ message = it.parseError(getString(genericErrorMessageId), resources)
+ )
+ }
+
+fun Activity.showMessageInSnackbar(
+ layoutId: Int = android.R.id.content,
+ message: CharSequence,
+ duration: Int = Snackbar.LENGTH_LONG
+) {
+ val bottomNavView = findViewById(R.id.bottom_nav_view)
+ val snackbar = Snackbar.make(findViewById(layoutId), message, duration)
+ if (bottomNavView?.isVisible == true) { snackbar.setAnchorView(bottomNavView) }
+ snackbar.show()
+}
+
+fun Activity.showSnackbarWithAction(
+ message: CharSequence,
+ actionText: CharSequence,
+ action: () -> Unit,
+ duration: Int = Snackbar.LENGTH_LONG,
+) {
+ val bottomNavView = findViewById(R.id.bottom_nav_view)
+ val snackbar = Snackbar.make(findViewById(android.R.id.content), message, duration)
+ if (bottomNavView?.isVisible == true) { snackbar.setAnchorView(bottomNavView) }
+ snackbar.setAction(actionText) { action() }.show()
+}
+
+fun Activity.showErrorInToast(
+ genericErrorMessageId: Int,
+ throwable: Throwable?,
+ duration: Int = Toast.LENGTH_SHORT
+) =
+ throwable?.let {
+ Toast.makeText(
+ this,
+ it.parseError(getString(genericErrorMessageId), resources),
+ duration
+ ).show()
+ }
+
+fun Activity.goToUrl(
+ url: String,
+ flags: Int? = null
+) {
+ if (url.isNotEmpty()) {
+ val uriUrl = Uri.parse(url)
+ val intent = Intent(Intent.ACTION_VIEW, uriUrl)
+ if (flags != null) intent.addFlags(flags)
+
+ try {
+ startActivity(intent)
+ } catch (e: ActivityNotFoundException) {
+ showMessageInSnackbar(message = this.getString(R.string.file_list_no_app_for_perform_action))
+ Timber.e("No Activity found to handle Intent")
+ }
+ }
+}
+
+fun Activity.openPrivacyPolicy() {
+ val urlPrivacyPolicy = getString(R.string.url_privacy_policy)
+
+ val cantBeOpenedWithWebView = urlPrivacyPolicy.endsWith("pdf")
+ if (cantBeOpenedWithWebView) {
+ goToUrl(urlPrivacyPolicy)
+ } else {
+ val intent = Intent(this, PrivacyPolicyActivity::class.java)
+ startActivity(intent)
+ }
+}
+
+fun Activity.sendEmail(
+ email: String,
+ subject: String? = null,
+ text: String? = null
+) {
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse(email)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ putExtra(Intent.EXTRA_SUBJECT, subject)
+ if (text != null) putExtra(Intent.EXTRA_TEXT, text)
+ }
+
+ try {
+ startActivity(intent)
+ } catch (e: ActivityNotFoundException) {
+ showMessageInSnackbar(message = this.getString(R.string.file_list_no_app_for_perform_action))
+ Timber.e("No Activity found to handle Intent")
+ }
+}
+
+private fun getIntentForSavedMimeType(data: Uri, type: String): Intent {
+ val intentForSavedMimeType = Intent(Intent.ACTION_VIEW)
+ intentForSavedMimeType.setDataAndType(data, type)
+ intentForSavedMimeType.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ return intentForSavedMimeType
+}
+
+private fun getIntentForGuessedMimeType(storagePath: String, type: String, data: Uri): Intent? {
+ var intentForGuessedMimeType: Intent? = null
+ if (storagePath.lastIndexOf('.') >= 0) {
+ val guessedMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(storagePath.substring(storagePath.lastIndexOf('.') + 1))
+ if (guessedMimeType != null && guessedMimeType != type) {
+ intentForGuessedMimeType = Intent(Intent.ACTION_VIEW)
+ intentForGuessedMimeType.setDataAndType(data, guessedMimeType)
+ intentForGuessedMimeType.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ }
+ }
+ return intentForGuessedMimeType
+}
+
+fun Activity.openFile(file: File?) {
+ if (file != null) {
+
+ val intentForSavedMimeType = getIntentForSavedMimeType(
+ getExposedFileUri(this, file.path)!!,
+ MimetypeIconUtil.getBestMimeTypeByFilename(file.name)
+ )
+
+ val intentForGuessedMimeType = getIntentForGuessedMimeType(
+ file.path,
+ MimetypeIconUtil.getBestMimeTypeByFilename(file.name), getExposedFileUri(this, file.path)!!
+ )
+
+ openFileWithIntent(intentForSavedMimeType, intentForGuessedMimeType)
+ } else {
+ Timber.e("Trying to open a NULL file")
+ }
+}
+
+private fun getExposedFileUri(context: Context, localPath: String): Uri? {
+ var exposedFileUri: Uri? = null
+
+ if (localPath.isEmpty()) {
+ return null
+ }
+
+ // Use the FileProvider to get a content URI
+ try {
+ exposedFileUri = FileProvider.getUriForFile(
+ context,
+ context.getString(R.string.file_provider_authority),
+ File(localPath)
+ )
+ } catch (e: IllegalArgumentException) {
+ Timber.e(e, "File can't be exported")
+ }
+
+ return exposedFileUri
+}
+
+fun Activity.openFileWithIntent(intentForSavedMimeType: Intent, intentForGuessedMimeType: Intent?) {
+ val openFileWithIntent: Intent = intentForGuessedMimeType ?: intentForSavedMimeType
+ val launchables: List =
+ this.packageManager.queryIntentActivities(openFileWithIntent, PackageManager.MATCH_DEFAULT_ONLY)
+ if (launchables.isNotEmpty()) {
+ try {
+ this.startActivity(
+ Intent.createChooser(
+ openFileWithIntent, this.getString(R.string.actionbar_open_with)
+ )
+ )
+ } catch (anfe: ActivityNotFoundException) {
+ Timber.i(anfe, "No app found for file type")
+ showMessageInSnackbar(
+ message = this.getString(
+ R.string.file_list_no_app_for_file_type
+ )
+ )
+ }
+ } else {
+ showMessageInSnackbar(
+ message = this.getString(
+ R.string.file_list_no_app_for_file_type
+ )
+ )
+ }
+}
+
+fun AppCompatActivity.sendFile(file: File?) {
+ if (file != null) {
+ val sendIntent: Intent = makeIntent(file, this)
+ val shareSheetIntent = ShareSheetHelper().getShareSheetIntent(
+ intent = sendIntent,
+ context = this,
+ title = R.string.activity_chooser_send_file_title,
+ packagesToExclude = arrayOf()
+ )
+ this.startActivity(shareSheetIntent)
+ } else {
+ Timber.e("Trying to send a NULL file")
+ }
+}
+
+private fun makeIntent(file: File?, context: Context): Intent {
+ val sendIntent = Intent(Intent.ACTION_SEND)
+ if (file != null) {
+ // set MimeType
+ sendIntent.type = MimetypeIconUtil.getBestMimeTypeByFilename(file.name)
+ sendIntent.putExtra(
+ Intent.EXTRA_STREAM,
+ getExposedFileUri(context, file.path)
+ )
+ }
+ sendIntent.putExtra(Intent.ACTION_SEND, true) // Send Action
+ return sendIntent
+}
+
+fun Activity.hideSoftKeyboard() {
+ val focusedView = currentFocus
+ focusedView?.let {
+ val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ inputMethodManager.hideSoftInputFromWindow(
+ focusedView.windowToken,
+ 0
+ )
+ }
+}
+
+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 -> {
+ 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)
+ }
+ }
+ }
+}
+
+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.sendEmailOrOpenFeedbackDialogAction(feedbackMail: String) {
+ if (feedbackMail.isNotEmpty()) {
+ val feedback = "Android v" + BuildConfig.VERSION_NAME + " - " + getString(R.string.prefs_feedback)
+ sendEmail(email = feedbackMail, subject = feedback)
+ } else {
+ openFeedbackDialog()
+ }
+}
+
+fun Activity.openFeedbackDialog() {
+ val getInContactDescription =
+ getString(
+ R.string.feedback_dialog_get_in_contact_description,
+ DrawerActivity.CENTRAL_URL,
+ DrawerActivity.GITHUB_URL
+ ).trimIndent()
+ val spannableString = HtmlCompat.fromHtml(getInContactDescription, HtmlCompat.FROM_HTML_MODE_LEGACY)
+
+ val getInContactDescriptionTextView = TextView(this).apply {
+ text = spannableString
+ setTextColor(getColor(android.R.color.black))
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
+ movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ val layout = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(64, 16, 64, 16)
+ addView(getInContactDescriptionTextView)
+ }
+ val builder = AlertDialog.Builder(this)
+ builder.apply {
+ setTitle(getString(R.string.drawer_feedback))
+ setView(layout)
+ setNegativeButton(R.string.drawer_close) { dialog, _ ->
+ dialog.dismiss()
+ }
+ setCancelable(false)
+ }
+ val alertDialog = builder.create()
+ alertDialog.show()
+}
+
+fun Activity.manageOptionLockSelected(type: LockType) {
+
+ OCSharedPreferencesProvider(this).let {
+ // Remove passcode
+ it.removePreference(PassCodeActivity.PREFERENCE_PASSCODE)
+ it.putBoolean(PassCodeActivity.PREFERENCE_SET_PASSCODE, false)
+
+ // Remove pattern
+ it.removePreference(PatternActivity.PREFERENCE_PATTERN)
+ it.putBoolean(PatternActivity.PREFERENCE_SET_PATTERN, false)
+
+ // Remove biometric
+ it.putBoolean(BiometricActivity.PREFERENCE_SET_BIOMETRIC, false)
+ }
+
+ when (type) {
+ LockType.PASSCODE -> startActivity(Intent(this, PassCodeActivity::class.java).apply {
+ action = PassCodeActivity.ACTION_CREATE
+ 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
+ putExtra(EXTRAS_LOCK_ENFORCED, true)
+ })
+ }
+}
+
+fun Activity.showBiometricDialog(iEnableBiometrics: EnableBiometrics) {
+ AlertDialog.Builder(this)
+ .setCancelable(false)
+ .setTitle(getString(R.string.biometric_dialog_title))
+ .setPositiveButton(R.string.common_yes) { dialog, _ ->
+ iEnableBiometrics.onOptionSelected(BiometricStatus.ENABLED_BY_USER)
+ dialog.dismiss()
+ }
+ .setNegativeButton(R.string.common_no) { dialog, _ ->
+ iEnableBiometrics.onOptionSelected(BiometricStatus.DISABLED_BY_USER)
+ dialog.dismiss()
+ }
+ .show()
+}
+
+fun FragmentActivity.sendDownloadedFilesByShareSheet(ocFiles: List) {
+ if (ocFiles.isEmpty()) throw IllegalArgumentException("Can't share anything")
+
+ val sendIntent = if (ocFiles.size == 1) {
+ Intent(Intent.ACTION_SEND).apply {
+ type = ocFiles.first().mimeType
+ putExtra(Intent.EXTRA_STREAM, getExposedFileUriForOCFile(this@sendDownloadedFilesByShareSheet, ocFiles.first()))
+ }
+ } else {
+ val fileUris = ocFiles.map { getExposedFileUriForOCFile(this@sendDownloadedFilesByShareSheet, it) }
+ Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+ type = ALL_FILES_SAF_REGEX
+ putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(fileUris))
+ }
+ }
+
+ val packagesToExclude = arrayOf(this@sendDownloadedFilesByShareSheet.packageName)
+ val shareSheetIntent = ShareSheetHelper().getShareSheetIntent(
+ sendIntent,
+ this@sendDownloadedFilesByShareSheet,
+ R.string.activity_chooser_send_file_title,
+ packagesToExclude
+ )
+ startActivity(shareSheetIntent)
+}
+
+fun Activity.openOCFile(ocFile: OCFile) {
+ val intentForSavedMimeType = Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(getExposedFileUriForOCFile(this@openOCFile, ocFile), ocFile.mimeType)
+ flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ if (ocFile.hasWritePermission) {
+ flags = flags or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ }
+ }
+
+ try {
+ startActivity(Intent.createChooser(intentForSavedMimeType, getString(R.string.actionbar_open_with)))
+ } catch (anfe: ActivityNotFoundException) {
+ showErrorInSnackbar(genericErrorMessageId = R.string.file_list_no_app_for_file_type, anfe)
+ }
+}
+
+fun FragmentActivity.collectLatestLifecycleFlow(
+ flow: Flow,
+ lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
+ collect: suspend (T) -> Unit
+) {
+ lifecycleScope.launch {
+ repeatOnLifecycle(lifecycleState) {
+ flow.collectLatest(collect)
+ }
+ }
+}
+
+fun Activity.adaptInfiniteEdges(root: View) {
+ val typedValue = TypedValue()
+ var actionBarHeight = 0
+ if (theme.resolveAttribute(android.R.attr.actionBarSize, typedValue, true)) {
+ actionBarHeight = TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics)
+ }
+
+ val maxInsetPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32f, resources.displayMetrics).toInt()
+ val extraTopPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12f, resources.displayMetrics).toInt()
+
+ // Adapt toolbar for infinite edges
+ findViewById(R.id.toolbar).apply {
+ ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets ->
+ val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val usedInsetFinal = min(systemInsets.top, maxInsetPx) + extraTopPx
+ updateLayoutParams { height = actionBarHeight + usedInsetFinal }
+ insets
+ }
+ }
+
+ // Adapt text in the toolbar
+ findViewById(R.id.standard_toolbar).apply {
+ ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets ->
+ val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val usedInsetFinal = min(systemInsets.top, maxInsetPx) + extraTopPx
+ updateLayoutParams { updateMargins(top = usedInsetFinal) }
+ insets
+ }
+ }
+
+ // Adapt content for infinite edges
+ ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->
+ val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ root.setPadding(systemInsets.left, root.paddingTop, systemInsets.right, systemInsets.bottom)
+ insets
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/AddMemberFragmentBindingExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/AddMemberFragmentBindingExt.kt
new file mode 100644
index 000000000000..bc469678838e
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/AddMemberFragmentBindingExt.kt
@@ -0,0 +1,56 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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 androidx.core.view.isVisible
+import com.owncloud.android.R
+import com.owncloud.android.databinding.AddMemberFragmentBinding
+import com.owncloud.android.domain.members.model.OCMember
+import com.owncloud.android.domain.members.model.OCMemberType
+import com.owncloud.android.presentation.spaces.members.SpaceRolesAdapter
+
+fun AddMemberFragmentBinding.showOrHideEmptyView(hasMembers: Boolean, searchMinLength: Int) {
+ membersRecyclerView.isVisible = hasMembers
+ emptyDataParent.apply {
+ val shouldShow = !hasMembers && searchBar.query.length >= searchMinLength
+ root.isVisible = shouldShow
+ if (shouldShow) {
+ listEmptyDatasetIcon.setImageResource(R.drawable.ic_share_generic_white)
+ listEmptyDatasetTitle.setText(R.string.members_search_failed)
+ listEmptyDatasetSubTitle.setText(R.string.members_search_empty)
+ }
+ }
+}
+
+fun AddMemberFragmentBinding.bindSelectedMember(member: OCMember) {
+ selectedMemberLayout.apply {
+ memberIcon.setImageResource(if (member.type == OCMemberType.GROUP) R.drawable.ic_group else R.drawable.ic_user)
+ memberName.text = member.displayName
+ memberRole.text = member.surname
+ }
+}
+
+fun AddMemberFragmentBinding.bindRoles(rolesAdapter: SpaceRolesAdapter, selectedRoleId: String?) {
+ selectedRoleId?.let {
+ inviteMemberButton.isEnabled = true
+ rolesAdapter.setSelectedRole(it)
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ContextExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ContextExt.kt
new file mode 100644
index 000000000000..f49ea52d36f0
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ContextExt.kt
@@ -0,0 +1,40 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+
+fun Context.createNotificationChannel(
+ id: String,
+ name: String,
+ description: String,
+ importance: Int
+) {
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ val notificationChannel = NotificationChannel(id, name, importance).apply {
+ setDescription(description)
+ }
+
+ notificationManager.createNotificationChannel(notificationChannel)
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/CursorExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/CursorExt.kt
new file mode 100644
index 000000000000..abb3a31527c9
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/CursorExt.kt
@@ -0,0 +1,38 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * Copyright (C) 2020 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.database.Cursor
+
+fun Cursor.getStringFromColumnOrThrow(
+ columnName: String
+): String? = getString(getColumnIndexOrThrow(columnName))
+
+fun Cursor.getStringFromColumnOrEmpty(
+ columnName: String
+): String = getColumnIndex(columnName).takeUnless { it < 0 }?.let { getString(it) }.orEmpty()
+
+fun Cursor.getIntFromColumnOrThrow(
+ columnName: String
+): Int = getInt(getColumnIndexOrThrow(columnName))
+
+fun Cursor.getLongFromColumnOrThrow(
+ columnName: String
+): Long = getLong(getColumnIndexOrThrow(columnName))
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/DialogExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/DialogExt.kt
new file mode 100644
index 000000000000..da5665ae3c87
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/DialogExt.kt
@@ -0,0 +1,31 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David Crespo Ríos
+ * Copyright (C) 2022 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.app.Dialog
+import android.view.WindowManager
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.R
+
+fun Dialog.avoidScreenshotsIfNeeded() {
+ if (!BuildConfig.DEBUG && context.resources?.getBoolean(R.bool.allow_screenshots) == false) {
+ window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FileExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FileExt.kt
new file mode 100644
index 000000000000..45d1b0ad2678
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FileExt.kt
@@ -0,0 +1,30 @@
+/*
+ * ownCloud Android client application
+ *
+ * @author Fernando Sanz Velasco
+ * Copyright (C) 2021 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.content.Context
+import com.owncloud.android.utils.DisplayUtils
+import java.io.File
+
+fun File.toLegibleStringSize(context: Context): String {
+ val bytes = if (!exists()) 0L else length()
+ return DisplayUtils.bytesToHumanReadable(bytes, context, true)
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FileListOptionExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FileListOptionExt.kt
new file mode 100644
index 000000000000..02fc881793d5
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FileListOptionExt.kt
@@ -0,0 +1,48 @@
+/**
+ * ownCloud Android client application
+ *
+ * Copyright (C) 2022 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 androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.owncloud.android.R
+import com.owncloud.android.domain.files.model.FileListOption
+
+@StringRes
+fun FileListOption.toTitleStringRes(): Int = when (this) {
+ FileListOption.ALL_FILES -> R.string.file_list_empty_title_all_files
+ FileListOption.SPACES_LIST -> R.string.spaces_list_empty_title
+ FileListOption.SHARED_BY_LINK -> R.string.file_list_empty_title_shared_by_links
+ FileListOption.AV_OFFLINE -> R.string.file_list_empty_title_available_offline
+}
+
+@StringRes
+fun FileListOption.toSubtitleStringRes(): Int = when (this) {
+ FileListOption.ALL_FILES -> R.string.file_list_empty_subtitle_all_files
+ FileListOption.SPACES_LIST -> R.string.spaces_list_empty_subtitle
+ FileListOption.SHARED_BY_LINK -> R.string.file_list_empty_subtitle_shared_by_links
+ FileListOption.AV_OFFLINE -> R.string.file_list_empty_subtitle_available_offline
+}
+
+@DrawableRes
+fun FileListOption.toDrawableRes(): Int = when (this) {
+ FileListOption.ALL_FILES -> R.drawable.ic_folder
+ FileListOption.SPACES_LIST -> R.drawable.ic_spaces
+ FileListOption.SHARED_BY_LINK -> R.drawable.ic_shared_by_link
+ FileListOption.AV_OFFLINE -> R.drawable.ic_available_offline
+}
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 000000000000..7a6e5bdc1a13
--- /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/FragmentActivityExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentActivityExt.kt
new file mode 100644
index 000000000000..15f5aca11832
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentActivityExt.kt
@@ -0,0 +1,38 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.extensions
+
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
+
+fun FragmentActivity.showDialogFragment(
+ newFragment: DialogFragment, fragmentTag: String
+) {
+ val ft = supportFragmentManager.beginTransaction()
+ val prev = supportFragmentManager.findFragmentByTag(fragmentTag)
+
+ if (prev != null) {
+ ft.remove(prev)
+ }
+ ft.addToBackStack(null)
+
+ newFragment.show(ft, fragmentTag)
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt
new file mode 100644
index 000000000000..9f642898940b
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/FragmentExt.kt
@@ -0,0 +1,193 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.app.AlertDialog
+import android.app.DatePickerDialog
+import android.content.Context
+import android.content.DialogInterface
+import android.icu.util.Calendar
+import android.view.Menu
+import android.view.MenuItem.SHOW_AS_ACTION_NEVER
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import androidx.core.view.isVisible
+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.databinding.AddMemberFragmentBinding
+import com.owncloud.android.domain.appregistry.model.AppRegistryProvider
+import com.owncloud.android.utils.DisplayUtils
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+
+fun Fragment.showErrorInSnackbar(genericErrorMessageId: Int, throwable: Throwable?) =
+ throwable?.let {
+ showMessageInSnackbar(it.parseError(getString(genericErrorMessageId), resources))
+ }
+
+fun Fragment.showMessageInSnackbar(
+ message: CharSequence,
+ duration: Int = Snackbar.LENGTH_LONG,
+) {
+ val requiredView = view ?: return
+ val rootView = view?.rootView ?: return
+ val bottomNavView = rootView.findViewById(R.id.bottom_nav_view)
+ val snackbar = Snackbar.make(requiredView, message, duration)
+ if (bottomNavView?.isVisible == true) { snackbar.setAnchorView(bottomNavView) }
+ snackbar.show()
+}
+
+fun Fragment.showSnackbarWithAction(
+ message: CharSequence,
+ actionText: CharSequence,
+ action: () -> Unit,
+ duration: Int = Snackbar.LENGTH_LONG
+) {
+ val requiredView = view ?: return
+ Snackbar.make(requiredView, message, duration)
+ .setAction(actionText) { action() }
+ .show()
+}
+
+fun Fragment.showAlertDialog(
+ title: String,
+ message: String,
+ positiveButtonText: String = getString(android.R.string.ok),
+ positiveButtonListener: ((DialogInterface, Int) -> Unit)? = null,
+ negativeButtonText: String = "",
+ negativeButtonListener: ((DialogInterface, Int) -> Unit)? = null
+) {
+ val requiredActivity = activity ?: return
+ AlertDialog.Builder(requiredActivity)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(positiveButtonText, positiveButtonListener)
+ .setNegativeButton(negativeButtonText, negativeButtonListener)
+ .show()
+ .avoidScreenshotsIfNeeded()
+}
+
+fun Fragment.hideSoftKeyboard() {
+ val focusedView = requireActivity().currentFocus
+ focusedView?.let {
+ val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ inputMethodManager.hideSoftInputFromWindow(
+ focusedView.windowToken,
+ 0
+ )
+ }
+}
+
+fun Fragment.collectLatestLifecycleFlow(
+ flow: Flow,
+ lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
+ collect: suspend (T) -> Unit
+) {
+ lifecycleScope.launch {
+ repeatOnLifecycle(lifecycleState) {
+ flow.collectLatest(collect)
+ }
+ }
+}
+
+fun Fragment.bindDatePickerDialog(
+ binding: AddMemberFragmentBinding,
+ expirationDate: String?,
+ onExpirationDateSelected: (String?) -> Unit,
+) {
+ binding.expirationDateLayout.expirationDateSwitch.setOnCheckedChangeListener { _, isChecked ->
+ if (isChecked) {
+ openDatePickerDialog(binding, expirationDate, onExpirationDateSelected)
+ } else {
+ binding.expirationDateLayout.expirationDateValue.visibility = View.GONE
+ onExpirationDateSelected(null)
+ }
+ }
+}
+
+fun Fragment.openDatePickerDialog(
+ binding: AddMemberFragmentBinding,
+ expirationDate: String?,
+ onExpirationDateSelected: (String?) -> Unit,
+) {
+ val calendar = Calendar.getInstance()
+ val formatter = SimpleDateFormat(DisplayUtils.DATE_FORMAT_ISO, Locale.ROOT).apply {
+ timeZone = TimeZone.getTimeZone("UTC")
+ }
+
+ expirationDate?.let {
+ calendar.time = formatter.parse(it)
+ }
+
+ DatePickerDialog(
+ requireContext(),
+ { _, selectedYear, selectedMonth, selectedDay ->
+ calendar.set(selectedYear, selectedMonth, selectedDay, 23, 59, 59)
+ calendar.set(Calendar.MILLISECOND, 999)
+ val isoExpirationDate = formatter.format(calendar.time)
+ onExpirationDateSelected(isoExpirationDate)
+ binding.expirationDateLayout.expirationDateValue.apply {
+ visibility = View.VISIBLE
+ text = DisplayUtils.displayDateToHumanReadable(isoExpirationDate)
+ }
+ },
+ calendar.get(Calendar.YEAR),
+ calendar.get(Calendar.MONTH),
+ calendar.get(Calendar.DAY_OF_MONTH)
+ ).apply {
+ datePicker.minDate = Calendar.getInstance().timeInMillis
+ show()
+ setOnCancelListener {
+ if (expirationDate == null) {
+ binding.expirationDateLayout.expirationDateSwitch.isChecked = false
+ }
+ }
+ }
+}
+
+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/ImageViewExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ImageViewExt.kt
new file mode 100644
index 000000000000..c29c0a2fbb04
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ImageViewExt.kt
@@ -0,0 +1,34 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Fernando Sanz Velasco
+ * Copyright (C) 2021 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.annotation.SuppressLint
+import android.widget.ImageView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+
+@SuppressLint("CheckResult")
+fun ImageView.setPicture(imageToLoad: Int) {
+ Glide.with(this)
+ .load(imageToLoad)
+ .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
+ .into(this)
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/LiveDataExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/LiveDataExt.kt
new file mode 100644
index 000000000000..bf0d66b4537a
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/LiveDataExt.kt
@@ -0,0 +1,59 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * Copyright (C) 2021 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 androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import androidx.work.WorkInfo
+import com.owncloud.android.workers.DownloadFileWorker.Companion.WORKER_KEY_PROGRESS
+
+fun LiveData.observeWorkerTillItFinishes(
+ owner: LifecycleOwner,
+ onWorkEnqueued: () -> Unit = {},
+ onWorkRunning: (progress: Int) -> Unit,
+ onWorkSucceeded: () -> Unit,
+ onWorkFailed: () -> Unit,
+ onWorkBlocked: () -> Unit = {},
+ onWorkCancelled: () -> Unit = {},
+ removeObserverAfterNull: Boolean = true,
+) {
+ observe(owner, object : Observer {
+ override fun onChanged(value: WorkInfo?) {
+ if (value == null) {
+ if (removeObserverAfterNull) {
+ removeObserver(this)
+ }
+ return
+ }
+
+ if (value.state.isFinished) {
+ removeObserver(this)
+ }
+ when (value.state) {
+ WorkInfo.State.ENQUEUED -> onWorkEnqueued()
+ WorkInfo.State.RUNNING -> onWorkRunning(value.progress.getInt(WORKER_KEY_PROGRESS, -1))
+ WorkInfo.State.SUCCEEDED -> onWorkSucceeded()
+ WorkInfo.State.FAILED -> onWorkFailed()
+ WorkInfo.State.BLOCKED -> onWorkBlocked()
+ WorkInfo.State.CANCELLED -> onWorkCancelled()
+ }
+ }
+ })
+}
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 000000000000..728cd1e7b6d8
--- /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/extensions/OCLinkExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/OCLinkExt.kt
new file mode 100644
index 000000000000..1a20370c6ed0
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/OCLinkExt.kt
@@ -0,0 +1,33 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.links.model.OCLinkType
+
+fun OCLinkType.toStringResId() =
+ when (this) {
+ OCLinkType.CAN_VIEW -> R.string.public_link_view
+ OCLinkType.CAN_EDIT -> R.string.public_link_edit
+ OCLinkType.CREATE_ONLY -> R.string.public_link_create_only
+ OCLinkType.CAN_UPLOAD -> R.string.public_link_upload
+ OCLinkType.INTERNAL -> R.string.public_link_internal
+ }
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/OCTransferExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/OCTransferExt.kt
new file mode 100644
index 000000000000..a0b6b884bdcc
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/OCTransferExt.kt
@@ -0,0 +1,70 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2025 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.content.Context
+import android.net.Uri
+import androidx.annotation.StringRes
+import androidx.documentfile.provider.DocumentFile
+import com.owncloud.android.R
+import com.owncloud.android.domain.transfers.model.OCTransfer
+import com.owncloud.android.domain.transfers.model.TransferResult
+import com.owncloud.android.domain.transfers.model.TransferStatus
+
+@StringRes
+fun OCTransfer.statusToStringRes(): Int =
+ when (status) {
+ TransferStatus.TRANSFER_IN_PROGRESS -> R.string.uploader_upload_in_progress_ticker
+ TransferStatus.TRANSFER_SUCCEEDED -> R.string.uploads_view_upload_status_succeeded
+ TransferStatus.TRANSFER_QUEUED -> R.string.uploads_view_upload_status_queued
+ TransferStatus.TRANSFER_FAILED -> when (lastResult) {
+ TransferResult.CREDENTIAL_ERROR -> R.string.uploads_view_upload_status_failed_credentials_error
+ TransferResult.FOLDER_ERROR -> R.string.uploads_view_upload_status_failed_folder_error
+ TransferResult.FILE_NOT_FOUND -> R.string.uploads_view_upload_status_failed_localfile_error
+ TransferResult.FILE_ERROR -> R.string.uploads_view_upload_status_failed_file_error
+ TransferResult.PRIVILEGES_ERROR -> R.string.uploads_view_upload_status_failed_permission_error
+ TransferResult.NETWORK_CONNECTION -> R.string.uploads_view_upload_status_failed_connection_error
+ TransferResult.DELAYED_FOR_WIFI -> R.string.uploads_view_upload_status_waiting_for_wifi
+ TransferResult.CONFLICT_ERROR -> R.string.uploads_view_upload_status_conflict
+ TransferResult.SERVICE_INTERRUPTED -> R.string.uploads_view_upload_status_service_interrupted
+ TransferResult.SERVICE_UNAVAILABLE -> R.string.service_unavailable
+ TransferResult.QUOTA_EXCEEDED -> R.string.failed_upload_quota_exceeded_text
+ TransferResult.SSL_RECOVERABLE_PEER_UNVERIFIED -> R.string.ssl_certificate_not_trusted
+ TransferResult.UNKNOWN -> R.string.uploads_view_upload_status_unknown_fail
+ TransferResult.FILE_TOO_LARGE -> R.string.uploads_view_upload_status_failed_payload_error
+ // Should not get here; cancelled uploads should be wiped out
+ TransferResult.CANCELLED -> R.string.uploads_view_upload_status_cancelled
+ // Should not get here; status should be UPLOAD_SUCCESS
+ TransferResult.UPLOADED -> R.string.uploads_view_upload_status_succeeded
+ // We don't know the specific forbidden error message because it is not being saved in transfers storage
+ TransferResult.SPECIFIC_FORBIDDEN -> R.string.uploads_view_upload_status_failed_permission_error
+ // We don't know the specific unavailable service error message because it is not being saved in transfers storage
+ TransferResult.SPECIFIC_SERVICE_UNAVAILABLE -> R.string.service_unavailable
+ // We don't know the specific unsupported media type error message because it is not being saved in transfers storage
+ TransferResult.SPECIFIC_UNSUPPORTED_MEDIA_TYPE -> R.string.uploads_view_unsupported_media_type
+ // Should not get here; status should be not null
+ null -> R.string.uploads_view_upload_status_unknown_fail
+ }
+ }
+
+fun OCTransfer.isContentUri(context: Context): Boolean =
+ DocumentFile.isDocumentUri(context, Uri.parse(localPath))
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMemberExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMemberExt.kt
new file mode 100644
index 000000000000..79e2954a43f1
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMemberExt.kt
@@ -0,0 +1,39 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.domain.members.model.OCMember
+import com.owncloud.android.domain.members.model.OCMemberType
+import com.owncloud.android.domain.sharing.shares.model.MemberPermission
+
+private const val GROUP_PREFIX = "g:"
+private const val USER_PREFIX = "u:"
+
+fun MemberPermission.toOCMember(): OCMember {
+ val isGroup = id.startsWith(GROUP_PREFIX)
+ val type = if (isGroup) OCMemberType.GROUP else OCMemberType.USER
+ return OCMember(
+ id = id.removePrefix(if (isGroup) GROUP_PREFIX else USER_PREFIX),
+ displayName = displayName,
+ surname = OCMemberType.toString(type),
+ type = type
+ )
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt
new file mode 100644
index 000000000000..4c65b00650dc
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/SpaceMenuOptionExt.kt
@@ -0,0 +1,46 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2025 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.spaces.model.SpaceMenuOption
+
+fun SpaceMenuOption.toStringResId() =
+ when (this) {
+ SpaceMenuOption.EDIT -> R.string.edit_space
+ SpaceMenuOption.EDIT_IMAGE -> R.string.edit_space_image
+ SpaceMenuOption.DISABLE -> R.string.disable_space
+ SpaceMenuOption.ENABLE -> R.string.enable_space
+ SpaceMenuOption.DELETE -> R.string.delete_space
+ SpaceMenuOption.SET_ICON -> R.string.set_space_icon
+ SpaceMenuOption.MEMBERS -> R.string.members_title
+ }
+
+fun SpaceMenuOption.toDrawableResId() =
+ when (this) {
+ SpaceMenuOption.EDIT -> R.drawable.ic_pencil
+ SpaceMenuOption.EDIT_IMAGE -> R.drawable.file_image
+ SpaceMenuOption.DISABLE -> R.drawable.ic_disable_space
+ SpaceMenuOption.ENABLE -> R.drawable.ic_enable_space
+ SpaceMenuOption.DELETE -> R.drawable.ic_action_delete_white
+ SpaceMenuOption.SET_ICON -> R.drawable.ic_set_space_icon
+ SpaceMenuOption.MEMBERS -> R.drawable.ic_share_generic_white
+ }
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt
new file mode 100644
index 000000000000..5e4076173736
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ThrowableExt.kt
@@ -0,0 +1,124 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.content.res.Resources
+import com.owncloud.android.R
+import com.owncloud.android.domain.exceptions.AccountNotNewException
+import com.owncloud.android.domain.exceptions.AccountNotTheSameException
+import com.owncloud.android.domain.exceptions.BadOcVersionException
+import com.owncloud.android.domain.exceptions.ConflictException
+import com.owncloud.android.domain.exceptions.ConflictMemberException
+import com.owncloud.android.domain.exceptions.ConflictShareException
+import com.owncloud.android.domain.exceptions.CopyIntoDescendantException
+import com.owncloud.android.domain.exceptions.CopyIntoSameFolderException
+import com.owncloud.android.domain.exceptions.FileAlreadyExistsException
+import com.owncloud.android.domain.exceptions.FileNotFoundException
+import com.owncloud.android.domain.exceptions.ForbiddenException
+import com.owncloud.android.domain.exceptions.IncompleteFileDataException
+import com.owncloud.android.domain.exceptions.IncorrectAddressException
+import com.owncloud.android.domain.exceptions.InstanceNotConfiguredException
+import com.owncloud.android.domain.exceptions.InvalidOverwriteException
+import com.owncloud.android.domain.exceptions.LocalFileNotFoundException
+import com.owncloud.android.domain.exceptions.MoveIntoDescendantException
+import com.owncloud.android.domain.exceptions.MoveIntoSameFolderException
+import com.owncloud.android.domain.exceptions.NetworkErrorException
+import com.owncloud.android.domain.exceptions.NoConnectionWithServerException
+import com.owncloud.android.domain.exceptions.NoNetworkConnectionException
+import com.owncloud.android.domain.exceptions.OAuth2ErrorAccessDeniedException
+import com.owncloud.android.domain.exceptions.OAuth2ErrorException
+import com.owncloud.android.domain.exceptions.PasswordEnforcedException
+import com.owncloud.android.domain.exceptions.PayloadTooLongException
+import com.owncloud.android.domain.exceptions.QuotaExceededException
+import com.owncloud.android.domain.exceptions.RedirectToNonSecureException
+import com.owncloud.android.domain.exceptions.ResourceLockedException
+import com.owncloud.android.domain.exceptions.SSLErrorException
+import com.owncloud.android.domain.exceptions.SSLRecoverablePeerUnverifiedException
+import com.owncloud.android.domain.exceptions.ServerConnectionTimeoutException
+import com.owncloud.android.domain.exceptions.ServerNotReachableException
+import com.owncloud.android.domain.exceptions.ServerResponseTimeoutException
+import com.owncloud.android.domain.exceptions.ServiceUnavailableException
+import com.owncloud.android.domain.exceptions.SpecificForbiddenException
+import com.owncloud.android.domain.exceptions.UnauthorizedException
+import com.owncloud.android.domain.exceptions.validation.FileNameException
+import java.util.Locale
+
+fun Throwable.parseError(
+ genericErrorMessage: String,
+ resources: Resources,
+ showJustReason: Boolean = false
+): CharSequence {
+ if (!this.message.isNullOrEmpty()) { // If there's an specific error message from layers below use it
+ return this.message as String
+ } else { // Build the error message otherwise
+ val reason = when (this) {
+ is AccountNotNewException -> resources.getString(R.string.auth_account_not_new)
+ is AccountNotTheSameException -> resources.getString(R.string.auth_account_not_the_same)
+ is BadOcVersionException -> resources.getString(R.string.auth_bad_oc_version_title)
+ is ConflictException -> resources.getString(R.string.error_conflict)
+ is ConflictMemberException -> resources.getString(R.string.members_add_conflict_error)
+ is ConflictShareException -> resources.getString(R.string.share_add_conflict_error)
+ is CopyIntoDescendantException -> resources.getString(R.string.copy_file_invalid_into_descendent)
+ is CopyIntoSameFolderException -> resources.getString(R.string.copy_file_invalid_overwrite)
+ is FileAlreadyExistsException -> resources.getString(R.string.file_already_exists)
+ is FileNameException -> resources.getString(when (this.type) {
+ FileNameException.FileNameExceptionType.FILE_NAME_EMPTY -> R.string.filename_empty
+ FileNameException.FileNameExceptionType.FILE_NAME_FORBIDDEN_CHARACTERS -> R.string.filename_forbidden_characters_from_server
+ FileNameException.FileNameExceptionType.FILE_NAME_TOO_LONG -> R.string.filename_too_long
+ })
+ is FileNotFoundException -> resources.getString(R.string.common_not_found)
+ is ForbiddenException -> resources.getString(R.string.uploads_view_upload_status_failed_permission_error)
+ is IncompleteFileDataException -> resources.getString(R.string.get_graph_shares_incomplete_data_error)
+ is IncorrectAddressException -> resources.getString(R.string.auth_incorrect_address_title)
+ is InstanceNotConfiguredException -> resources.getString(R.string.auth_not_configured_title)
+ is InvalidOverwriteException -> resources.getString(R.string.file_already_exists)
+ is LocalFileNotFoundException -> resources.getString(R.string.local_file_not_found_toast)
+ is MoveIntoDescendantException -> resources.getString(R.string.move_file_invalid_into_descendent)
+ is MoveIntoSameFolderException -> resources.getString(R.string.move_file_invalid_overwrite)
+ is NoConnectionWithServerException -> resources.getString(R.string.network_error_socket_exception)
+ is NoNetworkConnectionException -> resources.getString(R.string.error_no_network_connection)
+ is OAuth2ErrorAccessDeniedException -> resources.getString(R.string.auth_oauth_error_access_denied)
+ is OAuth2ErrorException -> resources.getString(R.string.auth_oauth_error)
+ is QuotaExceededException -> resources.getString(R.string.failed_upload_quota_exceeded_text)
+ is RedirectToNonSecureException -> resources.getString(R.string.auth_redirect_non_secure_connection_title)
+ is SSLErrorException -> resources.getString(R.string.auth_ssl_general_error_title)
+ is SSLRecoverablePeerUnverifiedException -> resources.getString(R.string.ssl_certificate_not_trusted)
+ is ServerConnectionTimeoutException -> resources.getString(R.string.network_error_connect_timeout_exception)
+ is ServerNotReachableException -> resources.getString(R.string.network_host_not_available)
+ is ServerResponseTimeoutException -> resources.getString(R.string.network_error_socket_timeout_exception)
+ is ServiceUnavailableException -> resources.getString(R.string.service_unavailable)
+ is SpecificForbiddenException -> resources.getString(R.string.uploads_view_upload_status_failed_permission_error)
+ is UnauthorizedException -> resources.getString(R.string.auth_unauthorized)
+ is NetworkErrorException -> resources.getString(R.string.network_error_message)
+ is ResourceLockedException -> resources.getString(R.string.resource_locked_error_message)
+ is PayloadTooLongException -> resources.getString(R.string.uploads_view_upload_status_failed_payload_error)
+ is PasswordEnforcedException -> resources.getString(R.string.public_link_password_enforced_error)
+ else -> resources.getString(R.string.common_error_unknown)
+ }
+
+ return if (showJustReason) {
+ reason
+ } else {
+ "$genericErrorMessage ${resources.getString(R.string.error_reason)} ${reason.lowercase(Locale.getDefault())}"
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/VectorExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/VectorExt.kt
new file mode 100644
index 000000000000..80125840713b
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/VectorExt.kt
@@ -0,0 +1,54 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author John Kalimeris
+ * Copyright (C) 2020 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.domain.files.model.OCFile
+import java.util.ArrayList
+import java.util.Locale
+import java.util.Vector
+
+fun Vector.filterByQuery(query: String): List {
+ val lowerCaseQuery = query.lowercase(Locale.ROOT)
+
+ val filteredList: MutableList = ArrayList()
+
+ for (fileToAdd in this) {
+ val nameOfTheFileToAdd: String = fileToAdd.fileName.lowercase(Locale.ROOT)
+ if (nameOfTheFileToAdd.contains(lowerCaseQuery)) {
+ filteredList.add(fileToAdd)
+ }
+ }
+
+ // Remove not matching files from this filelist
+ for (i in this.indices.reversed()) {
+ if (!filteredList.contains(this[i])) {
+ removeAt(i)
+ }
+ }
+
+ // Add matching files to this filelist
+ for (i in filteredList.indices) {
+ if (!contains(filteredList[i])) {
+ add(i, filteredList[i])
+ }
+ }
+
+ return this
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewExt.kt
new file mode 100644
index 000000000000..df76ce0ae12a
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewExt.kt
@@ -0,0 +1,36 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Aitor Ballesteros Pavón
+ *
+ * Copyright (C) 2024 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.View
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+
+fun View.setAccessibilityRole(className: Class<*>? = null, roleDescription: String? = null) {
+ ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(v: View, info: AccessibilityNodeInfoCompat) {
+ super.onInitializeAccessibilityNodeInfo(v, info)
+ className?.let { info.className = it.name }
+ roleDescription?.let { info.roleDescription = it }
+ }
+ })
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewModelExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewModelExt.kt
new file mode 100644
index 000000000000..8a3a01813dae
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ViewModelExt.kt
@@ -0,0 +1,185 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @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 androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.owncloud.android.domain.BaseUseCaseWithResult
+import com.owncloud.android.domain.exceptions.NoNetworkConnectionException
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.providers.ContextProvider
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import timber.log.Timber
+
+object ViewModelExt : KoinComponent {
+
+ private val contextProvider: ContextProvider by inject()
+
+ fun ViewModel.runUseCaseWithResult(
+ coroutineDispatcher: CoroutineDispatcher,
+ requiresConnection: Boolean = true,
+ showLoading: Boolean = false,
+ liveData: MediatorLiveData>>,
+ useCase: BaseUseCaseWithResult,
+ useCaseParams: Params,
+ postSuccess: Boolean = true,
+ postSuccessWithData: Boolean = true
+ ) {
+ viewModelScope.launch(coroutineDispatcher) {
+ if (showLoading) {
+ liveData.postValue(Event(UIResult.Loading()))
+ }
+
+ // If use case requires connection and is not connected, it is not needed to execute use case.
+ if (requiresConnection and !contextProvider.isConnected()) {
+ liveData.postValue(Event(UIResult.Error(error = NoNetworkConnectionException())))
+ Timber.w("${useCase.javaClass.simpleName} will not be executed due to lack of network connection")
+ return@launch
+ }
+
+ val useCaseResult = useCase(useCaseParams)
+
+ Timber.d("Use case executed: ${useCase.javaClass.simpleName} with result: $useCaseResult")
+
+ if (useCaseResult.isSuccess && postSuccess) {
+ if (postSuccessWithData) {
+ liveData.postValue(Event(UIResult.Success(useCaseResult.getDataOrNull())))
+ } else {
+ liveData.postValue(Event(UIResult.Success()))
+ }
+ } else if (useCaseResult.isError) {
+ liveData.postValue(Event(UIResult.Error(error = useCaseResult.getThrowableOrNull())))
+ }
+ }
+ }
+
+ fun ViewModel.runUseCaseWithResult(
+ coroutineDispatcher: CoroutineDispatcher,
+ requiresConnection: Boolean = true,
+ showLoading: Boolean = false,
+ flow: MutableStateFlow>?>,
+ useCase: BaseUseCaseWithResult,
+ useCaseParams: Params,
+ postSuccess: Boolean = true,
+ postSuccessWithData: Boolean = true
+ ) {
+ viewModelScope.launch(coroutineDispatcher) {
+ if (showLoading) {
+ flow.update { Event(UIResult.Loading()) }
+ }
+
+ // If use case requires connection and is not connected, it is not needed to execute use case
+ if (requiresConnection and !contextProvider.isConnected()) {
+ flow.update { Event(UIResult.Error(error = NoNetworkConnectionException())) }
+ Timber.w("${useCase.javaClass.simpleName} will not be executed due to lack of network connection")
+ return@launch
+ }
+
+ val useCaseResult = useCase(useCaseParams)
+
+ Timber.d("Use case executed: ${useCase.javaClass.simpleName} with result: $useCaseResult")
+
+ if (useCaseResult.isSuccess && postSuccess) {
+ if (postSuccessWithData) {
+ flow.update { Event(UIResult.Success(useCaseResult.getDataOrNull())) }
+ } else {
+ flow.update { Event(UIResult.Success()) }
+ }
+ } else if (useCaseResult.isError) {
+ flow.update { Event(UIResult.Error(error = useCaseResult.getThrowableOrNull())) }
+ }
+ }
+ }
+
+ fun ViewModel.runUseCaseWithResult(
+ coroutineDispatcher: CoroutineDispatcher,
+ requiresConnection: Boolean = true,
+ showLoading: Boolean = false,
+ sharedFlow: MutableSharedFlow>,
+ useCase: BaseUseCaseWithResult,
+ useCaseParams: Params,
+ postSuccess: Boolean = true,
+ postSuccessWithData: Boolean = true
+ ) {
+ viewModelScope.launch(coroutineDispatcher) {
+ if (showLoading) {
+ sharedFlow.emit(UIResult.Loading())
+ }
+
+ // If use case requires connection and is not connected, it is not needed to execute use case
+ if (requiresConnection and !contextProvider.isConnected()) {
+ sharedFlow.emit(UIResult.Error(error = NoNetworkConnectionException()))
+ Timber.w("${useCase.javaClass.simpleName} will not be executed due to lack of network connection")
+ return@launch
+ }
+
+ val useCaseResult = useCase(useCaseParams)
+ Timber.d("Use case executed: ${useCase.javaClass.simpleName} with result: $useCaseResult")
+
+ if (useCaseResult.isSuccess && postSuccess) {
+ if (postSuccessWithData) {
+ sharedFlow.emit(UIResult.Success(useCaseResult.getDataOrNull()))
+ } else {
+ sharedFlow.emit(UIResult.Success())
+ }
+ } else if (useCaseResult.isError) {
+ sharedFlow.emit(UIResult.Error(error = useCaseResult.getThrowableOrNull()))
+ }
+ }
+ }
+
+ fun ViewModel.runUseCaseWithResultAndUseCachedData(
+ coroutineDispatcher: CoroutineDispatcher,
+ requiresConnection: Boolean = true,
+ cachedData: T?,
+ liveData: MediatorLiveData>>,
+ useCase: BaseUseCaseWithResult,
+ useCaseParams: Params
+ ) {
+ viewModelScope.launch(coroutineDispatcher) {
+ liveData.postValue(Event(UIResult.Loading(cachedData)))
+
+ // If use case requires connection and is not connected, it is not needed to execute use case
+ if (requiresConnection && !contextProvider.isConnected()) {
+ liveData.postValue(Event(UIResult.Error(error = NoNetworkConnectionException(), data = cachedData)))
+ Timber.w("${useCase.javaClass.simpleName} will not be executed due to lack of network connection")
+ return@launch
+ }
+
+ val useCaseResult = useCase(useCaseParams)
+
+ Timber.d("Use case executed: ${useCase.javaClass.simpleName} with result: $useCaseResult")
+
+ if (useCaseResult.isError) {
+ liveData.postValue(Event(UIResult.Error(error = useCaseResult.getThrowableOrNull(), data = cachedData)))
+ }
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/WorkInfoExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/WorkInfoExt.kt
new file mode 100644
index 000000000000..2a47c4f1521b
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/WorkInfoExt.kt
@@ -0,0 +1,32 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * Copyright (C) 2022 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 androidx.work.WorkInfo
+import com.owncloud.android.domain.extensions.isOneOf
+import com.owncloud.android.workers.DownloadFileWorker
+import com.owncloud.android.workers.UploadFileFromContentUriWorker
+import com.owncloud.android.workers.UploadFileFromFileSystemWorker
+
+fun WorkInfo.isUpload() =
+ tags.any { it.isOneOf(UploadFileFromContentUriWorker::class.java.name, UploadFileFromFileSystemWorker::class.java.name) }
+
+fun WorkInfo.isDownload() =
+ tags.any { it.isOneOf(DownloadFileWorker::class.java.name) }
diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/WorkManagerExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/WorkManagerExt.kt
new file mode 100644
index 000000000000..5e4d1fcb0a5d
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/WorkManagerExt.kt
@@ -0,0 +1,78 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2022 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.accounts.Account
+import androidx.lifecycle.LiveData
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.WorkQuery
+import com.owncloud.android.domain.files.model.OCFile
+import com.owncloud.android.usecases.transfers.TRANSFER_TAG_DOWNLOAD
+
+val PENDING_WORK_STATUS = listOf(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING, WorkInfo.State.BLOCKED)
+val FINISHED_WORK_STATUS = listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED)
+
+/**
+ * Get a list of WorkInfo that matches EVERY tag.
+ */
+fun WorkManager.getWorkInfoByTags(tags: List): List =
+ this.getWorkInfos(buildWorkQuery(tags = tags)).get().filter { it.tags.containsAll(tags) }
+
+/**
+ * Get a list of WorkInfo of running workers that matches EVERY tag.
+ */
+fun WorkManager.getRunningWorkInfosByTags(tags: List): List =
+ getWorkInfos(buildWorkQuery(tags = tags, states = listOf(WorkInfo.State.RUNNING))).get().filter { it.tags.containsAll(tags) }
+
+/**
+ * Get a list of WorkInfo of running workers as LiveData that matches at least one of the tags.
+ */
+fun WorkManager.getRunningWorkInfosLiveData(tags: List): LiveData> =
+ getWorkInfosLiveData(buildWorkQuery(tags = tags, states = listOf(WorkInfo.State.RUNNING)))
+
+/**
+ * Check if a download is pending. It could be enqueued, downloading or blocked.
+ * @param account - Owner of the file
+ * @param file - File to check whether it is pending.
+ *
+ * @return true if the download is pending.
+ */
+fun WorkManager.isDownloadPending(account: Account, file: OCFile): Boolean =
+ this.getWorkInfoByTags(getTagsForDownload(file, account.name)).any { !it.state.isFinished }
+
+fun getTagsForDownload(file: OCFile, accountName: String) =
+ listOf(TRANSFER_TAG_DOWNLOAD, file.id.toString(), accountName)
+
+/**
+ * Take care with WorkQueries. It will return workers that match at least ONE of the tags.
+ * If we perform a query with tags {"account@server", "2"}, [WorkManager.getWorkInfos] will return workers that
+ * contains at least ONE of the tags, but not both of them. If we want workers that match every tag,
+ * @see getWorkInfoByTags
+ */
+fun buildWorkQuery(
+ tags: List,
+ states: List = listOf(),
+): WorkQuery = WorkQuery.Builder
+ .fromTags(tags)
+ .addStates(states)
+ .build()
diff --git a/owncloudApp/src/main/java/com/owncloud/android/media/MediaControlView.java b/owncloudApp/src/main/java/com/owncloud/android/media/MediaControlView.java
new file mode 100644
index 000000000000..e459398a5ba5
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/media/MediaControlView.java
@@ -0,0 +1,374 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David A. Velasco
+ * @author Christian Schabesberger
+ * @author David González Verdugo
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.media;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.MediaController.MediaPlayerControl;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.owncloud.android.R;
+import com.owncloud.android.utils.PreferenceUtils;
+
+import java.util.Formatter;
+import java.util.Locale;
+
+/**
+ * View containing controls for a {@link MediaPlayer}.
+ *
+ * Holds buttons "play / pause", "rewind", "fast forward"
+ * and a progress slider.
+ *
+ * It synchronizes itself with the state of the
+ * {@link MediaPlayer}.
+ */
+
+public class MediaControlView extends FrameLayout implements OnClickListener, OnSeekBarChangeListener {
+
+ private MediaPlayerControl mPlayer;
+ private Context mContext;
+ private View mRoot;
+ private ProgressBar mProgress;
+ private TextView mEndTime, mCurrentTime;
+ private boolean mDragging;
+ private static final int SHOW_PROGRESS = 1;
+ StringBuilder mFormatBuilder;
+ Formatter mFormatter;
+ private ImageButton mPauseButton;
+ private ImageButton mFfwdButton;
+ private ImageButton mRewButton;
+
+ public MediaControlView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+
+ FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ );
+ LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mRoot = inflate.inflate(R.layout.media_control, null);
+
+ // Allow or disallow touches with other visible windows
+ mRoot.setFilterTouchesWhenObscured(
+ PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context)
+ );
+
+ initControllerView(mRoot);
+ addView(mRoot, frameParams);
+
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+ requestFocus();
+ }
+
+ public void setMediaPlayer(MediaPlayerControl player) {
+ mPlayer = player;
+ mHandler.sendEmptyMessage(SHOW_PROGRESS);
+ updatePausePlay();
+ }
+
+ private void initControllerView(View v) {
+ mPauseButton = v.findViewById(R.id.playBtn);
+ if (mPauseButton != null) {
+ mPauseButton.requestFocus();
+ mPauseButton.setOnClickListener(this);
+ }
+
+ mFfwdButton = v.findViewById(R.id.forwardBtn);
+ if (mFfwdButton != null) {
+ mFfwdButton.setOnClickListener(this);
+ }
+
+ mRewButton = v.findViewById(R.id.rewindBtn);
+ if (mRewButton != null) {
+ mRewButton.setOnClickListener(this);
+ }
+
+ mProgress = v.findViewById(R.id.progressBar);
+ if (mProgress != null) {
+ if (mProgress instanceof SeekBar) {
+ SeekBar seeker = (SeekBar) mProgress;
+ seeker.setOnSeekBarChangeListener(this);
+ }
+ mProgress.setMax(1000);
+ }
+
+ mEndTime = v.findViewById(R.id.totalTimeText);
+ mCurrentTime = v.findViewById(R.id.currentTimeText);
+ mFormatBuilder = new StringBuilder();
+ mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
+
+ }
+
+ /**
+ * Disable pause or seek buttons if the stream cannot be paused or seeked.
+ * This requires the control interface to be a MediaPlayerControlExt
+ */
+ private void disableUnsupportedButtons() {
+ try {
+ if (mPauseButton != null && !mPlayer.canPause()) {
+ mPauseButton.setEnabled(false);
+ }
+ if (mRewButton != null && !mPlayer.canSeekBackward()) {
+ mRewButton.setEnabled(false);
+ }
+ if (mFfwdButton != null && !mPlayer.canSeekForward()) {
+ mFfwdButton.setEnabled(false);
+ }
+ } catch (IncompatibleClassChangeError ex) {
+ // We were given an old version of the interface, that doesn't have
+ // the canPause/canSeekXYZ methods. This is OK, it just means we
+ // assume the media can be paused and seeked, and so we don't disable
+ // the buttons.
+ }
+ }
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ int pos;
+ switch (msg.what) {
+ case SHOW_PROGRESS:
+ pos = setProgress();
+ if (!mDragging) {
+ msg = obtainMessage(SHOW_PROGRESS);
+ sendMessageDelayed(msg, 1000 - (pos % 1000));
+ }
+ break;
+ }
+ }
+ };
+
+ private String stringForTime(int timeMs) {
+ int totalSeconds = timeMs / 1000;
+
+ int seconds = totalSeconds % 60;
+ int minutes = (totalSeconds / 60) % 60;
+ int hours = totalSeconds / 3600;
+
+ mFormatBuilder.setLength(0);
+ if (hours > 0) {
+ return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+ } else {
+ return mFormatter.format("%02d:%02d", minutes, seconds).toString();
+ }
+ }
+
+ private int setProgress() {
+ if (mPlayer == null || mDragging) {
+ return 0;
+ }
+ int position = mPlayer.getCurrentPosition();
+ int duration = mPlayer.getDuration();
+ if (mProgress != null) {
+ if (duration > 0) {
+ // use long to avoid overflow
+ long pos = 1000L * position / duration;
+ mProgress.setProgress((int) pos);
+ }
+ int percent = mPlayer.getBufferPercentage();
+ mProgress.setSecondaryProgress(percent * 10);
+ }
+
+ if (mEndTime != null) {
+ mEndTime.setText(stringForTime(duration));
+ }
+ if (mCurrentTime != null) {
+ mCurrentTime.setText(stringForTime(position));
+ }
+
+ return position;
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ final boolean uniqueDown = event.getRepeatCount() == 0
+ && event.getAction() == KeyEvent.ACTION_DOWN;
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+ || keyCode == KeyEvent.KEYCODE_SPACE) {
+ if (uniqueDown) {
+ doPauseResume();
+ //show(sDefaultTimeout);
+ if (mPauseButton != null) {
+ mPauseButton.requestFocus();
+ }
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+ if (uniqueDown && !mPlayer.isPlaying()) {
+ mPlayer.start();
+ updatePausePlay();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+ if (uniqueDown && mPlayer.isPlaying()) {
+ mPlayer.pause();
+ updatePausePlay();
+ }
+ return true;
+ }
+
+ return super.dispatchKeyEvent(event);
+ }
+
+ public void updatePausePlay() {
+ if (mRoot == null || mPauseButton == null) {
+ return;
+ }
+
+ if (mPlayer.isPlaying()) {
+ mPauseButton.setImageResource(android.R.drawable.ic_media_pause);
+ } else {
+ mPauseButton.setImageResource(android.R.drawable.ic_media_play);
+ }
+ }
+
+ private void doPauseResume() {
+ if (mPlayer.isPlaying()) {
+ mPlayer.pause();
+ } else {
+ mPlayer.start();
+ }
+ updatePausePlay();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (mPauseButton != null) {
+ mPauseButton.setEnabled(enabled);
+ }
+ if (mFfwdButton != null) {
+ mFfwdButton.setEnabled(enabled);
+ }
+ if (mRewButton != null) {
+ mRewButton.setEnabled(enabled);
+ }
+ if (mProgress != null) {
+ mProgress.setEnabled(enabled);
+ }
+ disableUnsupportedButtons();
+ super.setEnabled(enabled);
+ }
+
+ @Override
+ public void onClick(View v) {
+ int pos;
+ boolean playing = mPlayer.isPlaying();
+ int id = v.getId();
+
+ // Using an if / else if / else block because Java switch statements
+ // with resource ID comparisons are no longer supported in AGP 9
+ if (id == R.id.playBtn) {
+ doPauseResume();
+ } else if (id == R.id.rewindBtn) {
+ pos = mPlayer.getCurrentPosition();
+ pos -= 5000;
+ mPlayer.seekTo(pos);
+ if (!playing) {
+ mPlayer.pause(); // necessary in some 2.3.x devices
+ }
+ setProgress();
+ } else if (id == R.id.forwardBtn) {
+ pos = mPlayer.getCurrentPosition();
+ pos += 15000;
+ mPlayer.seekTo(pos);
+ if (!playing) {
+ mPlayer.pause(); // necessary in some 2.3.x devices
+ }
+ setProgress();
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (!fromUser) {
+ // We're not interested in programmatically generated changes to
+ // the progress bar's position.
+ return;
+ }
+
+ long duration = mPlayer.getDuration();
+ long newposition = (duration * progress) / 1000L;
+ mPlayer.seekTo((int) newposition);
+ if (mCurrentTime != null) {
+ mCurrentTime.setText(stringForTime((int) newposition));
+ }
+ }
+
+ /**
+ * Called in devices with touchpad when the user starts to adjust the
+ * position of the seekbar's thumb.
+ *
+ * Will be followed by several onProgressChanged notifications.
+ */
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ mDragging = true; // monitors the duration of dragging
+ mHandler.removeMessages(SHOW_PROGRESS); // grants no more updates with media player progress while dragging
+ }
+
+ /**
+ * Called in devices with touchpad when the user finishes the
+ * adjusting of the seekbar.
+ */
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mDragging = false;
+ setProgress();
+ updatePausePlay();
+ mHandler.sendEmptyMessage(SHOW_PROGRESS); // grants future updates with media player progress
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(MediaControlView.class.getName());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(MediaControlView.class.getName());
+ }
+
+}
diff --git a/src/com/owncloud/android/media/MediaService.java b/owncloudApp/src/main/java/com/owncloud/android/media/MediaService.java
similarity index 75%
rename from src/com/owncloud/android/media/MediaService.java
rename to owncloudApp/src/main/java/com/owncloud/android/media/MediaService.java
index 8be6c614ed98..33bf50e00209 100644
--- a/src/com/owncloud/android/media/MediaService.java
+++ b/owncloudApp/src/main/java/com/owncloud/android/media/MediaService.java
@@ -1,26 +1,28 @@
/**
- * ownCloud Android client application
- *
- * @author David A. Velasco
- * Copyright (C) 2016 ownCloud Inc.
- *
- * 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 .
+ * ownCloud Android client application
*
+ * @author David A. Velasco
+ * @author Christian Schabesberger
+ * Copyright (C) 2020 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.media;
import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.OnAccountsUpdateListener;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
@@ -33,37 +35,41 @@
import android.media.MediaPlayer.OnPreparedListener;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
+import android.os.FileObserver;
import android.os.IBinder;
import android.os.PowerManager;
-import android.support.v7.app.NotificationCompat;
import android.widget.Toast;
+import androidx.core.app.NotificationCompat;
import com.owncloud.android.R;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.presentation.authentication.AccountUtils;
+import com.owncloud.android.domain.files.model.OCFile;
import com.owncloud.android.ui.activity.FileActivity;
import com.owncloud.android.ui.activity.FileDisplayActivity;
+import com.owncloud.android.utils.NotificationUtils;
+import timber.log.Timber;
+import java.io.File;
import java.io.IOException;
+import static com.owncloud.android.utils.NotificationConstantsKt.MEDIA_SERVICE_NOTIFICATION_CHANNEL_ID;
/**
- * Service that handles media playback, both audio and video.
- *
+ * Service that handles media playback, both audio and video.
+ *
* Waits for Intents which signal the service to perform specific operations: Play, Pause,
* Rewind, etc.
*/
public class MediaService extends Service implements OnCompletionListener, OnPreparedListener,
- OnErrorListener, AudioManager.OnAudioFocusChangeListener {
-
- private static final String TAG = MediaService.class.getSimpleName();
+ OnErrorListener, AudioManager.OnAudioFocusChangeListener {
private static final String MY_PACKAGE = MediaService.class.getPackage() != null ?
MediaService.class.getPackage().getName() : "com.owncloud.android.media";
-
+
/// Intent actions that we are prepared to handle
public static final String ACTION_PLAY_FILE = MY_PACKAGE + ".action.PLAY_FILE";
public static final String ACTION_STOP_ALL = MY_PACKAGE + ".action.STOP_ALL";
+ public static final String ACTION_STOP_FILE = MY_PACKAGE + ".action.STOP_FILE";
/// Keys to add extras to the action
public static final String EXTRA_FILE = MY_PACKAGE + ".extra.FILE";
@@ -71,55 +77,54 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
public static String EXTRA_START_POSITION = MY_PACKAGE + ".extra.START_POSITION";
public static final String EXTRA_PLAY_ON_LOAD = MY_PACKAGE + ".extra.PLAY_ON_LOAD";
-
/** Error code for specific messages - see regular error codes at {@link MediaPlayer} */
public static final int OC_MEDIA_ERROR = 0;
/** Time To keep the control panel visible when the user does not use it */
public static final int MEDIA_CONTROL_SHORT_LIFE = 4000;
-
+
/** Time To keep the control panel visible when the user does not use it */
public static final int MEDIA_CONTROL_PERMANENT = 0;
-
+
/** Volume to set when audio focus is lost and ducking is allowed */
private static final float DUCK_VOLUME = 0.1f;
/** Media player instance */
private MediaPlayer mPlayer = null;
-
+
/** Reference to the system AudioManager */
private AudioManager mAudioManager = null;
-
+ /** Reference to the system AccountManager */
+ private AccountManager mAccountManager;
+
/** Values to indicate the state of the service */
enum State {
STOPPED,
- PREPARING,
- PLAYING,
- PAUSED
- };
-
+ PREPARING,
+ PLAYING,
+ PAUSED
+ }
/** Current state */
private State mState = State.STOPPED;
-
+
/** Possible focus values */
enum AudioFocus {
- NO_FOCUS,
- NO_FOCUS_CAN_DUCK,
- FOCUS
+ NO_FOCUS,
+ NO_FOCUS_CAN_DUCK,
+ FOCUS
}
-
+
/** Current focus state */
private AudioFocus mAudioFocus = AudioFocus.NO_FOCUS;
-
/** 'True' when the current song is streaming from the network */
private boolean mIsStreaming = false;
/** Wifi lock kept to prevents the device from shutting off the radio when streaming a file. */
private WifiLock mWifiLock;
-
+
private static final String MEDIA_WIFI_LOCK_TAG = MY_PACKAGE + ".WIFI_LOCK";
/** Notification to keep in the notification bar while a song is playing */
@@ -127,16 +132,19 @@ enum AudioFocus {
/** File being played */
private OCFile mFile;
-
+
+ /** Observer being notified if played file is deleted */
+ private MediaFileObserver mFileObserver = null;
+
/** Account holding the file being played */
private Account mAccount;
- /** Flag signaling if the audio should be played immediately when the file is prepared */
+ /** Flag signaling if the audio should be played immediately when the file is prepared */
protected boolean mPlayOnPrepared;
- /** Position, in miliseconds, where the audio should be started */
+ /** Position, in milliseconds, where the audio should be started */
private int mStartPosition;
-
+
/** Interface to access the service through binding */
private IBinder mBinder;
@@ -145,21 +153,21 @@ enum AudioFocus {
/** Notification builder to create notifications, new reuse way since Android 6 */
private NotificationCompat.Builder mNotificationBuilder;
-
+
/**
* Helper method to get an error message suitable to show to users for errors occurred in media playback,
- *
+ *
* @param context A context to access string resources.
* @param what See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
* @param extra See {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
- * @return Message suitable to users.
+ * @return Message suitable to users.
*/
public static String getMessageForMediaError(Context context, int what, int extra) {
int messageId;
-
+
if (what == OC_MEDIA_ERROR) {
messageId = extra;
-
+
} else if (extra == MediaPlayer.MEDIA_ERROR_UNSUPPORTED) {
/* Added in API level 17
Bitstream is conforming to the related coding standard or file spec,
@@ -171,17 +179,17 @@ public static String getMessageForMediaError(Context context, int what, int extr
} else if (extra == MediaPlayer.MEDIA_ERROR_IO) {
/* Added in API level 17
File or network related operation errors.
- Constant Value: -1004 (0xfffffc14)
+ Constant Value: -1004 (0xfffffc14)
*/
messageId = R.string.media_err_io;
-
+
} else if (extra == MediaPlayer.MEDIA_ERROR_MALFORMED) {
/* Added in API level 17
Bitstream is not conforming to the related coding standard or file spec.
- Constant Value: -1007 (0xfffffc11)
+ Constant Value: -1007 (0xfffffc11)
*/
messageId = R.string.media_err_malformed;
-
+
} else if (extra == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {
/* Added in API level 17
Some operation takes too long to complete, usually more than 3-5 seconds.
@@ -196,7 +204,7 @@ public static String getMessageForMediaError(Context context, int what, int extr
Constant Value: 200 (0x000000c8)
*/
messageId = R.string.media_err_invalid_progressive_playback;
-
+
} else {
/* MediaPlayer.MEDIA_ERROR_UNKNOWN
Added in API level 1
@@ -207,61 +215,79 @@ public static String getMessageForMediaError(Context context, int what, int extr
Added in API level 1
Media server died. In this case, the application must release the MediaPlayer
object and instantiate a new one.
- Constant Value: 100 (0x00000064)
+ Constant Value: 100 (0x00000064)
*/
messageId = R.string.media_err_unknown;
}
return context.getString(messageId);
}
-
-
/**
* Initialize a service instance
- *
+ *
* {@inheritDoc}
*/
@Override
public void onCreate() {
super.onCreate();
- Log_OC.d(TAG, "Creating ownCloud media service");
+ Timber.d("Creating ownCloud media service");
- mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).
+ mWifiLock = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE)).
createWifiLock(WifiManager.WIFI_MODE_FULL, MEDIA_WIFI_LOCK_TAG);
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+
mNotificationBuilder = new NotificationCompat.Builder(this);
mNotificationBuilder.setColor(this.getResources().getColor(R.color.primary));
mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
mBinder = new MediaServiceBinder(this);
+
+ // add AccountsUpdatedListener
+ mAccountManager = AccountManager.get(this);
+ mAccountManager.addOnAccountsUpdatedListener(new OnAccountsUpdateListener() {
+ @Override
+ public void onAccountsUpdated(Account[] accounts) {
+ // stop playback if account of the played media files was removed
+ if (mAccount != null && !AccountUtils.exists(mAccount.name, MediaService.this)) {
+ processStopRequest(false);
+ }
+ }
+ }, null, false);
}
-
/**
* Entry point for Intents requesting actions, sent here via startService.
- *
+ *
* {@inheritDoc}
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
- if (action.equals(ACTION_PLAY_FILE)) {
+ if (action.equals(ACTION_PLAY_FILE)) {
processPlayFileRequest(intent);
-
+
} else if (action.equals(ACTION_STOP_ALL)) {
processStopRequest(true);
+ } else if (action.equals(ACTION_STOP_FILE)) {
+ processStopFileRequest(intent);
}
return START_NOT_STICKY; // don't want it to restart in case it's killed.
}
+ private void processStopFileRequest(Intent intent) {
+ OCFile file = intent.getExtras().getParcelable(EXTRA_FILE);
+ if (file != null && file.equals(mFile)) {
+ processStopRequest(true);
+ }
+ }
/**
* Processes a request to play a media file received as a parameter
- *
- * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
- *
- * @param intent Intent received in the request with the data to identify the file to play.
+ *
+ * TODO If a new request is received when a file is being prepared, it is ignored. Is this what we want?
+ *
+ * @param intent Intent received in the request with the data to identify the file to play.
*/
private void processPlayFileRequest(Intent intent) {
if (mState != State.PREPARING) {
@@ -274,7 +300,6 @@ private void processPlayFileRequest(Intent intent) {
}
}
-
/**
* Processes a request to play a media file.
*/
@@ -286,7 +311,7 @@ protected void processPlayRequest() {
if (mState == State.STOPPED) {
// (re)start playback
playMedia();
-
+
} else if (mState == State.PAUSED) {
// continue playback
mState = State.PLAYING;
@@ -295,7 +320,6 @@ protected void processPlayRequest() {
}
}
-
/**
* Makes sure the media player exists and has been reset. This will create the media player
* if needed. reset the existing media player if one already exists.
@@ -311,14 +335,14 @@ protected void createMediaPlayerIfNeeded() {
mPlayer.setOnPreparedListener(this);
mPlayer.setOnCompletionListener(this);
mPlayer.setOnErrorListener(this);
-
+
} else {
mPlayer.reset();
}
}
/**
- * Processes a request to pause the current playback
+ * Processes a request to pause the current playback
*/
protected void processPauseRequest() {
if (mState == State.PLAYING) {
@@ -328,24 +352,23 @@ protected void processPauseRequest() {
// TODO polite audio focus, instead of keep it owned; or not?
}
}
-
-
+
/**
* Processes a request to stop the playback.
- *
+ *
* @param force When 'true', the playback is stopped no matter the value of mState
*/
protected void processStopRequest(boolean force) {
if (mState != State.PREPARING || force) {
mState = State.STOPPED;
mFile = null;
+ stopFileObserver();
mAccount = null;
releaseResources(true);
giveUpAudioFocus();
stopSelf(); // service is no longer necessary
}
}
-
/**
* Releases resources used by the service for playback. This includes the "foreground service"
@@ -370,76 +393,73 @@ protected void releaseResources(boolean releaseMediaPlayer) {
}
}
-
/**
* Fully releases the audio focus.
*/
private void giveUpAudioFocus() {
- if (mAudioFocus == AudioFocus.FOCUS
- && mAudioManager != null
- && AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.abandonAudioFocus(this)) {
-
+ if (mAudioFocus == AudioFocus.FOCUS
+ && mAudioManager != null
+ && AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.abandonAudioFocus(this)) {
+
mAudioFocus = AudioFocus.NO_FOCUS;
}
}
-
/**
- * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
+ * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it.
*/
protected void configAndStartMediaPlayer() {
if (mPlayer == null) {
throw new IllegalStateException("mPlayer is NULL");
}
-
+
if (mAudioFocus == AudioFocus.NO_FOCUS) {
if (mPlayer.isPlaying()) {
- mPlayer.pause(); // have to be polite; but mState is not changed, to resume when focus is received again
+ mPlayer.pause(); // have to be polite; but mState is not changed, to resume when focus is
+ // received again
}
-
- } else {
+
+ } else {
if (mAudioFocus == AudioFocus.NO_FOCUS_CAN_DUCK) {
mPlayer.setVolume(DUCK_VOLUME, DUCK_VOLUME);
-
+
} else {
mPlayer.setVolume(1.0f, 1.0f); // full volume
}
-
+
if (!mPlayer.isPlaying()) {
mPlayer.start();
}
}
}
-
/**
- * Requests the audio focus to the Audio Manager
+ * Requests the audio focus to the Audio Manager
*/
private void tryToGetAudioFocus() {
- if (mAudioFocus != AudioFocus.FOCUS
- && mAudioManager != null
- && (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.requestAudioFocus( this,
- AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN))
- ) {
+ if (mAudioFocus != AudioFocus.FOCUS
+ && mAudioManager != null
+ && (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == mAudioManager.requestAudioFocus(this,
+ AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN))
+ ) {
mAudioFocus = AudioFocus.FOCUS;
}
}
-
/**
- * Starts playing the current media file.
+ * Starts playing the current media file.
*/
protected void playMedia() {
mState = State.STOPPED;
releaseResources(false); // release everything except MediaPlayer
try {
- if (mFile == null) {
+ if (mFile == null) {
Toast.makeText(this, R.string.media_err_nothing_to_play, Toast.LENGTH_LONG).show();
processStopRequest(true);
return;
-
+
} else if (mAccount == null) {
Toast.makeText(this, R.string.media_err_not_in_owncloud, Toast.LENGTH_LONG).show();
processStopRequest(true);
@@ -449,6 +469,7 @@ protected void playMedia() {
createMediaPlayerIfNeeded();
mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
String url = mFile.getStoragePath();
+ updateFileObserver(url);
/* Streaming is not possible right now
if (url == null || url.length() <= 0) {
url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
@@ -456,7 +477,7 @@ protected void playMedia() {
mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
*/
mIsStreaming = false;
-
+
mPlayer.setDataSource(url);
mState = State.PREPARING;
@@ -466,42 +487,54 @@ protected void playMedia() {
mPlayer.prepareAsync();
// prevent the Wifi from going to sleep when streaming
- if (mIsStreaming) {
+ if (mIsStreaming) {
mWifiLock.acquire();
} else if (mWifiLock.isHeld()) {
mWifiLock.release();
}
-
+
} catch (SecurityException e) {
- Log_OC.e(TAG, "SecurityException playing " + mAccount.name + mFile.getRemotePath(), e);
+ Timber.e(e, "SecurityException playing " + mAccount.name + mFile.getRemotePath());
Toast.makeText(this, String.format(getString(R.string.media_err_security_ex), mFile.getFileName()),
Toast.LENGTH_LONG).show();
processStopRequest(true);
-
+
} catch (IOException e) {
- Log_OC.e(TAG, "IOException playing " + mAccount.name + mFile.getRemotePath(), e);
+ Timber.e(e, "IOException playing " + mAccount.name + mFile.getRemotePath());
Toast.makeText(this, String.format(getString(R.string.media_err_io_ex), mFile.getFileName()),
Toast.LENGTH_LONG).show();
processStopRequest(true);
-
+
} catch (IllegalStateException e) {
- Log_OC.e(TAG, "IllegalStateException " + mAccount.name + mFile.getRemotePath(), e);
+ Timber.e(e, "IllegalStateException " + mAccount.name + mFile.getRemotePath());
Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()),
Toast.LENGTH_LONG).show();
processStopRequest(true);
-
+
} catch (IllegalArgumentException e) {
- Log_OC.e(TAG, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath(), e);
+ Timber.e(e, "IllegalArgumentException " + mAccount.name + mFile.getRemotePath());
Toast.makeText(this, String.format(getString(R.string.media_err_unexpected), mFile.getFileName()),
Toast.LENGTH_LONG).show();
processStopRequest(true);
}
}
-
+ private void updateFileObserver(String url) {
+ stopFileObserver();
+ mFileObserver = new MediaFileObserver(url);
+ mFileObserver.startWatching();
+ }
+
+ private void stopFileObserver() {
+ if (mFileObserver != null) {
+ mFileObserver.stopWatching();
+ }
+ }
+
/** Called when media player is done playing current song. */
public void onCompletion(MediaPlayer player) {
- Toast.makeText(this, String.format(getString(R.string.media_event_done, mFile.getFileName())), Toast.LENGTH_LONG).show();
+ Toast.makeText(this, String.format(getString(R.string.media_event_done), mFile.getFileName()),
+ Toast.LENGTH_LONG).show();
if (mMediaController != null) {
// somebody is still bound to the service
player.seekTo(0);
@@ -511,12 +544,10 @@ public void onCompletion(MediaPlayer player) {
// nobody is bound
processStopRequest(true);
}
- return;
}
-
- /**
- * Called when media player is done preparing.
+ /**
+ * Called when media player is done preparing.
*
* Time to start.
*/
@@ -531,14 +562,13 @@ public void onPrepared(MediaPlayer player) {
if (!mPlayOnPrepared) {
processPauseRequest();
}
-
+
if (mMediaController != null) {
mMediaController.updatePausePlay();
}
}
-
- /**
+ /**
* Updates the status notification
*/
private void updateNotification(String content) {
@@ -553,22 +583,22 @@ private void updateNotification(String content) {
mNotificationBuilder.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
(int) System.currentTimeMillis(),
showDetailsIntent,
- PendingIntent.FLAG_UPDATE_CURRENT));
+ NotificationUtils.pendingIntentFlags));
mNotificationBuilder.setWhen(System.currentTimeMillis());
mNotificationBuilder.setTicker(ticker);
mNotificationBuilder.setContentTitle(ticker);
mNotificationBuilder.setContentText(content);
+ mNotificationBuilder.setChannelId(MEDIA_SERVICE_NOTIFICATION_CHANNEL_ID);
mNotificationManager.notify(R.string.media_notif_ticker, mNotificationBuilder.build());
}
-
/**
* Configures the service as a foreground service.
- *
+ *
* The system will avoid finishing the service as much as possible when resources as low.
- *
- * A notification must be created to keep the user aware of the existance of the service.
+ *
+ * A notification must be created to keep the user aware of the existence of the service.
*/
private void setUpAsForeground(String content) {
String ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name));
@@ -579,7 +609,7 @@ private void setUpAsForeground(String content) {
//mNotification.tickerText = text;
mNotificationBuilder.setWhen(System.currentTimeMillis());
mNotificationBuilder.setOngoing(true);
-
+
/// includes a pending intent in the notification showing the details view of the file
Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class);
showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, mFile);
@@ -588,31 +618,32 @@ private void setUpAsForeground(String content) {
mNotificationBuilder.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
(int) System.currentTimeMillis(),
showDetailsIntent,
- PendingIntent.FLAG_UPDATE_CURRENT));
+ NotificationUtils.pendingIntentFlags));
mNotificationBuilder.setContentTitle(ticker);
mNotificationBuilder.setContentText(content);
+ mNotificationBuilder.setChannelId(MEDIA_SERVICE_NOTIFICATION_CHANNEL_ID);
startForeground(R.string.media_notif_ticker, mNotificationBuilder.build());
}
/**
- * Called when there's an error playing media.
- *
+ * Called when there's an error playing media.
+ *
* Warns the user about the error and resets the media player.
*/
public boolean onError(MediaPlayer mp, int what, int extra) {
- Log_OC.e(TAG, "Error in audio playback, what = " + what + ", extra = " + extra);
-
+ Timber.e("Error in audio playback, what = " + what + ", extra = " + extra);
+
String message = getMessageForMediaError(this, what, extra);
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
-
+
processStopRequest(true);
- return true;
+ return true;
}
/**
* Called by the system when another app tries to play some sound.
- *
+ *
* {@inheritDoc}
*/
@Override
@@ -621,23 +652,25 @@ public void onAudioFocusChange(int focusChange) {
// focus gain; check AudioManager.AUDIOFOCUS_* values
mAudioFocus = AudioFocus.FOCUS;
// restart media player with new focus settings
- if (mState == State.PLAYING)
+ if (mState == State.PLAYING) {
configAndStartMediaPlayer();
-
+ }
+
} else if (focusChange < 0) {
// focus loss; check AudioManager.AUDIOFOCUS_* values
boolean canDuck = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK == focusChange;
- mAudioFocus = canDuck ? AudioFocus.NO_FOCUS_CAN_DUCK : AudioFocus.NO_FOCUS;
- // start/restart/pause media player with new focus settings
- if (mPlayer != null && mPlayer.isPlaying())
- configAndStartMediaPlayer();
+ mAudioFocus = canDuck ? AudioFocus.NO_FOCUS_CAN_DUCK : AudioFocus.NO_FOCUS;
+ // start/restart/pause media player with new focus settings
+ if (mPlayer != null && mPlayer.isPlaying()) {
+ configAndStartMediaPlayer();
+ }
}
-
+
}
/**
* Called when the service is finished for final clean-up.
- *
+ *
* {@inheritDoc}
*/
@Override
@@ -648,64 +681,59 @@ public void onDestroy() {
stopForeground(true);
super.onDestroy();
}
-
/**
- * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the MediaService.
+ * Provides a binder object that clients can use to perform operations on the MediaPlayer managed by the
+ * MediaService.
*/
@Override
public IBinder onBind(Intent arg) {
return mBinder;
}
-
-
+
/**
* Called when ALL the bound clients were onbound.
- *
+ *
* The service is destroyed if playback stopped or paused
*/
@Override
public boolean onUnbind(Intent intent) {
- if (mState == State.PAUSED || mState == State.STOPPED) {
+ if (mState == State.PAUSED || mState == State.STOPPED) {
processStopRequest(false);
}
return false; // not accepting rebinding (default behaviour)
}
-
/**
* Accesses the current MediaPlayer instance in the service.
- *
- * To be handled carefully. Visibility is protected to be accessed only
- *
+ *
+ * To be handled carefully. Visibility is protected to be accessed only
+ *
* @return Current MediaPlayer instance handled by MediaService.
*/
protected MediaPlayer getPlayer() {
return mPlayer;
}
-
/**
* Accesses the current OCFile loaded in the service.
- *
- * @return The current OCFile loaded in the service.
+ *
+ * @return The current OCFile loaded in the service.
*/
protected OCFile getCurrentFile() {
return mFile;
}
-
/**
* Accesses the current {@link State} of the MediaService.
- *
- * @return The current {@link State} of the MediaService.
+ *
+ * @return The current {@link State} of the MediaService.
*/
protected State getState() {
return mState;
}
-
- protected void setMediaContoller(MediaControlView mediaController) {
+ protected void setMediaController(MediaControlView mediaController) {
mMediaController = mediaController;
}
@@ -713,4 +741,23 @@ protected MediaControlView getMediaController() {
return mMediaController;
}
+ /**
+ * Observer monitoring the media file currently played and stopping the playback in case
+ * that it's deleted or moved away from its storage location.
+ */
+ private class MediaFileObserver extends FileObserver {
+
+ public MediaFileObserver(String path) {
+ super((new File(path)).getParent(), FileObserver.DELETE | FileObserver.MOVED_FROM);
+ }
+
+ @Override
+ public void onEvent(int event, String path) {
+ if (path != null && path.equals(mFile.getFileName())) {
+ Timber.d("Media file deleted or moved out of sight, stopping playback");
+ processStopRequest(true);
+ }
+ }
+ }
+
}
diff --git a/src/com/owncloud/android/media/MediaServiceBinder.java b/owncloudApp/src/main/java/com/owncloud/android/media/MediaServiceBinder.java
similarity index 77%
rename from src/com/owncloud/android/media/MediaServiceBinder.java
rename to owncloudApp/src/main/java/com/owncloud/android/media/MediaServiceBinder.java
index e7d58aaee7ba..bcd0c215420e 100644
--- a/src/com/owncloud/android/media/MediaServiceBinder.java
+++ b/owncloudApp/src/main/java/com/owncloud/android/media/MediaServiceBinder.java
@@ -1,54 +1,50 @@
/**
- * ownCloud Android client application
- *
- * @author David A. Velasco
- * Copyright (C) 2016 ownCloud Inc.
- *
- * 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 .
+ * ownCloud Android client application
*
+ * @author David A. Velasco
+ * Copyright (C) 2016 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.media;
-
import android.accounts.Account;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Binder;
import android.widget.MediaController;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.domain.files.model.OCFile;
import com.owncloud.android.media.MediaService.State;
-
+import timber.log.Timber;
/**
* Binder allowing client components to perform operations on on the MediaPlayer managed by a MediaService instance.
- *
+ *
* Provides the operations of {@link MediaController.MediaPlayerControl}, and an extra method to check if
* an {@link OCFile} instance is handled by the MediaService.
*/
public class MediaServiceBinder extends Binder implements MediaController.MediaPlayerControl {
- private static final String TAG = MediaServiceBinder.class.getSimpleName();
/**
* {@link MediaService} instance to access with the binder
*/
private MediaService mService = null;
-
+
/**
* Public constructor
- *
+ *
* @param service A {@link MediaService} instance to access with the binder
*/
public MediaServiceBinder(MediaService service) {
@@ -57,13 +53,11 @@ public MediaServiceBinder(MediaService service) {
}
mService = service;
}
-
-
+
public boolean isPlaying(OCFile mFile) {
- return (mFile != null && mFile.equals(mService.getCurrentFile()));
+ return (mFile != null && mFile.equals(mService.getCurrentFile()));
}
-
@Override
public boolean canPause() {
return true;
@@ -110,10 +104,9 @@ public int getDuration() {
}
}
-
/**
* Reports if the MediaService is playing a file or not.
- *
+ *
* Considers that the file is being played when it is in preparation because the expected
* client of this method is a {@link MediaController} , and we do not want that the 'play'
* button is shown when the file is being prepared by the MediaService.
@@ -124,16 +117,15 @@ public boolean isPlaying() {
return (currentState == State.PLAYING || (currentState == State.PREPARING && mService.mPlayOnPrepared));
}
-
@Override
public void pause() {
- Log_OC.d(TAG, "Pausing through binder...");
+ Timber.d("Pausing through binder...");
mService.processPauseRequest();
}
@Override
public void seekTo(int pos) {
- Log_OC.d(TAG, "Seeking " + pos + " through binder...");
+ Timber.d("Seeking " + pos + " through binder...");
MediaPlayer currentPlayer = mService.getPlayer();
MediaService.State currentState = mService.getState();
if (currentPlayer != null && currentState != State.PREPARING && currentState != State.STOPPED) {
@@ -143,12 +135,12 @@ public void seekTo(int pos) {
@Override
public void start() {
- Log_OC.d(TAG, "Starting through binder...");
+ Timber.d("Starting through binder...");
mService.processPlayRequest(); // this will finish the service if there is no file preloaded to play
}
-
+
public void start(Account account, OCFile file, boolean playImmediately, int position) {
- Log_OC.d(TAG, "Loading and starting through binder...");
+ Timber.d("Loading and starting through binder...");
Intent i = new Intent(mService, MediaService.class);
i.putExtra(MediaService.EXTRA_ACCOUNT, account);
i.putExtra(MediaService.EXTRA_FILE, file);
@@ -158,16 +150,15 @@ public void start(Account account, OCFile file, boolean playImmediately, int pos
mService.startService(i);
}
-
public void registerMediaController(MediaControlView mediaController) {
- mService.setMediaContoller(mediaController);
+ mService.setMediaController(mediaController);
}
-
+
public void unregisterMediaController(MediaControlView mediaController) {
if (mediaController != null && mediaController == mService.getMediaController()) {
- mService.setMediaContoller(null);
+ mService.setMediaController(null);
}
-
+
}
public boolean isInPlaybackState() {
@@ -175,7 +166,6 @@ public boolean isInPlaybackState() {
return (currentState == MediaService.State.PLAYING || currentState == MediaService.State.PAUSED);
}
-
@Override
public int getAudioSessionId() {
return 1; // not really used
diff --git a/owncloudApp/src/main/java/com/owncloud/android/operations/CheckCurrentCredentialsOperation.java b/owncloudApp/src/main/java/com/owncloud/android/operations/CheckCurrentCredentialsOperation.java
new file mode 100644
index 000000000000..6bb5d4cd1a5d
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/operations/CheckCurrentCredentialsOperation.java
@@ -0,0 +1,61 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David A. Velasco
+ * @author Christian Schabesberger
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.operations;
+
+import android.accounts.Account;
+
+import com.owncloud.android.domain.files.model.OCFile;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.resources.files.CheckPathExistenceRemoteOperation;
+import com.owncloud.android.operations.common.SyncOperation;
+
+/**
+ * Checks validity of currently stored credentials for a given OC account
+ */
+public class CheckCurrentCredentialsOperation extends SyncOperation {
+
+ private Account mAccount;
+
+ public CheckCurrentCredentialsOperation(Account account) {
+ if (account == null) {
+ throw new IllegalArgumentException("NULL account");
+ }
+ mAccount = account;
+ }
+
+ @Override
+ protected RemoteOperationResult run(OwnCloudClient client) {
+ if (!getStorageManager().getAccount().name.equals(mAccount.name)) {
+ return new RemoteOperationResult<>(new IllegalStateException(
+ "Account to validate is not the account connected to!"));
+ } else {
+ RemoteOperation checkPathExistenceOperation = new CheckPathExistenceRemoteOperation(OCFile.ROOT_PATH, false, null);
+ final RemoteOperationResult existenceCheckResult = checkPathExistenceOperation.execute(client);
+ final RemoteOperationResult result
+ = new RemoteOperationResult<>(existenceCheckResult.getCode());
+ result.setData(mAccount);
+ return result;
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/operations/SyncProfileOperation.kt b/owncloudApp/src/main/java/com/owncloud/android/operations/SyncProfileOperation.kt
new file mode 100644
index 000000000000..7a74eef9dd9c
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/operations/SyncProfileOperation.kt
@@ -0,0 +1,97 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David A. Velasco
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.operations
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import com.owncloud.android.MainApp.Companion.appContext
+import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
+import com.owncloud.android.domain.user.usecases.GetUserAvatarAsyncUseCase
+import com.owncloud.android.domain.user.usecases.GetUserInfoAsyncUseCase
+import com.owncloud.android.domain.user.usecases.RefreshUserQuotaFromServerAsyncUseCase
+import com.owncloud.android.lib.common.accounts.AccountUtils
+import com.owncloud.android.presentation.avatar.AvatarManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import timber.log.Timber
+
+/**
+ * Performs the Profile synchronization for account step by step.
+ *
+ * First: Synchronize user info
+ * Second: Synchronize user quota
+ * Third: Synchronize user avatar
+ *
+ * If one step fails, next one is not performed since it may fail too.
+ */
+class SyncProfileOperation(
+ private val account: Account
+) : KoinComponent {
+ fun syncUserProfile() {
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+ val getUserInfoAsyncUseCase: GetUserInfoAsyncUseCase by inject()
+ val userInfoResult = getUserInfoAsyncUseCase(GetUserInfoAsyncUseCase.Params(account.name))
+ userInfoResult.getDataOrNull()?.let { userInfo ->
+ Timber.d("User info synchronized for account ${account.name}")
+
+ AccountManager.get(appContext).run {
+ setUserData(account, AccountUtils.Constants.KEY_DISPLAY_NAME, userInfo.displayName)
+ setUserData(account, AccountUtils.Constants.KEY_ID, userInfo.id)
+ }
+
+ val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase by inject()
+ val storedCapabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(account.name))
+
+ storedCapabilities?.let {
+ if (!it.isSpacesAllowed()) {
+ val refreshUserQuotaFromServerAsyncUseCase: RefreshUserQuotaFromServerAsyncUseCase by inject()
+ val userQuotaResult =
+ refreshUserQuotaFromServerAsyncUseCase(
+ RefreshUserQuotaFromServerAsyncUseCase.Params(
+ account.name
+ )
+ )
+ userQuotaResult.getDataOrNull()?.let {
+ Timber.d("User quota synchronized for oC10 account ${account.name}")
+ }
+ }
+ }
+ val shouldFetchAvatar = storedCapabilities?.isFetchingAvatarAllowed() ?: true
+ if (shouldFetchAvatar) {
+ val getUserAvatarAsyncUseCase: GetUserAvatarAsyncUseCase by inject()
+ val userAvatarResult = getUserAvatarAsyncUseCase(GetUserAvatarAsyncUseCase.Params(account.name))
+ AvatarManager().handleAvatarUseCaseResult(account, userAvatarResult)
+ if (userAvatarResult.isSuccess) {
+ Timber.d("Avatar synchronized for account ${account.name}")
+ }
+ } else {
+ Timber.d("Avatar for this account: ${account.name} won't be synced due to capabilities ")
+ }
+ } ?: Timber.d("User profile was not synchronized")
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "Exception while getting user profile")
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/operations/common/SyncOperation.java b/owncloudApp/src/main/java/com/owncloud/android/operations/common/SyncOperation.java
new file mode 100644
index 000000000000..ea7ce3394afe
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/operations/common/SyncOperation.java
@@ -0,0 +1,137 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David A. Velasco
+ * @author Christian Schabesberger
+ * Copyright (C) 2020 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.operations.common;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+
+/**
+ * Operation which execution involves both interactions with an ownCloud server and
+ * with local data in the device.
+ *
+ * Provides methods to execute the operation both synchronously or asynchronously.
+ */
+public abstract class SyncOperation extends RemoteOperation {
+
+ private FileDataStorageManager mStorageManager;
+
+ public FileDataStorageManager getStorageManager() {
+ return mStorageManager;
+ }
+
+ /**
+ * Synchronously executes the operation on the received ownCloud account.
+ *
+ * Do not call this method from the main thread.
+ *
+ * This method should be used whenever an ownCloud account is available, instead of
+ * {@link #execute(OwnCloudClient, com.owncloud.android.datamodel.FileDataStorageManager)}.
+ *
+ * @param storageManager
+ * @param context Android context for the component calling the method.
+ * @return Result of the operation.
+ */
+ public RemoteOperationResult execute(FileDataStorageManager storageManager, Context context) {
+ if (storageManager == null) {
+ throw new IllegalArgumentException("Trying to execute a sync operation with a " +
+ "NULL storage manager");
+ }
+ if (storageManager.getAccount() == null) {
+ throw new IllegalArgumentException("Trying to execute a sync operation with a " +
+ "storage manager for a NULL account");
+ }
+ mStorageManager = storageManager;
+ return super.execute(mStorageManager.getAccount(), context);
+ }
+
+ /**
+ * Synchronously executes the remote operation
+ *
+ * Do not call this method from the main thread.
+ *
+ * @param client Client object to reach an ownCloud server during the execution of the operation.
+ * @param storageManager Instance of local repository to sync with remote.
+ * @return Result of the operation.
+ */
+ public RemoteOperationResult execute(OwnCloudClient client,
+ FileDataStorageManager storageManager) {
+ if (storageManager == null) {
+ throw new IllegalArgumentException("Trying to execute a sync operation with a " +
+ "NULL storage manager");
+ }
+ mStorageManager = storageManager;
+ return super.execute(client);
+ }
+
+ /**
+ * Asynchronously executes the remote operation
+ *
+ * This method should be used whenever an ownCloud account is available,
+ * instead of {@link #execute(OwnCloudClient, OnRemoteOperationListener, Handler))}.
+ *
+ * @param storageManager Instance of local repository to sync with remote.
+ * @param context Android context for the component calling the method.
+ * @param listener Listener to be notified about the execution of the operation.
+ * @param listenerHandler Handler associated to the thread where the methods of the listener
+ * objects must be called.
+ * @return Thread were the remote operation is executed.
+ */
+ public Thread execute(FileDataStorageManager storageManager, Context context,
+ OnRemoteOperationListener listener, Handler listenerHandler) {
+ if (storageManager == null) {
+ throw new IllegalArgumentException("Trying to execute a sync operation " +
+ "with a NULL storage manager");
+ }
+ if (storageManager.getAccount() == null) {
+ throw new IllegalArgumentException("Trying to execute a sync operation with a" +
+ " storage manager for a NULL account");
+ }
+ mStorageManager = storageManager;
+ return super.execute(mStorageManager.getAccount(), context, listener, listenerHandler);
+ }
+
+ /**
+ * Asynchronously executes the remote operation
+ *
+ * @param client Client object to reach an ownCloud server during the
+ * execution of the operation.
+ * @param storageManager Instance of local repository to sync with remote.
+ * @param listener Listener to be notified about the execution of the operation.
+ * @param listenerHandler Handler associated to the thread where the methods of
+ * the listener objects must be called.
+ * @return Thread were the remote operation is executed.
+ */
+ public Thread execute(OwnCloudClient client, FileDataStorageManager storageManager,
+ OnRemoteOperationListener listener, Handler listenerHandler) {
+ if (storageManager == null) {
+ throw new IllegalArgumentException("Trying to execute a sync operation " +
+ "with a NULL storage manager");
+ }
+ mStorageManager = storageManager;
+ return super.execute(client, listener, listenerHandler);
+ }
+}
\ No newline at end of file
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/ManageAccountsAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/ManageAccountsAdapter.kt
new file mode 100644
index 000000000000..12512e10edfc
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/ManageAccountsAdapter.kt
@@ -0,0 +1,238 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Javier Rodríguez Pérez
+ * @author Aitor Ballesteros Pavón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2024 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.accounts
+
+import android.accounts.Account
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ProgressBar
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.R
+import com.owncloud.android.databinding.AccountActionBinding
+import com.owncloud.android.databinding.AccountItemBinding
+import com.owncloud.android.domain.user.model.UserQuotaState
+import com.owncloud.android.domain.user.model.UserQuota
+import com.owncloud.android.extensions.setAccessibilityRole
+import com.owncloud.android.lib.common.OwnCloudAccount
+import com.owncloud.android.presentation.authentication.AccountUtils
+import com.owncloud.android.presentation.avatar.AvatarUtils
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.PreferenceUtils
+import timber.log.Timber
+
+class ManageAccountsAdapter(
+ private val accountListener: AccountAdapterListener,
+) : RecyclerView.Adapter() {
+
+ private var accountItemsList = listOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+
+ return if (viewType == AccountManagementRecyclerItemViewType.ITEM_VIEW_ACCOUNT.ordinal) {
+ val view = inflater.inflate(R.layout.account_item, parent, false)
+ view.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(parent.context)
+ view.setAccessibilityRole(className = Button::class.java)
+ AccountManagementViewHolder(view)
+ } else {
+ val view = inflater.inflate(R.layout.account_action, parent, false)
+ view.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(parent.context)
+ NewAccountViewHolder(view)
+ }
+
+ }
+
+ fun submitAccountList(accountList: List) {
+ accountItemsList = accountList
+ notifyDataSetChanged()
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ when (holder) {
+ is AccountManagementViewHolder -> {
+ val accountItem = getItem(position) as AccountRecyclerItem.AccountItem
+ val account: Account = accountItem.account
+ val accountAvatarRadiusDimension = holder.itemView.context.resources.getDimension(R.dimen.list_item_avatar_icon_radius)
+
+ try {
+ val oca = OwnCloudAccount(account, holder.itemView.context)
+ holder.binding.name.text = oca.displayName
+ } catch (e: Exception) {
+ Timber.w(
+ e, "Account not found right after being read :\\ ; using account name instead of display name"
+ )
+ holder.binding.name.text = AccountUtils.getUsernameOfAccount(account.name)
+ }
+ holder.binding.name.tag = account.name
+
+ val accountText = DisplayUtils.convertIdn(account.name, false)
+ holder.binding.account.text = accountText
+
+ updateQuota(
+ quotaText = holder.binding.manageAccountsQuotaText,
+ quotaBar = holder.binding.manageAccountsQuotaBar,
+ userQuota = accountItem.userQuota,
+ context = holder.itemView.context
+ )
+
+
+ try {
+ val avatarUtils = AvatarUtils()
+ avatarUtils.loadAvatarForAccount(
+ holder.binding.icon,
+ account,
+ true,
+ accountAvatarRadiusDimension
+ )
+ } catch (e: java.lang.Exception) {
+ Timber.e(e, "Error calculating RGB value for account list item.")
+ // use user icon as a fallback
+ holder.binding.icon.setImageResource(R.drawable.ic_user)
+ }
+
+ if (AccountUtils.getCurrentOwnCloudAccount(holder.itemView.context).name == account.name) {
+ holder.binding.ticker.visibility = View.VISIBLE
+ } else {
+ holder.binding.ticker.visibility = View.INVISIBLE
+ }
+
+ /// bind listener to clean local storage from account
+ holder.binding.cleanAccountLocalStorageButton.apply {
+ setImageResource(R.drawable.ic_clean_account)
+ setOnClickListener { accountListener.cleanAccountLocalStorage(account) }
+ contentDescription = holder.itemView.context.getString(R.string.content_description_clean_account_storage, accountText)
+ }
+ /// bind listener to remove account
+ holder.binding.removeButton.apply {
+ setImageResource(R.drawable.ic_action_delete_grey)
+ setOnClickListener { accountListener.removeAccount(account) }
+ contentDescription = holder.itemView.context.getString(R.string.content_description_remove_account, accountText)
+ }
+
+ ///bind listener to switchAccount
+ holder.itemView.apply {
+ setOnClickListener { accountListener.switchAccount(position) }
+ }
+ }
+ is NewAccountViewHolder -> {
+ holder.binding.icon.setImageResource(R.drawable.ic_account_plus)
+ holder.binding.name.setText(R.string.prefs_add_account)
+ holder.binding.name.setAccessibilityRole(className = Button::class.java)
+
+ // bind action listener
+ holder.binding.constraintLayoutAction.setOnClickListener {
+ accountListener.createAccount()
+ }
+ }
+ }
+
+ }
+
+ override fun getItemCount(): Int = accountItemsList.size
+
+ fun getItem(position: Int) = accountItemsList[position]
+
+ private fun updateQuota(quotaText: TextView, quotaBar: ProgressBar, userQuota: UserQuota, context: Context) {
+ when {
+ userQuota.available == -4L -> { // Light users (oCIS)
+ quotaBar.visibility = View.GONE
+ quotaText.text = context.getString(R.string.drawer_unavailable_used_storage)
+ }
+
+ userQuota.available < 0 -> { // Pending, unknown or unlimited free storage. The progress bar is hid
+ quotaBar.visibility = View.GONE
+ quotaText.text = DisplayUtils.bytesToHumanReadable(userQuota.used, context, false)
+ }
+
+ userQuota.available == 0L -> { // Exceeded storage. Value over 100%
+ quotaBar.apply {
+ progress = 100
+ progressTintList = ColorStateList.valueOf(resources.getColor(R.color.quota_exceeded))
+ }
+ if (userQuota.state == UserQuotaState.EXCEEDED) { // oCIS
+ quotaText.text = String.format(
+ context.getString(R.string.manage_accounts_quota),
+ DisplayUtils.bytesToHumanReadable(userQuota.used, context, false),
+ DisplayUtils.bytesToHumanReadable(userQuota.getTotal(), context, false)
+ )
+ } else { // oC10
+ quotaText.text = context.getString(R.string.drawer_exceeded_quota)
+ }
+ }
+
+ else -> { // Limited storage. Value under 100%
+ if (userQuota.state == UserQuotaState.CRITICAL || userQuota.state == UserQuotaState.EXCEEDED ||
+ userQuota.state == UserQuotaState.NEARING) { // Value over 75%
+ quotaBar.apply {
+ progressTintList = ColorStateList.valueOf(resources.getColor(R.color.quota_exceeded))
+ }
+ }
+ quotaBar.progress = userQuota.getRelative().toInt()
+ quotaText.text = String.format(
+ context.getString(R.string.manage_accounts_quota),
+ DisplayUtils.bytesToHumanReadable(userQuota.used, context, false),
+ DisplayUtils.bytesToHumanReadable(userQuota.getTotal(), context, false)
+ )
+ }
+ }
+ }
+
+ sealed class AccountRecyclerItem {
+ data class AccountItem(val account: Account, val userQuota: UserQuota) : AccountRecyclerItem()
+ object NewAccount : AccountRecyclerItem()
+ }
+
+ class AccountManagementViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val binding = AccountItemBinding.bind(itemView)
+ }
+
+ class NewAccountViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val binding = AccountActionBinding.bind(itemView)
+ }
+
+ override fun getItemViewType(position: Int): Int =
+ when (getItem(position)) {
+ is AccountRecyclerItem.AccountItem -> AccountManagementRecyclerItemViewType.ITEM_VIEW_ACCOUNT.ordinal
+ is AccountRecyclerItem.NewAccount -> AccountManagementRecyclerItemViewType.ITEM_VIEW_ADD.ordinal
+ }
+
+ enum class AccountManagementRecyclerItemViewType {
+ ITEM_VIEW_ACCOUNT, ITEM_VIEW_ADD
+ }
+
+ /**
+ * Listener interface for Activities using the [ManageAccountsAdapter]
+ */
+ interface AccountAdapterListener {
+ fun removeAccount(account: Account)
+ fun cleanAccountLocalStorage(account: Account)
+ fun createAccount()
+ fun switchAccount(position: Int)
+ }
+
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/ManageAccountsDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/ManageAccountsDialogFragment.kt
new file mode 100644
index 000000000000..ca542e7c26bf
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/ManageAccountsDialogFragment.kt
@@ -0,0 +1,288 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Jorge Aguado Recio
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2024 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.accounts
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.accounts.AccountManagerFuture
+import android.accounts.OperationCanceledException
+import android.app.AlertDialog
+import android.app.Dialog
+import android.content.Intent
+import android.os.Bundle
+import android.view.ContextThemeWrapper
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.ProgressBar
+import androidx.core.view.isVisible
+import androidx.fragment.app.DialogFragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.MainApp
+import com.owncloud.android.R
+import com.owncloud.android.domain.user.model.UserQuota
+import com.owncloud.android.extensions.avoidScreenshotsIfNeeded
+import com.owncloud.android.extensions.collectLatestLifecycleFlow
+import com.owncloud.android.extensions.showErrorInSnackbar
+import com.owncloud.android.presentation.authentication.AccountUtils
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.ui.activity.FileActivity
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.activity.ToolbarActivity
+import com.owncloud.android.utils.PreferenceUtils
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import timber.log.Timber
+
+class ManageAccountsDialogFragment : DialogFragment(), ManageAccountsAdapter.AccountAdapterListener {
+
+ private lateinit var accountListAdapter: ManageAccountsAdapter
+ private var currentAccount: Account? = null
+
+ private lateinit var dialogView: View
+ private lateinit var parentActivity: ToolbarActivity
+ private lateinit var recyclerView: RecyclerView
+
+ private val manageAccountsViewModel: ManageAccountsViewModel by viewModel()
+
+ override fun onStart() {
+ super.onStart()
+
+ parentActivity = requireActivity() as ToolbarActivity
+ currentAccount = requireArguments().getParcelable(KEY_CURRENT_ACCOUNT)
+
+ subscribeToViewModels()
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = AlertDialog.Builder(ContextThemeWrapper(requireContext(), R.style.Theme_AppCompat_Dialog_Alert))
+ val inflater = this.layoutInflater
+ dialogView = inflater.inflate(R.layout.manage_accounts_dialog, null)
+ builder.setView(dialogView)
+
+ recyclerView = dialogView.findViewById(R.id.account_list_recycler_view)
+ recyclerView.apply {
+ filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(requireContext())
+ layoutManager = LinearLayoutManager(requireContext())
+ }
+
+ accountListAdapter = ManageAccountsAdapter(this)
+
+ val closeButton = dialogView.findViewById(R.id.cross)
+ closeButton.setOnClickListener {
+ dismiss()
+ }
+
+ val dialog = builder.create()
+ dialog.window?.setBackgroundDrawableResource(R.color.transparent)
+
+ return dialog
+ }
+
+ override fun removeAccount(account: Account) {
+ dialogView.isVisible = false
+ val hasAccountAttachedCameraUploads = manageAccountsViewModel.hasAutomaticUploadsAttached(account.name)
+ val dialog = AlertDialog.Builder(requireContext())
+ .setMessage(getString(
+ if (hasAccountAttachedCameraUploads) R.string.confirmation_remove_account_alert_camera_uploads
+ else R.string.confirmation_remove_account_alert, account.name)
+ )
+ .setPositiveButton(getString(R.string.common_yes)) { _, _ ->
+ val accountManager = AccountManager.get(MainApp.appContext)
+ accountManager.removeAccount(account, null, null)
+ if (manageAccountsViewModel.getLoggedAccounts().size > 1) {
+ dialogView.isVisible = true
+ }
+ }
+ .setNegativeButton(getString(R.string.common_no)) { _, _ ->
+ dialogView.isVisible = true
+ }
+ .setOnDismissListener {
+ dialogView.isVisible = true
+ }
+ .create()
+ dialog.avoidScreenshotsIfNeeded()
+ dialog.show()
+ }
+
+ override fun cleanAccountLocalStorage(account: Account) {
+ dialogView.isVisible = false
+ val dialog = AlertDialog.Builder(requireContext())
+ .setTitle(getString(R.string.clean_data_account_title))
+ .setIcon(R.drawable.ic_warning)
+ .setMessage(getString(R.string.clean_data_account_message, account.name))
+ .setPositiveButton(getString(R.string.clean_data_account_button_yes)) { _, _ ->
+ dialogView.isVisible = true
+ manageAccountsViewModel.cleanAccountLocalStorage(account.name)
+ }
+ .setNegativeButton(R.string.drawer_close) { _, _ ->
+ dialogView.isVisible = true
+ }
+ .setOnDismissListener {
+ dialogView.isVisible = true
+ }
+ .create()
+ dialog.avoidScreenshotsIfNeeded()
+ dialog.show()
+ }
+
+ override fun createAccount() {
+ val accountManager = AccountManager.get(MainApp.appContext)
+ accountManager.addAccount(
+ MainApp.accountType,
+ null,
+ null,
+ null,
+ parentActivity,
+ { future: AccountManagerFuture? ->
+ if (future != null) {
+ try {
+ val result = future.result
+ val name = result.getString(AccountManager.KEY_ACCOUNT_NAME)
+ val newAccount = AccountUtils.getOwnCloudAccountByName(parentActivity.applicationContext, name)
+ changeToAccountContext(newAccount)
+ } catch (e: OperationCanceledException) {
+ Timber.e(e, "Account creation canceled")
+ } catch (e: Exception) {
+ Timber.e(e, "Account creation finished in exception")
+ }
+ }
+ },
+ null
+ )
+ }
+
+ /**
+ * Switch current account to that contained in the received position of the list adapter.
+ *
+ * @param position A position of the account adapter containing an account.
+ */
+ override fun switchAccount(position: Int) {
+ val clickedAccount: Account = (accountListAdapter.getItem(position) as ManageAccountsAdapter.AccountRecyclerItem.AccountItem).account
+ if (currentAccount?.name == clickedAccount.name) {
+ // current account selected, just go back
+ dismiss()
+ } else {
+ // restart list of files with new account
+ parentActivity.showLoadingDialog(R.string.common_loading)
+ dismiss()
+ changeToAccountContext(clickedAccount)
+ }
+ }
+
+ private fun changeToAccountContext(account: Account) {
+ AccountUtils.setCurrentOwnCloudAccount(
+ parentActivity.applicationContext,
+ account.name
+ )
+ parentActivity.account = account
+ // Refresh dependencies to be used in selected account
+ MainApp.initDependencyInjection()
+ val i = Intent(
+ parentActivity.applicationContext,
+ FileDisplayActivity::class.java
+ )
+ i.putExtra(FileActivity.EXTRA_ACCOUNT, account)
+ i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ parentActivity.startActivity(i)
+ }
+
+ private fun subscribeToViewModels() {
+ collectLatestLifecycleFlow(manageAccountsViewModel.cleanAccountLocalStorageFlow) { event ->
+ event?.peekContent()?.let { uiResult ->
+ when (uiResult) {
+ is UIResult.Loading -> {
+ parentActivity.showLoadingDialog(R.string.common_loading)
+ dialogView.isVisible = false
+ }
+ is UIResult.Success -> {
+ parentActivity.dismissLoadingDialog()
+ dialogView.isVisible = true
+ }
+ is UIResult.Error -> {
+ parentActivity.dismissLoadingDialog()
+ showErrorInSnackbar(R.string.common_error_unknown, uiResult.error)
+ Timber.e(uiResult.error)
+ }
+ }
+ }
+ }
+
+ collectLatestLifecycleFlow(manageAccountsViewModel.userQuotas) { listUserQuotas ->
+ if (listUserQuotas.isNotEmpty()) {
+ manageAccountsViewModel.getCurrentAccount()?.let {
+ if (currentAccount != it) {
+ parentActivity.showLoadingDialog(R.string.common_loading)
+ dismiss()
+ changeToAccountContext(it)
+ }
+ }
+ // hide the progress bar and show manage accounts dialog
+ val indeterminateProgressBar = dialogView.findViewById(R.id.indeterminate_progress_bar)
+ indeterminateProgressBar.visibility = View.GONE
+ val manageAccountsLayout = dialogView.findViewById(R.id.manage_accounts_layout)
+ manageAccountsLayout.visibility = View.VISIBLE
+
+ accountListAdapter.submitAccountList(accountList = getAccountListItems(listUserQuotas))
+
+ recyclerView.adapter = accountListAdapter
+ } else {
+ createAccount()
+ }
+ }
+ }
+
+ /**
+ * creates the account list items list including the add-account action in case multiaccount_support is enabled.
+ *
+ * @return list of account list items
+ */
+ private fun getAccountListItems(userQuotasList: List): List {
+ val accountList = manageAccountsViewModel.getLoggedAccounts()
+ val provisionalAccountList = mutableListOf()
+ accountList.forEach { account ->
+ val userQuota = userQuotasList.firstOrNull { userQuota -> userQuota.accountName == account.name }
+ if (userQuota != null) {
+ provisionalAccountList.add(ManageAccountsAdapter.AccountRecyclerItem.AccountItem(account, userQuota))
+ }
+ }
+
+ // Add Create Account item at the end of account list if multi-account is enabled
+ if (resources.getBoolean(R.bool.multiaccount_support) || accountList.isEmpty()) {
+ provisionalAccountList.add(ManageAccountsAdapter.AccountRecyclerItem.NewAccount)
+ }
+ return provisionalAccountList
+ }
+
+ companion object {
+ const val MANAGE_ACCOUNTS_DIALOG = "MANAGE_ACCOUNTS_DIALOG"
+ const val KEY_CURRENT_ACCOUNT = "KEY_CURRENT_ACCOUNT"
+
+ fun newInstance(currentAccount: Account?): ManageAccountsDialogFragment {
+ val args = Bundle().apply {
+ putParcelable(KEY_CURRENT_ACCOUNT, currentAccount)
+ }
+ return ManageAccountsDialogFragment().apply { arguments = args }
+ }
+ }
+
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/ManageAccountsViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/ManageAccountsViewModel.kt
new file mode 100644
index 000000000000..adc1cb2089d9
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/accounts/ManageAccountsViewModel.kt
@@ -0,0 +1,100 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Javier Rodríguez Pérez
+ * @author Aitor Ballesteros Pavón
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2025 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.accounts
+
+import android.accounts.Account
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.owncloud.android.domain.user.model.UserQuota
+import com.owncloud.android.domain.automaticuploads.model.AutomaticUploadsConfiguration
+import com.owncloud.android.domain.automaticuploads.usecases.GetAutomaticUploadsConfigurationUseCase
+import com.owncloud.android.domain.user.usecases.GetStoredQuotaUseCase
+import com.owncloud.android.domain.user.usecases.GetUserQuotasAsStreamUseCase
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.providers.AccountProvider
+import com.owncloud.android.providers.CoroutinesDispatcherProvider
+import com.owncloud.android.usecases.files.RemoveLocalFilesForAccountUseCase
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+class ManageAccountsViewModel(
+ private val accountProvider: AccountProvider,
+ private val removeLocalFilesForAccountUseCase: RemoveLocalFilesForAccountUseCase,
+ private val getAutomaticUploadsConfigurationUseCase: GetAutomaticUploadsConfigurationUseCase,
+ private val getStoredQuotaUseCase: GetStoredQuotaUseCase,
+ getUserQuotasAsStreamUseCase: GetUserQuotasAsStreamUseCase,
+ private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider,
+) : ViewModel() {
+
+ private val _cleanAccountLocalStorageFlow = MutableStateFlow>?>(null)
+ val cleanAccountLocalStorageFlow: StateFlow>?> = _cleanAccountLocalStorageFlow
+
+ val userQuotas: Flow> = getUserQuotasAsStreamUseCase(Unit)
+
+ private var automaticUploadsConfiguration: AutomaticUploadsConfiguration? = null
+
+ init {
+ viewModelScope.launch(coroutinesDispatcherProvider.io) {
+ automaticUploadsConfiguration = getAutomaticUploadsConfigurationUseCase(Unit).getDataOrNull()
+ }
+ }
+
+ fun getLoggedAccounts(): Array =
+ accountProvider.getLoggedAccounts()
+
+ fun getCurrentAccount(): Account? =
+ accountProvider.getCurrentOwnCloudAccount()
+
+ fun cleanAccountLocalStorage(accountName: String) {
+ runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ showLoading = true,
+ flow = _cleanAccountLocalStorageFlow,
+ useCase = removeLocalFilesForAccountUseCase,
+ useCaseParams = RemoveLocalFilesForAccountUseCase.Params(accountName),
+ )
+ }
+
+ fun hasAutomaticUploadsAttached(accountName: String): Boolean =
+ accountName == automaticUploadsConfiguration?.pictureUploadsConfiguration?.accountName ||
+ accountName == automaticUploadsConfiguration?.videoUploadsConfiguration?.accountName
+
+ fun checkUserLight(accountName: String): Boolean = runBlocking(CoroutinesDispatcherProvider().io) {
+ val quota = withContext(CoroutinesDispatcherProvider().io) {
+ getStoredQuotaUseCase(GetStoredQuotaUseCase.Params(accountName))
+ }
+ quota.getDataOrNull()?.available == -4L
+ }
+
+ fun hasEnoughQuota(accountName: String): Boolean = runBlocking(CoroutinesDispatcherProvider().io) {
+ val quota = getStoredQuotaUseCase(GetStoredQuotaUseCase.Params(accountName))
+ quota.getDataOrNull()?.available != 0L
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AccountAuthenticator.java b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AccountAuthenticator.java
new file mode 100644
index 000000000000..de0062b2a6ec
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AccountAuthenticator.java
@@ -0,0 +1,472 @@
+/*
+ * ownCloud Android client application
+ *
+ * @author David A. Velasco
+ * @author Christian Schabesberger
+ * @author David González Verdugo
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2025 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.authentication;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.accounts.NetworkErrorException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.presentation.authentication.oauth.OAuthUtils;
+import com.owncloud.android.domain.UseCaseResult;
+import com.owncloud.android.domain.authentication.oauth.OIDCDiscoveryUseCase;
+import com.owncloud.android.domain.authentication.oauth.RequestTokenUseCase;
+import com.owncloud.android.domain.authentication.oauth.model.OIDCServerConfiguration;
+import com.owncloud.android.domain.authentication.oauth.model.TokenRequest;
+import com.owncloud.android.domain.authentication.oauth.model.TokenResponse;
+import com.owncloud.android.lib.common.accounts.AccountTypeUtils;
+import com.owncloud.android.lib.common.accounts.AccountUtils;
+import kotlin.Lazy;
+import org.jetbrains.annotations.NotNull;
+import timber.log.Timber;
+
+import java.io.File;
+
+import static com.owncloud.android.data.authentication.AuthenticationConstantsKt.KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE;
+import static com.owncloud.android.data.authentication.AuthenticationConstantsKt.KEY_CLIENT_REGISTRATION_CLIENT_ID;
+import static com.owncloud.android.data.authentication.AuthenticationConstantsKt.KEY_CLIENT_REGISTRATION_CLIENT_SECRET;
+import static com.owncloud.android.data.authentication.AuthenticationConstantsKt.KEY_OAUTH2_REFRESH_TOKEN;
+import static com.owncloud.android.presentation.authentication.AuthenticatorConstants.KEY_AUTH_TOKEN_TYPE;
+import static org.koin.java.KoinJavaComponent.inject;
+
+/**
+ * Authenticator for ownCloud accounts.
+ *
+ * Controller class accessed from the system AccountManager, providing integration of ownCloud accounts with the
+ * Android system.
+ */
+public class AccountAuthenticator extends AbstractAccountAuthenticator {
+
+ /**
+ * Is used by android system to assign accounts to authenticators. Should be
+ * used by application and all extensions.
+ */
+ private static final String KEY_REQUIRED_FEATURES = "requiredFeatures";
+ public static final String KEY_ACCOUNT = "account";
+
+ private Context mContext;
+
+ AccountAuthenticator(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Bundle addAccount(AccountAuthenticatorResponse response,
+ String accountType, String authTokenType,
+ String[] requiredFeatures, Bundle options) {
+ Timber.i("Adding account with type " + accountType + " and auth token " + authTokenType);
+
+ final Bundle bundle = new Bundle();
+
+ AccountManager accountManager = AccountManager.get(mContext);
+ Account[] accounts = accountManager.getAccountsByType(MainApp.Companion.getAccountType());
+
+ if (mContext.getResources().getBoolean(R.bool.multiaccount_support) || accounts.length < 1) {
+ try {
+ validateAccountType(accountType);
+ } catch (AuthenticatorException e) {
+ Timber.e(e, "Failed to validate account type %s", accountType);
+ return e.getFailureBundle();
+ }
+
+ final Intent intent = new Intent(mContext, LoginActivity.class);
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
+ intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType);
+ intent.putExtra(KEY_REQUIRED_FEATURES, requiredFeatures);
+ intent.putExtra(AuthenticatorConstants.EXTRA_ACTION, AuthenticatorConstants.ACTION_CREATE);
+
+ setIntentFlags(intent);
+
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+ }
+
+ return bundle;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Bundle confirmCredentials(AccountAuthenticatorResponse response,
+ Account account, Bundle options) {
+ try {
+ validateAccountType(account.type);
+ } catch (AuthenticatorException e) {
+ Timber.e(e, "Failed to validate account type %s", account.type);
+ return e.getFailureBundle();
+ }
+ Intent intent = new Intent(mContext, LoginActivity.class);
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
+ response);
+ intent.putExtra(KEY_ACCOUNT, account);
+
+ setIntentFlags(intent);
+
+ Bundle resultBundle = new Bundle();
+ resultBundle.putParcelable(AccountManager.KEY_INTENT, intent);
+ return resultBundle;
+ }
+
+ @Override
+ public Bundle editProperties(AccountAuthenticatorResponse response,
+ String accountType) {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse,
+ Account account, String authTokenType, Bundle options) {
+ /// validate parameters
+ try {
+ validateAccountType(account.type);
+ validateAuthTokenType(authTokenType);
+ } catch (AuthenticatorException e) {
+ Timber.e(e, "Failed to validate account type %s", account.type);
+ return e.getFailureBundle();
+ }
+
+ String accessToken;
+
+ /// check if required token is stored
+ final AccountManager accountManager = AccountManager.get(mContext);
+ if (authTokenType.equals(AccountTypeUtils.getAuthTokenTypePass(MainApp.Companion.getAccountType()))) {
+ // Basic
+ accessToken = accountManager.getPassword(account);
+ } else {
+ // OAuth, gets an auth token from the AccountManager's cache. If no auth token is cached for
+ // this account, null will be returned
+ accessToken = accountManager.peekAuthToken(account, authTokenType);
+ if (accessToken == null && canBeRefreshed(authTokenType) && clientSecretIsValid(accountManager, account)) {
+ accessToken = refreshToken(account, authTokenType, accountManager);
+ }
+ }
+
+ if (accessToken != null) {
+ final Bundle result = new Bundle();
+ result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+ result.putString(AccountManager.KEY_ACCOUNT_TYPE, MainApp.Companion.getAccountType());
+ result.putString(AccountManager.KEY_AUTHTOKEN, accessToken);
+ return result;
+ }
+
+ /// if not stored, return Intent to access the LoginActivity and UPDATE the token for the account
+ return prepareBundleToAccessLoginActivity(accountAuthenticatorResponse, account, authTokenType, options);
+ }
+
+ /**
+ * Check if the client has expired or not.
+ * If the client has expired, we can not refresh the token and user needs to re-authenticate.
+ *
+ * @return true if the client is still valid
+ */
+ private boolean clientSecretIsValid(AccountManager accountManager, Account account) {
+ String clientSecretExpiration = accountManager.getUserData(account,
+ KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE);
+
+ Timber.d("Client secret expiration [" + clientSecretExpiration + "]");
+ if (clientSecretExpiration == null) {
+ return true;
+ }
+
+ long currentTimeStamp = System.currentTimeMillis() / 1000L;
+ int clientSecretExpirationInt = Integer.parseInt(clientSecretExpiration);
+ boolean clientSecretIsValid = clientSecretExpirationInt == 0 || clientSecretExpirationInt > currentTimeStamp;
+
+ Timber.d("Current time [" + currentTimeStamp + "]");
+ Timber.d("Client is valid [" + clientSecretIsValid + "]");
+
+ return clientSecretIsValid;
+ }
+
+ @Override
+ public String getAuthTokenLabel(String authTokenType) {
+ return null;
+ }
+
+ @Override
+ public Bundle hasFeatures(AccountAuthenticatorResponse response,
+ Account account, String[] features) {
+ final Bundle result = new Bundle();
+ result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
+ return result;
+ }
+
+ @Override
+ public Bundle updateCredentials(AccountAuthenticatorResponse response,
+ Account account, String authTokenType, Bundle options) {
+ final Intent intent = new Intent(mContext, LoginActivity.class);
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
+ response);
+ intent.putExtra(KEY_ACCOUNT, account);
+ intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType);
+ setIntentFlags(intent);
+
+ final Bundle bundle = new Bundle();
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+ return bundle;
+ }
+
+ @Override
+ public Bundle getAccountRemovalAllowed(
+ AccountAuthenticatorResponse response, Account account)
+ throws NetworkErrorException {
+ return super.getAccountRemovalAllowed(response, account);
+ }
+
+ private void setIntentFlags(Intent intent) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ intent.addFlags(Intent.FLAG_FROM_BACKGROUND);
+ }
+
+ private void validateAccountType(String type)
+ throws UnsupportedAccountTypeException {
+ if (!type.equals(MainApp.Companion.getAccountType())) {
+ throw new UnsupportedAccountTypeException();
+ }
+ }
+
+ private void validateAuthTokenType(String authTokenType)
+ throws UnsupportedAuthTokenTypeException {
+ if (!authTokenType.equals(MainApp.Companion.getAuthTokenType()) &&
+ !authTokenType.equals(AccountTypeUtils.getAuthTokenTypePass(MainApp.Companion.getAccountType())) &&
+ !authTokenType.equals(AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.Companion.getAccountType())) &&
+ !authTokenType.equals(AccountTypeUtils.getAuthTokenTypeRefreshToken(MainApp.Companion.getAccountType()))
+ ) {
+ throw new UnsupportedAuthTokenTypeException();
+ }
+ }
+
+ public static class AuthenticatorException extends Exception {
+ private static final long serialVersionUID = 1L;
+ private Bundle mFailureBundle;
+
+ AuthenticatorException(int code, String errorMsg) {
+ mFailureBundle = new Bundle();
+ mFailureBundle.putInt(AccountManager.KEY_ERROR_CODE, code);
+ mFailureBundle
+ .putString(AccountManager.KEY_ERROR_MESSAGE, errorMsg);
+ }
+
+ Bundle getFailureBundle() {
+ return mFailureBundle;
+ }
+ }
+
+ public static class UnsupportedAccountTypeException extends
+ AuthenticatorException {
+ private static final long serialVersionUID = 1L;
+
+ UnsupportedAccountTypeException() {
+ super(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION,
+ "Unsupported account type");
+ }
+ }
+
+ public static class UnsupportedAuthTokenTypeException extends
+ AuthenticatorException {
+ private static final long serialVersionUID = 1L;
+
+ UnsupportedAuthTokenTypeException() {
+ super(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION,
+ "Unsupported auth token type");
+ }
+ }
+
+ private boolean canBeRefreshed(String authTokenType) {
+ return (authTokenType.equals(AccountTypeUtils.getAuthTokenTypeAccessToken(MainApp.Companion.
+ getAccountType())));
+ }
+
+ private String refreshToken(
+ Account account,
+ String authTokenType,
+ AccountManager accountManager
+ ) {
+
+ // Prepare everything to perform the token request
+ String refreshToken = accountManager.getUserData(account, KEY_OAUTH2_REFRESH_TOKEN);
+
+ if (refreshToken == null || refreshToken.isEmpty()) {
+ Timber.w("No refresh token stored for silent renewal of access token");
+ return null;
+ }
+
+ Timber.d("Ready to exchange for new tokens. Account: [ %s ], Refresh token: [ %s ]", account.name,
+ refreshToken);
+
+ String baseUrl = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL);
+
+ // OIDC Discovery
+ @NotNull Lazy oidcDiscoveryUseCase = inject(OIDCDiscoveryUseCase.class);
+ OIDCDiscoveryUseCase.Params oidcDiscoveryUseCaseParams = new OIDCDiscoveryUseCase.Params(baseUrl);
+ UseCaseResult oidcServerConfigurationUseCaseResult =
+ oidcDiscoveryUseCase.getValue().invoke(oidcDiscoveryUseCaseParams);
+
+ String tokenEndpoint;
+
+ String clientId = accountManager.getUserData(account, KEY_CLIENT_REGISTRATION_CLIENT_ID);
+ String clientSecret = accountManager.getUserData(account, KEY_CLIENT_REGISTRATION_CLIENT_SECRET);
+
+ String clientIdForRequest = null;
+ String clientSecretForRequest = null;
+ Boolean useAuthorizationHeader = true;
+
+ boolean isKiteworksServer = Boolean.parseBoolean(accountManager.getUserData(account, AccountUtils.Constants.KEY_IS_KITEWORKS_SERVER));
+
+ if (clientId == null) {
+ Timber.d("Client Id not stored. Let's use the hardcoded one");
+ if (isKiteworksServer) {
+ clientId = mContext.getString(R.string.kiteworks_client_id);
+ } else {
+ clientId = mContext.getString(R.string.oauth2_client_id);
+ }
+ }
+ if (clientSecret == null) {
+ Timber.d("Client Secret not stored. Let's use the hardcoded one");
+ if (isKiteworksServer) {
+ clientSecret = mContext.getString(R.string.kiteworks_client_secret);
+ } else {
+ clientSecret = mContext.getString(R.string.oauth2_client_secret);
+ }
+ }
+
+ if (oidcServerConfigurationUseCaseResult.isSuccess()) {
+ Timber.d("OIDC Discovery success. Server discovery info: [ %s ]",
+ oidcServerConfigurationUseCaseResult.getDataOrNull());
+
+ // Use token endpoint retrieved from oidc discovery
+ tokenEndpoint = oidcServerConfigurationUseCaseResult.getDataOrNull().getTokenEndpoint();
+
+ if (oidcServerConfigurationUseCaseResult.getDataOrNull() != null &&
+ oidcServerConfigurationUseCaseResult.getDataOrNull().isTokenEndpointAuthMethodSupportedClientSecretPost()) {
+ clientIdForRequest = clientId;
+ clientSecretForRequest = clientSecret;
+ useAuthorizationHeader = false;
+ }
+ } else {
+ Timber.d("OIDC Discovery failed. Server discovery info: [ %s ]",
+ oidcServerConfigurationUseCaseResult.getThrowableOrNull().toString());
+
+ tokenEndpoint = baseUrl + File.separator + mContext.getString(R.string.oauth2_url_endpoint_access);
+ }
+
+ String clientAuth = OAuthUtils.Companion.getClientAuth(clientSecret, clientId);
+
+ String scope;
+ if (isKiteworksServer) {
+ scope = mContext.getResources().getString(R.string.kiteworks_openid_scope);
+ } else {
+ scope = mContext.getResources().getString(R.string.oauth2_openid_scope);
+ }
+
+ TokenRequest oauthTokenRequest = new TokenRequest.RefreshToken(
+ baseUrl,
+ tokenEndpoint,
+ clientAuth,
+ scope,
+ clientIdForRequest,
+ clientSecretForRequest,
+ useAuthorizationHeader,
+ refreshToken
+ );
+
+ // Token exchange
+ @NotNull Lazy requestTokenUseCase = inject(RequestTokenUseCase.class);
+ RequestTokenUseCase.Params requestTokenParams = new RequestTokenUseCase.Params(oauthTokenRequest);
+ UseCaseResult tokenResponseResult = requestTokenUseCase.getValue().invoke(requestTokenParams);
+
+ TokenResponse safeTokenResponse = tokenResponseResult.getDataOrNull();
+ if (safeTokenResponse != null) {
+ return handleSuccessfulRefreshToken(safeTokenResponse,
+ account, authTokenType, accountManager, refreshToken);
+ } else {
+ Timber.e(tokenResponseResult.getThrowableOrNull(), "OAuth request to refresh access token failed. Preparing to access Login Activity");
+ return null;
+ }
+ }
+
+ private String handleSuccessfulRefreshToken(
+ TokenResponse tokenResponse,
+ Account account,
+ String authTokenType,
+ AccountManager accountManager,
+ String oldRefreshToken
+ ) {
+ String newAccessToken = tokenResponse.getAccessToken();
+ accountManager.setAuthToken(account, authTokenType, newAccessToken);
+
+ String refreshTokenToUseFromNowOn;
+ if (tokenResponse.getRefreshToken() != null) {
+ refreshTokenToUseFromNowOn = tokenResponse.getRefreshToken();
+ } else {
+ refreshTokenToUseFromNowOn = oldRefreshToken;
+ }
+ accountManager.setUserData(account, KEY_OAUTH2_REFRESH_TOKEN, refreshTokenToUseFromNowOn);
+
+ Timber.d("Token refreshed successfully. New access token: [ %s ]. New refresh token: [ %s ]",
+ newAccessToken, refreshTokenToUseFromNowOn);
+
+ return newAccessToken;
+ }
+
+ /**
+ * Return bundle with intent to access LoginActivity and UPDATE the token for the account
+ */
+ private Bundle prepareBundleToAccessLoginActivity(
+ AccountAuthenticatorResponse accountAuthenticatorResponse,
+ Account account,
+ String authTokenType,
+ Bundle options
+ ) {
+ final Intent intent = new Intent(mContext, LoginActivity.class);
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
+ accountAuthenticatorResponse);
+ intent.putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType);
+ intent.putExtra(AuthenticatorConstants.EXTRA_ACCOUNT, account);
+ intent.putExtra(
+ AuthenticatorConstants.EXTRA_ACTION,
+ AuthenticatorConstants.ACTION_UPDATE_EXPIRED_TOKEN
+ );
+
+ final Bundle bundle = new Bundle();
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+ return bundle;
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AccountAuthenticatorService.java b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AccountAuthenticatorService.java
new file mode 100644
index 000000000000..8e2b6f5bb6ca
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AccountAuthenticatorService.java
@@ -0,0 +1,41 @@
+/**
+ * ownCloud Android client application
+ *
+ * Copyright (C) 2011 Bartek Przybylski
+ * Copyright (C) 2016 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.authentication;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class AccountAuthenticatorService extends Service {
+
+ private AccountAuthenticator mAuthenticator;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mAuthenticator = new AccountAuthenticator(this);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mAuthenticator.getIBinder();
+ }
+
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AccountUtils.java b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AccountUtils.java
new file mode 100644
index 000000000000..95a038dd72a3
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AccountUtils.java
@@ -0,0 +1,249 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Aitor Ballesteros Pavón
+ *
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2024 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.authentication;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+
+import com.owncloud.android.MainApp;
+import com.owncloud.android.domain.capabilities.model.OCCapability;
+import com.owncloud.android.lib.common.accounts.AccountUtils.Constants;
+import timber.log.Timber;
+
+import java.util.Locale;
+
+import static com.owncloud.android.data.authentication.AuthenticationConstantsKt.KEY_FEATURE_ALLOWED;
+import static com.owncloud.android.data.authentication.AuthenticationConstantsKt.KEY_FEATURE_SPACES;
+import static com.owncloud.android.data.authentication.AuthenticationConstantsKt.SELECTED_ACCOUNT;
+import static com.owncloud.android.lib.common.accounts.AccountUtils.Constants.OAUTH_SUPPORTED_TRUE;
+
+public class AccountUtils {
+
+ private static final int ACCOUNT_VERSION = 1;
+
+ /**
+ * Can be used to get the currently selected ownCloud {@link Account} in the
+ * application preferences.
+ *
+ * @param context The current application {@link Context}
+ * @return The ownCloud {@link Account} currently saved in preferences, or the first
+ * {@link Account} available, if valid (still registered in the system as ownCloud
+ * account). If none is available and valid, returns null.
+ */
+ public static Account getCurrentOwnCloudAccount(Context context) {
+ Account[] ocAccounts = getAccounts(context);
+ Account defaultAccount = null;
+
+ SharedPreferences appPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ String accountName = appPreferences.getString(SELECTED_ACCOUNT, null);
+
+ // account validation: the saved account MUST be in the list of ownCloud Accounts known by the AccountManager
+ if (accountName != null) {
+ for (Account account : ocAccounts) {
+ if (account.name.equals(accountName)) {
+ defaultAccount = account;
+ break;
+ }
+ }
+ }
+
+ if (defaultAccount == null && ocAccounts.length != 0) {
+ // take first account as fallback
+ defaultAccount = ocAccounts[0];
+ }
+
+ return defaultAccount;
+ }
+
+ public static Account[] getAccounts(Context context) {
+ AccountManager accountManager = AccountManager.get(context);
+ return accountManager.getAccountsByType(MainApp.Companion.getAccountType());
+ }
+
+ public static void deleteAccounts(Context context) {
+ AccountManager accountManager = AccountManager.get(context);
+ Account[] accounts = getAccounts(context);
+ for (Account account : accounts) {
+ accountManager.removeAccount(account, null, null, null);
+ }
+ }
+
+ public static boolean exists(String accountName, Context context) {
+ Account[] ocAccounts = getAccounts(context);
+
+ if (accountName != null) {
+ int lastAtPos = accountName.lastIndexOf("@");
+ String hostAndPort = accountName.substring(lastAtPos + 1);
+ String username = accountName.substring(0, lastAtPos);
+ String otherHostAndPort, otherUsername;
+ Locale currentLocale = context.getResources().getConfiguration().locale;
+ for (Account otherAccount : ocAccounts) {
+ lastAtPos = otherAccount.name.lastIndexOf("@");
+ otherHostAndPort = otherAccount.name.substring(lastAtPos + 1);
+ otherUsername = otherAccount.name.substring(0, lastAtPos);
+ if (otherHostAndPort.equals(hostAndPort) &&
+ otherUsername.toLowerCase(currentLocale).
+ equals(username.toLowerCase(currentLocale))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * returns the user's name based on the account name.
+ *
+ * @param accountName the account name
+ * @return the user's name
+ */
+ public static String getUsernameOfAccount(String accountName) {
+ if (accountName != null) {
+ return accountName.substring(0, accountName.lastIndexOf("@"));
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns owncloud account identified by accountName or null if it does not exist.
+ *
+ * @param context
+ * @param accountName name of account to be returned
+ * @return owncloud account named accountName
+ */
+ public static Account getOwnCloudAccountByName(Context context, String accountName) {
+ Account[] ocAccounts = AccountManager.get(context).getAccountsByType(
+ MainApp.Companion.getAccountType());
+ for (Account account : ocAccounts) {
+ if (account.name.equals(accountName)) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ public static boolean isSpacesFeatureAllowedForAccount(Context context, Account account, OCCapability capability) {
+ if (capability == null || !capability.isSpacesAllowed()) {
+ return false;
+ }
+ AccountManager accountManager = AccountManager.get(context);
+ String spacesFeatureValue = accountManager.getUserData(account, KEY_FEATURE_SPACES);
+ return KEY_FEATURE_ALLOWED.equals(spacesFeatureValue);
+ }
+
+ public static boolean setCurrentOwnCloudAccount(Context context, String accountName) {
+ boolean result = false;
+ if (accountName != null) {
+ boolean found;
+ for (Account account : getAccounts(context)) {
+ found = (account.name.equals(accountName));
+ if (found) {
+ SharedPreferences.Editor appPrefs = PreferenceManager
+ .getDefaultSharedPreferences(context).edit();
+ appPrefs.putString(SELECTED_ACCOUNT, accountName);
+
+ appPrefs.apply();
+ result = true;
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Update the accounts in AccountManager to meet the current version of accounts expected by the app, if needed.
+ *
+ * Introduced to handle a change in the structure of stored account names needed to allow different OC servers
+ * in the same domain, but not in the same path.
+ *
+ * @param context Used to access the AccountManager.
+ */
+ public static void updateAccountVersion(Context context) {
+ Account currentAccount = AccountUtils.getCurrentOwnCloudAccount(context);
+ AccountManager accountMgr = AccountManager.get(context);
+
+ if (currentAccount != null) {
+ String currentAccountVersion = accountMgr.getUserData(currentAccount, Constants.KEY_OC_ACCOUNT_VERSION);
+
+ if (currentAccountVersion == null) {
+ Timber.i("Upgrading accounts to account version #%s", ACCOUNT_VERSION);
+ Account[] ocAccounts = accountMgr.getAccountsByType(MainApp.Companion.getAccountType());
+ String serverUrl, username, newAccountName, password;
+ Account newAccount;
+ for (Account account : ocAccounts) {
+ // build new account name
+ serverUrl = accountMgr.getUserData(account, Constants.KEY_OC_BASE_URL);
+ username = com.owncloud.android.lib.common.accounts.AccountUtils.
+ getUsernameForAccount(account);
+ newAccountName = com.owncloud.android.lib.common.accounts.AccountUtils.
+ buildAccountName(Uri.parse(serverUrl), username);
+
+ // migrate to a new account, if needed
+ if (!newAccountName.equals(account.name)) {
+ Timber.d("Upgrading " + account.name + " to " + newAccountName);
+
+ // create the new account
+ newAccount = new Account(newAccountName, MainApp.Companion.getAccountType());
+ password = accountMgr.getPassword(account);
+ accountMgr.addAccountExplicitly(newAccount, (password != null) ? password : "", null);
+
+ // copy base URL
+ accountMgr.setUserData(newAccount, Constants.KEY_OC_BASE_URL, serverUrl);
+
+ String isOauthStr = accountMgr.getUserData(account, Constants.KEY_SUPPORTS_OAUTH2);
+ boolean isOAuth = OAUTH_SUPPORTED_TRUE.equals(isOauthStr);
+ if (isOAuth) {
+ accountMgr.setUserData(newAccount, Constants.KEY_SUPPORTS_OAUTH2, OAUTH_SUPPORTED_TRUE);
+ }
+
+ // don't forget the account saved in preferences as the current one
+ if (currentAccount.name.equals(account.name)) {
+ AccountUtils.setCurrentOwnCloudAccount(context, newAccountName);
+ }
+
+ // remove the old account
+ accountMgr.removeAccount(account, null, null);
+ // will assume it succeeds, not a big deal otherwise
+
+ } else {
+ // servers which base URL is in the root of their domain need no change
+ Timber.d("%s needs no upgrade ", account.name);
+ newAccount = account;
+ }
+
+ // at least, upgrade account version
+ Timber.d("Setting version " + ACCOUNT_VERSION + " to " + newAccountName);
+ accountMgr.setUserData(
+ newAccount, Constants.KEY_OC_ACCOUNT_VERSION, Integer.toString(ACCOUNT_VERSION)
+ );
+
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000000..9b321261067f
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticationViewModel.kt
@@ -0,0 +1,282 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ *
+ * Copyright (C) 2024 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.authentication
+
+import androidx.lifecycle.LiveData
+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
+import com.owncloud.android.domain.authentication.oauth.model.TokenRequest
+import com.owncloud.android.domain.authentication.oauth.model.TokenResponse
+import com.owncloud.android.domain.authentication.usecases.GetBaseUrlUseCase
+import com.owncloud.android.domain.authentication.usecases.LoginBasicAsyncUseCase
+import com.owncloud.android.domain.authentication.usecases.LoginOAuthAsyncUseCase
+import com.owncloud.android.domain.authentication.usecases.SupportsOAuth2UseCase
+import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
+import com.owncloud.android.domain.capabilities.usecases.RefreshCapabilitiesFromServerAsyncUseCase
+import com.owncloud.android.domain.server.model.ServerInfo
+import com.owncloud.android.domain.server.usecases.GetServerInfoAsyncUseCase
+import com.owncloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.domain.webfinger.usecases.GetOwnCloudInstanceFromWebFingerUseCase
+import com.owncloud.android.domain.webfinger.usecases.GetOwnCloudInstancesFromAuthenticatedWebFingerUseCase
+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
+import timber.log.Timber
+
+class AuthenticationViewModel(
+ private val loginBasicAsyncUseCase: LoginBasicAsyncUseCase,
+ private val loginOAuthAsyncUseCase: LoginOAuthAsyncUseCase,
+ private val getServerInfoAsyncUseCase: GetServerInfoAsyncUseCase,
+ private val supportsOAuth2UseCase: SupportsOAuth2UseCase,
+ private val getBaseUrlUseCase: GetBaseUrlUseCase,
+ private val getOwnCloudInstancesFromAuthenticatedWebFingerUseCase: GetOwnCloudInstancesFromAuthenticatedWebFingerUseCase,
+ private val getOwnCloudInstanceFromWebFingerUseCase: GetOwnCloudInstanceFromWebFingerUseCase,
+ private val refreshCapabilitiesFromServerAsyncUseCase: RefreshCapabilitiesFromServerAsyncUseCase,
+ private val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase,
+ private val refreshSpacesFromServerAsyncUseCase: RefreshSpacesFromServerAsyncUseCase,
+ private val workManagerProvider: WorkManagerProvider,
+ private val requestTokenUseCase: RequestTokenUseCase,
+ private val registerClientUseCase: RegisterClientUseCase,
+ private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider,
+ private val contextProvider: ContextProvider,
+) : ViewModel() {
+
+ val codeVerifier: String = OAuthUtils().generateRandomCodeVerifier()
+ val codeChallenge: String = OAuthUtils().generateCodeChallenge(codeVerifier)
+ val oidcState: String = OAuthUtils().generateRandomState()
+
+ private val _legacyWebfingerHost = MediatorLiveData>>()
+ val legacyWebfingerHost: LiveData>> = _legacyWebfingerHost
+
+ private val _serverInfo = MediatorLiveData>>()
+ val serverInfo: LiveData>> = _serverInfo
+
+ private val _loginResult = MediatorLiveData>>()
+ val loginResult: LiveData>> = _loginResult
+
+ private val _supportsOAuth2 = MediatorLiveData>>()
+ val supportsOAuth2: LiveData>> = _supportsOAuth2
+
+ private val _baseUrl = MediatorLiveData>>()
+ val baseUrl: LiveData>> = _baseUrl
+
+ private val _registerClient = MediatorLiveData>>()
+ val registerClient: LiveData>> = _registerClient
+
+ private val _requestToken = MediatorLiveData>>()
+ val requestToken: LiveData>> = _requestToken
+
+ private val _accountDiscovery = MediatorLiveData>>()
+ val accountDiscovery: LiveData>> = _accountDiscovery
+
+ var launchedFromDeepLink = false
+
+ fun getLegacyWebfingerHost(
+ webfingerLookupServer: String,
+ webfingerUsername: String,
+ ) {
+ runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ showLoading = true,
+ liveData = _legacyWebfingerHost,
+ useCase = getOwnCloudInstanceFromWebFingerUseCase,
+ useCaseParams = GetOwnCloudInstanceFromWebFingerUseCase.Params(server = webfingerLookupServer, resource = webfingerUsername)
+ )
+ }
+
+ fun getServerInfo(
+ serverUrl: String,
+ creatingAccount: Boolean = false,
+ ) {
+ runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ showLoading = true,
+ liveData = _serverInfo,
+ useCase = getServerInfoAsyncUseCase,
+ useCaseParams = GetServerInfoAsyncUseCase.Params(
+ serverPath = serverUrl,
+ creatingAccount = creatingAccount,
+ enforceOIDC = contextProvider.getBoolean(R.bool.enforce_oidc),
+ secureConnectionEnforced = contextProvider.getBoolean(R.bool.enforce_secure_connection),
+ )
+ )
+ }
+
+ fun loginBasic(
+ username: String,
+ password: String,
+ updateAccountWithUsername: String?
+ ) = runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ liveData = _loginResult,
+ showLoading = true,
+ useCase = loginBasicAsyncUseCase,
+ useCaseParams = LoginBasicAsyncUseCase.Params(
+ serverInfo = serverInfo.value?.peekContent()?.getStoredData(),
+ username = username,
+ password = password,
+ updateAccountWithUsername = updateAccountWithUsername
+ )
+ )
+
+ fun loginOAuth(
+ serverBaseUrl: String,
+ username: String,
+ authTokenType: String,
+ accessToken: String,
+ refreshToken: String,
+ scope: String?,
+ updateAccountWithUsername: String? = null,
+ clientRegistrationInfo: ClientRegistrationInfo?
+ ) {
+ viewModelScope.launch(coroutinesDispatcherProvider.io) {
+ _loginResult.postValue(Event(UIResult.Loading()))
+
+ val serverInfo = serverInfo.value?.peekContent()?.getStoredData() ?: throw java.lang.IllegalArgumentException("Server info value cannot" +
+ " be null")
+
+ // Authenticated WebFinger needed only for account creations. Logged accounts already know their instances.
+ if (updateAccountWithUsername == null) {
+ val ownCloudInstancesAvailable = getOwnCloudInstancesFromAuthenticatedWebFingerUseCase(
+ GetOwnCloudInstancesFromAuthenticatedWebFingerUseCase.Params(
+ server = serverBaseUrl,
+ username = username,
+ accessToken = accessToken,
+ )
+ )
+ Timber.d("Instances retrieved from authenticated webfinger: $ownCloudInstancesAvailable")
+
+ // Multiple instances are not supported yet. Let's use the first instance we receive for the moment.
+ ownCloudInstancesAvailable.getDataOrNull()?.let {
+ if (it.isNotEmpty()) {
+ serverInfo.baseUrl = it.first()
+ }
+ }
+ }
+
+ val useCaseResult = loginOAuthAsyncUseCase(
+ LoginOAuthAsyncUseCase.Params(
+ serverInfo = serverInfo,
+ username = username,
+ authTokenType = authTokenType,
+ accessToken = accessToken,
+ refreshToken = refreshToken,
+ scope = scope,
+ updateAccountWithUsername = updateAccountWithUsername,
+ clientRegistrationInfo = clientRegistrationInfo,
+ )
+ )
+
+ if (useCaseResult.isSuccess) {
+ _loginResult.postValue(Event(UIResult.Success(useCaseResult.getDataOrNull())))
+ } else if (useCaseResult.isError) {
+ _loginResult.postValue(Event(UIResult.Error(error = useCaseResult.getThrowableOrNull())))
+ }
+ }
+ }
+
+ fun supportsOAuth2(
+ accountName: String
+ ) = runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ requiresConnection = false,
+ liveData = _supportsOAuth2,
+ useCase = supportsOAuth2UseCase,
+ useCaseParams = SupportsOAuth2UseCase.Params(
+ accountName = accountName
+ )
+ )
+
+ fun getBaseUrl(
+ accountName: String
+ ) = runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ requiresConnection = false,
+ liveData = _baseUrl,
+ useCase = getBaseUrlUseCase,
+ useCaseParams = GetBaseUrlUseCase.Params(
+ accountName = accountName
+ )
+ )
+
+ fun registerClient(
+ registrationEndpoint: String
+ ) {
+ val registrationRequest = OAuthUtils.buildClientRegistrationRequest(
+ registrationEndpoint = registrationEndpoint,
+ MainApp.appContext
+ )
+
+ runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ showLoading = false,
+ liveData = _registerClient,
+ useCase = registerClientUseCase,
+ useCaseParams = RegisterClientUseCase.Params(registrationRequest)
+ )
+ }
+
+ fun requestToken(
+ tokenRequest: TokenRequest
+ ) = runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ showLoading = false,
+ liveData = _requestToken,
+ useCase = requestTokenUseCase,
+ useCaseParams = RequestTokenUseCase.Params(tokenRequest = tokenRequest)
+ )
+
+ fun discoverAccount(accountName: String, discoveryNeeded: Boolean = false) {
+ Timber.d("Account Discovery for account: $accountName needed: $discoveryNeeded")
+ if (!discoveryNeeded) {
+ _accountDiscovery.postValue(Event(UIResult.Success()))
+ return
+ }
+ _accountDiscovery.postValue(Event(UIResult.Loading()))
+ viewModelScope.launch(coroutinesDispatcherProvider.io) {
+ // 1. Refresh capabilities for account
+ refreshCapabilitiesFromServerAsyncUseCase(RefreshCapabilitiesFromServerAsyncUseCase.Params(accountName))
+ val capabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))
+
+ val spacesAvailableForAccount = capabilities?.isSpacesAllowed() == true
+
+ // 2 If Account does not support spaces we can skip this
+ if (spacesAvailableForAccount) {
+ refreshSpacesFromServerAsyncUseCase(RefreshSpacesFromServerAsyncUseCase.Params(accountName))
+ }
+ _accountDiscovery.postValue(Event(UIResult.Success()))
+ }
+ workManagerProvider.enqueueAccountDiscovery(accountName)
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticatorConstants.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticatorConstants.kt
new file mode 100644
index 000000000000..0b3278b60938
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticatorConstants.kt
@@ -0,0 +1,45 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2020 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 .
+ */
+@file:JvmName("AuthenticatorConstants")
+
+package com.owncloud.android.presentation.authentication
+
+import com.owncloud.android.MainApp.Companion.accountType
+import com.owncloud.android.lib.common.accounts.AccountTypeUtils
+
+const val EXTRA_ACTION = "ACTION"
+const val EXTRA_ACCOUNT = "ACCOUNT"
+
+const val ACTION_CREATE: Byte = 0
+const val ACTION_UPDATE_TOKEN: Byte = 1 // requested by the user
+const val ACTION_UPDATE_EXPIRED_TOKEN: Byte = 2 // detected by the app
+
+const val KEY_AUTH_TOKEN_TYPE = "authTokenType"
+
+val BASIC_TOKEN_TYPE: String = AccountTypeUtils.getAuthTokenTypePass(
+ accountType
+)
+
+val OAUTH_TOKEN_TYPE: String = AccountTypeUtils.getAuthTokenTypeAccessToken(
+ accountType
+)
+
+const val UNTRUSTED_CERT_DIALOG_TAG = "UNTRUSTED_CERT_DIALOG"
+const val WAIT_DIALOG_TAG = "WAIT_DIALOG"
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
new file mode 100644
index 000000000000..2de3a4fb9708
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/LoginActivity.kt
@@ -0,0 +1,932 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Bartek Przybylski
+ * @author David A. Velasco
+ * @author masensio
+ * @author David González Verdugo
+ * @author Christian Schabesberger
+ * @author Shashvat Kedia
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2012 Bartek Przybylski
+ * Copyright (C) 2026 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.authentication
+
+import android.accounts.Account
+import android.accounts.AccountAuthenticatorResponse
+import android.accounts.AccountManager
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View.INVISIBLE
+import android.view.WindowManager.LayoutParams.FLAG_SECURE
+import androidx.appcompat.app.AppCompatActivity
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.core.net.toUri
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.isVisible
+import androidx.core.widget.doAfterTextChanged
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.MainApp
+import com.owncloud.android.MainApp.Companion.accountType
+import com.owncloud.android.R
+import com.owncloud.android.data.authentication.KEY_USER_ID
+import com.owncloud.android.databinding.AccountSetupBinding
+import com.owncloud.android.domain.authentication.oauth.model.ResponseType
+import com.owncloud.android.domain.authentication.oauth.model.TokenRequest
+import com.owncloud.android.domain.authentication.oauth.model.TokenResponse
+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
+import com.owncloud.android.domain.server.model.ServerInfo
+import com.owncloud.android.extensions.checkPasscodeEnforced
+import com.owncloud.android.extensions.goToUrl
+import com.owncloud.android.extensions.manageOptionLockSelected
+import com.owncloud.android.extensions.parseError
+import com.owncloud.android.extensions.showErrorInToast
+import com.owncloud.android.extensions.showMessageInSnackbar
+import com.owncloud.android.lib.common.accounts.AccountTypeUtils
+import com.owncloud.android.lib.common.accounts.AccountUtils
+import com.owncloud.android.lib.common.network.CertificateCombinedException
+import com.owncloud.android.presentation.authentication.AccountUtils.getAccounts
+import com.owncloud.android.presentation.authentication.AccountUtils.getUsernameOfAccount
+import com.owncloud.android.presentation.authentication.oauth.OAuthUtils
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.presentation.documentsprovider.DocumentsProviderUtils.notifyDocumentsProviderRoots
+import com.owncloud.android.presentation.security.LockType
+import com.owncloud.android.presentation.security.SecurityEnforced
+import com.owncloud.android.presentation.settings.SettingsActivity
+import com.owncloud.android.providers.ContextProvider
+import com.owncloud.android.providers.MdmProvider
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.dialog.SslUntrustedCertDialog
+import com.owncloud.android.utils.CONFIGURATION_OAUTH2_OPEN_ID_PROMPT
+import com.owncloud.android.utils.CONFIGURATION_OAUTH2_OPEN_ID_SCOPE
+import com.owncloud.android.utils.CONFIGURATION_SEND_LOGIN_HINT_AND_USER
+import com.owncloud.android.utils.CONFIGURATION_SERVER_URL
+import com.owncloud.android.utils.CONFIGURATION_SERVER_URL_INPUT_VISIBILITY
+import com.owncloud.android.utils.NO_MDM_RESTRICTION_YET
+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
+
+class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrustedCertListener, SecurityEnforced {
+
+ private val authenticationViewModel by viewModel()
+ private val contextProvider by inject()
+ private val mdmProvider by inject()
+
+ private var loginAction: Byte = ACTION_CREATE
+ private var authTokenType: String? = null
+ private var userAccount: Account? = null
+ private var username: String? = null
+ private lateinit var serverBaseUrl: String
+
+ private var oidcSupported = false
+ private var isKiteworksServer = false
+
+ private lateinit var binding: AccountSetupBinding
+
+ // For handling AbstractAccountAuthenticator responses
+ private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null
+ private var resultBundle: Bundle? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ checkPasscodeEnforced(this)
+
+ // Protection against screen recording
+ if (!BuildConfig.DEBUG) {
+ window.addFlags(FLAG_SECURE)
+ } // else, let it go, or taking screenshots & testing will not be possible
+
+ // Get values from intent
+ handleDeepLink()
+ loginAction = intent.getByteExtra(EXTRA_ACTION, ACTION_CREATE)
+ authTokenType = intent.getStringExtra(KEY_AUTH_TOKEN_TYPE)
+ userAccount = intent.getParcelableExtra(EXTRA_ACCOUNT)
+
+ // Get values from savedInstanceState
+ if (savedInstanceState == null) {
+ if (authTokenType == null && userAccount != null) {
+ authenticationViewModel.supportsOAuth2((userAccount as Account).name)
+ }
+ } else {
+ authTokenType = savedInstanceState.getString(KEY_AUTH_TOKEN_TYPE)
+ }
+
+ // UI initialization
+ binding = AccountSetupBinding.inflate(layoutInflater)
+ val view = binding.root
+ adaptInfiniteEdges()
+ setContentView(view)
+
+ if (loginAction != ACTION_CREATE) {
+ binding.accountUsername.isEnabled = false
+ binding.accountUsername.isFocusable = false
+ userAccount?.name?.let {
+ username = getUsernameOfAccount(it)
+ }
+
+ }
+
+ if (savedInstanceState == null) {
+ if (userAccount != null) {
+ authenticationViewModel.getBaseUrl((userAccount as Account).name)
+ } else {
+ serverBaseUrl = getString(R.string.server_url).trim()
+ }
+
+ userAccount?.let {
+ AccountUtils.getUsernameForAccount(it)?.let { username ->
+ binding.accountUsername.setText(username)
+ }
+ }
+ }
+
+ binding.root.filterTouchesWhenObscured =
+ PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(this@LoginActivity)
+
+ initBrandableOptionsUI()
+
+ binding.thumbnail.setOnClickListener { checkOcServer() }
+
+ binding.embeddedCheckServerButton.setOnClickListener { checkOcServer() }
+
+ binding.loginButton.setOnClickListener {
+ if (AccountTypeUtils.getAuthTokenTypeAccessToken(accountType) != authTokenType) { // Basic
+ authenticationViewModel.loginBasic(
+ binding.accountUsername.text.toString().trim(),
+ binding.accountPassword.text.toString(),
+ if (loginAction != ACTION_CREATE) userAccount?.name else null
+ )
+ }
+ }
+
+ binding.settingsLink.setOnClickListener {
+ val settingsIntent = Intent(this, SettingsActivity::class.java)
+ startActivity(settingsIntent)
+ }
+
+ accountAuthenticatorResponse = intent.getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)
+ accountAuthenticatorResponse?.onRequestContinued()
+
+ initLiveDataObservers()
+ }
+
+ private fun handleDeepLink() {
+ if (intent.data != null) {
+ authenticationViewModel.launchedFromDeepLink = true
+ if (getAccounts(baseContext).isNotEmpty()) {
+ launchFileDisplayActivity()
+ } else {
+ showMessageInSnackbar(message = baseContext.getString(R.string.uploader_wrn_no_account_title))
+ }
+ }
+ }
+
+ private fun launchFileDisplayActivity() {
+ val newIntent = Intent(this, FileDisplayActivity::class.java)
+ newIntent.data = intent.data
+ startActivity(newIntent)
+ finish()
+ }
+
+ private fun initLiveDataObservers() {
+ // LiveData observers
+ authenticationViewModel.legacyWebfingerHost.observe(this) { event ->
+ when (val uiResult = event.peekContent()) {
+ is UIResult.Loading -> getLegacyWebfingerIsLoading()
+ is UIResult.Success -> getLegacyWebfingerIsSuccess(uiResult)
+ is UIResult.Error -> getLegacyWebfingerIsError(uiResult)
+ }
+ }
+
+ authenticationViewModel.serverInfo.observe(this) { event ->
+ when (val uiResult = event.peekContent()) {
+ is UIResult.Loading -> getServerInfoIsLoading()
+ is UIResult.Success -> getServerInfoIsSuccess(uiResult)
+ is UIResult.Error -> getServerInfoIsError(uiResult)
+ }
+ }
+
+ authenticationViewModel.loginResult.observe(this) { event ->
+ when (val uiResult = event.peekContent()) {
+ is UIResult.Loading -> loginIsLoading()
+ is UIResult.Success -> loginIsSuccess(uiResult)
+ is UIResult.Error -> loginIsError(uiResult)
+ }
+ }
+
+ authenticationViewModel.accountDiscovery.observe(this) {
+ if (it.peekContent() is UIResult.Success) {
+ notifyDocumentsProviderRoots(applicationContext)
+ if (authenticationViewModel.launchedFromDeepLink) {
+ launchFileDisplayActivity()
+ } else {
+ finish()
+ }
+ } else {
+ binding.authStatusText.run {
+ text = context.getString(R.string.login_account_preparing)
+ isVisible = true
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.progress_small, 0, 0, 0)
+ }
+ }
+ }
+
+ authenticationViewModel.supportsOAuth2.observe(this) { event ->
+ when (val uiResult = event.peekContent()) {
+ is UIResult.Loading -> {}
+ is UIResult.Success -> updateAuthTokenTypeAndInstructions(uiResult)
+ is UIResult.Error -> showErrorInToast(
+ genericErrorMessageId = R.string.supports_oauth2_error,
+ throwable = uiResult.error
+ )
+ }
+ }
+
+ authenticationViewModel.baseUrl.observe(this) { event ->
+ when (val uiResult = event.peekContent()) {
+ is UIResult.Loading -> {}
+ is UIResult.Success -> updateBaseUrlAndHostInput(uiResult)
+ is UIResult.Error -> showErrorInToast(
+ genericErrorMessageId = R.string.get_base_url_error,
+ throwable = uiResult.error
+ )
+ }
+ }
+ }
+
+ private fun getLegacyWebfingerIsLoading() {
+ binding.webfingerStatusText.run {
+ text = getString(R.string.auth_testing_connection)
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.progress_small, 0, 0, 0)
+ isVisible = true
+ }
+ }
+
+ private fun getLegacyWebfingerIsSuccess(uiResult: UIResult.Success) {
+ val serverUrl = uiResult.data ?: return
+ username = binding.webfingerUsername.text.toString()
+ binding.webfingerLayout.isVisible = false
+ binding.mainLoginLayout.isVisible = true
+ binding.hostUrlInput.setText(serverUrl)
+ checkOcServer()
+ }
+
+ private fun getLegacyWebfingerIsError(uiResult: UIResult.Error) {
+ if (uiResult.error is NoNetworkConnectionException) {
+ binding.webfingerStatusText.run {
+ 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)
+ }
+ }
+ binding.webfingerStatusText.isVisible = true
+ }
+
+ private fun checkOcServer() {
+ val uri = binding.hostUrlInput.text.toString().trim()
+ if (uri.isNotEmpty()) {
+ authenticationViewModel.getServerInfo(serverUrl = uri, loginAction == ACTION_CREATE)
+ } else {
+ binding.serverStatusText.run {
+ text = getString(R.string.auth_can_not_auth_against_server).also { Timber.d(it) }
+ isVisible = true
+ }
+ }
+ }
+
+ private fun getServerInfoIsSuccess(uiResult: UIResult.Success) {
+ updateCenteredRefreshButtonVisibility(shouldBeVisible = false)
+ uiResult.data?.run {
+ val serverInfo = this
+ serverBaseUrl = baseUrl
+ binding.hostUrlInput.run {
+ setText(baseUrl)
+ doAfterTextChanged {
+ //If user modifies url, reset fields and force him to check url again
+ if (authenticationViewModel.serverInfo.value == null || baseUrl != binding.hostUrlInput.text.toString()) {
+ showOrHideBasicAuthFields(shouldBeVisible = false)
+ binding.loginButton.isVisible = false
+ binding.serverStatusText.run {
+ text = ""
+ visibility = INVISIBLE
+ }
+ }
+ }
+ }
+
+ binding.serverStatusText.run {
+ if (isSecureConnection) {
+ text = getString(R.string.auth_secure_connection)
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock, 0, 0, 0)
+ checkServerType(serverInfo)
+ } else {
+ text = getString(R.string.auth_connection_established)
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_open, 0, 0, 0)
+ val builder = AlertDialog.Builder(context)
+ builder.apply {
+ setTitle(context.getString(R.string.insecure_http_url_title_dialog))
+ setMessage(context.getString(R.string.insecure_http_url_message_dialog))
+ setPositiveButton(R.string.insecure_http_url_continue_button) { dialog, which ->
+ checkServerType(serverInfo)
+ }
+ setNegativeButton(android.R.string.cancel) { dialog, which ->
+ showOrHideBasicAuthFields(shouldBeVisible = false)
+ }
+ setCancelable(false)
+ show()
+ }
+ }
+ isVisible = true
+ }
+ }
+ }
+
+ private fun checkServerType(serverInfo: ServerInfo) {
+ if (BuildConfig.FLAVOR == MainApp.QA_FLAVOR) {
+ handleBasicAuth()
+ return
+ }
+
+ when (serverInfo) {
+ is ServerInfo.BasicServer -> {
+ handleBasicAuth()
+ }
+
+ is ServerInfo.OAuth2Server -> {
+ showOrHideBasicAuthFields(shouldBeVisible = false)
+ authTokenType = OAUTH_TOKEN_TYPE
+ oidcSupported = false
+
+ val oauth2authorizationEndpoint =
+ Uri.parse("$serverBaseUrl${File.separator}${getString(R.string.oauth2_url_endpoint_auth)}")
+ performGetAuthorizationCodeRequest(oauth2authorizationEndpoint)
+ }
+
+ is ServerInfo.OIDCServer -> {
+ showOrHideBasicAuthFields(shouldBeVisible = false)
+ authTokenType = OAUTH_TOKEN_TYPE
+ oidcSupported = true
+ val registrationEndpoint = serverInfo.oidcServerConfiguration.registrationEndpoint
+ if (registrationEndpoint != null) {
+ registerClient(
+ authorizationEndpoint = serverInfo.oidcServerConfiguration.authorizationEndpoint.toUri(),
+ registrationEndpoint = registrationEndpoint
+ )
+ } else {
+ performGetAuthorizationCodeRequest(serverInfo.oidcServerConfiguration.authorizationEndpoint.toUri())
+ }
+ }
+
+ else -> {
+ binding.serverStatusText.run {
+ text = getString(R.string.auth_unsupported_auth_method)
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0)
+ isVisible = true
+ }
+ }
+ }
+ }
+
+ private fun handleBasicAuth() {
+ authTokenType = BASIC_TOKEN_TYPE
+ oidcSupported = false
+ showOrHideBasicAuthFields(shouldBeVisible = true)
+ binding.accountUsername.doAfterTextChanged { updateLoginButtonVisibility() }
+ binding.accountPassword.doAfterTextChanged { updateLoginButtonVisibility() }
+ }
+
+ private fun getServerInfoIsLoading() {
+ binding.serverStatusText.run {
+ text = getString(R.string.auth_testing_connection)
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.progress_small, 0, 0, 0)
+ isVisible = true
+ }
+ }
+
+ private fun getServerInfoIsError(uiResult: UIResult.Error) {
+ updateCenteredRefreshButtonVisibility(shouldBeVisible = true)
+ when {
+ uiResult.error is CertificateCombinedException ->
+ showUntrustedCertDialog(uiResult.error)
+
+ uiResult.error is OwncloudVersionNotSupportedException -> binding.serverStatusText.run {
+ text = getString(R.string.server_not_supported)
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0)
+ }
+
+ 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)
+ }
+ }
+ binding.serverStatusText.isVisible = true
+ showOrHideBasicAuthFields(shouldBeVisible = false)
+ }
+
+ private fun loginIsSuccess(uiResult: UIResult.Success) {
+ binding.authStatusText.run {
+ isVisible = false
+ text = ""
+ }
+
+ // Return result to account authenticator, multiaccount does not work without this
+ val accountName = uiResult.data!!
+ val intent = Intent()
+ intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, accountName)
+ intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, contextProvider.getString(R.string.account_type))
+ resultBundle = intent.extras
+ setResult(Activity.RESULT_OK, intent)
+
+ authenticationViewModel.discoverAccount(accountName = accountName, discoveryNeeded = loginAction == ACTION_CREATE)
+ }
+
+ private fun loginIsLoading() {
+ binding.authStatusText.run {
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.progress_small, 0, 0, 0)
+ isVisible = true
+ text = getString(R.string.auth_trying_to_login)
+ }
+ }
+
+ private fun loginIsError(uiResult: UIResult.Error) {
+ when (uiResult.error) {
+ is NoNetworkConnectionException, is ServerNotReachableException -> {
+ binding.serverStatusText.run {
+ text = getString(R.string.error_no_network_connection)
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.no_network, 0, 0, 0)
+ }
+ showOrHideBasicAuthFields(shouldBeVisible = false)
+ }
+
+ else -> {
+ binding.serverStatusText.isVisible = false
+ binding.authStatusText.run {
+ text = uiResult.error?.parseError("", resources, true)
+ isVisible = true
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0)
+ }
+ }
+ }
+ }
+
+ /**
+ * Register client if possible.
+ */
+ private fun registerClient(
+ authorizationEndpoint: Uri,
+ registrationEndpoint: String
+ ) {
+ authenticationViewModel.registerClient(registrationEndpoint)
+ authenticationViewModel.registerClient.observe(this) {
+ when (val uiResult = it.peekContent()) {
+ is UIResult.Loading -> {}
+ is UIResult.Success -> {
+ Timber.d("Client registered: ${it.peekContent().getStoredData()}")
+ uiResult.data?.let { clientRegistrationInfo ->
+ performGetAuthorizationCodeRequest(
+ authorizationEndpoint = authorizationEndpoint,
+ clientId = clientRegistrationInfo.clientId
+ )
+ }
+ }
+
+ is UIResult.Error -> {
+ Timber.e(uiResult.error, "Client registration failed.")
+ performGetAuthorizationCodeRequest(authorizationEndpoint)
+ }
+ }
+ }
+ }
+
+ private fun performGetAuthorizationCodeRequest(
+ authorizationEndpoint: Uri,
+ clientId: String = getString(R.string.oauth2_client_id)
+ ) {
+ Timber.d("A browser should be opened now to authenticate this user.")
+
+ val customTabsBuilder: CustomTabsIntent.Builder = CustomTabsIntent.Builder()
+ val customTabsIntent: CustomTabsIntent = customTabsBuilder.build()
+
+ val serverInfo = authenticationViewModel.serverInfo.value?.peekContent()?.getStoredData()
+ isKiteworksServer = serverInfo is ServerInfo.OIDCServer && serverInfo.oidcServerConfiguration.isKiteworksServer
+
+ val authorizationEndpointUri = OAuthUtils.buildAuthorizationRequest(
+ authorizationEndpoint = authorizationEndpoint,
+ redirectUri = OAuthUtils.buildRedirectUri(applicationContext, isKiteworksServer).toString(),
+ clientId = if (isKiteworksServer) getString(R.string.kiteworks_client_id) else clientId,
+ responseType = ResponseType.CODE.string,
+ scope = getScope(),
+ prompt = getPrompt(),
+ codeChallenge = authenticationViewModel.codeChallenge,
+ state = authenticationViewModel.oidcState,
+ username = username,
+ sendLoginHintAndUser = mdmProvider.getBrandingBoolean(mdmKey = CONFIGURATION_SEND_LOGIN_HINT_AND_USER,
+ booleanKey = R.bool.send_login_hint_and_user),
+ )
+
+ try {
+ customTabsIntent.launchUrl(
+ this,
+ authorizationEndpointUri
+ )
+ } catch (e: ActivityNotFoundException) {
+ binding.serverStatusText.visibility = INVISIBLE
+ showMessageInSnackbar(message = this.getString(R.string.file_list_no_app_for_perform_action))
+ Timber.e("No Activity found to handle Intent")
+ }
+ }
+
+ private fun getScope(): String =
+ if (isKiteworksServer) {
+ getString(R.string.kiteworks_openid_scope)
+ } else if (oidcSupported) {
+ mdmProvider.getBrandingString(CONFIGURATION_OAUTH2_OPEN_ID_SCOPE, R.string.oauth2_openid_scope)
+ } else {
+ ""
+ }
+
+ private fun getScopeForLogin(tokenResponse: TokenResponse): String? =
+ if (isKiteworksServer) {
+ getString(R.string.kiteworks_openid_scope)
+ } else if (oidcSupported) {
+ mdmProvider.getBrandingString(CONFIGURATION_OAUTH2_OPEN_ID_SCOPE, R.string.oauth2_openid_scope)
+ } else {
+ tokenResponse.scope
+ }
+
+ private fun getPrompt(): String =
+ if (isKiteworksServer) {
+ getString(R.string.kiteworks_openid_prompt)
+ } else if (oidcSupported) {
+ mdmProvider.getBrandingString(CONFIGURATION_OAUTH2_OPEN_ID_PROMPT, R.string.oauth2_openid_prompt)
+ } else {
+ ""
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ handleGetAuthorizationCodeResponse(intent)
+ }
+
+ private fun handleGetAuthorizationCodeResponse(intent: Intent) {
+ val authorizationCode = intent.data?.getQueryParameter("code")
+ val state = intent.data?.getQueryParameter("state")
+
+ if (state != authenticationViewModel.oidcState) {
+ Timber.e("OAuth request to get authorization code failed. State mismatching, maybe somebody is trying a CSRF attack.")
+ updateOAuthStatusIconAndText(StateMismatchException())
+ } else {
+ if (authorizationCode != null) {
+ Timber.d("Authorization code received [$authorizationCode]. Let's exchange it for access token")
+ exchangeAuthorizationCodeForTokens(authorizationCode)
+ } else {
+ val authorizationError = intent.data?.getQueryParameter("error")
+ val authorizationErrorDescription = intent.data?.getQueryParameter("error_description")
+
+ Timber.e("OAuth request to get authorization code failed. Error: [$authorizationError]." +
+ " Error description: [$authorizationErrorDescription]")
+ val authorizationException =
+ if (authorizationError == "access_denied") UnauthorizedException() else Throwable("An unknown authorization error has " +
+ "occurred")
+ updateOAuthStatusIconAndText(authorizationException)
+ }
+ }
+ }
+
+ /**
+ * OAuth step 2: Exchange the received authorization code for access and refresh tokens
+ */
+ private fun exchangeAuthorizationCodeForTokens(authorizationCode: String) {
+ binding.serverStatusText.text = getString(R.string.auth_getting_authorization)
+
+ val clientRegistrationInfo = authenticationViewModel.registerClient.value?.peekContent()?.getStoredData()
+
+ val clientAuth = if (clientRegistrationInfo?.clientId != null && clientRegistrationInfo.clientSecret != null) {
+ OAuthUtils.getClientAuth(clientRegistrationInfo.clientSecret as String, clientRegistrationInfo.clientId)
+
+ } else {
+ if (isKiteworksServer) {
+ OAuthUtils.getClientAuth(getString(R.string.kiteworks_client_secret), getString(R.string.kiteworks_client_id))
+ } else {
+ OAuthUtils.getClientAuth(getString(R.string.oauth2_client_secret), getString(R.string.oauth2_client_id))
+ }
+ }
+
+ // Use oidc discovery one, or build an oauth endpoint using serverBaseUrl + Setup string.
+ val tokenEndPoint: String
+
+ var clientId: String? = null
+ var clientSecret: String? = null
+ var useAuthorizationHeader = true
+
+ val serverInfo = authenticationViewModel.serverInfo.value?.peekContent()?.getStoredData()
+ if (serverInfo is ServerInfo.OIDCServer) {
+ tokenEndPoint = serverInfo.oidcServerConfiguration.tokenEndpoint
+ if (serverInfo.oidcServerConfiguration.isTokenEndpointAuthMethodSupportedClientSecretPost()) {
+ val defaultClientId = if (isKiteworksServer) R.string.kiteworks_client_id else R.string.oauth2_client_id
+ val defaultClientSecret = if (isKiteworksServer) R.string.kiteworks_client_secret else R.string.oauth2_client_secret
+
+ clientId = clientRegistrationInfo?.clientId ?: contextProvider.getString(defaultClientId)
+ clientSecret = clientRegistrationInfo?.clientSecret ?: contextProvider.getString(defaultClientSecret)
+ useAuthorizationHeader = false
+ }
+ } else {
+ tokenEndPoint = "$serverBaseUrl${File.separator}${contextProvider.getString(R.string.oauth2_url_endpoint_access)}"
+ }
+
+ val requestToken = TokenRequest.AccessToken(
+ baseUrl = serverBaseUrl,
+ tokenEndpoint = tokenEndPoint,
+ clientAuth = clientAuth,
+ scope = getScope(),
+ clientId = clientId,
+ clientSecret = clientSecret,
+ authorizationCode = authorizationCode,
+ redirectUri = OAuthUtils.buildRedirectUri(applicationContext, isKiteworksServer).toString(),
+ codeVerifier = authenticationViewModel.codeVerifier,
+ useAuthorizationHeader = useAuthorizationHeader
+ )
+
+ authenticationViewModel.requestToken(requestToken)
+
+ authenticationViewModel.requestToken.observe(this) {
+ when (val uiResult = it.peekContent()) {
+ is UIResult.Loading -> {}
+ is UIResult.Success -> {
+ Timber.d("Tokens received ${uiResult.data}, trying to login, creating account and adding it to account manager")
+ val tokenResponse = uiResult.data ?: return@observe
+
+ authenticationViewModel.loginOAuth(
+ serverBaseUrl = serverBaseUrl,
+ username = tokenResponse.additionalParameters?.get(KEY_USER_ID).orEmpty(),
+ authTokenType = OAUTH_TOKEN_TYPE,
+ accessToken = tokenResponse.accessToken,
+ refreshToken = tokenResponse.refreshToken.orEmpty(),
+ scope = getScopeForLogin(tokenResponse),
+ updateAccountWithUsername = if (loginAction != ACTION_CREATE) userAccount?.name else null,
+ clientRegistrationInfo = clientRegistrationInfo
+ )
+ }
+
+ is UIResult.Error -> {
+ Timber.e(uiResult.error, "OAuth request to exchange authorization code for tokens failed")
+ updateOAuthStatusIconAndText(uiResult.error)
+ }
+ }
+ }
+ }
+
+ private fun updateAuthTokenTypeAndInstructions(uiResult: UIResult) {
+ val supportsOAuth2 = uiResult.getStoredData()
+ authTokenType = if (supportsOAuth2 != null && supportsOAuth2) OAUTH_TOKEN_TYPE else BASIC_TOKEN_TYPE
+
+ binding.instructionsMessage.run {
+ if (loginAction == ACTION_UPDATE_EXPIRED_TOKEN) {
+ text =
+ if (AccountTypeUtils.getAuthTokenTypeAccessToken(accountType) == authTokenType) {
+ getString(R.string.auth_expired_oauth_token_toast)
+ } else {
+ getString(R.string.auth_expired_basic_auth_toast)
+ }
+ isVisible = true
+ } else {
+ isVisible = false
+ }
+ }
+ }
+
+ private fun updateBaseUrlAndHostInput(uiResult: UIResult) {
+ uiResult.getStoredData()?.let { serverUrl ->
+ serverBaseUrl = serverUrl
+
+ binding.hostUrlInput.run {
+ setText(serverBaseUrl)
+ isEnabled = false
+ isFocusable = false
+ }
+
+ if (loginAction != ACTION_CREATE && serverBaseUrl.isNotEmpty()) {
+ checkOcServer()
+ }
+ }
+ }
+
+ /**
+ * Show untrusted cert dialog
+ */
+ private fun showUntrustedCertDialog(certificateCombinedException: CertificateCombinedException) { // Show a dialog with the certificate info
+ val dialog = SslUntrustedCertDialog.newInstanceForFullSslError(certificateCombinedException)
+ val fm = supportFragmentManager
+ val ft = fm.beginTransaction()
+ ft.addToBackStack(null)
+ dialog.show(ft, UNTRUSTED_CERT_DIALOG_TAG)
+ }
+
+ override fun onSavedCertificate() {
+ Timber.d("Server certificate is trusted")
+ checkOcServer()
+ }
+
+ override fun onCancelCertificate() {
+ Timber.d("Server certificate is not trusted")
+ binding.serverStatusText.run {
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_warning, 0, 0, 0)
+ text = getString(R.string.ssl_certificate_not_trusted)
+ }
+ }
+
+ override fun onFailedSavingCertificate() {
+ Timber.d("Server certificate could not be saved")
+ binding.serverStatusText.run {
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_warning, 0, 0, 0)
+ text = getString(R.string.ssl_validator_not_saved)
+ }
+ }
+
+ /* Show or hide Basic Auth fields and reset its values */
+ private fun showOrHideBasicAuthFields(shouldBeVisible: Boolean) {
+ binding.accountUsernameContainer.run {
+ isVisible = shouldBeVisible
+ isFocusable = shouldBeVisible
+ isEnabled = shouldBeVisible
+ if (shouldBeVisible) requestFocus()
+ }
+ binding.accountPasswordContainer.run {
+ isVisible = shouldBeVisible
+ isFocusable = shouldBeVisible
+ isEnabled = shouldBeVisible
+ }
+
+ if (!shouldBeVisible) {
+ binding.accountUsername.setText("")
+ binding.accountPassword.setText("")
+ }
+
+ binding.authStatusText.run {
+ isVisible = false
+ text = ""
+ }
+ binding.loginButton.isVisible = false
+ }
+
+ private fun updateCenteredRefreshButtonVisibility(shouldBeVisible: Boolean) {
+ if (!contextProvider.getBoolean(R.bool.show_server_url_input)) {
+ binding.centeredRefreshButton.isVisible = shouldBeVisible
+ }
+ }
+
+ private fun initBrandableOptionsUI() {
+ val showInput = mdmProvider.getBrandingBoolean(mdmKey = CONFIGURATION_SERVER_URL_INPUT_VISIBILITY, booleanKey = R.bool.show_server_url_input)
+ binding.hostUrlFrame.isVisible = showInput
+ binding.centeredRefreshButton.isVisible = !showInput
+ if (!showInput) {
+ binding.centeredRefreshButton.setOnClickListener { checkOcServer() }
+ }
+
+ val url = mdmProvider.getBrandingString(mdmKey = CONFIGURATION_SERVER_URL, stringKey = R.string.server_url)
+ if (url.isNotEmpty()) {
+ binding.hostUrlInput.setText(url)
+ }
+
+ binding.loginLayout.run {
+ if (contextProvider.getBoolean(R.bool.use_login_background_image)) {
+ binding.loginBackgroundImage.isVisible = true
+ } else {
+ setBackgroundColor(resources.getColor(R.color.login_background_color))
+ }
+ }
+
+ binding.welcomeLink.run {
+ if (contextProvider.getBoolean(R.bool.show_welcome_link)) {
+ isVisible = true
+ text = contextProvider.getString(R.string.login_welcome_text).takeUnless { it.isBlank() }
+ ?: String.format(contextProvider.getString(R.string.auth_register), contextProvider.getString(R.string.app_name))
+ setOnClickListener {
+ setResult(Activity.RESULT_CANCELED)
+ goToUrl(url = getString(R.string.welcome_link_url))
+ }
+ } else {
+ isVisible = false
+ }
+ }
+
+ val legacyWebfingerLookupServer = mdmProvider.getBrandingString(NO_MDM_RESTRICTION_YET, R.string.webfinger_lookup_server)
+ val shouldShowLegacyWebfingerFlow = loginAction == ACTION_CREATE && legacyWebfingerLookupServer.isNotBlank()
+ binding.webfingerLayout.isVisible = shouldShowLegacyWebfingerFlow
+ binding.mainLoginLayout.isVisible = !shouldShowLegacyWebfingerFlow
+
+ if (shouldShowLegacyWebfingerFlow) {
+ binding.webfingerButton.setOnClickListener {
+ val webfingerUsername = binding.webfingerUsername.text.toString()
+ if (webfingerUsername.isNotEmpty()) {
+ authenticationViewModel.getLegacyWebfingerHost(
+ legacyWebfingerLookupServer,
+ webfingerUsername
+ )
+ } else {
+ binding.webfingerStatusText.run {
+ text = getString(R.string.error_webfinger_username_empty).also { Timber.d(it) }
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0)
+ isVisible = true
+ }
+ }
+ }
+ }
+ }
+
+ private fun updateOAuthStatusIconAndText(authorizationException: Throwable?) {
+ binding.serverStatusText.run {
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0)
+ text =
+ if (authorizationException is UnauthorizedException) {
+ getString(R.string.auth_oauth_error_access_denied)
+ } else {
+ getString(R.string.auth_oauth_error)
+ }
+ }
+ }
+
+ private fun updateLoginButtonVisibility() {
+ binding.loginButton.run {
+ isVisible = binding.accountUsername.text.toString().isNotBlank() && binding.accountPassword.text.toString().isNotBlank()
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putString(KEY_AUTH_TOKEN_TYPE, authTokenType)
+ }
+
+ override fun finish() {
+ if (accountAuthenticatorResponse != null) { // send the result bundle back if set, otherwise send an error.
+ if (resultBundle != null) {
+ accountAuthenticatorResponse?.onResult(resultBundle)
+ } else {
+ accountAuthenticatorResponse?.onError(
+ AccountManager.ERROR_CODE_CANCELED,
+ "canceled"
+ )
+ }
+ accountAuthenticatorResponse = null
+ }
+ super.finish()
+ }
+
+ override fun optionLockSelected(type: LockType) {
+ manageOptionLockSelected(type)
+ }
+
+ private fun adaptInfiniteEdges() {
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
+ val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ binding.loginLayout.setPadding(systemInsets.left, 0, systemInsets.right, systemInsets.bottom)
+ insets
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/oauth/OAuthUtils.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/oauth/OAuthUtils.kt
new file mode 100644
index 000000000000..346f5156d108
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/oauth/OAuthUtils.kt
@@ -0,0 +1,149 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2025 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.authentication.oauth
+
+import android.content.Context
+import android.net.Uri
+import android.util.Base64
+import com.owncloud.android.MainApp
+import com.owncloud.android.R
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_CLIENT_ID
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_CODE_CHALLENGE
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_CODE_CHALLENGE_METHOD
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_LOGIN_HINT
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_PROMPT
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_REDIRECT_URI
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_RESPONSE_TYPE
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_SCOPE
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_STATE
+import com.owncloud.android.data.authentication.QUERY_PARAMETER_USER
+import com.owncloud.android.domain.authentication.oauth.model.ClientRegistrationRequest
+import java.net.URLEncoder
+import java.security.MessageDigest
+import java.security.SecureRandom
+
+class OAuthUtils {
+
+ fun generateRandomState(): String {
+ val secureRandom = SecureRandom()
+ val randomBytes = ByteArray(DEFAULT_STATE_ENTROPY)
+ secureRandom.nextBytes(randomBytes)
+ val encoding = Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE
+ return Base64.encodeToString(randomBytes, encoding)
+ }
+
+ fun generateRandomCodeVerifier(): String {
+ val secureRandom = SecureRandom()
+ val randomBytes = ByteArray(DEFAULT_CODE_VERIFIER_ENTROPY)
+ secureRandom.nextBytes(randomBytes)
+ val encoding = Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE
+ return Base64.encodeToString(randomBytes, encoding)
+ }
+
+ fun generateCodeChallenge(codeVerifier: String): String {
+ val bytes = codeVerifier.toByteArray()
+ val messageDigest = MessageDigest.getInstance(ALGORITHM_SHA_256)
+ messageDigest.update(bytes)
+ val digest = messageDigest.digest()
+ val encoding = Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE
+ return Base64.encodeToString(digest, encoding)
+ }
+
+ companion object {
+ private const val ALGORITHM_SHA_256 = "SHA-256"
+ private const val CODE_CHALLENGE_METHOD = "S256"
+ private const val DEFAULT_CODE_VERIFIER_ENTROPY = 64
+ private const val DEFAULT_STATE_ENTROPY = 15
+
+ fun buildClientRegistrationRequest(
+ registrationEndpoint: String,
+ context: Context
+ ): ClientRegistrationRequest =
+ ClientRegistrationRequest(
+ registrationEndpoint = registrationEndpoint,
+ clientName = MainApp.userAgent,
+ redirectUris = listOf(buildRedirectUri(context).toString())
+ )
+
+ fun getClientAuth(
+ clientSecret: String,
+ clientId: String
+ ): String {
+ // From the OAuth2 RFC, client ID and secret should be encoded prior to concatenation and
+ // conversion to Base64: https://tools.ietf.org/html/rfc6749#section-2.3.1
+ val encodedClientId = URLEncoder.encode(clientId, "utf-8")
+ val encodedClientSecret = URLEncoder.encode(clientSecret, "utf-8")
+ val credentials = "$encodedClientId:$encodedClientSecret"
+ return "Basic " + Base64.encodeToString(credentials.toByteArray(), Base64.NO_WRAP)
+ }
+
+ fun buildAuthorizationRequest(
+ authorizationEndpoint: Uri,
+ redirectUri: String,
+ clientId: String,
+ responseType: String,
+ scope: String,
+ prompt: String,
+ codeChallenge: String,
+ state: String,
+ username: String?,
+ sendLoginHintAndUser: Boolean,
+ ): Uri =
+ authorizationEndpoint.buildUpon().apply {
+ appendQueryParameter(QUERY_PARAMETER_REDIRECT_URI, redirectUri)
+ appendQueryParameter(QUERY_PARAMETER_CLIENT_ID, clientId)
+ appendQueryParameter(QUERY_PARAMETER_RESPONSE_TYPE, responseType)
+ appendQueryParameter(QUERY_PARAMETER_SCOPE, scope)
+ appendQueryParameter(QUERY_PARAMETER_PROMPT, prompt)
+ appendQueryParameter(QUERY_PARAMETER_CODE_CHALLENGE, codeChallenge)
+ appendQueryParameter(QUERY_PARAMETER_CODE_CHALLENGE_METHOD, CODE_CHALLENGE_METHOD)
+ appendQueryParameter(QUERY_PARAMETER_STATE, state)
+ if (sendLoginHintAndUser && !username.isNullOrEmpty()) {
+ appendQueryParameter(QUERY_PARAMETER_USER, username)
+ appendQueryParameter(QUERY_PARAMETER_LOGIN_HINT, username)
+ }
+ }.build()
+
+ fun buildRedirectUri(context: Context, isKiteworksServer: Boolean = false): Uri {
+ val scheme: String
+ val host: String
+ val path: String
+
+ if (isKiteworksServer) {
+ scheme = context.getString(R.string.kiteworks_redirect_uri_scheme)
+ host = context.getString(R.string.kiteworks_redirect_uri_host)
+ path = context.getString(R.string.kiteworks_redirect_uri_path)
+ } else {
+ scheme = context.getString(R.string.oauth2_redirect_uri_scheme)
+ host = context.getString(R.string.oauth2_redirect_uri_host)
+ path = context.getString(R.string.oauth2_redirect_uri_path)
+ }
+
+ return Uri.Builder()
+ .scheme(scheme)
+ .authority(host)
+ .path(path)
+ .build()
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/oauth/OAuthViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/oauth/OAuthViewModel.kt
new file mode 100644
index 000000000000..4da984931df9
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/oauth/OAuthViewModel.kt
@@ -0,0 +1,94 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * Copyright (C) 2020 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.authentication.oauth
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.ViewModel
+import com.owncloud.android.MainApp
+import com.owncloud.android.domain.authentication.oauth.OIDCDiscoveryUseCase
+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
+import com.owncloud.android.domain.authentication.oauth.model.OIDCServerConfiguration
+import com.owncloud.android.domain.authentication.oauth.model.TokenRequest
+import com.owncloud.android.domain.authentication.oauth.model.TokenResponse
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.providers.CoroutinesDispatcherProvider
+
+class OAuthViewModel(
+ private val getOIDCDiscoveryUseCase: OIDCDiscoveryUseCase,
+ private val requestTokenUseCase: RequestTokenUseCase,
+ private val registerClientUseCase: RegisterClientUseCase,
+ private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider
+) : ViewModel() {
+
+ val codeVerifier: String = OAuthUtils().generateRandomCodeVerifier()
+ val codeChallenge: String = OAuthUtils().generateCodeChallenge(codeVerifier)
+ val oidcState: String = OAuthUtils().generateRandomState()
+
+ private val _oidcDiscovery = MediatorLiveData>>()
+ val oidcDiscovery: LiveData>> = _oidcDiscovery
+
+ private val _registerClient = MediatorLiveData>>()
+ val registerClient: LiveData>> = _registerClient
+
+ private val _requestToken = MediatorLiveData>>()
+ val requestToken: LiveData>> = _requestToken
+
+ fun getOIDCServerConfiguration(
+ serverUrl: String
+ ) = runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ showLoading = false,
+ liveData = _oidcDiscovery,
+ useCase = getOIDCDiscoveryUseCase,
+ useCaseParams = OIDCDiscoveryUseCase.Params(baseUrl = serverUrl)
+ )
+
+ fun registerClient(
+ registrationEndpoint: String
+ ) {
+ val registrationRequest = OAuthUtils.buildClientRegistrationRequest(
+ registrationEndpoint = registrationEndpoint,
+ MainApp.appContext
+ )
+
+ runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ showLoading = false,
+ liveData = _registerClient,
+ useCase = registerClientUseCase,
+ useCaseParams = RegisterClientUseCase.Params(registrationRequest)
+ )
+ }
+
+ fun requestToken(
+ tokenRequest: TokenRequest
+ ) = runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ showLoading = false,
+ liveData = _requestToken,
+ useCase = requestTokenUseCase,
+ useCaseParams = RequestTokenUseCase.Params(tokenRequest = tokenRequest)
+ )
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/avatar/AvatarManager.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/avatar/AvatarManager.kt
new file mode 100644
index 000000000000..aba2d17e711b
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/avatar/AvatarManager.kt
@@ -0,0 +1,146 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * Copyright (C) 2020 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.avatar
+
+import android.accounts.Account
+import android.graphics.BitmapFactory
+import android.graphics.drawable.Drawable
+import android.media.ThumbnailUtils
+import com.owncloud.android.MainApp.Companion.appContext
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.ThumbnailsCacheManager
+import com.owncloud.android.domain.UseCaseResult
+import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
+import com.owncloud.android.domain.exceptions.FileNotFoundException
+import com.owncloud.android.domain.user.model.UserAvatar
+import com.owncloud.android.domain.user.usecases.GetUserAvatarAsyncUseCase
+import com.owncloud.android.ui.DefaultAvatarTextDrawable
+import com.owncloud.android.utils.BitmapUtils
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import org.koin.core.error.InstanceCreationException
+import timber.log.Timber
+import kotlin.math.roundToInt
+
+/**
+ * The avatar is loaded if available in the cache and bound to the received UI element. The avatar is not
+ * fetched from the server if not available, unless the parameter 'fetchFromServer' is set to 'true'.
+ *
+ * If there is no avatar stored and cannot be fetched, a colored icon is generated with the first
+ * letter of the account username.
+ *
+ * If this is not possible either, a predefined user icon is bound instead.
+ */
+class AvatarManager : KoinComponent {
+
+ fun getAvatarForAccount(
+ account: Account,
+ fetchIfNotCached: Boolean,
+ displayRadius: Float
+ ): Drawable? {
+ val imageKey = getImageKeyForAccount(account)
+
+ // Check disk cache in background thread
+ val avatarBitmap = ThumbnailsCacheManager.getBitmapFromDiskCache(imageKey)
+ avatarBitmap?.let {
+ Timber.i("Avatar retrieved from cache with imageKey: $imageKey")
+ return BitmapUtils.bitmapToCircularBitmapDrawable(appContext.resources, it)
+ }
+
+ val shouldFetchAvatar = try {
+ val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase by inject()
+ val storedCapabilities = getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(account.name))
+ storedCapabilities?.isFetchingAvatarAllowed() ?: true
+ } catch (instanceCreationException: InstanceCreationException) {
+ Timber.e(instanceCreationException, "Koin may not be initialized at this point")
+ true
+ }
+
+ // Avatar not found in disk cache, fetch from server.
+ if (fetchIfNotCached && shouldFetchAvatar) {
+ Timber.i("Avatar with imageKey $imageKey is not available in cache. Fetching from server...")
+ val getUserAvatarAsyncUseCase: GetUserAvatarAsyncUseCase by inject()
+ val useCaseResult =
+ getUserAvatarAsyncUseCase(GetUserAvatarAsyncUseCase.Params(accountName = account.name))
+ handleAvatarUseCaseResult(account, useCaseResult)?.let { return it }
+ }
+
+ // generate placeholder from user name
+ try {
+ Timber.i("Avatar with imageKey $imageKey is not available in cache. Generating one...")
+ return DefaultAvatarTextDrawable.createAvatar(account.name, displayRadius)
+
+ } catch (e: Exception) {
+ // nothing to do, return null to apply default icon
+ Timber.e(e, "Error calculating RGB value for active account icon.")
+ }
+ return null
+ }
+
+ /**
+ * Converts size of file icon from dp to pixel
+ *
+ * @return int
+ */
+ private fun getAvatarDimension(): Int = appContext.resources.getDimension(R.dimen.file_avatar_size).roundToInt()
+
+ private fun getImageKeyForAccount(account: Account) = "a_${account.name}"
+
+ /**
+ * If [GetUserAvatarAsyncUseCase] is success, add avatar to cache and return a circular drawable.
+ * If there is no avatar available in server, remove it from cache.
+ */
+ fun handleAvatarUseCaseResult(
+ account: Account,
+ useCaseResult: UseCaseResult
+ ): Drawable? {
+ Timber.d("Fetch avatar use case is success: ${useCaseResult.isSuccess}")
+ val imageKey = getImageKeyForAccount(account)
+
+ if (useCaseResult.isSuccess) {
+ val userAvatar = useCaseResult.getDataOrNull()
+ userAvatar?.let {
+ try {
+ var bitmap = BitmapFactory.decodeByteArray(it.avatarData, 0, it.avatarData.size)
+ bitmap = ThumbnailUtils.extractThumbnail(bitmap, getAvatarDimension(), getAvatarDimension())
+ // Add avatar to cache
+ bitmap?.let {
+ ThumbnailsCacheManager.addBitmapToCache(imageKey, bitmap)
+ Timber.d("User avatar saved into cache -> %s", imageKey)
+ return BitmapUtils.bitmapToCircularBitmapDrawable(appContext.resources, bitmap)
+ }
+ } catch (t: OutOfMemoryError) {
+ // the app should never break due to a problem with avatars
+ Timber.e(t, "Generation of avatar for $imageKey failed")
+ System.gc()
+ null
+ } catch (t: Throwable) {
+ Timber.e(t, "Generation of avatar for $imageKey failed")
+ null
+ }
+ }
+
+ } else if (useCaseResult.getThrowableOrNull() is FileNotFoundException) {
+ Timber.i("No avatar available, removing cached copy")
+ ThumbnailsCacheManager.removeBitmapFromCache(imageKey)
+ }
+ return null
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/avatar/AvatarUtils.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/avatar/AvatarUtils.kt
new file mode 100644
index 000000000000..9c1653f6290b
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/avatar/AvatarUtils.kt
@@ -0,0 +1,97 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * Copyright (C) 2020 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.avatar
+
+import android.accounts.Account
+import android.view.MenuItem
+import android.widget.ImageView
+import com.owncloud.android.R
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class AvatarUtils : KoinComponent {
+
+ private val avatarManager: AvatarManager by inject()
+
+ /**
+ * Show the avatar corresponding to the received account in an {@ImageView}.
+ *
+ * The avatar is shown if available locally in {@link ThumbnailsCacheManager}. The avatar is not
+ * fetched from the server if not available.
+ *
+ * If there is no avatar stored, a colored icon is generated with the first letter of the account username.
+ *
+ * If this is not possible either, a predefined user icon is shown instead.
+ *
+ * @param account OC account which avatar will be shown.
+ * @param displayRadius The radius of the circle where the avatar will be clipped into.
+ * @param fetchIfNotCached When 'true', if there is no avatar stored in the cache, it's fetched from
+ * the server. When 'false', server is not accessed, the fallback avatar is
+ * generated instead.
+ */
+ fun loadAvatarForAccount(
+ imageView: ImageView,
+ account: Account,
+ fetchIfNotCached: Boolean = false,
+ displayRadius: Float
+ ) {
+ // Tech debt: Move this to a viewModel and use its viewModelScope instead
+ CoroutineScope(Dispatchers.IO).launch {
+ val drawable = avatarManager.getAvatarForAccount(
+ account = account,
+ fetchIfNotCached = fetchIfNotCached,
+ displayRadius = displayRadius
+ )
+ withContext(Dispatchers.Main) {
+ if (drawable != null) {
+ imageView.setImageDrawable(drawable)
+ } else {
+ imageView.setImageResource(R.drawable.ic_account_circle)
+ }
+ }
+ }
+ }
+
+ fun loadAvatarForAccount(
+ menuItem: MenuItem,
+ account: Account,
+ fetchIfNotCached: Boolean = false,
+ displayRadius: Float
+ ) {
+ CoroutineScope(Dispatchers.IO).launch {
+ val drawable = avatarManager.getAvatarForAccount(
+ account = account,
+ fetchIfNotCached = fetchIfNotCached,
+ displayRadius = displayRadius
+ )
+ withContext(Dispatchers.Main) {
+ if (drawable != null) {
+ menuItem.icon = drawable
+ } else {
+ menuItem.setIcon(R.drawable.ic_account_circle)
+ }
+ }
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt
new file mode 100644
index 000000000000..0ce70ae3fff7
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/capabilities/CapabilityViewModel.kt
@@ -0,0 +1,89 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2026 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.capabilities
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.ViewModel
+import com.owncloud.android.domain.capabilities.model.OCCapability
+import com.owncloud.android.domain.capabilities.usecases.GetCapabilitiesAsLiveDataUseCase
+import com.owncloud.android.domain.capabilities.usecases.GetStoredCapabilitiesUseCase
+import com.owncloud.android.domain.capabilities.usecases.RefreshCapabilitiesFromServerAsyncUseCase
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResultAndUseCachedData
+import com.owncloud.android.presentation.common.UIResult
+import com.owncloud.android.providers.CoroutinesDispatcherProvider
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+/**
+ * View Model to keep a reference to the capability repository and an up-to-date capability
+ */
+class CapabilityViewModel(
+ private val accountName: String,
+ getCapabilitiesAsLiveDataUseCase: GetCapabilitiesAsLiveDataUseCase,
+ private val refreshCapabilitiesFromServerAsyncUseCase: RefreshCapabilitiesFromServerAsyncUseCase,
+ private val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase,
+ private val coroutineDispatcherProvider: CoroutinesDispatcherProvider
+) : ViewModel() {
+
+ private val _capabilities = MediatorLiveData>>()
+ val capabilities: LiveData>> = _capabilities
+
+ private var capabilitiesLiveData: LiveData = getCapabilitiesAsLiveDataUseCase(
+ GetCapabilitiesAsLiveDataUseCase.Params(
+ accountName = accountName
+ )
+ )
+
+ init {
+ _capabilities.addSource(capabilitiesLiveData) { capabilities ->
+ _capabilities.postValue(Event(UIResult.Success(capabilities)))
+ }
+
+ refreshCapabilitiesFromNetwork()
+ }
+
+ fun refreshCapabilitiesFromNetwork() = runUseCaseWithResultAndUseCachedData(
+ coroutineDispatcher = coroutineDispatcherProvider.io,
+ cachedData = capabilitiesLiveData.value,
+ liveData = _capabilities,
+ useCase = refreshCapabilitiesFromServerAsyncUseCase,
+ useCaseParams = RefreshCapabilitiesFromServerAsyncUseCase.Params(
+ accountName = accountName
+ )
+ )
+
+ fun isOcisServer(): Boolean = runBlocking(coroutineDispatcherProvider.io) {
+ getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))?.isSpacesAllowed() == true
+ }
+
+ fun checkMultiPersonal(): Boolean = runBlocking(CoroutinesDispatcherProvider().io) {
+ val capabilities = withContext(CoroutinesDispatcherProvider().io) {
+ getStoredCapabilitiesUseCase(GetStoredCapabilitiesUseCase.Params(accountName))
+ }
+ capabilities?.spaces?.hasMultiplePersonalSpaces == true
+ }
+
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/common/BottomSheetFragmentItemView.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/common/BottomSheetFragmentItemView.kt
new file mode 100644
index 000000000000..74af93f728fd
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/common/BottomSheetFragmentItemView.kt
@@ -0,0 +1,91 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * @author Juan Carlos Garrote Gascón
+ * @author Aitor Ballesteros Pavón
+ *
+ * Copyright (C) 2024 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.common
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import com.owncloud.android.R
+import com.owncloud.android.databinding.BottomSheetFragmentItemBinding
+
+class BottomSheetFragmentItemView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : ConstraintLayout(context, attrs, defStyle) {
+
+ private var _binding: BottomSheetFragmentItemBinding? = null
+ private val binding get() = _binding!!
+
+ var itemIcon: Drawable?
+ get() = binding.itemIcon.drawable
+ set(value) {
+ binding.itemIcon.setImageDrawable(value)
+ }
+
+ var title: CharSequence?
+ get() = binding.itemTitle.text
+ set(value) {
+ binding.itemTitle.text = value
+ }
+
+ var itemAdditionalIcon: Drawable?
+ get() = binding.itemAdditionalIcon.drawable
+ set(value) {
+ binding.itemAdditionalIcon.setImageDrawable(value)
+ }
+
+ init {
+ _binding = BottomSheetFragmentItemBinding.inflate(LayoutInflater.from(context), this, true)
+
+ val a = context.theme.obtainStyledAttributes(attrs, R.styleable.BottomSheetFragmentItemView, 0, 0)
+ try {
+ itemIcon = a.getDrawable(R.styleable.BottomSheetFragmentItemView_itemIcon)
+ title = a.getString(R.styleable.BottomSheetFragmentItemView_title)
+ } finally {
+ a.recycle()
+ }
+ }
+
+ fun setSelected(iconAdditional: Int) {
+ itemAdditionalIcon = ContextCompat.getDrawable(context, iconAdditional)
+ val selectedColor = ContextCompat.getColor(context, R.color.primary)
+ binding.itemIcon.setColorFilter(selectedColor)
+ binding.itemTitle.setTextColor(selectedColor)
+ binding.itemAdditionalIcon.setColorFilter(selectedColor)
+ }
+
+ fun removeDefaultTint() {
+ binding.itemIcon.imageTintList = null
+ }
+
+ fun addDefaultTint(tintColor: Int) {
+ val itemColor = ContextCompat.getColor(context, tintColor)
+ val itemColorStateList = ColorStateList.valueOf(itemColor)
+ binding.itemIcon.imageTintList = itemColorStateList
+ }
+}
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
new file mode 100644
index 000000000000..0e7acd21f10b
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/common/DrawerViewModel.kt
@@ -0,0 +1,102 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * @author Abel García de Prada
+ * @author Aitor Ballesteros Pavón
+ * @author Jorge Aguado Recio
+ *
+ * Copyright (C) 2024 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.common
+
+import android.accounts.Account
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.owncloud.android.R
+import com.owncloud.android.data.providers.LocalStorageProvider
+import com.owncloud.android.domain.user.model.UserQuota
+import com.owncloud.android.domain.user.usecases.GetStoredQuotaAsStreamUseCase
+import com.owncloud.android.domain.user.usecases.GetUserQuotasUseCase
+import com.owncloud.android.domain.utils.Event
+import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult
+import com.owncloud.android.presentation.authentication.AccountUtils
+import com.owncloud.android.providers.ContextProvider
+import com.owncloud.android.providers.CoroutinesDispatcherProvider
+import com.owncloud.android.usecases.accounts.RemoveAccountUseCase
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import timber.log.Timber
+
+class DrawerViewModel(
+ private val getStoredQuotaAsStreamUseCase: GetStoredQuotaAsStreamUseCase,
+ private val removeAccountUseCase: RemoveAccountUseCase,
+ private val getUserQuotasUseCase: GetUserQuotasUseCase,
+ private val localStorageProvider: LocalStorageProvider,
+ private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider,
+ private val contextProvider: ContextProvider,
+) : ViewModel() {
+
+ private val _userQuota = MutableStateFlow>>?>(null)
+ val userQuota: StateFlow>>?> = _userQuota
+
+ fun getUserQuota(accountName: String) {
+ runUseCaseWithResult(
+ coroutineDispatcher = coroutinesDispatcherProvider.io,
+ requiresConnection = false,
+ showLoading = true,
+ flow = _userQuota,
+ useCase = getStoredQuotaAsStreamUseCase,
+ useCaseParams = GetStoredQuotaAsStreamUseCase.Params(accountName = accountName),
+ )
+ }
+
+ fun getAccounts(context: Context): List =
+ AccountUtils.getAccounts(context).asList()
+
+ fun getUsernameOfAccount(accountName: String): String =
+ AccountUtils.getUsernameOfAccount(accountName)
+
+ fun getFeedbackMail() = contextProvider.getString(R.string.mail_feedback)
+
+ fun removeAccount(context: Context) {
+ viewModelScope.launch(coroutinesDispatcherProvider.io) {
+ val loggedAccounts = AccountUtils.getAccounts(context)
+ localStorageProvider.deleteUnusedUserDirs(loggedAccounts)
+
+ val userQuotas = getUserQuotasUseCase(Unit)
+ val loggedAccountsNames = loggedAccounts.map { it.name }
+ val totalAccountsNames = userQuotas.map { it.accountName }
+ val removedAccountsNames = mutableListOf()
+ for (accountName in totalAccountsNames) {
+ if (!loggedAccountsNames.contains(accountName)) {
+ removedAccountsNames.add(accountName)
+ }
+ }
+ removedAccountsNames.forEach { removedAccountName ->
+ Timber.d("$removedAccountName is being removed")
+ removeAccountUseCase(
+ RemoveAccountUseCase.Params(accountName = removedAccountName)
+ )
+ localStorageProvider.removeLocalStorageForAccount(removedAccountName)
+ }
+ }
+ }
+
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/common/ShareSheetHelper.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/common/ShareSheetHelper.kt
new file mode 100644
index 000000000000..ae08307794e9
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/common/ShareSheetHelper.kt
@@ -0,0 +1,60 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author Abel García de Prada
+ * Copyright (C) 2020 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.common
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Parcelable
+import androidx.annotation.StringRes
+
+class ShareSheetHelper {
+
+ fun getShareSheetIntent(
+ intent: Intent,
+ context: Context,
+ @StringRes title: Int,
+ packagesToExclude: Array
+ ): Intent {
+
+ // Get excluding specific targets by component. We want to hide oC targets.
+ val resInfo: List =
+ context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
+ val excludeLists = ArrayList()
+ if (resInfo.isNotEmpty()) {
+ for (info in resInfo) {
+ val activityInfo = info.activityInfo
+ for (packageToExclude in packagesToExclude) {
+ if (activityInfo != null && activityInfo.packageName == packageToExclude) {
+ excludeLists.add(ComponentName(activityInfo.packageName, activityInfo.name))
+ }
+ }
+ }
+ }
+
+ // Return a new ShareSheet intent
+ return Intent.createChooser(intent, "").apply {
+ putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, excludeLists.toArray(arrayOf()))
+ putExtra(Intent.EXTRA_TITLE, context.getString(title))
+ }
+ }
+}
diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/common/UIResult.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/common/UIResult.kt
new file mode 100644
index 000000000000..c94df13a9ed3
--- /dev/null
+++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/common/UIResult.kt
@@ -0,0 +1,72 @@
+/**
+ * ownCloud Android client application
+ *
+ * @author David González Verdugo
+ * Copyright (C) 2020 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.common
+
+sealed class UIResult {
+ data class Loading(val data: T? = null) : UIResult()
+ data class Success(val data: T? = null) : UIResult()
+ data class Error(val error: Throwable? = null, val data: T? = null) : UIResult()
+
+ val isLoading get() = this is Loading
+ val isSuccess get() = this is Success
+ val isError get() = this is Error
+
+ @Deprecated(message = "Start to use new extensions")
+ fun getStoredData(): T? =
+ when (this) {
+ is Loading -> data
+ is Success -> data
+ is Error -> data // Even when there's an error we still want to show database data
+ }
+
+ fun getThrowableOrNull(): Throwable? =
+ if (this is Error) {
+ error
+ } else {
+ null
+ }
+}
+
+fun UIResult.onLoading(action: (data: T?) -> Unit): UIResult