Compare commits

..

55 Commits

Author SHA1 Message Date
Ricki Hirner
218559afb6 Update comments in HttpClientBuilder.kt for clarity 2026-01-26 17:45:27 +01:00
Ricki Hirner
256b3381c9 [WIP] Cache SSLContext by certificate alias
- Add context cache using Guava CacheBuilder
- Cache SSLContext in getContext method
2026-01-25 13:25:03 +01:00
Ricki Hirner
ee57967152 Implement connection security manager for HTTP client
- Introduce `ConnectionSecurityManager` and `ConnectionSecurityContext` classes
- Refactor `HttpClientBuilder` to use the new security manager for SSL context setup
2026-01-25 13:07:06 +01:00
Ricki Hirner
a144180c70 Reuse CustomCertManager
- Update bitfire-cert4android to 75cc6913fd
- Refactor HttpClientBuilder to use Optional for customTrustManager and customHostnameVerifier
- Add CustomCertManagerModule for dependency injection
2026-01-25 12:43:48 +01:00
Ricki Hirner
47685e6693 [WebDAV] Rewrite COPY/MOVE (including rename) to Ktor (#1940)
* [WebDAV] Refactor RenameDocumentOperation to Ktor

- Update imports to use Ktor-based classes
- Refactor `RenameDocumentOperation` to use Ktor HTTP client
- Add support for both HttpException types in `throwForDocumentProvider`

* Rewrite CopyDocumentOperation.kt to Ktor

* Refactor URLBuilder usage

- Update URLBuilder usage in RenameDocumentOperation.kt
- Update URLBuilder usage in CopyDocumentOperation.kt
- Update URLBuilder usage in MoveDocumentOperation.kt

* - Pass `ioDispatcher` to `runBlocking` in WebDAV operations
- Refactor timeout configuration in HttpClientBuilder for reusability

* Add logging to DocumentProviderUtils

- Introduce a logger instance
- Log URI when notifying folder changes
2026-01-24 19:28:50 +01:00
Ricki Hirner
2c7b36ecd5 Use Ktor for Push registration (#1930)
* Replace OkHttp with Ktor for push notifications

* Use Ktor HttpHeaders for Location and Expires
2026-01-23 10:27:16 +01:00
Ricki Hirner
cf80b11808 [WebDAV] Rewrite OpenDocumentThumbnailOperation to Ktor (#1931)
* Add Ktor HTTP client support

- Introduce `buildKtor` method in `DavHttpClientBuilder` for creating Ktor HTTP clients.
- Update `OpenDocumentThumbnailOperation` to use Ktor for downloading and creating thumbnails.

* Refactor HttpClientBuilder creation

- Extract common logic into `createBuilder` method
- Update `build` and `buildKtor` methods to use `createBuilder`

* Refactor OpenDocumentThumbnailOperation

- Remove unnecessary `withContext` call
- Use `HttpHeaders.Accept` and `ContentType.Image.Any` for HTTP header
- Simplify the function structure

* Refactor thumbnail generation

- Remove redundant `accessScope`
- Simplify and encapsulate thumbnail creation logic
- Ensure proper cancellation handling

* Update OpenDocumentThumbnailOperation logging

- Enhance cancellation log message with document ID
- Improve URL conversion warning message

* Update WebDAV operations and document handling

- Add `@MustBeClosed` annotation to `buildKtor` method in `DavHttpClientBuilder`
- Remove unnecessary imports and update URL conversion in `OpenDocumentThumbnailOperation`
- Add `toKtorUrl` method in `WebDavDocument` for URL conversion

* Use streaming bitmap decoding

* Add comments to OpenDocumentThumbnailOperation for future improvements

---------

Co-authored-by: Arnau Mora <arnyminerz@proton.me>
2026-01-22 17:05:32 +01:00
Sunik Kupfer
18649f711a DmfsTaskList refactoring (#1934)
* DmfsTaskList refactoring

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Update synctools

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2026-01-21 13:17:34 +01:00
Sunik Kupfer
377a159e75 Ignore test with flaky behaviour in CI (#1936)
Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2026-01-21 13:17:23 +01:00
Ricki Hirner
393d22f720 AGP 9.0: update Hilt, remove Kotlin Android plugin (#1935)
- Remove `android.builtInKotlin` from `gradle.properties`
- Update Hilt version to 2.59
- Remove Kotlin Android plugin from `libs.versions.toml` and build scripts
2026-01-21 11:28:40 +01:00
dependabot[bot]
5b12ecf6b6 Bump the app-dependencies group across 1 directory with 2 updates (#1933)
Bumps the app-dependencies group with 2 updates in the / directory: androidx.compose:compose-bom and [dnsjava:dnsjava](https://github.com/dnsjava/dnsjava).


Updates `androidx.compose:compose-bom` from 2025.12.01 to 2026.01.00

Updates `dnsjava:dnsjava` from 3.6.3 to 3.6.4
- [Release notes](https://github.com/dnsjava/dnsjava/releases)
- [Changelog](https://github.com/dnsjava/dnsjava/blob/master/Changelog)
- [Commits](https://github.com/dnsjava/dnsjava/compare/v3.6.3...v3.6.4)

---
updated-dependencies:
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2026.01.00
  dependency-type: direct:production
  dependency-group: app-dependencies
- dependency-name: dnsjava:dnsjava
  dependency-version: 3.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 16:34:51 +01:00
Ricki Hirner
f8f6134640 Update AGP to 9.0.0 (#1929)
* Update gradle wrapper

* Update AGP to 9.0.0 (legacy mode) and synctools (which now also uses AGP 9.0.0)
2026-01-20 12:45:58 +01:00
Ricki Hirner
0f7908da23 Remove Transifex config/scripts (#1924) 2026-01-19 10:59:34 +01:00
Weblate (bot)
40741f52e1 Translations update from Hosted Weblate (#1928)
* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ app strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/pt_BR/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ app strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/zh_Hans/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ app strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/et/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ app strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/nl/

* Added translation using Weblate (Lithuanian)

* Translated using Weblate (Lithuanian)

Currently translated at 7.6% (33 of 430 strings)

Translation: davx5/DAVx⁵ app strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/lt/

* Translated using Weblate (Georgian)

Currently translated at 84.6% (364 of 430 strings)

Translation: davx5/DAVx⁵ app strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/ka/

---------

Co-authored-by: LucasMZ <git@lucasmz.dev>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Vaclovas Intas <Gateway_31@protonmail.com>
Co-authored-by: Temuri Doghonadze <temuri.doghonadze@gmail.com>
2026-01-19 09:48:24 +01:00
Ricki Hirner
210a03bd1a Bump version to 4.5.8 2026-01-19 09:35:38 +01:00
Ricki Hirner
714c92b8d9 Bump version to 4.5.8-rc.2 2026-01-16 15:45:48 +01:00
Ricki Hirner
5ea937a0f9 Update dav4jvm to catch URLDecodeException when converting String to Ktor Url (#1923) 2026-01-16 15:43:54 +01:00
Ricki Hirner
126b742887 Decode data URIs of vCard 3 PHOTOs (#1921)
* Rename ResourceDownloader to ResourceRetriever (because it should support `data` URLs that don't have to be downloaded)

* Update `ResourceRetriever` to handle data URIs and HTTP/HTTPS URLs

* Handle invalid data URIs
2026-01-16 11:52:49 +01:00
Weblate (bot)
2de7e09c82 Translations update from Hosted Weblate (#1917)
* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ app strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/pt_BR/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ app strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/zh_Hans/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ app strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/et/

---------

Co-authored-by: LucasMZ <git@lucasmz.dev>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
2026-01-14 12:03:49 +01:00
Ricki Hirner
0312f59aab Bump version to 4.5.8-rc.1 2026-01-14 12:00:05 +01:00
Weblate (bot)
b3682ded1a Translations update from Hosted Weblate (#1915)
* Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Translation: davx5/DAVx⁵ strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/pt_BR/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ strings (main)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-strings/zh_Hans/

* Translated using Weblate (Catalan)

Currently translated at 100.0% (4 of 4 strings)

Translation: davx5/DAVx⁵ app metadata (for F-Droid)
Translate-URL: https://hosted.weblate.org/projects/davx5/davx5-ose-fastlane/ca/

---------

Co-authored-by: LucasMZ <git@lucasmz.dev>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Arnau Mora <arnyminer.z@gmail.com>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2026-01-13 16:00:16 +01:00
Weblate (bot)
529378000a Translations update from Hosted Weblate (#1913)
* Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Translation: davx5/DAVx⁵ Main Strings
Translate-URL: https://hosted.weblate.org/projects/davx5/davx-main-strings/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (430 of 430 strings)

Translation: davx5/DAVx⁵ Main Strings
Translate-URL: https://hosted.weblate.org/projects/davx5/davx-main-strings/zh_Hans/

---------

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2026-01-13 12:41:45 +01:00
Ricki Hirner
8644c94c34 [Backport from davx5] Remove empty fa-rIR translations 2026-01-13 12:07:08 +01:00
Ricki Hirner
5357bdefb8 Fastlane app descriptions: use fr-FR translation for French (#1912) 2026-01-12 14:43:36 +01:00
Arnau Mora
07b646e4e7 Remove r from lang names as per Fastlane requirement (#1897)
* Remove `r` from lang names as per Fastlane requirement

* Add `lang_map` to fastlane resources

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2026-01-12 14:31:09 +01:00
Sunik Kupfer
77cb3e659d Improve pagination (#1911)
* Cache paging data flow in view model

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Replace let with early return

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Move only-personal filtering decision to repository

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Move companion object to the end of class

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Use default dispatcher for account settings interaction

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Add androidx prefixes

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Add kdoc

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Move cachedIn to use-case

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Update kdoc

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2026-01-12 13:48:02 +01:00
Ricki Hirner
42d65c872e Explain clear-text traffic in network security policy (bitfireAT/davx5#760)
Allow clear-text traffic for all variants; more documentation
2026-01-12 10:43:29 +01:00
Sunik Kupfer
c3fd28f820 Return null when owner account does not exist to skip flaky test (#1908)
Return null when owner account does not exist

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2026-01-08 12:18:37 +01:00
Ricki Hirner
9ad98e4a16 Enable HTTP connection reuse for okhttp (#1907)
* Enable HTTP connection reuse

- Create a shared OkHttpClient instance for connection pooling
- Utilize sharedOkHttpClient.newBuilder() for new client instances

* Add test

* KDoc clarification
2026-01-06 13:28:48 +01:00
dependabot[bot]
27c1be948f Bump org.unifiedpush.android:connector from 3.1.2 to 3.2.0 in the app-dependencies group (#1906)
Bump org.unifiedpush.android:connector in the app-dependencies group

Bumps the app-dependencies group with 1 update: org.unifiedpush.android:connector.


Updates `org.unifiedpush.android:connector` from 3.1.2 to 3.2.0

---
updated-dependencies:
- dependency-name: org.unifiedpush.android:connector
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-05 16:50:08 +01:00
Ricki Hirner
ac7ef1a7e5 Update README and CONTRIBUTING (#1905) 2026-01-05 14:26:10 +01:00
Ricki Hirner
e0eb13f57b CodeQL: run for main branch PRs (#1904)
CodeQL: run for main branch PRs, adapt build command
2026-01-05 12:27:25 +01:00
dependabot[bot]
85c4fc76f2 Bump the app-dependencies group across 1 directory with 5 updates (#1902)
Bumps the app-dependencies group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| androidx.activity:activity-compose | `1.12.1` | `1.12.2` |
| androidx.compose:compose-bom | `2025.12.00` | `2025.12.01` |
| [com.mikepenz:aboutlibraries-compose-m3](https://github.com/mikepenz/AboutLibraries) | `13.1.0` | `13.2.1` |
| com.mikepenz.aboutlibraries.plugin.android | `13.1.0` | `13.2.1` |
| [com.google.devtools.ksp](https://github.com/google/ksp) | `2.3.3` | `2.3.4` |



Updates `androidx.activity:activity-compose` from 1.12.1 to 1.12.2

Updates `androidx.compose:compose-bom` from 2025.12.00 to 2025.12.01

Updates `com.mikepenz:aboutlibraries-compose-m3` from 13.1.0 to 13.2.1
- [Release notes](https://github.com/mikepenz/AboutLibraries/releases)
- [Commits](https://github.com/mikepenz/AboutLibraries/compare/13.1.0...13.2.1)

Updates `com.mikepenz.aboutlibraries.plugin.android` from 13.1.0 to 13.2.1

Updates `com.google.devtools.ksp` from 2.3.3 to 2.3.4
- [Release notes](https://github.com/google/ksp/releases)
- [Commits](https://github.com/google/ksp/compare/2.3.3...2.3.4)

Updates `com.mikepenz.aboutlibraries.plugin.android` from 13.1.0 to 13.2.1

---
updated-dependencies:
- dependency-name: androidx.activity:activity-compose
  dependency-version: 1.12.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2025.12.01
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.mikepenz:aboutlibraries-compose-m3
  dependency-version: 13.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.mikepenz.aboutlibraries.plugin.android
  dependency-version: 13.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.google.devtools.ksp
  dependency-version: 2.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.mikepenz.aboutlibraries.plugin.android
  dependency-version: 13.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-04 12:38:52 +01:00
Ricki Hirner
6bd9422f3b [CI] Don't require build cache secrets (#1900)
* Don't require build cache secrets

* Use our labels for Dependabot PRs
2026-01-04 12:06:28 +01:00
Ricki Hirner
9754770238 [CI] Fix pre-populating configuration cache 2025-12-18 10:36:29 +01:00
Ricki Hirner
6be15fd366 [CI] Actually use configuration cache (#1891)
* Cache configurations per job

* Use separate job for Dependency submission

* Use GRADLE_OPTS to enable build and configuration cache

* Test .android

* Cache .android for configuration cache

* Disable CodeQL for PRs

* Fix AVD path
2025-12-18 10:27:01 +01:00
Ricki Hirner
0cb27f0c2f [CI] Add gradle remote build cache (bitfireAT/davx5#752)
* [CI] Add gradle remote build cache

* Update workflow

* Don't cache local build cache; pre-populate configuration cache

* Allow configuration caching of tasks

* Free some disk space before running instrumented tests; cache whole .android (not only .android/avd)

* Allow branches to update configuration cache

* Use dry run to pre-populate configuration cache

* Test runs: don't cache

* Fix remote build cache configuration for non-CI builds

* Add comment
2025-12-17 18:06:17 +01:00
Sunik Kupfer
776305bd12 Rename dismissInvalidResource to dismissCollectionError (#1887)
Rename method for clarity

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-12-17 13:14:06 +01:00
dependabot[bot]
01f54df3c0 [CI] Bump actions/cache from 4 to 5 in the ci-actions group (#1886)
Bumps the ci-actions group with 1 update: [actions/cache](https://github.com/actions/cache).


Updates `actions/cache` from 4 to 5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 13:38:54 +01:00
Ricki Hirner
4944ce59b1 Version bump to 4.5.8-alpha.1 2025-12-12 15:32:30 +01:00
Sunik Kupfer
0e455d8371 LocalTaskList: Stop subclassing DmfsTaskList (#1882)
* LocalTaskList: Stop subclassing DmfsTaskList

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

# Conflicts:
#	gradle/libs.versions.toml

* Dont touch agp

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Update synctools

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-12-11 16:22:02 +01:00
Ricki Hirner
a938b511cd Nextcloud Login Flow: handle non-success status codes (#1878)
* Nextcloud Login Flow: handle non-success status codes

* Update error message to use class name when localized message is null

* Update dav4jvm to get HTTP reason phrases in HttpException
2025-12-11 15:30:32 +01:00
Ricki Hirner
d32b86789b Update AGP 2025-12-11 15:13:15 +01:00
Sunik Kupfer
84d58f73db Assume initial state for test updatesOwnerAccount (#1874)
* Assume initial state

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Enhance comment

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Don't pass provider

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-12-11 14:16:03 +01:00
dependabot[bot]
cc43998148 Bump the app-dependencies group with 4 updates (#1867)
* Bump the app-dependencies group with 4 updates

Bumps the app-dependencies group with 4 updates: androidx.activity:activity-compose, androidx.compose:compose-bom, [io.mockk:mockk](https://github.com/mockk/mockk) and [io.mockk:mockk-android](https://github.com/mockk/mockk).


Updates `androidx.activity:activity-compose` from 1.12.0 to 1.12.1

Updates `androidx.compose:compose-bom` from 2025.11.01 to 2025.12.00

Updates `io.mockk:mockk` from 1.14.5 to 1.14.7
- [Release notes](https://github.com/mockk/mockk/releases)
- [Commits](https://github.com/mockk/mockk/compare/1.14.5...1.14.7)

Updates `io.mockk:mockk-android` from 1.14.5 to 1.14.7
- [Release notes](https://github.com/mockk/mockk/releases)
- [Commits](https://github.com/mockk/mockk/compare/1.14.5...1.14.7)

Updates `io.mockk:mockk-android` from 1.14.5 to 1.14.7
- [Release notes](https://github.com/mockk/mockk/releases)
- [Commits](https://github.com/mockk/mockk/compare/1.14.5...1.14.7)

---
updated-dependencies:
- dependency-name: androidx.activity:activity-compose
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2025.12.00
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: io.mockk:mockk
  dependency-version: 1.14.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: io.mockk:mockk-android
  dependency-version: 1.14.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: io.mockk:mockk-android
  dependency-version: 1.14.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* Override SDK level

* Suppress lint warnings for LaunchedEffect / Context.getString

* Suppress lint warnings for other Context.getStrings (or replace by stringResource if possible)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Arnau Mora Gras <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-12-11 11:27:37 +01:00
Sunik Kupfer
b354bfebc2 LocalTask: Don't subclass DmfsTask (#1862)
* LocalTask: Don't subclass DmfsTask

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Adapt usages

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Update synctools

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-12-10 14:37:23 +01:00
Sunik Kupfer
29240ea16f Skip flaky test when not moving into anticipated forever pending sync state (#1872)
* Assume we moved into forever pending sync state

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

* Update comment

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-12-10 10:25:58 +01:00
Ricki Hirner
e7b88f9aa8 Version bump to 4.5.7.1 2025-12-09 11:49:50 +01:00
Arnau Mora
4d71517cde Delegate fileName into DmfsTask.syncId (#1871)
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-12-09 10:40:22 +01:00
Ricki Hirner
0d4f154baf Use Ktor for Nextcloud Login Flow (#1817)
* [WIP] Use Ktor for Nextcloud login flow

- Replace OkHttp with Ktor for HTTP requests
- Update URL handling to use Ktor's `Url` class
- Adjust `postForJson` method to use Ktor's HTTP client
- Refactor URL building logic for login flow initiation

* Use Ktor for Nextcloud login flow

- Migrate to Ktor's ContentNegotiation plugin for JSON handling
- Update dependencies and configuration for Ktor serialization
- Refactor `NextcloudLoginFlow` to use Ktor's JSON serialization

* Add tests

* Allow unit tests that mock/use HttpClient without Conscrypt

* KDoc

* Minor fixes

* Use toUrlOrNull from dav4jvm

* Don't change strings in this PR

* Update dav4jvm and synctools
2025-12-08 15:40:39 +01:00
Ricki Hirner
9eb70a5564 Bump version to 4.5.7 2025-12-08 12:19:38 +01:00
Ricki Hirner
24e0a864bd Update AVD caching in CI workflow (#1865)
- Split AVD cache handling into restore and save steps
- Add condition to save AVD cache only if restore misses
2025-12-05 16:25:25 +01:00
Ricki Hirner
10ec0c3b6d Fetch translations from Transifex 2025-12-05 15:20:46 +01:00
Ricki Hirner
bd3349cc38 Bump version to 4.5.7-rc.1 2025-12-05 15:19:43 +01:00
Ricki Hirner
c7bc2b317b Update synctools (#1864)
* Update synctools (fixes #1797, closes #1859)

* Use `com.github.bitfireAT:synctools` because `com.github.bitfireat:synctools` is not available on Jitpack for this commit
2025-12-05 15:16:29 +01:00
175 changed files with 3529 additions and 6539 deletions

View File

@@ -9,6 +9,9 @@ updates:
interval: "weekly"
commit-message:
prefix: "[CI] "
labels:
- "github_actions"
- "dependencies"
groups:
ci-actions:
patterns: ["*"]
@@ -17,6 +20,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
labels: # don't create "java" label (default for gradle ecosystem)
- "dependencies"
groups:
app-dependencies:
patterns: ["*"]

View File

@@ -8,6 +8,7 @@ on:
branches: [ main-ose ]
schedule:
- cron: '22 10 * * 1'
concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true
@@ -21,11 +22,6 @@ jobs:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -42,15 +38,11 @@ jobs:
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
languages: java-kotlin
build-mode: manual # autobuild uses older JDK
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
#- name: Autobuild
# uses: github/codeql-action/autobuild@v2
- name: Build
run: ./gradlew --build-cache --configuration-cache --no-daemon app:assembleOseDebug
- name: Build # we must not use build cache here
run: ./gradlew --no-daemon --configuration-cache app:assembleDebug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4

View File

@@ -0,0 +1,24 @@
name: Dependency Submission
on:
push:
branches: [ 'main-ose' ]
permissions:
contents: write
jobs:
dependency-submission:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Generate and submit dependency graph
uses: gradle/actions/dependency-submission@v5
with:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
dependency-graph-exclude-configurations: '.*[Tt]est.* .*[cC]heck.*'

View File

@@ -9,10 +9,19 @@ concurrency:
group: test-dev-${{ github.ref }}
cancel-in-progress: true
# We provide a remote gradle build cache. Take the settings from the secrets and enable
# configuration and build cache for all gradle jobs.
#
# Note: The secrets are not available for forks and Dependabot PRs.
env:
GRADLE_BUILDCACHE_URL: ${{ secrets.gradle_buildcache_url }}
GRADLE_BUILDCACHE_USERNAME: ${{ secrets.gradle_buildcache_username }}
GRADLE_BUILDCACHE_PASSWORD: ${{ secrets.gradle_buildcache_password }}
GRADLE_OPTS: -Dorg.gradle.caching=true -Dorg.gradle.configuration-cache=true
jobs:
compile:
name: Compile for build cache
if: ${{ github.ref == 'refs/heads/main-ose' }}
name: Compile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
@@ -22,16 +31,31 @@ jobs:
java-version: 21
# See https://community.gradle.org/github-actions/docs/setup-gradle/ for more information
- uses: gradle/actions/setup-gradle@v5 # creates build cache when on main branch
- uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
dependency-graph: generate-and-submit # submit Github Dependency Graph info
cache-read-only: false # allow branches to update their configuration cache
gradle-home-cache-excludes: caches/build-cache-1 # don't cache local build cache because we use a remote cache
- run: ./gradlew --build-cache --configuration-cache app:compileOseDebugSource
- name: Cache Android environment
uses: actions/cache@v5
with:
path: ~/.config/.android # needs to be cached so that configuration cache can work
key: android-${{ hashFiles('app/build.gradle.kts') }}
test:
- name: Compile
run: ./gradlew app:compileOseDebugSource
# Cache configurations for the other jobs (including assemble for CodeQL)
- name: Populate configuration cache
run: |
./gradlew --dry-run app:assembleDebug
./gradlew --dry-run app:lintOseDebug
./gradlew --dry-run app:testOseDebugUnitTest
./gradlew --dry-run app:virtualOseDebugAndroidTest
unit_tests:
needs: compile
if: ${{ !cancelled() }} # even if compile didn't run (because not on main branch)
name: Lint and unit tests
runs-on: ubuntu-latest
steps:
@@ -45,14 +69,20 @@ jobs:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
cache-read-only: true
- name: Run lint
run: ./gradlew --build-cache --configuration-cache app:lintOseDebug
- name: Run unit tests
run: ./gradlew --build-cache --configuration-cache app:testOseDebugUnitTest
- name: Restore Android environment
uses: actions/cache/restore@v5
with:
path: ~/.config/.android
key: android-${{ hashFiles('app/build.gradle.kts') }}
test_on_emulator:
- name: Lint checks
run: ./gradlew app:lintOseDebug
- name: Unit tests
run: ./gradlew app:testOseDebugUnitTest
instrumented_tests:
needs: compile
if: ${{ !cancelled() }} # even if compile didn't run (because not on main branch)
name: Instrumented tests
runs-on: ubuntu-latest
steps:
@@ -66,17 +96,41 @@ jobs:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
cache-read-only: true
- name: Restore Android environment
uses: actions/cache/restore@v5
with:
path: ~/.config/.android
key: android-${{ hashFiles('app/build.gradle.kts') }}
# gradle and Android SDK often take more space than what is available on the default runner.
# We try to free a few GB here to make gradle-managed devices more reliable.
- name: Free some disk space
uses: jlumbroso/free-disk-space@main
with:
android: false # we need the Android SDK
large-packages: false # apt takes too long
swap-storage: false # gradle needs much memory
- name: Restore AVD
id: restore-avd
uses: actions/cache/restore@v5
with:
path: ~/.config/.android/avd # where AVD is stored
key: avd-${{ hashFiles('app/build.gradle.kts') }} # gradle-managed devices are defined there
# Enable virtualization for Android emulator
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Cache AVD
uses: actions/cache@v4
with:
path: ~/.config/.android/avd
key: avd-${{ hashFiles('app/build.gradle.kts') }} # gradle-managed devices are defined there
- name: Instrumented tests
run: ./gradlew app:virtualOseDebugAndroidTest
- name: Run device tests
run: ./gradlew --build-cache --configuration-cache app:virtualCheck
- name: Cache AVD
uses: actions/cache/save@v5
if: steps.restore-avd.outputs.cache-hit != 'true'
with:
path: ~/.config/.android/avd # where AVD is stored
key: avd-${{ hashFiles('app/build.gradle.kts') }} # gradle-managed devices are defined there

View File

@@ -1,30 +0,0 @@
[main]
host = https://www.transifex.com
lang_map = ar_SA: ar, en_GB: en-rGB, fa_IR: fa-rIR, fi_FI: fi, nb_NO: nb, pt_BR: pt-rBR, sk_SK: sk, sl_SI: sl, tr_TR: tr, zh_CN: zh, zh_TW: zh-rTW
[o:bitfireAT:p:davx5:r:app]
file_filter = app/src/main/res/values-<lang>/strings.xml
source_file = app/src/main/res/values/strings.xml
source_lang = en
type = ANDROID
minimum_perc = 20
resource_name = App strings (all flavors)
# Attention: fastlane directories are like "en-us", not "en-rUS"!
[o:bitfireAT:p:davx5:r:metadata-short-description]
file_filter = fastlane/metadata/android/<lang>/short_description.txt
source_file = fastlane/metadata/android/en-US/short_description.txt
source_lang = en
type = TXT
minimum_perc = 100
resource_name = Metadata: short description
[o:bitfireAT:p:davx5:r:metadata-full-description]
file_filter = fastlane/metadata/android/<lang>/full_description.txt
source_file = fastlane/metadata/android/en-US/full_description.txt
source_lang = en
type = TXT
minimum_perc = 100
resource_name = Metadata: full description

View File

@@ -14,24 +14,11 @@ If you send us a pull request, our CLA bot will ask you to sign the
Contributor's License Agreement so that we can use your contribution.
# Copyright
# Copyright notice
Make sure that every file that contains significant work (at least every code file)
starts with the copyright header:
```
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
```
You can set this in Android Studio:
1. Settings / Editor / Copyright / Copyright Profiles
2. Paste the text above (without the stars).
3. Set Formatting so that the preview exactly looks like above; one blank line after the block.
4. Set this copyright profile as the default profile for the project.
5. Apply copyright: right-click in file tree / Update copyright.
starts with the copyright header. Android Studio should do so automatically because the
configuration is stored in the repository (`.idea/copyright`).
# Style guide
@@ -110,8 +97,3 @@ Test classes should be in the appropriate directory (see existing tests) and in
tested class. Tests are usually be named like `methodToBeTested_Condition()`, see
[Test apps on Android](https://developer.android.com/training/testing/).
# Authors
If you make significant contributions, feel free to add yourself to the [AUTHORS file](AUTHORS).

View File

@@ -1,9 +1,9 @@
[![Website](https://img.shields.io/website?style=flat-square&up_color=%237cb342&url=https%3A%2F%2Fwww.davx5.com)](https://www.davx5.com/)
[![F-Droid](https://img.shields.io/f-droid/v/at.bitfire.davdroid?style=flat-square)](https://f-droid.org/packages/at.bitfire.davdroid/)
[![License](https://img.shields.io/github/license/bitfireAT/davx5-ose?style=flat-square)](https://github.com/bitfireAT/davx5-ose/blob/main/LICENSE)
[![Follow @davx5app@fosstodon.org](https://img.shields.io/mastodon/follow/109598783742737223?domain=https%3A%2F%2Ffosstodon.org&style=flat-square)](https://fosstodon.org/@davx5app)
[![Development tests](https://github.com/bitfireAT/davx5-ose/actions/workflows/test-dev.yml/badge.svg)](https://github.com/bitfireAT/davx5-ose/actions/workflows/test-dev.yml)
[![Website](https://img.shields.io/website?style=flat-square&up_color=%237cb342&url=https%3A%2F%2Fwww.davx5.com)](https://www.davx5.com/)
[![License](https://img.shields.io/github/license/bitfireAT/davx5-ose?style=flat-square)](https://github.com/bitfireAT/davx5-ose/blob/main/LICENSE)
[![F-Droid](https://img.shields.io/f-droid/v/at.bitfire.davdroid?style=flat-square)](https://f-droid.org/packages/at.bitfire.davdroid/)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/bitfireAT/davx5-ose/total?label=GitHub%20downloads)
![DAVx⁵ logo](app/src/main/res/mipmap-xxxhdpi/ic_launcher.png)
@@ -11,8 +11,10 @@
DAVx⁵
========
Please see the [DAVx⁵ Web site](https://www.davx5.com) for
comprehensive information about DAVx⁵, including a list of services it has been tested with.
> [!IMPORTANT]
> Please see the [DAVx⁵ Web site](https://www.davx5.com) for
> comprehensive information about DAVx⁵, including a list of services it has been tested with,
> a manual and FAQ.
DAVx⁵ is licensed under the [GPLv3 License](LICENSE).

View File

@@ -6,9 +6,8 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.mikepenz.aboutLibraries.android)
}
@@ -19,8 +18,8 @@ android {
defaultConfig {
applicationId = "at.bitfire.davdroid"
versionCode = 405060100
versionName = "4.5.6.1"
versionCode = 405080003
versionName = "4.5.8"
base.archivesName = "davx5-ose-$versionName"
@@ -56,28 +55,16 @@ android {
flavorDimensions += "distribution"
productFlavors {
create("standard")
create("ose") {
dimension = "distribution"
versionNameSuffix = "-ose"
}
create("gplay") {
versionNameSuffix = "-gplay"
}
}
sourceSets {
getByName("standard") {
kotlin.srcDirs("src/standard/kotlin", "src/davdroid/kotlin")
res.srcDirs("src/standard/res", "src/davdroid/res")
}
getByName("androidTest") {
assets.srcDir("$projectDir/schemas")
}
getByName("gplay") {
kotlin.srcDirs("src/gplay/kotlin", "src/davdroid/kotlin")
res.srcDirs("src/gplay/res", "src/davdroid/res")
}
}
signingConfigs {
@@ -171,21 +158,21 @@ dependencies {
// Jetpack Compose
implementation(libs.compose.accompanist.permissions)
implementation(platform(libs.compose.bom))
implementation(libs.compose.material3)
implementation(libs.compose.materialIconsExtended)
debugImplementation(libs.compose.ui.tooling)
implementation(libs.compose.ui.toolingPreview)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.materialIconsExtended)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.compose.ui.toolingPreview)
// Glance Widgets
implementation(libs.glance.base)
implementation(libs.glance.material)
implementation(libs.androidx.glance.base)
implementation(libs.androidx.glance.material)
// Jetpack Room
implementation(libs.room.runtime)
implementation(libs.room.base)
implementation(libs.room.paging)
ksp(libs.room.compiler)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.base)
implementation(libs.androidx.room.paging)
ksp(libs.androidx.room.compiler)
// own libraries
implementation(libs.bitfire.cert4android)
@@ -202,8 +189,10 @@ dependencies {
implementation(libs.conscrypt)
implementation(libs.dnsjava)
implementation(libs.guava)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.mikepenz.aboutLibraries.m3)
implementation(libs.okhttp.base)
implementation(libs.okhttp.brotli)
@@ -223,6 +212,7 @@ dependencies {
// for tests
androidTestImplementation(libs.androidx.arch.core.testing)
androidTestImplementation(libs.androidx.room.testing)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.junit)
androidTestImplementation(libs.androidx.test.rules)
@@ -233,7 +223,6 @@ dependencies {
androidTestImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.mockk.android)
androidTestImplementation(libs.okhttp.mockwebserver)
androidTestImplementation(libs.room.testing)
testImplementation(libs.bitfire.dav4jvm)
testImplementation(libs.junit)
@@ -241,14 +230,3 @@ dependencies {
testImplementation(libs.okhttp.mockwebserver)
testImplementation(libs.robolectric)
}
// build variants (flavors)
val gplayImplementation by configurations {
dependencies {
implementation(libs.android.billing)
implementation(libs.android.review)
implementation(libs.confettikit)
}
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
<!-- account management permissions not required for own accounts since API level 22 -->
@@ -7,4 +8,9 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22"/>
<!--
Since Mockk 1.14.7 it's required to use minSdk 26. We use 24, so override for tests.
-->
<uses-sdk tools:overrideLibrary="io.mockk.android,io.mockk.proxy.android" />
</manifest>

View File

@@ -4,7 +4,6 @@
package at.bitfire.davdroid.db
import android.security.NetworkSecurityPolicy
import androidx.test.filters.SmallTest
import at.bitfire.dav4jvm.okhttp.DavResource
import at.bitfire.dav4jvm.property.webdav.WebDAV
@@ -19,7 +18,6 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -42,7 +40,6 @@ class CollectionTest {
hiltRule.inject()
httpClient = httpClientBuilder.build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
}

View File

@@ -4,7 +4,6 @@
package at.bitfire.davdroid.network
import android.security.NetworkSecurityPolicy
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.ktor.client.request.get
@@ -17,7 +16,6 @@ import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -49,6 +47,14 @@ class HttpClientBuilderTest {
}
@Test
fun testBuild_SharesConnectionPoolAndDispatcher() {
val client1 = httpClientBuilder.get().build()
val client2 = httpClientBuilder.get().build()
assertEquals(client1.connectionPool, client2.connectionPool)
assertEquals(client1.dispatcher, client2.dispatcher)
}
@Test
fun testBuildKtor_CreatesWorkingClient() = runTest {
server.enqueue(MockResponse()
@@ -64,7 +70,6 @@ class HttpClientBuilderTest {
@Test
fun testCookies() {
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
val url = server.url("/test")
// set cookie for root path (/) and /test path in first response

View File

@@ -20,7 +20,9 @@ import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assume
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
@@ -61,10 +63,11 @@ class LocalCalendarStoreTest {
}
@Ignore("Flaky in CI")
@Test
fun testUpdateAccount_updatesOwnerAccount() {
// Verify initial state
verifyOwnerAccountIs(provider, "InitialAccountName")
// Verify initial state (assume to skip and prevent flaky test failures)
Assume.assumeTrue("InitialAccountName" == getOwnerAccount())
// Rename account
val oldAccount = account
@@ -74,8 +77,7 @@ class LocalCalendarStoreTest {
localCalendarStore.updateAccount(oldAccount, account, provider)
// Verify [Calendar.OWNER_ACCOUNT] of local calendar was updated
verifyOwnerAccountIs(provider, "ChangedAccountName")
assertEquals("ChangedAccountName", getOwnerAccount())
}
@@ -95,7 +97,7 @@ class LocalCalendarStoreTest {
)
)!!.asSyncAdapter(account)
private fun verifyOwnerAccountIs(provider: ContentProviderClient, expectedOwnerAccount: String) {
private fun getOwnerAccount(): String? {
provider.query(
calendarUri,
arrayOf(Calendars.OWNER_ACCOUNT),
@@ -103,9 +105,9 @@ class LocalCalendarStoreTest {
arrayOf(account.name),
null
)!!.use { cursor ->
cursor.moveToNext()
val ownerAccount = cursor.getString(0)
assertEquals(expectedOwnerAccount, ownerAccount)
if (!cursor.moveToNext())
return null
return cursor.getString(0)
}
}

View File

@@ -4,7 +4,6 @@
package at.bitfire.davdroid.servicedetection
import android.security.NetworkSecurityPolicy
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
@@ -22,7 +21,6 @@ import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -70,7 +68,6 @@ class CollectionsWithoutHomeSetRefresherTest {
// build HTTP client
client = httpClientBuilder.build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
// insert test service
val serviceId = db.serviceDao().insertOrReplace(

View File

@@ -4,7 +4,6 @@
package at.bitfire.davdroid.servicedetection
import android.security.NetworkSecurityPolicy
import at.bitfire.dav4jvm.okhttp.DavResource
import at.bitfire.dav4jvm.property.carddav.CardDAV
import at.bitfire.dav4jvm.property.webdav.WebDAV
@@ -25,7 +24,6 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -76,7 +74,6 @@ class DavResourceFinderTest {
client = httpClientBuilder
.authenticate(domain = null, getCredentials = { credentials })
.build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
val baseURI = URI.create("/")
finder = resourceFinderFactory.create(baseURI, credentials)

View File

@@ -4,7 +4,6 @@
package at.bitfire.davdroid.servicedetection
import android.security.NetworkSecurityPolicy
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.HomeSet
@@ -28,7 +27,6 @@ import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -76,7 +74,6 @@ class HomeSetRefresherTest {
// build HTTP client
client = httpClientBuilder.build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
// insert test service
val serviceId = db.serviceDao().insertOrReplace(

View File

@@ -4,7 +4,6 @@
package at.bitfire.davdroid.servicedetection
import android.security.NetworkSecurityPolicy
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Principal
@@ -23,7 +22,6 @@ import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -71,7 +69,6 @@ class PrincipalsRefresherTest {
// build HTTP client
client = httpClientBuilder.build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
// insert test service
val serviceId = db.serviceDao().insertOrReplace(

View File

@@ -4,7 +4,6 @@
package at.bitfire.davdroid.servicedetection
import android.security.NetworkSecurityPolicy
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.network.HttpClientBuilder
@@ -17,7 +16,6 @@ import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -58,7 +56,6 @@ class ServiceRefresherTest {
// build HTTP client
client = httpClientBuilder.build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
// insert test service
val serviceId = db.serviceDao().insertOrReplace(

View File

@@ -22,7 +22,7 @@ import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
@@ -83,8 +83,8 @@ class AccountSettingsMigration21Test {
inPendingState.first { it }
}
// Assert again that we are now in the forever pending state
assertTrue(ContentResolver.isSyncPending(account, authority))
// Assume that we are now in the forever pending state (Skips test otherwise)
Assume.assumeTrue(ContentResolver.isSyncPending(account, authority))
// Run the migration which should cancel the forever pending sync for all accounts
migration.migrate(account)

View File

@@ -5,7 +5,6 @@
package at.bitfire.davdroid.sync
import android.accounts.Account
import at.bitfire.dav4jvm.HttpUtils.toKtorUrl
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Credentials
import at.bitfire.davdroid.sync.account.TestAccount
@@ -20,7 +19,6 @@ import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -28,7 +26,7 @@ import java.net.InetAddress
import javax.inject.Inject
@HiltAndroidTest
class ResourceDownloaderTest {
class ResourceRetrieverTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@@ -37,7 +35,7 @@ class ResourceDownloaderTest {
lateinit var accountSettingsFactory: AccountSettings.Factory
@Inject
lateinit var resourceDownloaderFactory: ResourceDownloader.Factory
lateinit var resourceRetrieverFactory: ResourceRetriever.Factory
lateinit var account: Account
lateinit var server: MockWebServer
@@ -64,21 +62,35 @@ class ResourceDownloaderTest {
@Test
fun testDownload_ExternalDomain() = runTest {
fun testRetrieve_DataUri() = runTest {
val downloader = resourceRetrieverFactory.create(account, "example.com")
val result = downloader.retrieve("data:image/png;base64,dGVzdA==")
assertArrayEquals("test".toByteArray(), result)
}
@Test
fun testRetrieve_DataUri_Invalid() = runTest {
val downloader = resourceRetrieverFactory.create(account, "example.com")
val result = downloader.retrieve("data:;INVALID,INVALID")
assertNull(result)
}
@Test
fun testRetrieve_ExternalDomain() = runTest {
val baseUrl = server.url("/")
val localhostIp = InetAddress.getByName(baseUrl.host).hostAddress!!
// URL should be http://localhost, replace with http://127.0.0.1 to have other domain
Assume.assumeTrue(baseUrl.host == "localhost")
val baseUrlIp = baseUrl.newBuilder()
.host(InetAddress.getByName(baseUrl.host).hostAddress!!)
.host(localhostIp)
.build()
server.enqueue(MockResponse()
.setResponseCode(200)
.setBody("TEST"))
val downloader = resourceDownloaderFactory.create(account, baseUrl.host)
val result = downloader.download(baseUrlIp.toKtorUrl())
val downloader = resourceRetrieverFactory.create(account, baseUrl.host)
val result = downloader.retrieve(baseUrlIp.toString())
// authentication was NOT sent because request is not for original domain
val sentAuth = server.takeRequest().getHeader(HttpHeaders.Authorization)
@@ -89,14 +101,28 @@ class ResourceDownloaderTest {
}
@Test
fun testDownload_SameDomain() = runTest {
fun testRetrieve_FtpUrl() = runTest {
val downloader = resourceRetrieverFactory.create(account, "example.com")
val result = downloader.retrieve("ftp://example.com/photo.jpg")
assertNull(result)
}
@Test
fun testRetrieve_RelativeHttpsUrl() = runTest {
val downloader = resourceRetrieverFactory.create(account, "example.com")
val result = downloader.retrieve("https:photo.jpg")
assertNull(result)
}
@Test
fun testRetrieve_SameDomain() = runTest {
server.enqueue(MockResponse()
.setResponseCode(200)
.setBody("TEST"))
val baseUrl = server.url("/")
val downloader = resourceDownloaderFactory.create(account, baseUrl.host)
val result = downloader.download(baseUrl.toKtorUrl())
val downloader = resourceRetrieverFactory.create(account, baseUrl.host)
val result = downloader.retrieve(baseUrl.toString())
// authentication was sent
val sentAuth = server.takeRequest().getHeader(HttpHeaders.Authorization)

View File

@@ -1,105 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import android.content.Context
import androidx.test.core.app.launchActivity
import at.bitfire.davdroid.settings.SettingsManager
import com.google.android.play.core.review.testing.FakeReviewManager
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.every
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit4.MockKRule
import io.mockk.mockkObject
import io.mockk.spyk
import io.mockk.verify
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class EarnBadgesActivityTest {
@Inject @ApplicationContext
lateinit var context: Context
@RelaxedMockK
lateinit var settings: SettingsManager
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val mockkRule = MockKRule(this)
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun testShowRatingRequest() {
launchActivity<EarnBadgesActivity>().use { scenario ->
scenario.onActivity { activity ->
val fakeReviewManager = spyk(FakeReviewManager(activity))
activity.showRatingRequest(fakeReviewManager)
verify {
fakeReviewManager.requestReviewFlow()
}
}
}
}
@Test
fun testShouldShowRatingRequest_firstInstallTimeIntervalPassed() {
// Interval is two weeks
mockkObject(EarnBadgesActivity) {
every { EarnBadgesActivity.currentTime() } returns 1652343892058 // "now" = 12.05.22
every { EarnBadgesActivity.installTime(context) } returns 1651087147000 // "15 days ago" = 27.04.2022
every { settings.getLongOrNull(EarnBadgesActivity.LAST_REVIEW_PROMPT) } returns 0 // "never" = 0
assertTrue(EarnBadgesActivity.shouldShowRatingRequest(context, settings)) // 15 > 14 => true
}
}
@Test
fun testShouldShowRatingRequest_firstInstallTimeIntervalNotPassed() {
// Interval is two weeks
mockkObject(EarnBadgesActivity) {
every { EarnBadgesActivity.currentTime() } returns 1652306400000 // "now" = 12.05.22
every { EarnBadgesActivity.installTime(context) } returns 1652133600000 // "2 days ago" = 10.05.2022
every { settings.getLongOrNull(EarnBadgesActivity.LAST_REVIEW_PROMPT) } returns 0 // "never" = 0
assertFalse(EarnBadgesActivity.shouldShowRatingRequest(context, settings)) // 2 > 14 => false
}
}
@Test
fun testShouldShowRatingRequest_firstInstallTimeAndLastReviewPromptIntervalPassed() {
// Interval is two weeks
mockkObject(EarnBadgesActivity) {
every { EarnBadgesActivity.currentTime() } returns 1652306400000 // "now" = 12.05.22
every { EarnBadgesActivity.installTime(context) } returns 1651087147000 // "15 days ago" = 27.04.2022
every { settings.getLongOrNull(EarnBadgesActivity.LAST_REVIEW_PROMPT) } returns 1651087147000 // "15 days ago" = 27.04.2022
assertTrue(EarnBadgesActivity.shouldShowRatingRequest(context, settings)) // 15 > 14 => true
}
}
@Test
fun testShouldShowRatingRequest_lastReviewPromptIntervalNotPassed() {
// Interval is two weeks
mockkObject(EarnBadgesActivity) {
every { EarnBadgesActivity.currentTime() } returns 1652306400000 // "now" = 12.05.22
every { EarnBadgesActivity.installTime(context) } returns 1652343892058 // "15 days ago" = 27.04.2022
every { settings.getLongOrNull(EarnBadgesActivity.LAST_REVIEW_PROMPT) } returns 1652306400000 // "now" = 12.05.22
assertFalse(EarnBadgesActivity.shouldShowRatingRequest(context, settings)) // 0 > 14 => false
}
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.di
import at.bitfire.davdroid.ui.intro.StandardAndGplayIntroPageFactory
import at.bitfire.davdroid.ui.intro.IntroPageFactory
import at.bitfire.davdroid.ui.setup.LoginTypesProvider
import at.bitfire.davdroid.ui.setup.StandardLoginTypesProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.components.SingletonComponent
interface StandardAndGplayModules {
@Module
@InstallIn(ActivityComponent::class)
interface ForActivities {
@Binds
fun loginTypesProvider(impl: StandardLoginTypesProvider): LoginTypesProvider
}
@Module
@InstallIn(ViewModelComponent::class)
interface ForViewModels {
@Binds
fun loginTypesProvider(impl: StandardLoginTypesProvider): LoginTypesProvider
}
@Module
@InstallIn(SingletonComponent::class)
interface Global {
@Binds
fun introPageFactory(impl: StandardAndGplayIntroPageFactory): IntroPageFactory
}
}

View File

@@ -1,148 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.HelpCenter
import androidx.compose.material.icons.filled.CloudOff
import androidx.compose.material.icons.filled.CorporateFare
import androidx.compose.material.icons.filled.Forum
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.VolunteerActivism
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.ExternalUris.Homepage
import at.bitfire.davdroid.ui.ExternalUris.Social
import at.bitfire.davdroid.ui.ExternalUris.withStatParams
import javax.inject.Inject
open class StandardAccountsDrawerHandler @Inject constructor(): AccountsDrawerHandler() {
@Composable
override fun MenuEntries(
snackbarHostState: SnackbarHostState
) {
val uriHandler = LocalUriHandler.current
// Most important entries
ImportantEntries(snackbarHostState)
// News
MenuHeading(R.string.navigation_drawer_news_updates)
MenuEntry(
icon = painterResource(R.drawable.mastodon),
title = Social.fediverseHandle,
onClick = {
uriHandler.openUri(Social.fediverseUrl.toString())
}
)
// Tools
Tools()
// Support the project
MenuHeading(R.string.navigation_drawer_support_project)
Contribute(onContribute = {
uriHandler.openUri(
Homepage.baseUrl.buildUpon()
.appendPath(Homepage.PATH_OPEN_SOURCE)
.withStatParams(javaClass.simpleName)
.build().toString()
)
})
MenuEntry(
icon = Icons.Default.Forum,
title = stringResource(R.string.navigation_drawer_community),
onClick = {
uriHandler.openUri(Social.discussionsUrl.toString())
}
)
// External links
MenuHeading(R.string.navigation_drawer_external_links)
MenuEntry(
icon = Icons.Default.Home,
title = stringResource(R.string.navigation_drawer_website),
onClick = {
uriHandler.openUri(
Homepage.baseUrl
.buildUpon()
.withStatParams(javaClass.simpleName)
.build().toString())
}
)
MenuEntry(
icon = Icons.Default.Info,
title = stringResource(R.string.navigation_drawer_manual),
onClick = {
uriHandler.openUri(ExternalUris.Manual.baseUrl.toString())
}
)
MenuEntry(
icon = Icons.AutoMirrored.Default.HelpCenter,
title = stringResource(R.string.navigation_drawer_faq),
onClick = {
uriHandler.openUri(
Homepage.baseUrl.buildUpon()
.appendPath(Homepage.PATH_FAQ)
.withStatParams(javaClass.simpleName)
.build().toString()
)
}
)
MenuEntry(
icon = Icons.Default.CorporateFare,
title = stringResource(R.string.navigation_drawer_managed),
onClick = {
uriHandler.openUri(
Homepage.baseUrl.buildUpon()
.appendPath(Homepage.PATH_ORGANIZATIONS)
.appendPath(Homepage.PATH_ORGANIZATIONS_MANAGED)
.withStatParams(javaClass.simpleName)
.build().toString()
)
}
)
MenuEntry(
icon = Icons.Default.CloudOff,
title = stringResource(R.string.navigation_drawer_privacy_policy),
onClick = {
uriHandler.openUri(
Homepage.baseUrl.buildUpon()
.appendPath(Homepage.PATH_PRIVACY)
.withStatParams(javaClass.simpleName)
.build().toString()
)
}
)
}
@Composable
@Preview
fun MenuEntries_Standard_Preview() {
Column {
MenuEntries(SnackbarHostState())
}
}
@Composable
open fun Contribute(onContribute: () -> Unit) {
MenuEntry(
icon = Icons.Default.VolunteerActivism,
title = stringResource(R.string.navigation_drawer_contribute),
onClick = onContribute
)
}
}

View File

@@ -1,167 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
@Suppress("MemberVisibilityCanBePrivate")
object M3ColorScheme {
// All colors hand-crafted because Material Theme Builder generates unbelievably ugly colors
val primaryLight = Color(0xFF7cb342)
val onPrimaryLight = Color(0xFFffffff)
val primaryContainerLight = Color(0xFFb4e47d)
val onPrimaryContainerLight = Color(0xFF232d18)
val secondaryLight = Color(0xFFff7f2a)
val onSecondaryLight = Color(0xFFFFFFFF)
val secondaryContainerLight = Color(0xFFffa565)
val onSecondaryContainerLight = Color(0xFF3a271b)
val tertiaryLight = Color(0xFF658a24)
val onTertiaryLight = Color(0xFFFFFFFF)
val tertiaryContainerLight = Color(0xFFb0d08e)
val onTertiaryContainerLight = Color(0xFF263015)
val errorLight = Color(0xFFd71717)
val onErrorLight = Color(0xFFFFFFFF)
val errorContainerLight = Color(0xFFefb6b6)
val onErrorContainerLight = Color(0xFF3a0b0b)
val backgroundLight = Color(0xFFfcfcfc)
val onBackgroundLight = Color(0xFF2a2a2a)
val surfaceLight = Color(0xFFf5f5f5)
val onSurfaceLight = Color(0xFF4d4d4d)
val surfaceVariantLight = Color(0xFFe4e4e4)
val onSurfaceVariantLight = Color(0xFF2a2a2a)
val outlineLight = Color(0xFF838383)
val outlineVariantLight = Color(0xFFd4d4d4)
val scrimLight = Color(0xFF000000)
val inverseSurfaceLight = Color(0xFF2e322b)
val inverseOnSurfaceLight = Color(0xFFfafaf8)
val inversePrimaryLight = Color(0xFFb4e47d)
val surfaceDimLight = Color(0xFFe3e3e3)
val surfaceBrightLight = Color(0xFFf9f9f9)
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
val surfaceContainerLowLight = Color(0xFFfafafa)
val surfaceContainerLight = Color(0xFFf5f5f5)
val surfaceContainerHighLight = Color(0xFFf0f0ef)
val surfaceContainerHighestLight = Color(0xFFebebea)
val primaryDark = Color(0xFFc4e3a4)
val onPrimaryDark = Color(0xFF2b4310)
val primaryContainerDark = Color(0xFF7cb342)
val onPrimaryContainerDark = Color(0xFFedf5e4)
val secondaryDark = Color(0xFFe5c3ac)
val onSecondaryDark = Color(0xFF3e332e)
val secondaryContainerDark = Color(0xFFff7f2a)
val onSecondaryContainerDark = Color(0xFFffeadb)
val tertiaryDark = Color(0xFFc6e597)
val onTertiaryDark = Color(0xFF4b661b)
val tertiaryContainerDark = Color(0xFF658a24)
val onTertiaryContainerDark = Color(0xFFf0f8e2)
val errorDark = Color(0xFFf6d0d0)
val onErrorDark = Color(0xFF4f1212)
val errorContainerDark = Color(0xFFe93434)
val onErrorContainerDark = Color(0xFFfcdede)
val backgroundDark = Color(0xFF1a1a1a)
val onBackgroundDark = Color(0xFFf0f0f0)
val surfaceDark = Color(0xFF292929)
val onSurfaceDark = Color(0xFFdedede)
val surfaceVariantDark = Color(0xFF363636)
val onSurfaceVariantDark = Color(0xFFededed)
val outlineDark = Color(0xFFa3a3a3)
val outlineVariantDark = Color(0xFF7cb342)
val scrimDark = Color(0xFF000000)
val inverseSurfaceDark = Color(0xFFdbdbdb)
val inverseOnSurfaceDark = Color(0xFF292929)
val inversePrimaryDark = Color(0xFF7cb342)
val surfaceDimDark = Color(0xFF333333)
val surfaceBrightDark = Color(0xFF4d4d4d)
val surfaceContainerLowestDark = Color(0xFF141414)
val surfaceContainerLowDark = Color(0xFF1f1f1f)
val surfaceContainerDark = Color(0xff3a3a3a)
val surfaceContainerHighDark = Color(0xFF383838)
val surfaceContainerHighestDark = Color(0xFF434343)
// Copied from Material Theme Builder: Theme.kt
val lightScheme = lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
val darkScheme = darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
}

View File

@@ -1,22 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.intro
import javax.inject.Inject
class StandardAndGplayIntroPageFactory @Inject constructor(
batteryOptimizationsPage: BatteryOptimizationsPage,
permissionsIntroPage: PermissionsIntroPage,
tasksIntroPage: TasksIntroPage
): IntroPageFactory {
override val introPages = arrayOf(
WelcomePage(),
tasksIntroPage,
permissionsIntroPage,
batteryOptimizationsPage
)
}

View File

@@ -1,121 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.setup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.ExternalUris
import at.bitfire.davdroid.ui.ExternalUris.withStatParams
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
import at.bitfire.davdroid.ui.composable.Assistant
@Composable
fun StandardLoginTypePage(
selectedLoginType: LoginType,
onSelectLoginType: (LoginType) -> Unit,
@Suppress("unused") // for build variants
setInitialLoginInfo: (LoginInfo) -> Unit,
onContinue: () -> Unit = {}
) {
Assistant(
nextLabel = stringResource(R.string.login_continue),
nextEnabled = true,
onNext = onContinue
) {
Column(Modifier.padding(8.dp)) {
Text(
stringResource(R.string.login_generic_login),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(vertical = 8.dp)
)
for (type in StandardLoginTypesProvider.genericLoginTypes)
LoginTypeSelector(
title = stringResource(type.title),
selected = type == selectedLoginType,
onSelect = { onSelectLoginType(type) }
)
Text(
stringResource(R.string.login_provider_login),
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
)
for (type in StandardLoginTypesProvider.specificLoginTypes)
LoginTypeSelector(
title = stringResource(type.title),
selected = type == selectedLoginType,
onSelect = { onSelectLoginType(type) }
)
HorizontalDivider(Modifier.padding(vertical = 12.dp))
val privacyPolicy = ExternalUris.Homepage.baseUrl.buildUpon()
.appendPath(ExternalUris.Homepage.PATH_PRIVACY)
.withStatParams("StandardLoginTypePage")
.build().toString()
val privacy = HtmlCompat.fromHtml(
stringResource(R.string.login_privacy_hint, stringResource(R.string.app_name), privacyPolicy),
HtmlCompat.FROM_HTML_MODE_COMPACT)
Text(
text = privacy.toAnnotatedString(),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
fun LoginTypeSelector(
title: String,
selected: Boolean,
onSelect: () -> Unit = {}
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(onClick = onSelect)
.padding(bottom = 4.dp)
) {
RadioButton(
selected = selected,
onClick = onSelect
)
Text(
title,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
}
}
}
@Composable
@Preview
fun StandardLoginTypePage_Preview() {
StandardLoginTypePage(
selectedLoginType = StandardLoginTypesProvider.genericLoginTypes.first(),
onSelectLoginType = {},
setInitialLoginInfo = {},
onContinue = {}
)
}

View File

@@ -1,66 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.setup
import android.content.Intent
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import at.bitfire.davdroid.ui.setup.LoginTypesProvider.LoginAction
import java.util.logging.Logger
import javax.inject.Inject
class StandardLoginTypesProvider @Inject constructor(
private val logger: Logger
) : LoginTypesProvider {
companion object {
val genericLoginTypes = listOf(
UrlLogin,
EmailLogin,
AdvancedLogin
)
val specificLoginTypes = listOf(
FastmailLogin,
GoogleLogin,
NextcloudLogin
)
}
override val defaultLoginType = UrlLogin
override fun intentToInitialLoginType(intent: Intent): LoginAction =
intent.data?.normalizeScheme().let { uri ->
when {
intent.hasExtra(LoginActivity.EXTRA_LOGIN_FLOW) ->
LoginAction(NextcloudLogin, true)
uri?.scheme == "mailto" ->
LoginAction(EmailLogin, true)
listOf("caldavs", "carddavs", "davx5", "http", "https").any { uri?.scheme == it } ->
LoginAction(UrlLogin, true)
else -> {
logger.warning("Did not understand login intent: $intent")
LoginAction(defaultLoginType, false) // Don't skip login type page if intent is unclear
}
}
}
@Composable
override fun LoginTypePage(
snackbarHostState: SnackbarHostState,
selectedLoginType: LoginType,
onSelectLoginType: (LoginType) -> Unit,
setInitialLoginInfo: (LoginInfo) -> Unit,
onContinue: () -> Unit
) {
StandardLoginTypePage(
selectedLoginType = selectedLoginType,
onSelectLoginType = onSelectLoginType,
setInitialLoginInfo = setInitialLoginInfo,
onContinue = onContinue
)
}
}

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--suppress AndroidUnknownAttribute -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="108"
android:viewportHeight="108"
android:width="108dp"
android:height="108dp">
<group
android:translateX="-213.3939"
android:translateY="-709.5244">
<group
android:scaleX="0.0662553"
android:scaleY="0.0662553"
android:translateX="233.5464"
android:translateY="729.8276">
<path
android:pathData="M282.78183 280.50711l-85.53115 -85.53116 -0.00001 228.08306 228.08306 0 -85.53115 -85.53114c94.36922 -94.36921 247.75506 -94.36951 342.12458 0.00001 28.79533 28.79533 49.03756 48.10677 59.87183 84.60006l83.25028 0c-12.82982 -57.30603 -41.34018 -96.85969 -86.10134 -141.62083 -126.01589 -126.0159 -330.15022 -126.0159 -456.1661 0zm399.14533 399.14532c-94.36952 94.36952 -247.75535 94.36921 -342.12458 0 -28.79562 -28.79564 -49.03816 -48.10677 -59.8718 -84.60006l-83.25029 0c12.82949 57.30571 41.33988 96.85938 86.10134 141.62083 126.01588 126.0159 330.15021 126.0159 456.1661 0l85.53115 85.53115 0.00001 -228.08305 -228.08307 0z"
android:fillColor="#ffffff" />
<path
android:pathData="M201.33878 550.34595l27.70423 0c25.78414 0 44.43649 -13.44067 44.43649 -44.98509 0 -31.54442 -18.65235 -44.16219 -45.80799 -44.16219l-26.33273 0zm23.58974 -18.92666l0 -51.29397 1.3715 0c12.89207 0 23.04114 4.38879 23.04114 25.23554 0 20.84675 -10.14907 26.05843 -23.04114 26.05843z"
android:fillColor="#ffffff" />
<path
android:pathData="M311.13854 507.00666c2.1944 -8.50328 4.38879 -19.20096 6.30889 -28.25283l0.5486 0c2.19439 8.91472 4.38879 19.74955 6.58318 28.25283l1.50865 6.17173 -16.45796 0zm-34.28741 43.33929l24.13834 0 4.38879 -18.92666 24.96124 0 4.38878 18.92666 24.96124 0 -27.15563 -89.14728 -28.52713 0z"
android:fillColor="#ffffff" />
<path
android:pathData="M380.56703 550.34595l28.52713 0 26.33273 -89.14728 -24.13834 0 -9.05188 38.9505c-2.33154 9.46333 -4.11449 18.65236 -6.58318 28.25283l-0.5486 0c-2.46869 -9.60047 -4.11449 -18.7895 -6.58318 -28.25283l-9.32618 -38.9505 -24.96124 0z"
android:fillColor="#ffffff" />
<path
android:pathData="M449.1301 596.08783l39.35586 0 6.19081 -15.47702c2.4321 -6.41191 5.0853 -12.82382 7.51741 -19.01463l0.8844 0c3.3165 6.19081 6.41191 12.82382 9.72841 19.01463l8.84401 15.47702 40.68246 0 -33.16504 -53.06408 31.39624 -57.48608 -39.35586 0 -5.3064 15.47703c-1.98991 6.1908 -4.64311 12.82381 -6.63301 19.01462l-0.8844 0c-2.87431 -6.19081 -5.96971 -12.82382 -8.84402 -19.01462l-7.95961 -15.47703 -40.68245 0 31.39624 53.06408z"
android:fillColor="#ffffff" />
<path
android:pathData="M601.43782 509.37229c18.53926 0 34.49164 -11.78465 34.49164 -32.19221 0 -19.25783 -13.50922 -28.16817 -29.3179 -28.16817 -3.01801 0 -5.46117 0.28743 -8.62291 1.43715l1.14972 -13.2218 32.76707 0 0 -20.69499 -54.03691 0 -2.29945 46.85116 10.63493 6.89832c5.46117 -3.44916 7.76062 -4.31145 12.64693 -4.31145 7.18576 0 12.35951 4.02402 12.35951 11.78464 0 8.04806 -4.88632 11.78465 -13.50923 11.78465 -6.6109 0 -12.93437 -3.73659 -18.39554 -8.62291l-10.92236 15.52124c7.47319 7.47319 18.10812 12.93437 33.0545 12.93437z"
android:fillColor="#ffffff" />
</group>
</group>
</vector>

View File

@@ -1,8 +0,0 @@
<!-- drawable/mastodon.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#6364FF" android:pathData="M20.94,14C20.66,15.41 18.5,16.96 15.97,17.26C14.66,17.41 13.37,17.56 12,17.5C9.75,17.39 8,16.96 8,16.96V17.58C8.32,19.8 10.22,19.93 12.03,20C13.85,20.05 15.47,19.54 15.47,19.54L15.55,21.19C15.55,21.19 14.27,21.87 12,22C10.75,22.07 9.19,21.97 7.38,21.5C3.46,20.45 2.78,16.26 2.68,12L2.67,8.57C2.67,4.23 5.5,2.96 5.5,2.96C6.95,2.3 9.41,2 11.97,2H12.03C14.59,2 17.05,2.3 18.5,2.96C18.5,2.96 21.33,4.23 21.33,8.57C21.33,8.57 21.37,11.78 20.94,14M18,8.91C18,7.83 17.7,7 17.15,6.35C16.59,5.72 15.85,5.39 14.92,5.39C13.86,5.39 13.05,5.8 12.5,6.62L12,7.5L11.5,6.62C10.94,5.8 10.14,5.39 9.07,5.39C8.15,5.39 7.41,5.72 6.84,6.35C6.29,7 6,7.83 6,8.91V14.17H8.1V9.06C8.1,8 8.55,7.44 9.46,7.44C10.46,7.44 10.96,8.09 10.96,9.37V12.16H13.03V9.37C13.03,8.09 13.53,7.44 14.54,7.44C15.44,7.44 15.89,8 15.89,9.06V14.17H18V8.91Z" />
</vector>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/primaryColor" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config cleartextTrafficPermitted="true" tools:ignore="InsecureBaseConfiguration">
<trust-anchors>
<certificates src="system"/>
<certificates src="user" tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -1,31 +0,0 @@
<!--
~ Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application android:icon="@mipmap/ic_launcher">
<activity
android:name="at.bitfire.davdroid.ui.EarnBadgesActivity"
android:exported="false"
android:parentActivityName=".ui.account.AccountActivity"/>
<!-- AppAuth login flow redirect (duplicated in standard/AndroidManifest.xml) -->
<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
tools:ignore="AppLinkUrlError"
android:scheme="at.bitfire.davdroid"
android:path="/oauth2/redirect"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -1,433 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid
import android.app.Activity
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.LifecycleCoroutineScope
import at.bitfire.davdroid.di.IoDispatcher
import at.bitfire.davdroid.ui.icons.Badge1UpExtralife
import at.bitfire.davdroid.ui.icons.BadgeCoffee
import at.bitfire.davdroid.ui.icons.BadgeCupcake
import at.bitfire.davdroid.ui.icons.BadgeDavx5Decade
import at.bitfire.davdroid.ui.icons.BadgeEnergyBooster
import at.bitfire.davdroid.ui.icons.BadgeLifeBuoy
import at.bitfire.davdroid.ui.icons.BadgeLocalBar
import at.bitfire.davdroid.ui.icons.BadgeMedal
import at.bitfire.davdroid.ui.icons.BadgeNinthAnniversary
import at.bitfire.davdroid.ui.icons.BadgeOfflineBolt
import at.bitfire.davdroid.ui.icons.BadgeSailboat
import at.bitfire.davdroid.ui.icons.BadgesIcons
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.PendingPurchasesParams
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.PurchasesResponseListener
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.acknowledgePurchase
import com.android.billingclient.api.queryProductDetails
import com.android.billingclient.api.queryPurchasesAsync
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.io.Closeable
import java.util.logging.Logger
class PlayClient @AssistedInject constructor(
@Assisted val activity: Activity,
@IoDispatcher val ioDispatcher: CoroutineDispatcher,
@Assisted val lifecycleScope: LifecycleCoroutineScope,
val logger: Logger
) : Closeable,
PurchasesUpdatedListener,
BillingClientStateListener,
PurchasesResponseListener
{
@AssistedFactory
interface Factory {
fun create(activity: Activity, lifecycleScope: LifecycleCoroutineScope): PlayClient
}
/**
* The product details; IE title, description, price, etc.
*/
val productDetailsList = MutableStateFlow<List<ProductDetails>>(emptyList())
/**
* The purchases that have been made.
*/
val purchases = MutableStateFlow<List<Purchase>>(emptyList())
/**
* Short message to display to the user
*/
val message = MutableStateFlow<String?>(null)
val purchaseSuccessful = MutableStateFlow(false)
private val billingClient: BillingClient = BillingClient.newBuilder(activity)
.setListener(this)
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
.enableAutoServiceReconnection() // Less SERVICE_DISCONNECTED responses (still need to handle them)
.build()
private var connectionTriesCount: Int = 0
/**
* Set up the billing client and connect when the activity is created.
* Product details and purchases are loaded from play store app cache,
* but this will give a more responsive user experience, when buying a product.
*/
init {
if (!billingClient.isReady) {
logger.fine("Start connection...")
billingClient.startConnection(this)
}
}
fun resetMessage() { message.value = null }
fun resetPurchaseSuccessful() { purchaseSuccessful.value = false }
/**
* Query the product details and purchases
*/
fun queryProductsAndPurchases() {
// Make sure billing client is available
if (!billingClient.isReady) {
logger.warning("BillingClient is not ready")
message.value = activity.getString(R.string.billing_unavailable)
return
}
// Only request product details if not found already
if (productDetailsList.value.isEmpty()) {
logger.fine("No products loaded yet, requesting")
// Query product details
queryProductDetails()
}
// Query purchases
// Purchases are stored locally by gplay app
// Result is received in [onQueryPurchasesResponse]
billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build(), this)
}
/**
* Start the purchase flow for a product
*/
fun purchaseProduct(badge: Badge) {
// Make sure billing client is available
if (!billingClient.isReady) {
logger.warning("BillingClient is not ready")
message.value = activity.getString(R.string.billing_unavailable)
return
}
// Build and send purchase request
val params = BillingFlowParams.newBuilder().setProductDetailsParamsList(listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(badge.productDetails)
.build()
)).build()
val billingResult = billingClient.launchBillingFlow(activity, params)
// Check purchase was successful
if (!billingResultOk(billingResult)) {
logBillingResult("launchBillingFlow", billingResult)
message.value = activity.getString(R.string.purchase_failed)
}
}
/**
* Stop the billing client connection (ie. when the activity is destroyed)
*/
override fun close() {
logger.fine("Closing connection...")
billingClient.endConnection()
}
/**
* Continue if connected
*/
override fun onBillingSetupFinished(billingResult: BillingResult) {
logBillingResult("onBillingSetupFinished", billingResult)
if (billingResultOk(billingResult)) {
logger.fine("Play client ready")
queryProductsAndPurchases()
}
}
/**
* Retry starting the billingClient a few times
*/
override fun onBillingServiceDisconnected() {
connectionTriesCount++
val maxTries = BILLINGCLIENT_CONNECTION_MAX_RETRIES
logger.warning("Connecting to BillingService failed. Retrying $connectionTriesCount/$maxTries times")
if (connectionTriesCount > maxTries) {
logger.warning("Failed to connect to BillingService. Given up on re-trying")
return
}
// Try to restart the connection on the next request
billingClient.startConnection(this)
}
/**
* Ask google servers for product details to display (ie. id, price, description, etc)
*/
private fun queryProductDetails() = lifecycleScope.launch(ioDispatcher) {
// Build request and query product details
val productList = productIds.map {
QueryProductDetailsParams.Product.newBuilder()
.setProductId(it)
.setProductType(BillingClient.ProductType.INAPP)
.build()
}
val params = QueryProductDetailsParams.newBuilder().setProductList(productList).build()
val productDetailsResult = billingClient.queryProductDetails(params)
// Handle billing result for request
val billingResult = productDetailsResult.billingResult
logBillingResult("onProductDetailsResponse", billingResult)
if (!billingResultOk(billingResult)) {
logger.warning("Failed to retrieve product details")
return@launch
}
// Check amount of products received is correct
val productDetails = productDetailsResult.productDetailsList
if (productDetails?.size != productIds.size) {
logger.warning("Missing products. Expected ${productIds.size}, but got ${productDetails?.size} product details from server.")
return@launch
}
// Save product details to be shown on screen
logger.fine("Got product details!\n$productDetails")
productDetailsList.emit(productDetails)
}
/**
* Callback from the billing library when [queryPurchasesAsync] is called.
*/
override fun onQueryPurchasesResponse(billingResult: BillingResult, purchasesList: MutableList<Purchase>) {
logBillingResult("onQueryPurchasesResponse", billingResult)
if (billingResultOk(billingResult)) {
logger.fine("Received purchases list")
processPurchases(purchasesList)
}
}
/**
* Called by the Billing Library when new purchases are detected.
*/
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
logBillingResult("onPurchasesUpdated", billingResult)
if (billingResultOk(billingResult) && !purchases.isNullOrEmpty()){
logger.fine("Received updated purchases list")
processPurchases(purchases)
}
}
/**
* Process purchases
*/
private fun processPurchases(purchasesList: MutableList<Purchase>) {
// Return early if purchases list has not changed
if (purchasesList == purchases.value)
return
// Handle purchases
logPurchaseStatus(purchasesList)
for (purchase in purchasesList) {
logger.info("Handling purchase with state: ${purchase.purchaseState}")
// Verify purchase state
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) {
// purchase pending or in undefined state (ie. refunded or consumed)
purchasesList.remove(purchase)
continue
}
// Check acknowledgement
if (!purchase.isAcknowledged) {
// Don't entitle user to purchase yet remove from purchases list for now
purchasesList.remove(purchase)
// Try to acknowledge purchase
acknowledgePurchase(purchase)
}
}
logAcknowledgementStatus(purchasesList)
// Update list
val mergedPurchases = (purchases.value + purchasesList).distinctBy {
it.purchaseToken
}
logger.info("Purchases: $mergedPurchases")
purchases.value = mergedPurchases
}
/**
* Requests acknowledgement of a purchase
*/
private fun acknowledgePurchase(purchase: Purchase) = lifecycleScope.launch(ioDispatcher) {
logger.info("Acknowledging purchase")
val params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
val billingResult = billingClient.acknowledgePurchase(params)
logBillingResult("acknowledgePurchase", billingResult)
// Check billing result
if (!billingResultOk(billingResult)) {
logger.warning("Acknowledging Purchase failed!")
// Notify user about failure
message.value = activity.getString(R.string.purchase_acknowledgement_failed)
return@launch
}
// Billing result OK! Acknowledgement successful
// Now entitle user to purchase (Add to purchases list)
val purchasesList = purchases.value.toMutableList()
purchasesList.add(purchase)
purchases.emit(purchasesList)
// Notify user about success
message.value = activity.getString(R.string.purchase_acknowledgement_successful)
purchaseSuccessful.value = true
}
/**
* Checks if the billing result response code is ok. Logs and may set error message if not.
*/
private fun billingResultOk(result: BillingResult): Boolean {
when (result.responseCode) {
BillingResponseCode.SERVICE_DISCONNECTED,
BillingResponseCode.SERVICE_UNAVAILABLE ->
message.value = activity.getString(R.string.network_problems)
BillingResponseCode.BILLING_UNAVAILABLE ->
message.value = activity.getString(R.string.billing_unavailable)
BillingResponseCode.USER_CANCELED ->
logger.info("User canceled the purchase")
BillingResponseCode.ITEM_ALREADY_OWNED ->
logger.info("The user already owns this item")
BillingResponseCode.DEVELOPER_ERROR ->
logger.warning("Google Play does not recognize the application configuration." +
"Do the product IDs match and is the APK in use signed with release keys?")
}
return result.responseCode == BillingResponseCode.OK
}
// /**
// * DANGER: Use only for testing!
// * Consumes a purchased item, so it will be available for purchasing again.
// * Used only for revoking a test purchase.
// */
// @Suppress("unused")
// private fun consumePurchase(purchase: Purchase) {
// if (BuildConfig.BUILD_TYPE != "debug")
// return
//
// logger.info("Trying to consume purchase with token: ${purchase.purchaseToken}")
// val consumeParams = ConsumeParams.newBuilder()
// .setPurchaseToken(purchase.purchaseToken)
// .build()
// lifecycleScope.launch(ioDispatcher) {
// val consumeResult = billingClient.consumePurchase(consumeParams)
// when (consumeResult.billingResult.responseCode) {
// BillingResponseCode.OK ->
// logger.info("Successfully consumed item with purchase token: '${consumeResult.purchaseToken}'")
// BillingResponseCode.ITEM_NOT_OWNED ->
// logger.info("Failed to consume item with purchase token: '${consumeResult.purchaseToken}'. Item not owned")
// else ->
// logger.info("Failed to consume item with purchase token: '${consumeResult.purchaseToken}'. BillingResult: ${consumeResult.billingResult}")
// }
// }
// }
// logging helpers
/**
* Log billing result the same way each time
*/
private fun logBillingResult(source: String, result: BillingResult) {
logger.fine("$source: responseCode=${result.responseCode}, message=${result.debugMessage}")
}
/**
* Log the number of purchases that are acknowledge and not acknowledged.
*/
private fun logAcknowledgementStatus(purchasesList: List<Purchase>) {
var ackYes = 0
var ackNo = 0
for (purchase in purchasesList)
if (purchase.isAcknowledged) ackYes++ else ackNo++
logger.info("logAcknowledgementStatus: acknowledged=$ackYes unacknowledged=$ackNo")
}
/**
* Log the number of purchases that are acknowledge and not acknowledged.
*/
private fun logPurchaseStatus(purchasesList: List<Purchase>) {
var undefined = 0
var purchased = 0
var pending = 0
for (purchase in purchasesList)
when (purchase.purchaseState) {
Purchase.PurchaseState.UNSPECIFIED_STATE -> undefined++
Purchase.PurchaseState.PURCHASED -> purchased++
Purchase.PurchaseState.PENDING -> pending++
}
logger.info("Purchases status: purchased=$purchased pending=$pending undefined=$undefined")
}
/**
* Support badge product
* @param productDetails
* @param yearBought
* @param count - amount of badge items of this badge type
*/
data class Badge(val productDetails: ProductDetails, var yearBought: String?, val count: Int) {
val name = productDetails.name
val description = productDetails.description
val price = productDetails.oneTimePurchaseOfferDetails?.formattedPrice
val purchased = yearBought != null
}
companion object {
const val BILLINGCLIENT_CONNECTION_MAX_RETRIES = 4
val BADGE_ICONS = mapOf(
"helping_hands.2022" to (BadgesIcons.BadgeLifeBuoy to Color(0xFFFF6A00)),
"a_coffee_for_you.2022" to (BadgesIcons.BadgeCoffee to Color(0xFF352B1B) ),
"loyal_foss_backer.2022" to (BadgesIcons.BadgeMedal to Color(0xFFFFC200)),
"part_of_the_journey.2022" to (BadgesIcons.BadgeSailboat to Color(0xFF083D77)),
"9th_anniversary.2022" to (BadgesIcons.BadgeNinthAnniversary to Color(0xFFFA8072)),
"1up_extralife.2023" to (BadgesIcons.Badge1UpExtralife to Color(0xFFD32F2F)),
"energy_booster.2023" to (BadgesIcons.BadgeEnergyBooster to Color(0xFFFDD835)),
"davx5_decade" to (BadgesIcons.BadgeDavx5Decade to Color(0xFF43A047)),
"push_development" to (BadgesIcons.BadgeOfflineBolt to Color(0xFFFDD835)),
"davx5_cocktail" to (BadgesIcons.BadgeLocalBar to Color(0xFF29CC00)),
"11th_anniversary" to (BadgesIcons.BadgeCupcake to Color(0xFFF679E5)),
)
val productIds = BADGE_ICONS.keys.toList()
}
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.di
import at.bitfire.davdroid.ui.AboutActivity
import at.bitfire.davdroid.ui.AccountsDrawerHandler
import at.bitfire.davdroid.ui.GplayAccountsDrawerHandler
import at.bitfire.davdroid.ui.GplayLicenseInfoProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
interface GplayModules {
@Module
@InstallIn(ActivityComponent::class)
interface ForActivities {
@Binds
fun accountsDrawerHandler(impl: GplayAccountsDrawerHandler): AccountsDrawerHandler
@Binds
fun appLicenseInfoProvider(impl: GplayLicenseInfoProvider): AboutActivity.AppLicenseInfoProvider
}
}

View File

@@ -1,96 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import android.content.Context
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import at.bitfire.davdroid.PlayClient
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.ExternalUris.withStatParams
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory
import dagger.hilt.android.AndroidEntryPoint
import jakarta.inject.Inject
import java.util.logging.Level
import java.util.logging.Logger
@AndroidEntryPoint
class EarnBadgesActivity() : AppCompatActivity() {
@Inject lateinit var logger: Logger
@Inject lateinit var playClientFactory: PlayClient.Factory
@Inject lateinit var settingsManager: SettingsManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Show rating API dialog one week after the app has been installed
if (shouldShowRatingRequest(this, settingsManager))
showRatingRequest(ReviewManagerFactory.create(this))
setContent {
AppTheme {
val uriHandler = LocalUriHandler.current
EarnBadgesScreen(
playClient = playClientFactory.create(this, lifecycleScope),
onStartRating = { uriHandler.openUri(
"market://details?id=$packageName".toUri()
.buildUpon()
.withStatParams(javaClass.simpleName)
.build().toString()
) },
onNavUp = ::onNavigateUp
)
}
}
}
/**
* Starts the in-app review API to trigger the review request
* Once the user has rated the app, it will still trigger, but won't show up anymore.
*/
fun showRatingRequest(manager: ReviewManager) {
// Try prompting for review/rating
manager.requestReviewFlow().addOnSuccessListener { reviewInfo ->
logger.log(Level.INFO, "Launching app rating flow")
manager.launchReviewFlow(this, reviewInfo)
}
}
companion object {
internal const val LAST_REVIEW_PROMPT = "lastReviewPrompt"
/** Time between rating interval prompts in milliseconds */
private const val RATING_INTERVAL = 2*7*24*60*60*1000 // Two weeks
/**
* Determines whether we should show a rating prompt to the user depending on whether
* - the RATING_INTERVAL has passed once after first installation, or
* - the last rating prompt is older than RATING_INTERVAL
*
* If the return value is `true`, also updates the `LAST_REVIEW_PROMPT` setting to the current time
* so that the next call won't be `true` again for the time specified in `RATING_INTERVAL`.
*/
fun shouldShowRatingRequest(context: Context, settings: SettingsManager): Boolean {
val now = currentTime()
val firstInstall = installTime(context)
val lastPrompt = settings.getLongOrNull(LAST_REVIEW_PROMPT) ?: now
val shouldShowRatingRequest = (now > firstInstall + RATING_INTERVAL) && (now > lastPrompt + RATING_INTERVAL)
Logger.getGlobal().info("now=$now, firstInstall=$firstInstall, lastPrompt=$lastPrompt, shouldShowRatingRequest=$shouldShowRatingRequest")
if (shouldShowRatingRequest)
settings.putLong(LAST_REVIEW_PROMPT, now)
return shouldShowRatingRequest
}
fun currentTime() = System.currentTimeMillis()
fun installTime(context: Context) = context.packageManager.getPackageInfo(context.packageName, 0).firstInstallTime
}
}

View File

@@ -1,107 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.PlayClient
import at.bitfire.davdroid.PlayClient.Badge
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.logging.Logger
@HiltViewModel(assistedFactory = EarnBadgesModel.Factory::class)
class EarnBadgesModel @AssistedInject constructor(
@ApplicationContext val context: Context,
private val logger: Logger,
@Assisted val playClient: PlayClient
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(playClient: PlayClient): EarnBadgesModel
}
init {
// Load the current state of bought badges
playClient.queryProductsAndPurchases()
}
val message = playClient.message
val purchaseSuccessful = playClient.purchaseSuccessful
/**
* List of badges available to buy
*/
val availableBadges = combine(
playClient.productDetailsList,
playClient.purchases
) { productDetails, purchases ->
logger.info("Creating new list of badges from product details and purchases")
logger.info("Product IDs: ${productDetails.map {"\n" + it.productId}}")
logger.info("Purchases: ${purchases.map { "\nPurchase: ${it.products}"}}")
// Create badges
productDetails.map { productDetails ->
// If the product/badge has been bought in one of the purchases, find the year and amount
var yearBought: String? = null
var count = 0
for (purchase in purchases)
if (purchase.products.contains(productDetails.productId)) {
yearBought = SimpleDateFormat("yyyy", Locale.getDefault()).format(Date(purchase.purchaseTime))
count = purchase.quantity
}
Badge(productDetails, yearBought, count)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
/**
* Bought badges
*/
val boughtBadges = availableBadges.map { allBadges ->
logger.info("Finding bought badges")
// Filter for the bought badges
val boughtBadges = allBadges.filter { badge ->
badge.purchased
}
// Create duplicates for the ones that have been bought multiple times
boughtBadges.toMutableList().apply {
addAll(flatMap { badge -> List(badge.count - 1) { badge } })
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun buyBadge(badge: Badge) = playClient.purchaseProduct(badge)
fun onResetMessage() = playClient.resetMessage()
fun onResetPurchaseSuccessful() = playClient.resetPurchaseSuccessful()
override fun onCleared() = playClient.close()
}

View File

@@ -1,308 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.StarRate
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import at.bitfire.davdroid.PlayClient
import at.bitfire.davdroid.PlayClient.Badge
import at.bitfire.davdroid.PlayClient.Companion.BADGE_ICONS
import at.bitfire.davdroid.R
import io.github.vinceglb.confettikit.compose.ConfettiKit
import io.github.vinceglb.confettikit.core.Angle
import io.github.vinceglb.confettikit.core.Party
import io.github.vinceglb.confettikit.core.Position
import io.github.vinceglb.confettikit.core.emitter.Emitter
import io.github.vinceglb.confettikit.core.models.Shape
import io.github.vinceglb.confettikit.core.models.Size
import kotlin.time.Duration.Companion.seconds
@Composable
fun EarnBadgesScreen(
playClient: PlayClient,
onStartRating: () -> Unit = {},
onNavUp: () -> Unit = {},
model: EarnBadgesModel = hiltViewModel(
creationCallback = { factory: EarnBadgesModel.Factory ->
factory.create(playClient)
}
)
) {
val availableBadges by model.availableBadges.collectAsStateWithLifecycle()
val boughtBadges by model.boughtBadges.collectAsStateWithLifecycle()
val errorMessage by model.message.collectAsStateWithLifecycle()
val purchaseSuccessful by model.purchaseSuccessful.collectAsStateWithLifecycle()
EarnBadges(
availableBadges = availableBadges,
boughtBadges = boughtBadges,
message = errorMessage,
purchaseSuccessful = purchaseSuccessful,
onBuyBadge = model::buyBadge,
onResetMessage = model::onResetMessage,
onResetPurchaseSuccessful = model::onResetPurchaseSuccessful,
onStartRating = onStartRating,
onNavUp = onNavUp
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EarnBadges(
availableBadges: List<Badge>,
boughtBadges: List<Badge>,
message: String?,
purchaseSuccessful: Boolean,
onBuyBadge: (badge: Badge) -> Unit = {},
onResetMessage: () -> Unit = {},
onResetPurchaseSuccessful: () -> Unit = {},
onStartRating: () -> Unit = {},
onNavUp: () -> Unit = {}
) {
// Show snackbar when some message needs to be displayed
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(message != null) {
message?.let {
snackbarHostState.showSnackbar(message, duration = SnackbarDuration.Long)
onResetMessage()
}
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = onNavUp) {
Icon(
Icons.AutoMirrored.Default.ArrowBack,
stringResource(R.string.navigate_up)
)
}
},
title = {
Text(
stringResource(R.string.earn_badges),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
actions = {
IconButton(onClick = onStartRating) {
Icon(Icons.Default.StarRate, stringResource(R.string.nav_rate_us))
}
}
)
},
snackbarHost = {
SnackbarHost(snackbarHostState)
},
) { padding ->
if (purchaseSuccessful) {
ConfettiKit(
modifier = Modifier.fillMaxSize().navigationBarsPadding().zIndex(1f),
parties = listOf(
Party(
angle = Angle.TOP,
spread = 60,
speed = 70f,
size = listOf(Size(10), Size(15), Size(20)),
shapes = listOf(
Shape.Vector(rememberVectorPainter(Icons.Default.Favorite)),
Shape.Vector(rememberVectorPainter(Icons.Default.FavoriteBorder)),
Shape.Circle,
Shape.Square,
),
emitter = Emitter(duration = 3.seconds).perSecond(50),
position = Position.Relative(x = 0.5, y = 1.0)
)
),
onParticleSystemEnded = { _, _ -> onResetPurchaseSuccessful() }
)
}
Column(
modifier = Modifier
.padding(padding)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
if (boughtBadges.isNotEmpty()) {
TextHeading(
pluralStringResource(
R.plurals.you_earned_badges,
boughtBadges.size,
boughtBadges.size
)
)
LazyVerticalGrid(
modifier = Modifier.heightIn(max = 1000.dp),
columns = GridCells.Adaptive(minSize = 60.dp)
) {
items(boughtBadges.size) { index ->
BoughtBadgeListItem(boughtBadges[index])
}
}
}
TextHeading(stringResource(R.string.available_badges))
if (availableBadges.isEmpty())
TextBody(stringResource(R.string.available_badges_empty))
availableBadges.forEach { badge ->
BuyBadgeListItem(badge, onBuyBadge)
}
TextHeading(stringResource(R.string.what_are_badges_title))
TextBody(stringResource(R.string.what_are_badges_body))
TextHeading(stringResource(R.string.why_badges_title))
TextBody(stringResource(R.string.why_badges_body))
}
}
}
@Composable
fun BoughtBadgeListItem(badge: Badge) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
IconButton(
modifier = Modifier
.fillMaxSize(),
onClick = { /* could start an animation */ }
) {
Card(
modifier = Modifier
.aspectRatio(1f),
colors = CardDefaults.cardColors(
containerColor = Color.White,
),
) {
val (icon, tint) = BADGE_ICONS[badge.productDetails.productId]!!
Icon(
imageVector = icon,
contentDescription = badge.productDetails.productId,
tint = tint,
modifier = Modifier
.size(65.dp)
.padding(3.dp)
)
}
}
Text(badge.yearBought ?: "?", fontSize = 14.sp, maxLines = 1)
}
}
@Composable
fun BuyBadgeListItem(
badge: Badge,
onBuyBadge: (badge: Badge) -> Unit,
) {
Card(
Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
) {
val (icon, tint) = BADGE_ICONS[badge.productDetails.productId]!!
Icon(
imageVector = icon,
contentDescription = badge.productDetails.productId,
tint = tint,
modifier = Modifier.size(30.dp)
)
Column(
modifier = Modifier
.weight(3f, true)
.padding(horizontal = 16.dp)
) {
Text(badge.name, fontSize = 14.sp, fontWeight = FontWeight.Bold)
Text(badge.description.replace("\n", ""), fontSize = 12.sp, lineHeight = 14.sp)
}
Button(
onClick = { onBuyBadge(badge) },
enabled = !badge.purchased
) {
Icon(
imageVector = if (!badge.purchased) Icons.Default.Star else Icons.Default.Favorite,
contentDescription = null,
modifier = Modifier.size(20.dp),
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
if (!badge.purchased)
Text(badge.price ?: stringResource(R.string.button_buy_badge_free))
else
Text(stringResource(R.string.button_buy_badge_bought))
}
}
}
}
@Composable
fun TextHeading(text: String) = Text(
text,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(top = 20.dp, bottom = 16.dp)
)
@Composable
fun TextBody(text: String) = Text(
text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(bottom = 16.dp)
)

View File

@@ -1,102 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.VolunteerActivism
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import com.google.android.play.core.review.ReviewManagerFactory
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
/**
* Overrides some navigationd drawer actions for Google Play
*/
class GplayAccountsDrawerHandler @Inject constructor(
private val logger: Logger
) : StandardAccountsDrawerHandler() {
@Composable
override fun Contribute(onContribute: () -> Unit) {
val context = LocalContext.current
MenuEntry(
icon = Icons.Default.VolunteerActivism,
title = stringResource(R.string.earn_badges),
onClick = {
context.startActivity(Intent(context, EarnBadgesActivity::class.java))
}
)
}
@Composable
@Preview
fun MenuEntries_Gplay_Preview() {
Column {
MenuEntries(SnackbarHostState())
}
}
override fun onBetaFeedback(
context: Context,
onShowSnackbar: (message: String, actionLabel: String, action: () -> Unit) -> Unit
) {
// use In-App Review API to submit private feedback
val manager = ReviewManagerFactory.create(context)
val request = manager.requestReviewFlow()
request.addOnCompleteListener { task ->
if (task.isSuccessful) {
logger.info("Launching in-app review flow")
if (context is Activity)
manager.launchReviewFlow(context, task.result)
// provide alternative for the case that the in-app review flow didn't show up
onShowSnackbar(
context.getString(R.string.nav_feedback_inapp_didnt_appear),
context.getString(R.string.nav_feedback_google_play),
{
if (!openInStore(context))
// couldn't open in store, fall back to email
super.onBetaFeedback(context, onShowSnackbar)
}
)
} else {
logger.log(Level.WARNING, "Couldn't start in-app review flow", task.exception)
openInStore(context)
}
}
}
private fun openInStore(context: Context): Boolean {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("https://play.google.com/store/apps/details?id=${BuildConfig.APPLICATION_ID}")
setPackage("com.android.vending") // Google Play only (this is only for the gplay flavor)
}
return try {
context.startActivity(intent)
Toast.makeText(context, R.string.nav_feedback_scroll_to_reviews, Toast.LENGTH_LONG).show()
true
} catch (e: ActivityNotFoundException) {
// fall back to email
false
}
}
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import at.bitfire.davdroid.R
import javax.inject.Inject
class GplayLicenseInfoProvider @Inject constructor() : AboutActivity.AppLicenseInfoProvider {
@Composable
override fun LicenseInfo() {
Text(stringResource(R.string.about_flavor_info))
}
@Composable
@Preview
fun LicenseInfo_Preview() {
LicenseInfo()
}
}

View File

@@ -1,84 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.Badge1UpExtralife: ImageVector
get() {
if (_Badge1UpExtralife != null) {
return _Badge1UpExtralife!!
}
_Badge1UpExtralife = ImageVector.Builder(
name = "Badge1UpExtralife",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
autoMirror = true
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(10f, 19f)
verticalLineTo(19f)
curveTo(9.4f, 19f, 9f, 18.6f, 9f, 18f)
verticalLineTo(17f)
curveTo(9f, 16.5f, 9.4f, 16f, 10f, 16f)
verticalLineTo(16f)
curveTo(10.5f, 16f, 11f, 16.4f, 11f, 17f)
verticalLineTo(18f)
curveTo(11f, 18.6f, 10.6f, 19f, 10f, 19f)
moveTo(15f, 18f)
verticalLineTo(17f)
curveTo(15f, 16.5f, 14.6f, 16f, 14f, 16f)
verticalLineTo(16f)
curveTo(13.5f, 16f, 13f, 16.4f, 13f, 17f)
verticalLineTo(18f)
curveTo(13f, 18.5f, 13.4f, 19f, 14f, 19f)
verticalLineTo(19f)
curveTo(14.6f, 19f, 15f, 18.6f, 15f, 18f)
moveTo(22f, 12f)
curveTo(22f, 14.6f, 20.4f, 16.9f, 18f, 18.4f)
verticalLineTo(20f)
arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 16f, 22f)
horizontalLineTo(8f)
arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 6f, 20f)
verticalLineTo(18.4f)
curveTo(3.6f, 16.9f, 2f, 14.6f, 2f, 12f)
arcTo(10f, 10f, 0f, isMoreThanHalf = false, isPositiveArc = true, 12f, 2f)
arcTo(10f, 10f, 0f, isMoreThanHalf = false, isPositiveArc = true, 22f, 12f)
moveTo(7f, 10f)
curveTo(7f, 8.9f, 6.4f, 7.9f, 5.5f, 7.4f)
curveTo(4.5f, 8.7f, 4f, 10.3f, 4f, 12f)
curveTo(4f, 12.3f, 4f, 12.7f, 4.1f, 13f)
curveTo(5.7f, 12.9f, 7f, 11.6f, 7f, 10f)
moveTo(9f, 9f)
curveTo(9f, 10.7f, 10.3f, 12f, 12f, 12f)
curveTo(13.7f, 12f, 15f, 10.7f, 15f, 9f)
curveTo(15f, 7.3f, 13.7f, 6f, 12f, 6f)
curveTo(10.3f, 6f, 9f, 7.3f, 9f, 9f)
moveTo(16f, 20f)
verticalLineTo(15.5f)
curveTo(14.8f, 15.2f, 13.4f, 15f, 12f, 15f)
curveTo(10.6f, 15f, 9.2f, 15.2f, 8f, 15.5f)
verticalLineTo(20f)
horizontalLineTo(16f)
moveTo(19.9f, 13f)
curveTo(20f, 12.7f, 20f, 12.3f, 20f, 12f)
curveTo(20f, 10.3f, 19.5f, 8.7f, 18.5f, 7.4f)
curveTo(17.6f, 7.9f, 17f, 8.9f, 17f, 10f)
curveTo(17f, 11.6f, 18.3f, 12.9f, 19.9f, 13f)
close()
}
}.build()
return _Badge1UpExtralife!!
}
@Suppress("ObjectPropertyName")
private var _Badge1UpExtralife: ImageVector? = null

View File

@@ -1,57 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeCoffee: ImageVector
get() {
if (_BadgeCoffee != null) {
return _BadgeCoffee!!
}
_BadgeCoffee = ImageVector.Builder(
name = "BadgeCoffee",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
autoMirror = true
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(20f, 3f)
lineTo(4f, 3f)
verticalLineToRelative(10f)
curveToRelative(0f, 2.21f, 1.79f, 4f, 4f, 4f)
horizontalLineToRelative(6f)
curveToRelative(2.21f, 0f, 4f, -1.79f, 4f, -4f)
verticalLineToRelative(-3f)
horizontalLineToRelative(2f)
curveToRelative(1.11f, 0f, 2f, -0.9f, 2f, -2f)
lineTo(22f, 5f)
curveToRelative(0f, -1.11f, -0.89f, -2f, -2f, -2f)
close()
moveTo(20f, 8f)
horizontalLineToRelative(-2f)
lineTo(18f, 5f)
horizontalLineToRelative(2f)
verticalLineToRelative(3f)
close()
moveTo(4f, 19f)
horizontalLineToRelative(16f)
verticalLineToRelative(2f)
lineTo(4f, 21f)
close()
}
}.build()
return _BadgeCoffee!!
}
@Suppress("ObjectPropertyName")
private var _BadgeCoffee: ImageVector? = null

View File

@@ -1,62 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeCupcake: ImageVector
get() {
if (_BadgeCupcake != null) {
return _BadgeCupcake!!
}
_BadgeCupcake = ImageVector.Builder(
name = "BadgeCupcake",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(12f, 1.5f)
arcTo(2.5f, 2.5f, 0f, isMoreThanHalf = false, isPositiveArc = true, 14.5f, 4f)
arcTo(2.5f, 2.5f, 0f, isMoreThanHalf = false, isPositiveArc = true, 12f, 6.5f)
arcTo(2.5f, 2.5f, 0f, isMoreThanHalf = false, isPositiveArc = true, 9.5f, 4f)
arcTo(2.5f, 2.5f, 0f, isMoreThanHalf = false, isPositiveArc = true, 12f, 1.5f)
moveTo(15.87f, 5f)
curveTo(18f, 5f, 20f, 7f, 20f, 9f)
curveTo(22.7f, 9f, 22.7f, 13f, 20f, 13f)
horizontalLineTo(4f)
curveTo(1.3f, 13f, 1.3f, 9f, 4f, 9f)
curveTo(4f, 7f, 6f, 5f, 8.13f, 5f)
curveTo(8.57f, 6.73f, 10.14f, 8f, 12f, 8f)
curveTo(13.86f, 8f, 15.43f, 6.73f, 15.87f, 5f)
moveTo(5f, 15f)
horizontalLineTo(8f)
lineTo(9f, 22f)
horizontalLineTo(7f)
lineTo(5f, 15f)
moveTo(10f, 15f)
horizontalLineTo(14f)
lineTo(13f, 22f)
horizontalLineTo(11f)
lineTo(10f, 15f)
moveTo(16f, 15f)
horizontalLineTo(19f)
lineTo(17f, 22f)
horizontalLineTo(15f)
lineTo(16f, 15f)
close()
}
}.build()
return _BadgeCupcake!!
}
@Suppress("ObjectPropertyName")
private var _BadgeCupcake: ImageVector? = null

View File

@@ -1,65 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeDavx5Decade: ImageVector
get() {
if (_BadgeDavx5Decade != null) {
return _BadgeDavx5Decade!!
}
_BadgeDavx5Decade = ImageVector.Builder(
name = "BadgeDavx5Decade",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
autoMirror = true
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(14f, 9f)
horizontalLineTo(16f)
verticalLineTo(15f)
horizontalLineTo(14f)
verticalLineTo(9f)
moveTo(21f, 5f)
verticalLineTo(19f)
curveTo(21f, 20.11f, 20.11f, 21f, 19f, 21f)
horizontalLineTo(5f)
arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 3f, 19f)
verticalLineTo(5f)
arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = true, 5f, 3f)
horizontalLineTo(19f)
curveTo(20.11f, 3f, 21f, 3.9f, 21f, 5f)
moveTo(10f, 7f)
horizontalLineTo(6f)
verticalLineTo(9f)
horizontalLineTo(8f)
verticalLineTo(17f)
horizontalLineTo(10f)
verticalLineTo(7f)
moveTo(18f, 9f)
arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 16f, 7f)
horizontalLineTo(14f)
arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 12f, 9f)
verticalLineTo(15f)
curveTo(12f, 16.11f, 12.9f, 17f, 14f, 17f)
horizontalLineTo(16f)
curveTo(17.11f, 17f, 18f, 16.11f, 18f, 15f)
verticalLineTo(9f)
close()
}
}.build()
return _BadgeDavx5Decade!!
}
@Suppress("ObjectPropertyName")
private var _BadgeDavx5Decade: ImageVector? = null

View File

@@ -1,42 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeEnergyBooster: ImageVector
get() {
if (_BadgeEnergyBooster != null) {
return _BadgeEnergyBooster!!
}
_BadgeEnergyBooster = ImageVector.Builder(
name = "BadgeEnergyBooster",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
autoMirror = true
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(11f, 15f)
horizontalLineTo(6f)
lineTo(13f, 1f)
verticalLineTo(9f)
horizontalLineTo(18f)
lineTo(11f, 23f)
verticalLineTo(15f)
close()
}
}.build()
return _BadgeEnergyBooster!!
}
@Suppress("ObjectPropertyName")
private var _BadgeEnergyBooster: ImageVector? = null

View File

@@ -1,70 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeLifeBuoy: ImageVector
get() {
if (_BadgeLifeBuoy != null) {
return _BadgeLifeBuoy!!
}
_BadgeLifeBuoy = ImageVector.Builder(
name = "BadgeLifeBuoy",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
autoMirror = true
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(12f, 2f)
curveTo(6.48f, 2f, 2f, 6.48f, 2f, 12f)
curveToRelative(0f, 5.52f, 4.48f, 10f, 10f, 10f)
reflectiveCurveToRelative(10f, -4.48f, 10f, -10f)
curveTo(22f, 6.48f, 17.52f, 2f, 12f, 2f)
close()
moveTo(19.46f, 9.12f)
lineToRelative(-2.78f, 1.15f)
curveToRelative(-0.51f, -1.36f, -1.58f, -2.44f, -2.95f, -2.94f)
lineToRelative(1.15f, -2.78f)
curveTo(16.98f, 5.35f, 18.65f, 7.02f, 19.46f, 9.12f)
close()
moveTo(12f, 15f)
curveToRelative(-1.66f, 0f, -3f, -1.34f, -3f, -3f)
reflectiveCurveToRelative(1.34f, -3f, 3f, -3f)
reflectiveCurveToRelative(3f, 1.34f, 3f, 3f)
reflectiveCurveTo(13.66f, 15f, 12f, 15f)
close()
moveTo(9.13f, 4.54f)
lineToRelative(1.17f, 2.78f)
curveToRelative(-1.38f, 0.5f, -2.47f, 1.59f, -2.98f, 2.97f)
lineTo(4.54f, 9.13f)
curveTo(5.35f, 7.02f, 7.02f, 5.35f, 9.13f, 4.54f)
close()
moveTo(4.54f, 14.87f)
lineToRelative(2.78f, -1.15f)
curveToRelative(0.51f, 1.38f, 1.59f, 2.46f, 2.97f, 2.96f)
lineToRelative(-1.17f, 2.78f)
curveTo(7.02f, 18.65f, 5.35f, 16.98f, 4.54f, 14.87f)
close()
moveTo(14.88f, 19.46f)
lineToRelative(-1.15f, -2.78f)
curveToRelative(1.37f, -0.51f, 2.45f, -1.59f, 2.95f, -2.97f)
lineToRelative(2.78f, 1.17f)
curveTo(18.65f, 16.98f, 16.98f, 18.65f, 14.88f, 19.46f)
close()
}
}.build()
return _BadgeLifeBuoy!!
}
@Suppress("ObjectPropertyName")
private var _BadgeLifeBuoy: ImageVector? = null

View File

@@ -1,53 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeLocalBar: ImageVector
get() {
if (_BadgeLocalBar != null) {
return _BadgeLocalBar!!
}
_BadgeLocalBar = ImageVector.Builder(
name = "BadgeLocalBar",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(21f, 5f)
verticalLineTo(3f)
horizontalLineTo(3f)
verticalLineToRelative(2f)
lineToRelative(8f, 9f)
verticalLineToRelative(5f)
horizontalLineTo(6f)
verticalLineToRelative(2f)
horizontalLineToRelative(12f)
verticalLineToRelative(-2f)
horizontalLineToRelative(-5f)
verticalLineToRelative(-5f)
lineToRelative(8f, -9f)
close()
moveTo(7.43f, 7f)
lineTo(5.66f, 5f)
horizontalLineToRelative(12.69f)
lineToRelative(-1.78f, 2f)
horizontalLineTo(7.43f)
close()
}
}.build()
return _BadgeLocalBar!!
}
@Suppress("ObjectPropertyName")
private var _BadgeLocalBar: ImageVector? = null

View File

@@ -1,60 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeMedal: ImageVector
get() {
if (_BadgeMedal != null) {
return _BadgeMedal!!
}
_BadgeMedal = ImageVector.Builder(
name = "BadgeMedal",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
autoMirror = true
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(17f, 10.43f)
verticalLineTo(2f)
horizontalLineTo(7f)
verticalLineToRelative(8.43f)
curveToRelative(0f, 0.35f, 0.18f, 0.68f, 0.49f, 0.86f)
lineToRelative(4.18f, 2.51f)
lineToRelative(-0.99f, 2.34f)
lineToRelative(-3.41f, 0.29f)
lineToRelative(2.59f, 2.24f)
lineTo(9.07f, 22f)
lineTo(12f, 20.23f)
lineTo(14.93f, 22f)
lineToRelative(-0.78f, -3.33f)
lineToRelative(2.59f, -2.24f)
lineToRelative(-3.41f, -0.29f)
lineToRelative(-0.99f, -2.34f)
lineToRelative(4.18f, -2.51f)
curveTo(16.82f, 11.11f, 17f, 10.79f, 17f, 10.43f)
close()
moveTo(13f, 12.23f)
lineToRelative(-1f, 0.6f)
lineToRelative(-1f, -0.6f)
verticalLineTo(3f)
horizontalLineToRelative(2f)
verticalLineTo(12.23f)
close()
}
}.build()
return _BadgeMedal!!
}
@Suppress("ObjectPropertyName")
private var _BadgeMedal: ImageVector? = null

View File

@@ -1,78 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeNinthAnniversary: ImageVector
get() {
if (_BadgeNinthAnniversary != null) {
return _BadgeNinthAnniversary!!
}
_BadgeNinthAnniversary = ImageVector.Builder(
name = "BadgeNinthAnniversary",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
autoMirror = true
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(12f, 6f)
curveToRelative(1.11f, 0f, 2f, -0.9f, 2f, -2f)
curveToRelative(0f, -0.38f, -0.1f, -0.73f, -0.29f, -1.03f)
lineTo(12f, 0f)
lineToRelative(-1.71f, 2.97f)
curveToRelative(-0.19f, 0.3f, -0.29f, 0.65f, -0.29f, 1.03f)
curveToRelative(0f, 1.1f, 0.9f, 2f, 2f, 2f)
close()
moveTo(16.6f, 15.99f)
lineToRelative(-1.07f, -1.07f)
lineToRelative(-1.08f, 1.07f)
curveToRelative(-1.3f, 1.3f, -3.58f, 1.31f, -4.89f, 0f)
lineToRelative(-1.07f, -1.07f)
lineToRelative(-1.09f, 1.07f)
curveTo(6.75f, 16.64f, 5.88f, 17f, 4.96f, 17f)
curveToRelative(-0.73f, 0f, -1.4f, -0.23f, -1.96f, -0.61f)
lineTo(3f, 21f)
curveToRelative(0f, 0.55f, 0.45f, 1f, 1f, 1f)
horizontalLineToRelative(16f)
curveToRelative(0.55f, 0f, 1f, -0.45f, 1f, -1f)
verticalLineToRelative(-4.61f)
curveToRelative(-0.56f, 0.38f, -1.23f, 0.61f, -1.96f, 0.61f)
curveToRelative(-0.92f, 0f, -1.79f, -0.36f, -2.44f, -1.01f)
close()
moveTo(18f, 9f)
horizontalLineToRelative(-5f)
lineTo(13f, 7f)
horizontalLineToRelative(-2f)
verticalLineToRelative(2f)
lineTo(6f, 9f)
curveToRelative(-1.66f, 0f, -3f, 1.34f, -3f, 3f)
verticalLineToRelative(1.54f)
curveToRelative(0f, 1.08f, 0.88f, 1.96f, 1.96f, 1.96f)
curveToRelative(0.52f, 0f, 1.02f, -0.2f, 1.38f, -0.57f)
lineToRelative(2.14f, -2.13f)
lineToRelative(2.13f, 2.13f)
curveToRelative(0.74f, 0.74f, 2.03f, 0.74f, 2.77f, 0f)
lineToRelative(2.14f, -2.13f)
lineToRelative(2.13f, 2.13f)
curveToRelative(0.37f, 0.37f, 0.86f, 0.57f, 1.38f, 0.57f)
curveToRelative(1.08f, 0f, 1.96f, -0.88f, 1.96f, -1.96f)
lineTo(20.99f, 12f)
curveTo(21f, 10.34f, 19.66f, 9f, 18f, 9f)
close()
}
}.build()
return _BadgeNinthAnniversary!!
}
@Suppress("ObjectPropertyName")
private var _BadgeNinthAnniversary: ImageVector? = null

View File

@@ -1,47 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeOfflineBolt: ImageVector
get() {
if (_BadgeOfflineBolt != null) {
return _BadgeOfflineBolt!!
}
_BadgeOfflineBolt = ImageVector.Builder(
name = "BadgeOfflineBolt",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(12f, 2.02f)
curveToRelative(-5.51f, 0f, -9.98f, 4.47f, -9.98f, 9.98f)
reflectiveCurveToRelative(4.47f, 9.98f, 9.98f, 9.98f)
reflectiveCurveToRelative(9.98f, -4.47f, 9.98f, -9.98f)
reflectiveCurveTo(17.51f, 2.02f, 12f, 2.02f)
close()
moveTo(11.48f, 20f)
verticalLineToRelative(-6.26f)
horizontalLineTo(8f)
lineTo(13f, 4f)
verticalLineToRelative(6.26f)
horizontalLineToRelative(3.35f)
lineTo(11.48f, 20f)
close()
}
}.build()
return _BadgeOfflineBolt!!
}
@Suppress("ObjectPropertyName")
private var _BadgeOfflineBolt: ImageVector? = null

View File

@@ -1,72 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
val BadgesIcons.BadgeSailboat: ImageVector
get() {
if (_BadgeSailboat != null) {
return _BadgeSailboat!!
}
_BadgeSailboat = ImageVector.Builder(
name = "BadgeSailboat",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(fill = SolidColor(Color(0xFF000000))) {
moveTo(11f, 13.5f)
verticalLineTo(2f)
lineTo(3f, 13.5f)
horizontalLineTo(11f)
close()
moveTo(21f, 13.5f)
curveTo(21f, 6.5f, 14.5f, 1f, 12.5f, 1f)
curveToRelative(0f, 0f, 1f, 3f, 1f, 6.5f)
reflectiveCurveToRelative(-1f, 6f, -1f, 6f)
horizontalLineTo(21f)
close()
moveTo(22f, 15f)
horizontalLineTo(2f)
curveToRelative(0.31f, 1.53f, 1.16f, 2.84f, 2.33f, 3.73f)
curveTo(4.98f, 18.46f, 5.55f, 18.01f, 6f, 17.5f)
curveTo(6.73f, 18.34f, 7.8f, 19f, 9f, 19f)
reflectiveCurveToRelative(2.27f, -0.66f, 3f, -1.5f)
curveToRelative(0.73f, 0.84f, 1.8f, 1.5f, 3f, 1.5f)
reflectiveCurveToRelative(2.26f, -0.66f, 3f, -1.5f)
curveToRelative(0.45f, 0.51f, 1.02f, 0.96f, 1.67f, 1.23f)
curveTo(20.84f, 17.84f, 21.69f, 16.53f, 22f, 15f)
close()
moveTo(22f, 23f)
verticalLineToRelative(-2f)
horizontalLineToRelative(-1f)
curveToRelative(-1.04f, 0f, -2.08f, -0.35f, -3f, -1f)
curveToRelative(-1.83f, 1.3f, -4.17f, 1.3f, -6f, 0f)
curveToRelative(-1.83f, 1.3f, -4.17f, 1.3f, -6f, 0f)
curveToRelative(-0.91f, 0.65f, -1.96f, 1f, -3f, 1f)
horizontalLineTo(2f)
lineToRelative(0f, 2f)
horizontalLineToRelative(1f)
curveToRelative(1.03f, 0f, 2.05f, -0.25f, 3f, -0.75f)
curveToRelative(1.89f, 1f, 4.11f, 1f, 6f, 0f)
curveToRelative(1.89f, 1f, 4.11f, 1f, 6f, 0f)
horizontalLineToRelative(0f)
curveToRelative(0.95f, 0.5f, 1.97f, 0.75f, 3f, 0.75f)
horizontalLineTo(22f)
close()
}
}.build()
return _BadgeSailboat!!
}
@Suppress("ObjectPropertyName")
private var _BadgeSailboat: ImageVector? = null

View File

@@ -1,7 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.icons
object BadgesIcons

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Подкрепете ни със значки</string>
<string name="nav_rate_us">Оценки в Google Play</string>
<string name="nav_feedback_inapp_didnt_appear">Оценката не се вижда в приложението?</string>
<string name="nav_feedback_google_play">Изпращане към Google Play</string>
<string name="nav_feedback_scroll_to_reviews">Прелистете до оценките, за да изпратите своя</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Тази версия може да се разпространява само чрез Google Play.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Получили сте значка, благодарим ви!</item>
<item quantity="other">Получили сте %d значки, благодарим ви!</item>
</plurals>
<string name="available_badges">Налични значки</string>
<string name="available_badges_empty">Значките не могат да бъдат заредени, съжаляваме. Влезли ли сте в Play Store?</string>
<string name="what_are_badges">Какво представляват значките?</string>
<string name="what_are_badges_title">Какво представляват значките?</string>
<string name="what_are_badges_body">Значките са еднократни плащания в приложението. Ще спечелите хубава малка значка и с нея ще можете да ни подкрепите във времето.</string>
<string name="why_badges_title">Защо DAVx5 предлага значки без функционалност?</string>
<string name="why_badges_body">DAVx5 наистина се разрасна през годините! Все още активно разработваме нови възможности, осигуряваме поддръжка и винаги обновяваме приложението за предстоящите издания на Андроид. Тези значки са еднократни плащания, при които можете просто да покажете подкрепата си, като ни почерпите кафе или две&#8230; или 10 :-) Желанието ни е да бъдем възможно най-отворени, така че никога и по никакъв начин няма да има нови неща, заключени зад плащане в приложението.</string>
<string name="button_buy_badge_free">БЕЗПЛАТНО</string>
<string name="button_buy_badge_bought">Благодаря!</string>
<string name="earn_badges">Печелете значки, за да ни подкрепите!</string>
<string name="billing_unavailable">Billing API е недостъпно</string>
<string name="network_problems">Проблеми с мрежата, опитайте по-късно.</string>
</resources>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Ajudeu-nos amb insígnies</string>
<string name="nav_rate_us">Ressenya al Google Play</string>
<string name="nav_feedback_inapp_didnt_appear">La revisió a l\'aplicació no ha aparegut?</string>
<string name="nav_feedback_google_play">Envia en Google Play</string>
<string name="nav_feedback_scroll_to_reviews">Desplaceu-vos a la secció de revisió per a enviar comentaris</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Aquesta versió només pot ser distribuïda a través de Google Play.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Heu guanyat una insígnia, gràcies!</item>
<item quantity="other">Heu guanyat %d insígnies, gràcies!</item>
</plurals>
<string name="available_badges">Insígnies disponibles</string>
<string name="available_badges_empty">No s\'han pogut carregar les insígnies. Heu iniciat la sessió a la Play Store?</string>
<string name="what_are_badges">Què són les insígnies?</string>
<string name="what_are_badges_title">Què són les insígnies?</string>
<string name="what_are_badges_body">Les insígnies són pagaments d\'un sol ús en l\'aplicació. Guanyareu una petita insígnia i amb ella podreu ajudar-nos al llarg del temps.</string>
<string name="why_badges_title">Perquè DAVx5 ofereix insígnies sense característiques?</string>
<string name="why_badges_body">DAVx5 ha crescut realment amb els anys! Encara estem desenvolupant activament característiques noves, proporcionant suport i sempre actualitzem l\'aplicació per a les properes versions d\'Android. Aquestes insígnies són pagaments puntuals en els quals simplement podeu mostrar el vostre suport comprant-nos un cafè, o dos&#8230; o 10 :-) Volem ser el més oberts possible, així que mai hi haurà noves coses bloquejades per a un pagament en l\'aplicació.</string>
<string name="button_buy_badge_free">Lliure</string>
<string name="button_buy_badge_bought">Gràcies!</string>
<string name="earn_badges">Guanyeu insígnies per ajudar-nos!</string>
<string name="billing_unavailable">L\'API de facturació no està disponible</string>
<string name="network_problems">Hi ha problemes de xarxa. Torneu-ho a provar més tard.</string>
</resources>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Podpořte nás odznáčky</string>
<string name="nav_rate_us">Napsat recenzi v Google Play</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Získali jste odznáček, děkujeme!</item>
<item quantity="few">Získali jste %d odznáčky, děkujeme!</item>
<item quantity="many">Získali jste %d odznáčků, děkujeme!</item>
<item quantity="other">Získali jste %d odznáčků, děkujeme!</item>
</plurals>
<string name="available_badges">Odznáčky k dispozici</string>
<string name="available_badges_empty">Omlouváme se, nelze načíst odznáčky. Jste přihlášeni do obchodu Google Play?</string>
<string name="what_are_badges">Co jsou odznáčky?</string>
<string name="what_are_badges_title">Co jsou odznáčky?</string>
<string name="what_are_badges_body">Odznáčky jsou jednoduché jednorázové platby v aplikaci. Získáte hezký malý odznáček a pomocí něho nás v čase můžete podporovat.</string>
<string name="why_badges_title">Proč DAVx5 nabízí odznáčky bez funkcí?</string>
<string name="why_badges_body">DAVx5 v průběhu let značně vyrostl! Stále aktivně vyvíjíme nové funkce, poskytujeme podporu a vždy aktualizujeme aplikaci pro nejnovější verze Androidu. Tyto odznáčky jsou jednorázové platby, kterými nás můžete podpořit koupením kávy, nebo dvou&#8230; nebo 10 :-) Chceme mít co nejotevřenější kód, takže nové funkce nikdy nebudou zamčeny za další platby.</string>
<string name="button_buy_badge_free">ZDARMA</string>
<string name="button_buy_badge_bought">Děkujeme!</string>
<string name="earn_badges">Získejte odznáčky pro naši podporu!</string>
</resources>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Støt os med emblemer</string>
<string name="nav_rate_us">Bedøm i Google Play</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Du har fortjent et emblem, tak!</item>
<item quantity="other">Du har optjent %d emblemer, tak!</item>
</plurals>
<string name="available_badges">Mulige emblemer</string>
<string name="available_badges_empty">Beklager, kunne ikke indlæse emblemer. Er du logget ind i Play Store?</string>
<string name="what_are_badges">Hvad er emblemer?</string>
<string name="what_are_badges_title">Hvad er emblemer?</string>
<string name="what_are_badges_body">Badges er enkle engangsbetalinger i appen. Du optjener et fint lille emblem, og støtter os dermed.</string>
<string name="why_badges_title">Hvorfor tilbyder DAVx5 emblemer uden funktioner?</string>
<string name="why_badges_body">DAVx5 er virkelig vokset gennem årene! Vi udvikler stadig aktivt nye funktioner, yder support, og vi opdaterer altid appen til kommende Android-versioner. Disse badges er engangsbetalinger, hvor du blot kan vise din støtte ved at købe os en kaffe eller to&#8230; eller 10 :-) Vi prøver at være så åbne som muligt, så der vil aldrig være nye funktioner, der er gemt bag betaling i appen.</string>
<string name="button_buy_badge_free">GRATIS</string>
<string name="button_buy_badge_bought">Tak!</string>
<string name="earn_badges">Optjen emblemer for at støtte os!</string>
</resources>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Unterstütze uns mit Badges</string>
<string name="nav_rate_us">In Google Play bewerten</string>
<string name="nav_feedback_inapp_didnt_appear">Rezension innerhalb der App erschien nicht?</string>
<string name="nav_feedback_google_play">In Google Play senden</string>
<string name="nav_feedback_scroll_to_reviews">Zum Rezensionsabschnitt blättern, um Feedback zu übermitteln</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Diese Version ist ausschließlich zur Verteilung über Google Play bestimmt.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Du hast dir Abzeichen verdient, danke!</item>
<item quantity="other">Sie haben sich %d Abzeichen verdient, vielen Dank!</item>
</plurals>
<string name="available_badges">Verfügbare Abzeichen</string>
<string name="available_badges_empty">Tut mir leid, ich konnte keine Abzeichen laden. Sind Sie im Play Store eingeloggt?</string>
<string name="what_are_badges">Was sind Abzeichen?</string>
<string name="what_are_badges_title">Was sind Abzeichen?</string>
<string name="what_are_badges_body">Abzeichen sind einfache In-App-Zahlungen, die nur einmalig fällig werden. Sie verdienen sich ein nettes kleines Abzeichen und können uns damit über einen längeren Zeitraum unterstützen.</string>
<string name="why_badges_title">Warum bietet DAVx5 funktionslose Abzeichen an?</string>
<string name="why_badges_body">DAVx5 ist im Laufe der Jahre wirklich gewachsen! Wir entwickeln immer noch aktiv neue Funktionen, bieten Support und aktualisieren die App immer für kommende Android-Versionen. Diese Badges sind einmalige Zahlungen, mit denen Sie uns einfach Ihre Unterstützung zeigen können, indem Sie uns einen Kaffee kaufen, oder zwei&#8230; oder 10 :-) Wir wollen so offen wie möglich sein, daher wird es niemals neue Dinge geben, die an die In-App-Zahlung gebunden sind.</string>
<string name="button_buy_badge_free">GRATIS</string>
<string name="button_buy_badge_bought">Danke!</string>
<string name="earn_badges">Sammle Abzeichen, um uns zu unterstützen!</string>
<string name="billing_unavailable">Keine API zur Abrechnung vorhanden</string>
<string name="network_problems">Netzwerkprobleme, bitte später nochmal probieren.</string>
</resources>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Υποστηρίξτε μας με τα εμβλήματα</string>
<string name="nav_rate_us">Κριτική στο Google Play</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Κερδίσατε ένα σήμα, σας ευχαριστούμε!</item>
<item quantity="other">Κερδίσατε %d εμβλήματα, σας ευχαριστούμε!</item>
</plurals>
<string name="available_badges">Διαθέσιμα εμβλήματα</string>
<string name="available_badges_empty">Συγγνώμη, αδυναμία φόρτωσης εμβλημάτων. Έχετε συνδεθεί στο Play Store;</string>
<string name="what_are_badges">Τι είναι τα εμβλήματα;</string>
<string name="what_are_badges_title">Τι είναι τα εμβλήματα;</string>
<string name="what_are_badges_body">Τα εμβλήματα είναι απλές πληρωμές μίας χρήσης εντός της εφαρμογής. Θα κερδίσετε ένα ωραίο μικρό έμβλημα και με αυτό μπορείτε να μας υποστηρίξετε με την πάροδο του χρόνου.</string>
<string name="why_badges_title">Γιατί το DAVx5 προσφέρει εμβλήματα χωρίς χαρακτηριστικά;</string>
<string name="why_badges_body">Το DAVx5 έχει πραγματικά μεγαλώσει με τα χρόνια! Εξακολουθούμε να αναπτύσσουμε ενεργά νέα χαρακτηριστικά, να παρέχουμε υποστήριξη και να ενημερώνουμε πάντα την εφαρμογή για τις επερχόμενες εκδόσεις Android. Αυτά τα εμβλήματα είναι εφάπαξ πληρωμές όπου μπορείτε απλά να δείξετε την υποστήριξή σας αγοράζοντας μας έναν καφέ, ή δύο&#8230; ή 10 :-) Θέλουμε να είμαστε όσο το δυνατόν πιο ανοιχτοί, οπότε δεν θα υπάρξουν ποτέ-ποτέ νέα πράγματα κλειδωμένα με πληρωμή εντός της εφαρμογής.</string>
<string name="button_buy_badge_free">ΔΩΡΕΑΝ</string>
<string name="button_buy_badge_bought">Σας ευχαριστούμε!</string>
<string name="earn_badges">Κερδίστε εμβλήματα για να μας υποστηρίξετε!</string>
</resources>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Support us with badges</string>
<string name="nav_rate_us">Review in Google Play</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">You\'ve earned a badge, thank you!</item>
<item quantity="other">You have earned %d badges, thank you!</item>
</plurals>
<string name="available_badges">Available badges</string>
<string name="available_badges_empty">Sorry, could not load badges. Are you logged in to the Play Store?</string>
<string name="what_are_badges">What are badges?</string>
<string name="what_are_badges_title">What are badges?</string>
<string name="what_are_badges_body">Badges are simple in-app one-time-payments. You will earn a nice little badge and with it you can support us over time.</string>
<string name="why_badges_title">Why does DAVx5 offer feature-free badges?</string>
<string name="why_badges_body">DAVx5 has really grown over the years! We are still actively developing new features, providing support and we always update the app for upcoming Android versions. These badges are one-time payments where you can simply show your support by buying us a coffee, or two&#8230; or 10 :-) We want to be as open as possible, so there will never-ever be any new stuff locked to in-app payment.</string>
<string name="button_buy_badge_free">FREE</string>
<string name="button_buy_badge_bought">Thank you!</string>
<string name="earn_badges">Earn badges to support us!</string>
</resources>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Danos soporte con medallas</string>
<string name="nav_rate_us">Valora en Google Play</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Has ganado una medalla, gracias!</item>
<item quantity="many">Has ganado %d medallas, gracias!</item>
<item quantity="other">Has ganado %d medallas, gracias!</item>
</plurals>
<string name="available_badges">Medallas disponibles</string>
<string name="available_badges_empty">Perdona, no se han podido cargar las medallas. Tienes la sesión iniciada en el Play Store?</string>
<string name="what_are_badges">Qué son las medallas?</string>
<string name="what_are_badges_title">Qué son las medallas?</string>
<string name="what_are_badges_body">Las medallas son transacciones en la aplicación de un solo uso. Tendrás una pequeña medalla, y nos podrás dar soporte a lo largo del tiempo.</string>
<string name="why_badges_title">Por qué DAVx5 ofrece medallas sin funciones?</string>
<string name="why_badges_body">DAVx5 ha crecido mucho durante los años! Aún estamos desarrollando nuevas funciones continuamente, dando soporte, y actualizando la app para nuevas versiones de Android. Estas medallas son pagos únicos, con los que puedes mostrar soporte comprándonos un café, o dos&#8230; o 10 :-) Queremos ser lo más abiertos posible, así que nunca habrán nuevas funciones bloqueadas con microtransacciones.</string>
<string name="button_buy_badge_free">GRATIS</string>
<string name="button_buy_badge_bought">Gracias!</string>
<string name="earn_badges">Obtén medallas para darnos soporte!</string>
</resources>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Toeta meid reklaamsiltidega</string>
<string name="nav_rate_us">Koosta arvustus Google Plays</string>
<string name="nav_feedback_inapp_didnt_appear">Rakenduse-sisest arvustust pole näha?</string>
<string name="nav_feedback_google_play">Saada Google Plays</string>
<string name="nav_feedback_scroll_to_reviews">Tagasiside koostamiseks keri arvustuste lõiguni</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Seda versiooni on võimalik levitada Google Play kaudu.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Suur tänu, sa oled teeninud ühe reklaamsildi!</item>
<item quantity="other">Suur tänu, sa oled teeninud %d reklaamsilti!</item>
</plurals>
<string name="available_badges">Saadavalolevad reklaamsildid</string>
<string name="available_badges_empty">Vabandust, reklaamsiltide laadimine ei õnnestunud. Kas sa ikka oled Play Store\'i sisse loginud?</string>
<string name="what_are_badges">Mis on reklaamsildid?</string>
<string name="what_are_badges_title">Mis on reklaamsildid?</string>
<string name="what_are_badges_body">Reklaamsildid on lihtsad ühekordsed rakendusesisesed maksed. Sa teenid lihtsa ja toreda reklaamsildi ja sellega saad meid rahaliselt toetada.</string>
<string name="why_badges_title">Miks pakub DAVx⁵ funktsionaalsuseta reklaamsilte?</string>
<string name="why_badges_body">DAVx⁵ on aastate jooksul tõesti kasvanud! Me oleme seda jätkuvalt arendamas, tagame kasutajatoe ning alati kohandate teda järgmise uue Androidi versiooni jaoks. Need reklaamsildid on ühekordsed maksed, millega saad meid toetada, ostes ühe kohvi või kaks või kasvõi 10 :-) Me soovime olla võimalikult avatud ja rakendusesisestes maksetes ei saa kunagi olema midagi muud.</string>
<string name="button_buy_badge_free">TASUTA</string>
<string name="button_buy_badge_bought">Suur tänu!</string>
<string name="earn_badges">Meie toetamiseks teeni reklaamsilte!</string>
<string name="billing_unavailable">Billing API pole saadaval</string>
<string name="network_problems">Probleem võrguühendusega, palun proovi hiljem uuesti.</string>
</resources>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Lagundu bereizgarriekin</string>
<string name="nav_rate_us">Eman iritzia Google Play-en</string>
<string name="nav_feedback_inapp_didnt_appear">Ez da aplikazioko berrikuspenik agertu?</string>
<string name="nav_feedback_google_play">Bidali Google Play-n</string>
<string name="nav_feedback_scroll_to_reviews">Joan berrikuspen atalera iritzia bidaltzeko</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Bertsio hau Google Play bidez soilik banatzeko aukera dago.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one"> Bereizgarri bat irabazi dituzu, eskerrik asko!</item>
<item quantity="other">%d bereizgarri irabazi dituzu, eskerrik asko!</item>
</plurals>
<string name="available_badges">Bereizgarri eskuragarriak</string>
<string name="available_badges_empty">Sentitzen dugu, ezin izan dira bereizgarriak kargatu. Saioa hasi duzu Play Store-n?</string>
<string name="what_are_badges">Zer dira bereizgarriak</string>
<string name="what_are_badges_title">Zer dira bereizgarriak</string>
<string name="what_are_badges_body">Bereizgarriak aplikazioan egindako ordainketa bakun sinpleak dira. Bereizgarri bat irabaziko duzu eta honekin lagundu ahal diguzu denboran zehar.</string>
<string name="why_badges_title">Zergatik DAVx5-ek ezaugarririk gabeko bereizgarriak ematen ditu?</string>
<string name="why_badges_body">DAVx5 asko hazi egin da urteekin! Funtzionalitate berriak garatzen jarraitzen ditugu, laguntza ematen, eta aplikazioa eguneratzen dugu hurrengo Android bertsioetarako. Bereizgarri horiek ordainketa puntualak dira, eta kafe bat, edo bi&#8230; edo 10 :-) erostearekin proiektua babesten duzula erakusten diguzu. Ahalik eta irekien izan nahi gara eta, beraz, inoiz ez da gauza berririk egongo aplikazioan ordainketarik behar duenik.</string>
<string name="button_buy_badge_free">DOAN</string>
<string name="button_buy_badge_bought">Eskerrik asko!</string>
<string name="earn_badges">Irabazi bereizgarriak laguntzeko!</string>
<string name="billing_unavailable">Fakturazio APIa ez dago erabilgarri</string>
<string name="network_problems">Sareko arazoak, saiatu berriro geroago.</string>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Soutenez-nous avec des badges</string>
<string name="nav_rate_us">Avis sur Google Play</string>
<string name="available_badges">Badges disponibles</string>
<string name="available_badges_empty">Désolé, impossible de charger les badges. Êtes-vous connecté au Play Store ?</string>
<string name="what_are_badges">Qu\'est-ce que sont les badges ?</string>
<string name="what_are_badges_title">Qu\'est-ce que sont les badges ?</string>
<string name="what_are_badges_body">Les badges sont des paiements unitaires dans l\'application. Vous gagnerez un petit badge sympathique, qui permet de nous soutenir dans le temps.</string>
<string name="why_badges_title">Pourquoi DAVx5 propose-t-il des badges sans fonctionnalité ?</string>
<string name="why_badges_body">DAVx5 a considérablement grandi au fil des années ! Nous continions à développer activement de nouvelles fonctionnalités, faire du support et nous mettrons toujours à jour l\'application pour les prochaines versions d\'Android. Ces badges sont un paiement unitaire, mais vous pouvez aussi nous montrer votre soutien en nous payant un café ou 2 ; ou 10 :-) Nous voulons rester open-source autant que possible, il n\'y aura jamais de fonctionnalités verrouillée, nécessitant de payer pour les débloquer.</string>
<string name="button_buy_badge_free">GRATUIT</string>
<string name="button_buy_badge_bought">Merci !</string>
<string name="earn_badges">Acheter des badges pour nous soutenir !</string>
</resources>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Axúdanos con insignias</string>
<string name="nav_rate_us">Recensión en Google Play</string>
<string name="nav_feedback_inapp_didnt_appear">Non aparece a recensión na propia app?</string>
<string name="nav_feedback_google_play">Enviar en Google Play</string>
<string name="nav_feedback_scroll_to_reviews">Ir á sección de recensións para enviar a túa experiencia</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Esta versión só se pode distribuír en Google Play.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Gañaches unha insignia, grazas!</item>
<item quantity="other">Gañaches %d insignias, grazas!</item>
</plurals>
<string name="available_badges">Insignias dispoñibles</string>
<string name="available_badges_empty">Lamentámolo, pero non cargaron as insignias. Iniciaches sesión na Play Store?</string>
<string name="what_are_badges">Que son as insignias?</string>
<string name="what_are_badges_title">Que son as insignias?</string>
<string name="what_are_badges_body">As insignias son simples pagamentos únicos dentro da app. Gañarás unha pequena insignia coa que nos axudarás.</string>
<string name="why_badges_title">Por que DAVx5 ofrece insignias sen recompensa?</string>
<string name="why_badges_body">DAVx5 medrou moito nos últimos anos! Seguimos co seu desenvolvemento de xeito activo, proporcionando axuda e actualizando a app para as vindeiras versións de Android. Estas insignias son pagamentos únicos cos que mostras o teu apoio convidándonos a un café, ou two&#8230; ou 10 :-) Queremos ser o máis transparentes posible, polo que nunca engadiremos funcións que estén detrás dun valado de pagamento.</string>
<string name="button_buy_badge_free">DE BALDE</string>
<string name="button_buy_badge_bought">Grazas!</string>
<string name="earn_badges">Consigue insignias para axudarnos!</string>
<string name="billing_unavailable">API de pagos non dispoñible</string>
<string name="network_problems">Problemas coa rede, inténtao máis tarede.</string>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Támogatás jelvényekkel</string>
<string name="nav_rate_us">Értékelés a Google Playen</string>
<string name="nav_feedback_inapp_didnt_appear">Az alkalmazáson belüli értékelés nem jelent meg?</string>
<string name="nav_feedback_google_play">Küldés a Google Playen</string>
<string name="nav_feedback_scroll_to_reviews">Görgessen vissza a visszajelzési részhez, hogy visszajelzést küldjön</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Ez a verzió csak a Google Playen keresztül terjeszthető</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Egy jelvényt szerzett, köszönjük!</item>
<item quantity="other">%d jelvényt szerzett, köszönjük!</item>
</plurals>
<string name="available_badges">Választható jelvények</string>
<string name="available_badges_empty">A jelvények betöltése nem sikerült. Be van jelentkezve a Play Áruházba?</string>
<string name="what_are_badges">Mik azok a jelvények?</string>
<string name="what_are_badges_title">Mik azok a jelvények?</string>
<string name="what_are_badges_body">A jelvények egyszerű alkalmazáson belüli egyszeri fizetések. Egy szép kis jelvényt fog szerezni, miközben támogat minket.</string>
<string name="why_badges_title">Miért kínál a DAVx5 funkció nélküli jelvényeket?</string>
<string name="why_badges_body">A DAVx5 az évek során nagyon sokat fejlődött! Még mindig aktívan fejlesztünk új funkciókat, támogatást nyújtunk, és mindig frissítjük az alkalmazást az újabb Android-verziókhoz. Ezek a jelvények egyszeri befizetések, ahol egyszerűen megmutathatja a támogatását azzal, hogy vesz nekünk egy kávét, vagy kettőt&#8230; vagy 10-et :-) A lehető legnyitottabbak szeretnénk lenni, így soha-soha nem lesz semmilyen új funkcionalitás, ami alkalmazáson belüli fizetéshez kötött.</string>
<string name="button_buy_badge_free">INGYENES</string>
<string name="button_buy_badge_bought">Köszönjük!</string>
<string name="earn_badges">Szerezzen jelvényeket és támogasson minket!</string>
<string name="billing_unavailable">A számlázási API nem érhető el</string>
<string name="network_problems">Hálózati problémák, próbálja újra később.</string>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="nav_rate_us">Recensisci su Google Play</string>
<string name="what_are_badges">Cosa sono i badge?</string>
<string name="what_are_badges_title">Cosa sono i badge?</string>
<string name="what_are_badges_body">I badge sono semplici pagamenti una tantum in-app. Guadagnerai un piccolo badge e puoi usarlo per supportarci nel tempo.</string>
<string name="button_buy_badge_free">GRATIS</string>
<string name="button_buy_badge_bought">Grazie!</string>
<string name="earn_badges">Guadagna badge per supportarci!</string>
</resources>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">バッジで私たちを支援</string>
<string name="nav_rate_us">Google Play でレビュー</string>
<string name="nav_feedback_inapp_didnt_appear">アプリ内レビューが表示されませんか?</string>
<string name="nav_feedback_google_play">Google Play で送信</string>
<string name="nav_feedback_scroll_to_reviews">レビューまでスクロールして、フィードバックを送信してください</string>
<!-- AboutActivity -->
<string name="about_flavor_info">このバージョンは Google Play 経由でのみ配布されます。</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="other">%d 個のバッジをお持ちです。ありがとう!</item>
</plurals>
<string name="available_badges">利用できるバッジ</string>
<string name="available_badges_empty">バッジを読み込めませんでした。Play ストアにログインしていますか?</string>
<string name="what_are_badges">バッジとは何ですか?</string>
<string name="what_are_badges_title">バッジとは何ですか?</string>
<string name="what_are_badges_body">アプリ内で買い切りのバッジを購入できます。小さなバッジですが、私たちへの大きな支援になります。</string>
<string name="why_badges_title">DAVx5 のバッジを購入しても追加機能がないのはなぜですか?</string>
<string name="why_badges_body">DAVx5 は何年もの時を経て大きく成長してきました! 今も新たな機能の開発やサポートを継続しており、Android のバージョンアップにも常に対応してきました。これらのバッジはあなたが私たちを 1 杯、2 杯&#8230; 10 杯のコーヒーの分支援してくださったことを表します:-) 私たちはできるだけオープンでありたいと願っており、アプリ内購入が必要な新機能を導入することは - 今までもこれからも - ありません。</string>
<string name="button_buy_badge_free">無料</string>
<string name="button_buy_badge_bought">ありがとう!</string>
<string name="earn_badges">支援のためにバッジを入手!</string>
<string name="billing_unavailable">支払い API が利用できません</string>
<string name="network_problems">ネットワークに問題があります。後でもう一度お試しください。</string>
</resources>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">დაგვიჭირეთ მხარი ბეიჯებით</string>
<string name="nav_rate_us">Google Play-ში შეფასება</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">თქვენ მიიღეთ ბეიჯი, გმადლობთ!</item>
<item quantity="other">თქვენ მიიღეთ %d ბეიჯი, გმადლობთ!</item>
</plurals>
<string name="available_badges">ხელმისაწვდომი ბეიჯები</string>
<string name="available_badges_empty">უკაცრავად, ბეიჯები ვერ ჩაიტვირთა. შესული ხართ Play მაღაზიაში?</string>
<string name="what_are_badges">რა არის ბეიჯები?</string>
<string name="what_are_badges_title">რა არის ბეიჯები?</string>
<string name="what_are_badges_body">ბეიჯები არის მარტივი აპის შიდა ერთჯერადი გადახდები. შეგიძლიათ მიიღოთ ლამაზი ბეიჯი და მის საშუალებით, დროის განმავლობაში, დაგვიჭიროთ მხარი.</string>
<string name="why_badges_title">რატომ სთავაზობს DAVx⁵ ფუნქციების გარეშე ბეიჯებს?</string>
<string name="why_badges_body">DAVx⁵ საკმაოდ გაიზარდა წლების განმავლობაში! ჩვენ კვლავ აქტიურად ვამატებთ ახალ ფუნქციების, ვუწევთ მხარდაჭერას და ყოველთვის ვანახლებთ აპს მომავალი Android-ის ვერსიებისთვის. ეს ბეიჯები წარმოადგენს ერთჯერად გადახდებს, რომლითაც თქვენ მარტივად გვიჩვენებთ თქვენს მხარდაჭერას ჩვენთვის ერთი-ორი&#8230; ან 10 ყავის შეძენით :-) ჩვენ გვინდა ვიყოთ ღია, რამდენადაც ეს არის შესაძლებელი, ამიტომ არასდროს არ იქნება ახალი რამ, რაც იქნება დაბლოკილი აპის შიდა გადახდაზე.</string>
<string name="button_buy_badge_free">უფასო</string>
<string name="button_buy_badge_bought">გმადლობთ!</string>
<string name="earn_badges">მიიღეთ ბეიჯები ჩვენს მხარდასაჭერად!</string>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="available_badges">उपलब्ध बिल्ले </string>
<string name="what_are_badges">बिलयांचा अर्थ ?</string>
<string name="what_are_badges_title">बिलयांचा अर्थ ?</string>
<string name="button_buy_badge_free">मुफ्ट </string>
<string name="button_buy_badge_bought">धन्यवाद !</string>
</resources>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Bakke oss opp med emblemer</string>
<string name="available_badges_empty">Unnskyld, emblemer kan ikke lader. Er du anmeldet i Play Store?</string>
<string name="what_are_badges">Hva er emblemer?</string>
<string name="what_are_badges_title">Hva er emblemer?</string>
<string name="what_are_badges_body">Emblemer er simpel in-app engangsbetalinger. Di vil tjene en sött liten emblem og kan bakke oss opp med dem med tiden.</string>
<string name="why_badges_title">Hvorfor byr DAVx5 funksjon-fri emblemer?</string>
<string name="button_buy_badge_free">FRI</string>
<string name="button_buy_badge_bought">Tussen Takk!</string>
<string name="earn_badges">Tjene emblemer for å bakke oss opp!</string>
</resources>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Steun ons met badges</string>
<string name="nav_rate_us">Beoordeel in Google Play</string>
<string name="nav_feedback_inapp_didnt_appear">Is de in-app-recensie niet verschenen?</string>
<string name="nav_feedback_google_play">Verzenden in Google Play</string>
<string name="nav_feedback_scroll_to_reviews">Scroll naar het beoordelingsgedeelte om feedback te geven</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Deze versie is alleen geschikt voor distributie via Google Play.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Je hebt badges verdiend, bedankt!</item>
<item quantity="other">Je hebt %d badges verdiend, bedankt!</item>
</plurals>
<string name="available_badges">Beschikbare badges</string>
<string name="available_badges_empty">Sorry, kon geen badges laden. Ben je ingelogd in de Play Store?</string>
<string name="what_are_badges">Wat zijn badges?</string>
<string name="what_are_badges_title">Wat zijn badges?</string>
<string name="what_are_badges_body">Badges zijn eenvoudige eenmalige in-app betalingen. Je verdient een leuke kleine badge en daarmee kun je ons in de loop der tijd steunen.</string>
<string name="why_badges_title">Waarom biedt DAVx5 functievrije badges aan?</string>
<string name="why_badges_body">DAVx5 is in de loop der jaren echt gegroeid! We ontwikkelen nog steeds actief nieuwe functies, bieden ondersteuning en we werken de app altijd bij voor komende Android versies. Deze badges zijn eenmalige betalingen waarbij je gewoon je steun kunt tonen door ons een kopje koffie te kopen, of twee&#8230; of 10 :-) We willen zo open mogelijk zijn, dus zullen er nooit nieuwe dingen worden vergrendeld voor in-app betaling.</string>
<string name="button_buy_badge_free">Gratis</string>
<string name="button_buy_badge_bought">Dank U!</string>
<string name="earn_badges">Verdien badges om ons te steunen!</string>
<string name="billing_unavailable">Facturerings-API niet beschikbaar</string>
<string name="network_problems">Er zijn netwerkproblemen. Probeer het later opnieuw.</string>
</resources>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Wspieraj nas z odznakami</string>
<string name="nav_rate_us">Oceń w Google Play</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Zdobyłeś odznakę, dziękujemy!</item>
<item quantity="few">Zdobyłeś %d odznaki, dziękujemy</item>
<item quantity="many">Zdobyłeś %d odznaki, dziękujemy!</item>
<item quantity="other">Zdobyłeś %d odznak, dziękujemy!</item>
</plurals>
<string name="available_badges">Dostępne odznaki</string>
<string name="available_badges_empty">Przepraszamy, nie można załadować odznak. Czy jesteś zalogowany do Play Store?</string>
<string name="what_are_badges">Czym są odznaki?</string>
<string name="what_are_badges_title">Czym są odznaki?</string>
<string name="what_are_badges_body">Odznaki są po prostu jednorazowymi płatnościami w aplikacji. Zdobędziesz ładna, małą odznakę a przez to możesz nas wspierać.</string>
<string name="why_badges_title">Dlaczego DAVx5 offeruje odznaki bez funkcjonalności?</string>
<string name="why_badges_body">DAVx5 naprawdę rozrósł się przez lata! Wciąż aktywnie rozwijamy nowe funkcjonalności, dostarczamy wsparcie i zawsze aktualizujemy aplikację dla nadchodzących wersji systemu Android. Te odznaki są jednorazowymi płatnościami, przez które możesz okazac swoje wsparcie kupując nam kawę, albo dwie&#8230; albo 10 :-) Chcemy byc tak otwarci jak to możliwe więc nigdy ale to nigdy nie będzie nowych elementów ograniczonych poprzez płatności w aplikacji.</string>
<string name="button_buy_badge_free">DARMOWY</string>
<string name="button_buy_badge_bought">Dziękujemy!</string>
<string name="earn_badges">Zdobądź odznaki aby nas wspierać!</string>
</resources>

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Nos apoie com emblemas</string>
<string name="nav_rate_us">Avalie na Google Play</string>
<string name="nav_feedback_inapp_didnt_appear">A avaliação dentro do app não apareceu?</string>
<string name="nav_feedback_google_play">Enviar no Google Play</string>
<string name="nav_feedback_scroll_to_reviews">Deslize para a seção de avaliações para enviar um retorno</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Essa versão só é válida para distribuição pelo Google Play.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Você ganhou um emblema, obrigado!</item>
<item quantity="many">Você ganhou %d emblemas, obrigado!</item>
<item quantity="other">Você ganhou %d emblemas, obrigado!</item>
</plurals>
<string name="available_badges">Emblemas disponíveis</string>
<string name="available_badges_empty">Desculpe, não foi possível carregar os emblemas. Você está logado na Play Store?</string>
<string name="what_are_badges">O que são emblemas?</string>
<string name="what_are_badges_title">O que são emblemas?</string>
<string name="what_are_badges_body">Emblemas são simples pagamentos feitos uma vez só no app. Você ganha um pequeno emblema legal e com ele você nos apoia ao correr do tempo.</string>
<string name="why_badges_title">Por que que o DAVx5 oferece emblemas sem função?</string>
<string name="why_badges_body">O DAVx5 cresceu muito ao longo dos anos! Nós ainda estamos desenvolvendo ativamente novas funções, dando suporte, e sempre atualizamos o app para novas versões do Android. Esses emblemas são pagamentos feito-só-uma-vez onde você pode mostrar seu apoio comprando a nos um café, ou dois&#8230; ou 10 :-) Nós queremos ser o mais abertos possível, por causa disso nunca teremos novas coisas bloqueadas por pagamentos dentro do app.</string>
<string name="button_buy_badge_free">GRÁTIS</string>
<string name="button_buy_badge_bought">Obrigado!</string>
<string name="earn_badges">Ganhe emblemas para nos apoiar!</string>
<string name="billing_unavailable">API de cobrança indisponível</string>
<string name="network_problems">Problemas de rede, tente novamente mais tarde.</string>
</resources>

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Sprijină-ne cu insigne</string>
<string name="nav_rate_us">Recenzie în Google Play</string>
<string name="nav_feedback_inapp_didnt_appear">Examinarea în aplicație nu a apărut?</string>
<string name="nav_feedback_google_play">Trimite în Google Play</string>
<string name="nav_feedback_scroll_to_reviews">Derulează la secțiunea de revizuire pentru a trimite feedback</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Această versiune este eligibilă pentru distribuție numai prin Google Play.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Ai câștigat o insignă, îți mulțumim!</item>
<item quantity="few">Ai câștigat %d insigne, îți mulțumim!</item>
<item quantity="other">Ai câștigat %d insigne, îți mulțumim!</item>
</plurals>
<string name="available_badges">Ecusoane disponibile</string>
<string name="available_badges_empty">Ne pare rău, nu s-au putut încărca insignele. Ești autentificat în Magazinul Play?</string>
<string name="what_are_badges">Ce sunt insignele?</string>
<string name="what_are_badges_title">Ce sunt insignele?</string>
<string name="what_are_badges_body">Insignele sunt plăți simple în aplicație, o singură dată. Vei câștiga o insignă drăguță și cu ea ne poți susține în timp.</string>
<string name="why_badges_title">De ce DAVx5 oferă insigne fără funcții?</string>
<string name="why_badges_body">DAVx5 a crescut cu adevărat de-a lungul anilor! Încă dezvoltăm în mod activ noi funcții, oferim asistență și actualizăm întotdeauna aplicația pentru versiunile viitoare de Android. Aceste insigne sunt plăți unice în care poți pur și simplu să-ți arăți sprijinul cumpărându-ne o cafea, două&#8230; sau 10 :-) Vrem să fim cât mai deschiși posibil, așa că nu vor fi niciodată lucruri noi blocate prin plată prin aplicație.</string>
<string name="button_buy_badge_free">GRATUIT</string>
<string name="button_buy_badge_bought">Mulțumesc!</string>
<string name="earn_badges">Câștigă insigne pentru a ne susține!</string>
<string name="billing_unavailable">API-ul de facturare nu este disponibil</string>
<string name="network_problems">Probleme de rețea, încearcă din nou mai târziu.</string>
</resources>

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Поддержать нас значками</string>
<string name="nav_rate_us">Отзыв в Google Play</string>
<string name="nav_feedback_inapp_didnt_appear">Не появился отзыв о приложении?</string>
<string name="nav_feedback_google_play">Отправить в Google Play</string>
<string name="nav_feedback_scroll_to_reviews">Перейдите к разделу отзывов, чтобы оставить свой</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Эта версия может распространяться только через Google Play.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Вы заработали значок, спасибо!</item>
<item quantity="few">Вы заработали %d значка, спасибо!</item>
<item quantity="many">Вы заработали %d значков, спасибо!</item>
<item quantity="other">Вы заработали %d значков, спасибо!</item>
</plurals>
<string name="available_badges">Доступные значки</string>
<string name="available_badges_empty">К сожалению, не удалось загрузить значки. Вы авторизовались в Play Store?</string>
<string name="what_are_badges">Что такое значки?</string>
<string name="what_are_badges_title">Что такое значки?</string>
<string name="what_are_badges_body">Значки — это обычные одноразовые платежи в приложении. Вы заработаете миленький значок, и с его помощью вы сможете поддерживать нас в течение долгого времени.</string>
<string name="why_badges_title">Почему DAVx5 предлагает значки без каких-либо возможностей?</string>
<string name="why_badges_body">DAVx5 действительно вырос за эти годы! Мы по-прежнему активно разрабатываем новые функции, оказываем поддержку и всегда обновляем приложение для новых версий Android. Эти значки - одноразовые платежи, которыми вы можете просто показать свою поддержку, купив нам кофе, или два&#8230; или 10 :-) Мы хотим быть максимально открытыми, поэтому в приложении никогда не будет никаких новых функций, которые можно было бы оплатить через приложение.</string>
<string name="button_buy_badge_free">БЕСПЛАТНО</string>
<string name="button_buy_badge_bought">Спасибо!</string>
<string name="earn_badges">Зарабатывайте значки, поддерживая нас!</string>
<string name="billing_unavailable">API биллинга недоступен</string>
<string name="network_problems">Проблемы с сетью, попробуйте позже.</string>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Stöd oss med märken</string>
<string name="nav_rate_us">Betygsätt i Google Play</string>
<string name="nav_feedback_inapp_didnt_appear">Betygsättning i appen visades inte?</string>
<string name="nav_feedback_google_play">Skicka i Google Play</string>
<string name="nav_feedback_scroll_to_reviews">Bläddra till granskningssektionen för att skicka feedback</string>
<!-- AboutActivity -->
<string name="about_flavor_info">Den här versionen är endast kvalificerad för distribution över Google Play.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">Du har förtjänat ett märke. Stort tack!</item>
<item quantity="other">Du har förtjänat %d märken. Stort tack!</item>
</plurals>
<string name="available_badges">Tillgängliga märken</string>
<string name="available_badges_empty">Ledsen, men märken kunde inte hämtas. Är du inloggad i Play Store?</string>
<string name="what_are_badges">Vad är märken?</string>
<string name="what_are_badges_title">Vad är märken?</string>
<string name="what_are_badges_body">Märken är enkla engångsköp i appen. Du erhåller ett märke och stöder oss över tid.</string>
<string name="why_badges_title">Varför erbjuder DAVx5 märken utan funktioner?</string>
<string name="why_badges_body">DAVx5 har verkligen vuxit under årens lopp! Vi utvecklar aktivt nya funktioner, ger support och uppdaterar alltid appen för kommande Android-versioner. Dessa märken är engångsbetalningar där du helt enkelt kan visa ditt stöd genom att bjuda oss på en kaffe, eller två&#8230; eller 10 :-) Vi vill vara så öppna som möjligt och det kommer aldrig finnas funktioner som kräver köp i appen.</string>
<string name="button_buy_badge_free">GRATIS</string>
<string name="button_buy_badge_bought">Tack!</string>
<string name="earn_badges">Tjäna märken genom att stödja oss!</string>
<string name="billing_unavailable">Fakturerings-API inte tillgängligt</string>
<string name="network_problems">Nätverksproblem, försök igen senare</string>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">用徽章支持我们</string>
<string name="nav_rate_us">在 Google Play 中打分</string>
<string name="nav_feedback_inapp_didnt_appear">应用内预览没有出现?</string>
<string name="nav_feedback_google_play">在 Google Play 中发送</string>
<string name="nav_feedback_scroll_to_reviews">滚动到评价部分来提交反馈</string>
<!-- AboutActivity -->
<string name="about_flavor_info">此版本只允许在 Google Play 上发行。</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="other">你已赚得 %d 枚徽章,谢谢!</item>
</plurals>
<string name="available_badges">可用的徽章</string>
<string name="available_badges_empty">抱歉,无法加载徽章。你登录 Play Store 了吗?</string>
<string name="what_are_badges">什么是徽章?</string>
<string name="what_are_badges_title">什么是徽章?</string>
<string name="what_are_badges_body">徽章是一种简单的应用内一次性付费。付款后,你将获得一个漂亮的小徽章,你可以用它支持我们。</string>
<string name="why_badges_title">为何 DAVx5 提供无功能徽章?</string>
<string name="why_badges_body">DAVx5 这些年真的成长了很多!我们仍在积极开发新功能,提供支持,我们总是为即将到来的 Android 版本更新应用。这些徽章是一次性付款,你可以支付一两杯或 &#8230; 10 杯咖啡钱来表示你的支持 :-) 我们希望尽可能地开放,确保不会有任何新功能需要付费才能使用。</string>
<string name="button_buy_badge_free">免费</string>
<string name="button_buy_badge_bought">谢谢!</string>
<string name="earn_badges">用徽章表示支持!</string>
<string name="billing_unavailable">账单 API 不可用</string>
<string name="network_problems">网络问题,请稍后再试</string>
</resources>

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Navigation drawer -->
<string name="nav_earn_badges">Support us with badges</string>
<string name="nav_rate_us">Review in Google Play</string>
<string name="nav_feedback_inapp_didnt_appear">In-app review didn\'t appear?</string>
<string name="nav_feedback_google_play">Send in Google Play</string>
<string name="nav_feedback_scroll_to_reviews">Scroll to review section to submit feedback</string>
<!-- AboutActivity -->
<string name="about_flavor_info">This version is only eligible for distribution over Google Play.</string>
<!-- EarnBadgesActivity -->
<plurals name="you_earned_badges">
<item quantity="one">You\'ve earned a badge, thank you!</item>
<item quantity="other">You have earned %d badges, thank you!</item>
</plurals>
<string name="available_badges">Available badges</string>
<string name="available_badges_empty">Sorry, could not load badges. Are you logged in to the Play Store?</string>
<string name="what_are_badges">What are badges?</string>
<string name="what_are_badges_title">What are badges?</string>
<string name="what_are_badges_body">Badges are simple in-app one-time-payments. You will earn a nice little badge and with it you can support us over time.</string>
<string name="why_badges_title">Why does DAVx5 offer feature-free badges?</string>
<string name="why_badges_body">DAVx5 has really grown over the years! We are still actively developing new features, providing support and we always update the app for upcoming Android versions. These badges are one-time payments where you can simply show your support by buying us a coffee, or two&#8230; or 10 :-) We want to be as open as possible, so there will never-ever be any new stuff locked to in-app payment.</string>
<string name="button_buy_badge_free">FREE</string>
<string name="button_buy_badge_bought">Thank you!</string>
<string name="earn_badges">Earn badges to support us!</string>
<string name="billing_unavailable">Billing API not available</string>
<string name="purchase_acknowledgement_failed">Failed to acknowledge purchase</string>
<string name="purchase_acknowledgement_successful">Thank you for the support!</string>
<string name="purchase_failed">Purchase failed</string>
<string name="network_problems">Network problems, please try again later.</string>
</resources>

View File

@@ -13,8 +13,10 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import at.bitfire.dav4jvm.HttpUtils.toKtorUrl
import at.bitfire.davdroid.util.DavUtils.MEDIA_TYPE_OCTET_STREAM
import at.bitfire.davdroid.webdav.DocumentState
import io.ktor.http.Url
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@@ -128,6 +130,9 @@ data class WebDavDocument(
return builder.build()
}
suspend fun toKtorUrl(db: AppDatabase): Url =
toHttpUrl(db).toKtorUrl()
/**
* Represents a WebDAV document in a given state (with a given ETag/Last-Modified).

View File

@@ -0,0 +1,65 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.di
import android.content.Context
import at.bitfire.cert4android.CustomCertManager
import at.bitfire.cert4android.CustomCertStore
import at.bitfire.cert4android.SettingsProvider
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.ForegroundTracker
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.internal.tls.OkHostnameVerifier
import java.util.Optional
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
/**
* cert4android integration module
*/
class CustomCertManagerModule {
@Provides
@Singleton
fun customCertManager(
@ApplicationContext context: Context,
settings: SettingsManager
): Optional<CustomCertManager> =
if (BuildConfig.allowCustomCerts)
Optional.of(
CustomCertManager(
certStore = CustomCertStore.getInstance(context),
settings = object : SettingsProvider {
override val appInForeground: Boolean
get() = ForegroundTracker.inForeground.value
override val trustSystemCerts: Boolean
get() = !settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES)
}
))
else
Optional.empty()
@Provides
@Singleton
fun customHostnameVerifier(
customCertManager: Optional<CustomCertManager>
): Optional<CustomCertManager.HostnameVerifier> =
if (BuildConfig.allowCustomCerts && customCertManager.isPresent) {
val hostnameVerifier = customCertManager.get().HostnameVerifier(OkHostnameVerifier)
Optional.of(hostnameVerifier)
} else
Optional.empty()
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.network
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
class ConnectionSecurityContext(
val sslSocketFactory: SSLSocketFactory?,
val trustManager: X509TrustManager?,
val hostnameVerifier: HostnameVerifier?,
val disableHttp2: Boolean
)

Some files were not shown because too many files have changed in this diff Show More