Compare commits

..

396 Commits

Author SHA1 Message Date
Ricki Hirner
38bad81c55 [WIP] Split into packages 2026-02-05 14:36:39 +01:00
Ricki Hirner
31a7ad0939 Move OSE code to separate package
- Move DebugInfoCrashHandler.kt to com.davx5.ose
- Move StandardLoginTypePage.kt to com.davx5.ose.ui.setup
- Move StandardLoginTypesProvider.kt to com.davx5.ose.ui.setup
- Move CustomCertManagerModule.kt to com.davx5.ose.di
- Move OseIntroPageFactory.kt to com.davx5.ose.ui.intro
- Move OseColorSchemesModule.kt to com.davx5.ose.di
- Move OseFlavorModule.kt to com.davx5.ose.di
- Move OpenSourceLicenseInfoProvider.kt to com.davx5.ose.ui.about
- Move OseTheme.kt to com.davx5.ose.ui
2026-02-05 14:10:14 +01:00
Ricki Hirner
5e84648fb4 Replace BuildConfig.allowCustomCerts by DI (#1971)
* Update IntroScreen colors

- Replace M3ColorScheme with MaterialTheme.colorScheme
- Update background and icon colors to use MaterialTheme

* Update color scheme references

- Replace `M3ColorScheme.primaryLight` with `MaterialTheme.colorScheme.primary` in `WelcomePage.kt` and `AccountsDrawerHandler.kt`.

* Update AppTheme to accept custom color schemes

- Add `lightColorScheme` and `darkColorScheme` parameters
- Replace hardcoded color schemes with the new parameters

* Add color scheme dependency injection

- Add `LightColorScheme` and `DarkColorScheme` qualifiers
- Create `OseColorSchemes` module for providing color schemes
- Update `AppTheme` to use injected color schemes

* Update glance material dependency to material3

- Update `androidx.glance.material` to `androidx.glance.material3`
- Adjust imports and usage in `IconSyncButtonWidget.kt` and `LabeledSyncButtonWidget.kt` to use `GlanceTheme` and `ColorProviders` for color schemes
- Replace deprecated `ColorProvider` with `GlanceTheme.colors` for primary and onPrimary colors

* Refactor widget receivers and widgets to use dependency injection more properly

- Update `LabeledSyncButtonWidgetReceiver` and `IconSyncButtonWidgetReceiver` to use Hilt for dependency injection.
- Inject `SyncWidgetModel`, `LightColorScheme`, and `DarkColorScheme` into both widget receivers.
- Remove the use of `EntryPoint` and `EntryPointAccessors` from `LabeledSyncButtonWidget` and `IconSyncButtonWidget`.
- Pass injected dependencies directly to the widget constructors.

* Rename ThemeColors to OseTheme

- Update imports and references to use OseTheme
- Rename object M3ColorScheme to OseTheme

* Move AppTheme to ui.composable package because it's a reusable Composable

* Update AboutApp to use dynamic version info instead of BuildConfig

- Pass versionName and versionCode from AboutModel to AboutApp
- Remove dependency on BuildConfig in AboutApp
- Update AboutApp_Preview with sample version info

* Update URI statistics parameters

- Update `withStatParams` to include package name and version
- Replace `BuildConfig.APPLICATION_ID` with `context.packageName`
- Add context parameter to `withStatParams` in various activities

* Don't depend on BuildConfig for application name and version

- Introduce `ProductIds` for managing product IDs and User-Agent
- Update various classes to use `ProductIds` for product ID generation
- Move `TextTable` class from `at.bitfire.davdroid` to `at.bitfire.davdroid.util`

* Refactor OAuth classes for dependency injection

- Convert `OAuthFastmail` and `OAuthGoogle` to injectable classes
- Update `FastmailLoginModel` and `GoogleLoginModel` to use injected instances
- Move `redirectUri` initialization to `OAuthIntegration` constructor

* Adapt DI

- Move CustomCertManagerModule to ose configuration
- Move coroutine scopes to scope package

* minor changes

* Remove custom certificate build config

- Remove `allowCustomCerts` build config field
- Replace `@Singleton` with `@Reusable` in CustomCertManagerModule

* Update imports and LogcatHandler initialization

- Update imports to use scoped dispatchers
- Replace BuildConfig.APPLICATION_ID with javaClass.name in LogcatHandler initialization
2026-02-05 13:59:31 +01:00
Ricki Hirner
490abcb88a Reduce BuildConfig dependencies (#1969)
* Update IntroScreen colors

- Replace M3ColorScheme with MaterialTheme.colorScheme
- Update background and icon colors to use MaterialTheme

* Update color scheme references

- Replace `M3ColorScheme.primaryLight` with `MaterialTheme.colorScheme.primary` in `WelcomePage.kt` and `AccountsDrawerHandler.kt`.

* Update AppTheme to accept custom color schemes

- Add `lightColorScheme` and `darkColorScheme` parameters
- Replace hardcoded color schemes with the new parameters

* Add color scheme dependency injection

- Add `LightColorScheme` and `DarkColorScheme` qualifiers
- Create `OseColorSchemes` module for providing color schemes
- Update `AppTheme` to use injected color schemes

* Update glance material dependency to material3

- Update `androidx.glance.material` to `androidx.glance.material3`
- Adjust imports and usage in `IconSyncButtonWidget.kt` and `LabeledSyncButtonWidget.kt` to use `GlanceTheme` and `ColorProviders` for color schemes
- Replace deprecated `ColorProvider` with `GlanceTheme.colors` for primary and onPrimary colors

* Refactor widget receivers and widgets to use dependency injection more properly

- Update `LabeledSyncButtonWidgetReceiver` and `IconSyncButtonWidgetReceiver` to use Hilt for dependency injection.
- Inject `SyncWidgetModel`, `LightColorScheme`, and `DarkColorScheme` into both widget receivers.
- Remove the use of `EntryPoint` and `EntryPointAccessors` from `LabeledSyncButtonWidget` and `IconSyncButtonWidget`.
- Pass injected dependencies directly to the widget constructors.

* Rename ThemeColors to OseTheme

- Update imports and references to use OseTheme
- Rename object M3ColorScheme to OseTheme

* Move AppTheme to ui.composable package because it's a reusable Composable

* Update AboutApp to use dynamic version info instead of BuildConfig

- Pass versionName and versionCode from AboutModel to AboutApp
- Remove dependency on BuildConfig in AboutApp
- Update AboutApp_Preview with sample version info

* Update URI statistics parameters

- Update `withStatParams` to include package name and version
- Replace `BuildConfig.APPLICATION_ID` with `context.packageName`
- Add context parameter to `withStatParams` in various activities

* Don't depend on BuildConfig for application name and version

- Introduce `ProductIds` for managing product IDs and User-Agent
- Update various classes to use `ProductIds` for product ID generation
- Move `TextTable` class from `at.bitfire.davdroid` to `at.bitfire.davdroid.util`

* Refactor OAuth classes for dependency injection

- Convert `OAuthFastmail` and `OAuthGoogle` to injectable classes
- Update `FastmailLoginModel` and `GoogleLoginModel` to use injected instances
- Move `redirectUri` initialization to `OAuthIntegration` constructor
2026-02-05 12:20:21 +01:00
Ricki Hirner
cca12e79d8 [UI] Properly provide color schemes over DI (#1966)
* Update IntroScreen colors

- Replace M3ColorScheme with MaterialTheme.colorScheme
- Update background and icon colors to use MaterialTheme

* Update color scheme references

- Replace `M3ColorScheme.primaryLight` with `MaterialTheme.colorScheme.primary` in `WelcomePage.kt` and `AccountsDrawerHandler.kt`.

* Update AppTheme to accept custom color schemes

- Add `lightColorScheme` and `darkColorScheme` parameters
- Replace hardcoded color schemes with the new parameters

* Add color scheme dependency injection

- Add `LightColorScheme` and `DarkColorScheme` qualifiers
- Create `OseColorSchemes` module for providing color schemes
- Update `AppTheme` to use injected color schemes

* Update glance material dependency to material3

- Update `androidx.glance.material` to `androidx.glance.material3`
- Adjust imports and usage in `IconSyncButtonWidget.kt` and `LabeledSyncButtonWidget.kt` to use `GlanceTheme` and `ColorProviders` for color schemes
- Replace deprecated `ColorProvider` with `GlanceTheme.colors` for primary and onPrimary colors

* Refactor widget receivers and widgets to use dependency injection more properly

- Update `LabeledSyncButtonWidgetReceiver` and `IconSyncButtonWidgetReceiver` to use Hilt for dependency injection.
- Inject `SyncWidgetModel`, `LightColorScheme`, and `DarkColorScheme` into both widget receivers.
- Remove the use of `EntryPoint` and `EntryPointAccessors` from `LabeledSyncButtonWidget` and `IconSyncButtonWidget`.
- Pass injected dependencies directly to the widget constructors.

* Rename ThemeColors to OseTheme

- Update imports and references to use OseTheme
- Rename object M3ColorScheme to OseTheme

* Move AppTheme to ui.composable package because it's a reusable Composable

* Always use Light Color Scheme for certain intro UI

- Inject `lightColorScheme` in `IntroActivity`
- Pass `lightColorScheme` to `IntroScreen`
- Use `lightColorScheme` for background and color in `IntroScreen` and `WelcomePage`

* Minor syntax
2026-02-05 11:43:55 +01:00
Ricki Hirner
915cf73027 Update version to 4.5.9 2026-02-04 10:58:57 +01:00
Ricki Hirner
53773eaf83 Go back to 4.5.9-rc.2 (4.5.9 with version code 405090003 was never released) 2026-02-03 16:00:55 +01:00
Ricki Hirner
9cd685982d Update synctools to correctly process tel: URIs in vCards (#1963) 2026-02-03 15:59:46 +01:00
Sunik Kupfer
d4902e84ce [synctools] Tasks rewrite: Use reader/writer (#1959)
* Update synctools; Use writer/reader

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-02-03 15:22:15 +01:00
dependabot[bot]
ec485fcfa5 Bump the app-dependencies group with 8 updates (#1961)
Bumps the app-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| gradle-wrapper | `9.3.0` | `9.3.1` |
| androidx.activity:activity-compose | `1.12.2` | `1.12.3` |
| androidx.compose:compose-bom | `2026.01.00` | `2026.01.01` |
| androidx.paging:paging-runtime-ktx | `3.3.6` | `3.4.0` |
| androidx.paging:paging-compose | `3.3.6` | `3.4.0` |
| androidx.work:work-runtime-ktx | `2.11.0` | `2.11.1` |
| androidx.work:work-testing | `2.11.0` | `2.11.1` |
| [com.google.devtools.ksp](https://github.com/google/ksp) | `2.3.4` | `2.3.5` |


Updates `gradle-wrapper` from 9.3.0 to 9.3.1

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

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

Updates `androidx.paging:paging-runtime-ktx` from 3.3.6 to 3.4.0

Updates `androidx.paging:paging-compose` from 3.3.6 to 3.4.0

Updates `androidx.paging:paging-compose` from 3.3.6 to 3.4.0

Updates `androidx.work:work-runtime-ktx` from 2.11.0 to 2.11.1

Updates `androidx.work:work-testing` from 2.11.0 to 2.11.1

Updates `androidx.work:work-testing` from 2.11.0 to 2.11.1

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

---
updated-dependencies:
- dependency-name: gradle-wrapper
  dependency-version: 9.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.activity:activity-compose
  dependency-version: 1.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2026.01.01
  dependency-type: direct:production
  dependency-group: app-dependencies
- dependency-name: androidx.paging:paging-runtime-ktx
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.paging:paging-compose
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.paging:paging-compose
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-runtime-ktx
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-testing
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-testing
  dependency-version: 2.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.google.devtools.ksp
  dependency-version: 2.3.5
  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-02-03 14:57:10 +01:00
Ricki Hirner
5709aaa2e5 Bump version to 4.5.9 2026-02-03 12:12:05 +01:00
Ricki Hirner
a19c397ef6 Sync files with davx5 repo (#1958)
* Sync changes with davx5 repo

* Remove unnecessary test launcher icons
2026-02-02 12:30:04 +01:00
Ricki Hirner
dad4298dd5 Bump version to 4.5.9-rc.1 2026-02-02 10:12:52 +01:00
Weblate (bot)
8e78e6e3ac Translations update from Hosted Weblate (#1955)
* 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/

* Translated using Weblate (Romanian)

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/ro/

* Translated using Weblate (Italian)

Currently translated at 88.1% (379 of 430 strings)

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

* Translated using Weblate (Italian)

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/it/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (437 of 437 strings)

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

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

Currently translated at 100.0% (437 of 437 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (437 of 437 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (437 of 437 strings)

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

---------

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>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Alì Mortacci <newscpq@vivaldi.net>
2026-02-02 10:12:10 +01:00
Ricki Hirner
fc878d519f Update synctools to process phone numbers with VALUE=URI (#1956) 2026-02-02 10:11:40 +01:00
Ricki Hirner
45d5d809fc Bump version to 4.5.9-alpha.2 2026-01-29 14:17:59 +01:00
Ricki Hirner
ef1d90f740 Cache SSLSocketFactories to allow okhttp HTTPS connection reuse (#1942)
* Reuse CustomCertManager

- Update bitfire-cert4android to 75cc6913fd
- Refactor HttpClientBuilder to use Optional for customTrustManager and customHostnameVerifier
- Add CustomCertManagerModule for dependency injection

* Implement connection security manager for HTTP client

- Introduce `ConnectionSecurityManager` and `ConnectionSecurityContext` classes
- Refactor `HttpClientBuilder` to use the new security manager for SSL context setup

* [WIP] Cache SSLContext by certificate alias

- Add context cache using Guava CacheBuilder
- Cache SSLContext in getContext method

* Update comments in HttpClientBuilder.kt for clarity

* Update ConnectionSecurityManager to use SSLSocketFactory caching

* Refactor socket factory caching logic for better clarity

* Add tests

* Refactor socket factory cache to store only SSLSocketFactory

* Minor changes
- Change socketFactoryCache to use LinkedHashMap instead of ConcurrentHashMap
- Update cache key handling to use String? instead of Optional<String>

* Add tests for caching

* Add logging

* Indenting

* Minor simplification

* Fix tests
2026-01-29 14:09:09 +01:00
Arnau Mora
5efcbfc5a3 Set Calendars.IS_PRIMARY to 0 by default (#1945)
* Start setting `IS_PRIMARY`

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

* Set `IS_PRIMARY` to `0`

* Remove unused injection

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

* Improve explanation

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
2026-01-29 12:30:23 +01:00
Sunik Kupfer
4f3ff69b43 Update synctools (#1952)
Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2026-01-29 12:18:29 +01:00
Ricki Hirner
afe00c275e Update CODEOWNERS (#1949) 2026-01-28 13:18:28 +01:00
Sunik Kupfer
03c4aa9938 Ignore AccountSettingsMigration21Test.testCancelsSyncAndClearsPendingState with flaky behaviour in CI (#1948)
Ignore test with flaky behaviour in CI

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2026-01-28 11:25:00 +01:00
dependabot[bot]
63a5359c06 Bump the app-dependencies group with 7 updates (#1944)
Bumps the app-dependencies group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [io.ktor:ktor-client-content-negotiation](https://github.com/ktorio/ktor) | `3.3.3` | `3.4.0` |
| [io.ktor:ktor-client-core](https://github.com/ktorio/ktor) | `3.3.3` | `3.4.0` |
| [io.ktor:ktor-client-okhttp](https://github.com/ktorio/ktor) | `3.3.3` | `3.4.0` |
| [io.ktor:ktor-serialization-kotlinx-json](https://github.com/ktorio/ktor) | `3.3.3` | `3.4.0` |
| [io.mockk:mockk](https://github.com/mockk/mockk) | `1.14.7` | `1.14.9` |
| [io.mockk:mockk-android](https://github.com/mockk/mockk) | `1.14.7` | `1.14.9` |
| [org.robolectric:robolectric](https://github.com/robolectric/robolectric) | `4.16` | `4.16.1` |


Updates `io.ktor:ktor-client-content-negotiation` from 3.3.3 to 3.4.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.3...3.4.0)

Updates `io.ktor:ktor-client-core` from 3.3.3 to 3.4.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.3...3.4.0)

Updates `io.ktor:ktor-client-okhttp` from 3.3.3 to 3.4.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.3...3.4.0)

Updates `io.ktor:ktor-serialization-kotlinx-json` from 3.3.3 to 3.4.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.3...3.4.0)

Updates `io.ktor:ktor-client-core` from 3.3.3 to 3.4.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.3...3.4.0)

Updates `io.ktor:ktor-client-okhttp` from 3.3.3 to 3.4.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.3...3.4.0)

Updates `io.ktor:ktor-serialization-kotlinx-json` from 3.3.3 to 3.4.0
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.3...3.4.0)

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

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

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

Updates `org.robolectric:robolectric` from 4.16 to 4.16.1
- [Release notes](https://github.com/robolectric/robolectric/releases)
- [Commits](https://github.com/robolectric/robolectric/compare/robolectric-4.16...robolectric-4.16.1)

---
updated-dependencies:
- dependency-name: io.ktor:ktor-client-content-negotiation
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: io.ktor:ktor-client-core
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: io.ktor:ktor-client-okhttp
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: io.ktor:ktor-serialization-kotlinx-json
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: io.ktor:ktor-client-core
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: io.ktor:ktor-client-okhttp
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: io.ktor:ktor-serialization-kotlinx-json
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: io.mockk:mockk
  dependency-version: 1.14.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: io.mockk:mockk-android
  dependency-version: 1.14.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: io.mockk:mockk-android
  dependency-version: 1.14.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: org.robolectric:robolectric
  dependency-version: 4.16.1
  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-26 17:26:59 +01:00
Weblate (bot)
89a7cd2885 Translations update from Hosted Weblate (#1943)
* 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/

* Translated using Weblate (Romanian)

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/ro/

* Translated using Weblate (Italian)

Currently translated at 88.1% (379 of 430 strings)

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

* Translated using Weblate (Italian)

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/it/

---------

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>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Alì Mortacci <newscpq@vivaldi.net>
2026-01-25 18:14:56 +01:00
Ricki Hirner
db25570581 Bump version to 4.5.9-alpha.1 2026-01-25 17:52:15 +01:00
Arnau Mora
3de34e53d0 Migrate translation contributors credits to Weblate (#1918)
* Update loading method for Weblate

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

* Add fetch script

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

* Add test function

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

* Add credits from Transifex

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

* Add workflow for updating Weblate credits

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

* Improve styling

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

* Add line break at the end

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

* Filter Ricki in translation contributions

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

* Display Transifex translations

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

* Modify PRs, not the base branch

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

* Fix paddings

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

* Rename weblateTranslations flow and loadWeblateTranslations function

* Rename variables and functions from `translations` to `translators`

* - Move AboutActivity.Model class to AboutModel

* Rename translators files

* Minor renaming

* Refactor sorting logic into `sortTranslators` function

* Rename script to fetch Weblate translators

* - Remove `AboutActivityTest` class
- Move `loadTransifexTranslators` function to `AboutModel`
- Update `loadWeblateTranslators` function in `AboutModel`

* Update Weblate workflow

* - Combine Weblate and Transifex translators into a single list
- Update AboutActivity to use the combined list
- Add tests for the new functionality

* Merge Transifex and Weblate translators by username, don't show language, add Engage widget

* Extract TranslationsTab

* AboutModel: update tests

* Add thanks message for translation contributors

* Move translation credits to Weblate

- Update `OpenSourceLicenseInfoProvider` path
- Add new strings for translations credits
- Update `TranslationsTab` with new strings

* Add Accept-Language, update Context

* Update Weblate Translators Workflow

- Allow workflow to run on specific branch for testing
- Remove unnecessary fetch-depth comment

* Update Weblate Translators Workflow (2)

* Remove workflow

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2026-01-25 17:51:34 +01:00
Ricki Hirner
af084fb5d0 Add "Backups reminder" intro screen (#1937)
* Add BackupsPage to intro UI

* Add BackupsPage UI

- Implement BackupsPage composable with UI elements
- Add strings for backups reminder and acceptance

* Add to "Reset hints"

* Add another paragraph

* Update backup screen hints

- Update backups reminder text
- Remove redundant versioning note
- Update acceptance message
2026-01-25 17:48:09 +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
Sunik Kupfer
2d10cbb07d Improve closing of content provider in verify account owner test (#1838)
* Optimize imports

* Remove the ignore annotation

* Move provider use out of verify method

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

* Remove unnecessary provider.use blocks

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

* Add spaces

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

* Rename lambda param provider to client in LocalDataStore implementations

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

* Enhance kdoc

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

* Improve provider client usage

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

* Replace calling apply with assignment

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

* Remove whitespace

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

* Add nullable returns even though they never return null

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

* Apply WillNotClose annotation to client parameter instead of method

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-12-02 10:34:39 +01:00
dependabot[bot]
88928792af Bump the app-dependencies group with 2 updates (#1860)
Bumps the app-dependencies group with 2 updates: [io.ktor:ktor-client-core](https://github.com/ktorio/ktor) and [io.ktor:ktor-client-okhttp](https://github.com/ktorio/ktor).


Updates `io.ktor:ktor-client-core` from 3.3.2 to 3.3.3
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.2...3.3.3)

Updates `io.ktor:ktor-client-okhttp` from 3.3.2 to 3.3.3
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.2...3.3.3)

Updates `io.ktor:ktor-client-okhttp` from 3.3.2 to 3.3.3
- [Release notes](https://github.com/ktorio/ktor/releases)
- [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ktorio/ktor/compare/3.3.2...3.3.3)

---
updated-dependencies:
- dependency-name: io.ktor:ktor-client-core
  dependency-version: 3.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: io.ktor:ktor-client-okhttp
  dependency-version: 3.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: io.ktor:ktor-client-okhttp
  dependency-version: 3.3.3
  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>
2025-12-01 18:36:53 +01:00
Sunik Kupfer
b5e8c80db1 Try to fix pending sync state test failures by using a hot flow (#1839)
* Remove the ignore annotation

* Turn inPendingState in to a hot state flow for the test duration

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

* Rename methods registering the sync state observer

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-12-01 14:40:52 +01:00
Ricki Hirner
6f09f55e1a Update synctools (ignores DTEND < DTSTART and inverts negative event durations) (#1858) 2025-12-01 12:02:42 +01:00
Ricki Hirner
b08f10a98f HttpClientBuilder: clarify documentation for authDomain (#1857)
* Update authentication domain parameter

- Rename `onlyHost` to `authDomain` in `fromAccount`
- Update `authenticate` method to use `domain` instead of `host`
- Clarify documentation for `authDomain` parameter

* More KDoc

* Fix other calls / tests
2025-12-01 11:54:36 +01:00
Sunik Kupfer
a3a952d875 LocalTaskList/LocalTask: Consume fields provided by synctools (#1811)
* Move companion object to end of class

* Update synctools

* Make DmfsTaskList final

* Use DmfsTaskList SyncState

* Drop fields now provided in DmfsTask and adapt constructors

* Use column constants from DmfsTask instead

* Use DmfsTask column constants

* Update synctools

* Don't handle scheduleTag

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>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-11-27 16:35:17 +01:00
Ricki Hirner
e9fc570895 Extract ResourceDownloader from ContactsSyncManager, add tests (#1849)
* Add ResourceDownloader and tests

- Introduce `ResourceDownloader` class for downloading external resources
- Add unit tests for `ResourceDownloader`
- Refactor `ContactsSyncManager` to use `ResourceDownloader`

* KDoc

- Add detailed documentation for `download` method
- Clarify authentication handling and return behavior

* Minor changes
2025-11-27 16:10:33 +01:00
Ricki Hirner
098b7d5b12 Log warning instead of throwing exception on multiple build calls (#1847)
Log warning instead of throwing IllegalStateException on multiple build calls

- Change `build()` to log a warning instead of throwing an exception on subsequent calls.
- Change `buildKtor()` to log a warning instead of throwing an exception on subsequent calls.
2025-11-27 16:08:52 +01:00
Ricki Hirner
a38dc29cca Update WebDAV property names (#1841)
* Update WebDAV property names according to new dav4jvm naming scheme

* Use new supported-report-set; fix `MaxResourceSize` property reference in CalendarSyncManager

* Remove comment
2025-11-27 11:46:10 +01:00
Ricki Hirner
84b9a14ba1 Update version to 4.5.6.1 2025-11-27 11:15:39 +01:00
Arnau Mora
cda95dc789 Fix HTTP Client provider for contact resource sync from URL (#1844)
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-11-27 11:15:06 +01:00
dependabot[bot]
f64882ca2a Bump the app-dependencies group with 15 updates (#1840)
Bumps the app-dependencies group with 15 updates:

| Package | From | To |
| --- | --- | --- |
| androidx.activity:activity-compose | `1.11.0` | `1.12.0` |
| androidx.lifecycle:lifecycle-runtime-compose | `2.9.4` | `2.10.0` |
| androidx.lifecycle:lifecycle-viewmodel-ktx | `2.9.4` | `2.10.0` |
| androidx.lifecycle:lifecycle-viewmodel-compose | `2.9.4` | `2.10.0` |
| androidx.compose:compose-bom | `2025.11.00` | `2025.11.01` |
| [com.squareup.okhttp3:okhttp](https://github.com/square/okhttp) | `5.3.0` | `5.3.2` |
| [com.squareup.okhttp3:okhttp-brotli](https://github.com/square/okhttp) | `5.3.0` | `5.3.2` |
| [com.squareup.okhttp3:logging-interceptor](https://github.com/square/okhttp) | `5.3.0` | `5.3.2` |
| [com.squareup.okhttp3:mockwebserver](https://github.com/square/okhttp) | `5.3.0` | `5.3.2` |
| androidx.room:room-ktx | `2.8.3` | `2.8.4` |
| androidx.room:room-compiler | `2.8.3` | `2.8.4` |
| androidx.room:room-paging | `2.8.3` | `2.8.4` |
| androidx.room:room-runtime | `2.8.3` | `2.8.4` |
| androidx.room:room-testing | `2.8.3` | `2.8.4` |
| [com.google.devtools.ksp](https://github.com/google/ksp) | `2.3.2` | `2.3.3` |


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

Updates `androidx.lifecycle:lifecycle-runtime-compose` from 2.9.4 to 2.10.0

Updates `androidx.lifecycle:lifecycle-viewmodel-ktx` from 2.9.4 to 2.10.0

Updates `androidx.lifecycle:lifecycle-viewmodel-compose` from 2.9.4 to 2.10.0

Updates `androidx.lifecycle:lifecycle-viewmodel-ktx` from 2.9.4 to 2.10.0

Updates `androidx.lifecycle:lifecycle-viewmodel-compose` from 2.9.4 to 2.10.0

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

Updates `com.squareup.okhttp3:okhttp` from 5.3.0 to 5.3.2
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.3.0...parent-5.3.2)

Updates `com.squareup.okhttp3:okhttp-brotli` from 5.3.0 to 5.3.2
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.3.0...parent-5.3.2)

Updates `com.squareup.okhttp3:logging-interceptor` from 5.3.0 to 5.3.2
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.3.0...parent-5.3.2)

Updates `com.squareup.okhttp3:mockwebserver` from 5.3.0 to 5.3.2
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.3.0...parent-5.3.2)

Updates `com.squareup.okhttp3:okhttp-brotli` from 5.3.0 to 5.3.2
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.3.0...parent-5.3.2)

Updates `com.squareup.okhttp3:logging-interceptor` from 5.3.0 to 5.3.2
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.3.0...parent-5.3.2)

Updates `com.squareup.okhttp3:mockwebserver` from 5.3.0 to 5.3.2
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.3.0...parent-5.3.2)

Updates `androidx.room:room-ktx` from 2.8.3 to 2.8.4

Updates `androidx.room:room-compiler` from 2.8.3 to 2.8.4

Updates `androidx.room:room-paging` from 2.8.3 to 2.8.4

Updates `androidx.room:room-runtime` from 2.8.3 to 2.8.4

Updates `androidx.room:room-testing` from 2.8.3 to 2.8.4

Updates `androidx.room:room-compiler` from 2.8.3 to 2.8.4

Updates `androidx.room:room-paging` from 2.8.3 to 2.8.4

Updates `androidx.room:room-runtime` from 2.8.3 to 2.8.4

Updates `androidx.room:room-testing` from 2.8.3 to 2.8.4

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

---
updated-dependencies:
- dependency-name: androidx.activity:activity-compose
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.lifecycle:lifecycle-runtime-compose
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-ktx
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-compose
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-ktx
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-compose
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2025.11.01
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:okhttp
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:okhttp-brotli
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:logging-interceptor
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:mockwebserver
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:okhttp-brotli
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:logging-interceptor
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:mockwebserver
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-ktx
  dependency-version: 2.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.google.devtools.ksp
  dependency-version: 2.3.3
  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>
2025-11-24 18:42:28 +01:00
Ricki Hirner
7c2dcf3d70 Comment out failing tests (#1836) 2025-11-24 16:00:53 +01:00
dependabot[bot]
66a34ebd9f [CI] Bump actions/checkout from 5 to 6 in the ci-actions group (#1837)
Bumps the ci-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  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-11-24 16:00:24 +01:00
Ricki Hirner
365364aa89 Update version to 4.5.6 2025-11-24 13:26:18 +01:00
Ricki Hirner
114543f4c5 Fetch translations from Transifex 2025-11-24 13:22:09 +01:00
Ricki Hirner
3bd3f56e1b Fix DeadObjectException handling in SyncManager and Syncer (#1834)
- Update SyncManager to handle LocalStorageException with DeadObjectException cause
- Refactor Syncer to catch all exceptions and handle specific cases
2025-11-24 13:14:40 +01:00
Ricki Hirner
5263172376 [Ktor] Add MustBeClosed annotation to buildKtor method (#1829)
Add @MustBeClosed annotation to buildKtor method

This commit adds the `@MustBeClosed` annotation to the `buildKtor` method in `HttpClientBuilder.kt` to indicate that the returned `HttpClient` instance must be closed by the caller. It also updates the test in `HttpClientBuilderTest.kt` to use the `use` function to ensure proper resource management.
2025-11-21 12:48:04 +01:00
Ricki Hirner
babd52cfb1 Bump version to 4.5.6-rc.1 2025-11-20 16:45:40 +01:00
Ricki Hirner
3d4d533b92 Update synctools (#1825)
* Update synctools

* Update commit ID
2025-11-20 16:42:40 +01:00
Ricki Hirner
76fc024ef6 Lower default minimum log level from INFO to FINE (#1827)
* Increase logging level

* Adjust log levels for visibility in non-verbose logs

- Change log level from FINER to FINE in StreamingFileDescriptor
- Update log level from FINER to FINE in AccountSettingsMigration8
- Add note about log levels in LogManager documentation

* Fix KDoc typo

* Update comment
2025-11-20 11:37:24 +01:00
Sunik Kupfer
794b4c1c7f DebugInfo: Support viewing jtx Board resources (#1818)
* Support viewing jtxBoard resources from debug info

* Correct value of EXTRA_LOCAL_RESOURCE_URI

* Correct comment

* Use the same intent for journals, notes, calendar and tasks

* State working task authorities explicitly

* Use edit action to not crash opentasks

* Use getViewIntentUriFor for jtx Board tasks

* Remove explicit tasks authority for jtx Board

* Remove explicit tasks authority for jtx Board

* Remove early return statement

* Dont handle jtxBoard tasks in LocalTask which is only for Dmfs tasks

* Add some kdoc to LocalTask and LocalJtxICalObject

* Use when with in list

* Add FLAG_GRANT_READ_URI_PERMISSION to the correct intent

* Correct line endings to from CRLF to LF
2025-11-20 09:33:24 +01:00
Ricki Hirner
aac6356722 Delete app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt
Remove empty file
2025-11-19 16:52:35 +01:00
Ricki Hirner
9bc46d4194 Update dependencies, including our libs (#1822) 2025-11-19 15:35:06 +01:00
Ricki Hirner
a3aac44775 Ignore failing test: testVerifySyncAlwaysPending_wrongBehaviour_android14 (#1824)
Comment out test
2025-11-19 15:14:41 +01:00
Arnau Mora
084ba3b630 Update dav4jvm to new okhttp package (#1786)
* Upgrade dav4jvm

* Exclude ktor from dav4jvm

* Fix imports and fix usages

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

* Fix imports for instrumented tests

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

* Fix imports

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

* Upgrade dav4jvm

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

* Do not exclude ktor in dav4jvm

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

* Upgrade dav4jvm

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-11-17 13:38:10 +01:00
Ricki Hirner
28dcf90775 Refactor default reminder builder to dedicated class + unit tests (#1815)
* [WIP] Move default reminder builder to dedicated class + unit tests

* Add tests

* Just turn off Conscrypt for now

* Fix library name
2025-11-17 10:34:36 +01:00
Ricki Hirner
70766affd9 [Ktor] Allow building a Ktor client (#1810)
* Add Ktor dependency

* Add buildKtor method

* Add test and deprecation notice

* KDoc
2025-11-17 09:38:08 +01:00
Ricki Hirner
d00292f421 Only use cert4android when needed (#1802)
* Refactor ClientCertKeyManager and HttpClientBuilder

- Add logging to `ClientCertKeyManager` for better error handling.
- Update `HttpClientBuilder` to conditionally use custom trust manager and hostname verifier based on `allowCustomCerts` flag.
- Rename `customCertsUI` to `allowCustomCerts` in build configuration.

* Update trust manager and hostname verifier selection logic

- Improve logging and error handling in `ClientCertKeyManager`

* App settings: hide certificate settings when custom certificates are not allowed

* Typo
2025-11-12 11:04:13 +01:00
dependabot[bot]
6b5c4f191a Bump the app-dependencies group with 2 updates (#1803)
Bumps the app-dependencies group with 2 updates: androidx.compose:compose-bom and [com.google.devtools.ksp](https://github.com/google/ksp).


Updates `androidx.compose:compose-bom` from 2025.10.01 to 2025.11.00

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

---
updated-dependencies:
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2025.11.00
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.google.devtools.ksp
  dependency-version: 2.3.2
  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>
2025-11-10 17:28:43 +01:00
Sunik Kupfer
5c7b792e7f Support sync adapter pending sync indication on Android 14+ (#1676)
* Add AccountSettingsMigration21 to cancel pending address book syncs

* Add application context annotation

* Add log statement

* Increase account settings current version

* Add and update kdoc

* Call cancelSync via integration

* Optimize imports

* Update kdoc

* Updating log statement

* Also cancel calendar syncs

* Don't infer authority from account type

* Update kdoc

* Cancel only on Android 14+

* Cancel for all authorities and update kdoc

* Use cancelSync directly in migration

* Enable forever pending sync workaround by canceling sync adapter framework syncs on Android 14+

* Stop always returning false for pending sync state of sync adapter framework

* Cancel by request and empty bundle

* Cancel syncs for calendar, tasks, and contacts separately

* Minor edits to log statement and kdoc

* Add migration test; Update migration

* Log all extras instead of just upload flag

* Use lazy on syncFrameworkIntegration injection

* Multiple changes

- don't cancel address book accounts of all main accounts
- merge loops

* Add authority to log statement

* Replace complex state verification logic by status changed flow

* Cancel syncs account wide across all authorities

* Add some delay to allow dummy sync requests to be created

* Reduce wait until pending

* Drop Thread.sleep()

* Use a callback flow instead of mutable state flow

* Shorten first true filter

* Shorten remaining first true filter
2025-11-10 15:46:58 +01:00
Ricki Hirner
c9da496142 Bump version to 4.5.6-beta.1 2025-11-06 14:30:07 +01:00
Ricki Hirner
ee098c4a83 Explicitly integrate Conscrypt (#1796)
* Merge HttpClient and HttpClientBuilder

- Remove `HttpClient` class and replace with `OkHttpClient`
- Update all references to `HttpClient` to use `OkHttpClient`
- Add new `HttpClientBuilder` class for building `OkHttpClient` instances
- Update all builder usages to use the new `HttpClientBuilder` class

* KDoc

* Integrate Conscrypt for TLS

- Add Conscrypt dependency
- Initialize Conscrypt in HttpClientBuilder
- Create ConscryptIntegration utility

* KDoc

* Make object a class, better test

* Update cert4android to the latest version (doesn't bundle Conscrypt anymore)
2025-11-06 10:07:40 +01:00
Ricki Hirner
a8bd296520 Merge HttpClient and HttpClientBuilder (#1795)
* Merge HttpClient and HttpClientBuilder

- Remove `HttpClient` class and replace with `OkHttpClient`
- Update all references to `HttpClient` to use `OkHttpClient`
- Add new `HttpClientBuilder` class for building `OkHttpClient` instances
- Update all builder usages to use the new `HttpClientBuilder` class

* KDoc
2025-11-05 13:20:02 +01:00
Sunik Kupfer
0959624dee Update Calendars.OWNER_ACCOUNT when renaming an account (#1751)
* Fix typo

* Also set OWNER_ACCOUNT when updating calendar because renaming account

* Add test

* Update comment clarifying content values

* Assume calendar provider is present and drop null checks
2025-11-05 12:40:34 +01:00
Ricki Hirner
bd13d27e38 HttpClient: remove unnecessary close() (#1792)
* Remove unnecessary AutoCloseable implementations and client.close() calls

- Remove AutoCloseable from NextcloudLoginFlow and DavResourceFinder
- Remove client.close() calls in various classes and tests
- Update HttpClient to remove close() method

* Fix test

* Fix annotations / KDoc
2025-11-04 16:38:22 +01:00
dependabot[bot]
85548163ca Bump the app-dependencies group with 4 updates (#1790)
Bumps the app-dependencies group with 4 updates: [com.squareup.okhttp3:okhttp](https://github.com/square/okhttp), [com.squareup.okhttp3:okhttp-brotli](https://github.com/square/okhttp), [com.squareup.okhttp3:logging-interceptor](https://github.com/square/okhttp) and [com.squareup.okhttp3:mockwebserver](https://github.com/square/okhttp).


Updates `com.squareup.okhttp3:okhttp` from 5.2.1 to 5.3.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.1...parent-5.3.0)

Updates `com.squareup.okhttp3:okhttp-brotli` from 5.2.1 to 5.3.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.1...parent-5.3.0)

Updates `com.squareup.okhttp3:logging-interceptor` from 5.2.1 to 5.3.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.1...parent-5.3.0)

Updates `com.squareup.okhttp3:mockwebserver` from 5.2.1 to 5.3.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.1...parent-5.3.0)

Updates `com.squareup.okhttp3:okhttp-brotli` from 5.2.1 to 5.3.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.1...parent-5.3.0)

Updates `com.squareup.okhttp3:logging-interceptor` from 5.2.1 to 5.3.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.1...parent-5.3.0)

Updates `com.squareup.okhttp3:mockwebserver` from 5.2.1 to 5.3.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.2.1...parent-5.3.0)

---
updated-dependencies:
- dependency-name: com.squareup.okhttp3:okhttp
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:okhttp-brotli
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:logging-interceptor
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:mockwebserver
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:okhttp-brotli
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:logging-interceptor
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:mockwebserver
  dependency-version: 5.3.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>
2025-11-03 16:07:44 +01:00
Ricki Hirner
026750eca3 Bump version to 4.5.6-alpha.2 2025-11-01 21:56:47 +01:00
Ricki Hirner
d365a504e8 Refactor sequence handling in calendar sync (#1789)
* Refactor sequence handling in calendar sync

- Move sequence update logic to SequenceUpdater
- Update LocalCalendar to use new SequenceUpdater
- Remove redundant methods from LocalCalendar
- Update tests and dependencies

* Minor KDoc
2025-11-01 21:55:28 +01:00
Ricki Hirner
c64cb1e7ec Remove okhttp caching (bitfireAT/davx5#715)
* Remove okhttp caching
* Simplification
2025-11-01 12:38:26 +01:00
Ricki Hirner
837b5e5d50 Bump version to 4.5.6-alpha.1 2025-10-30 14:29:29 +01:00
Ricki Hirner
98aefc4fee Synchronize without Event data class (#1783)
* [WIP] Proof of concept for syncing without `Event` data class

* Replace AndroidEvent2 with EventsContract

* Update synctools, refactor upload logic in `CalendarSyncManager`

* KDoc

* Update UID immediately in `ContactsSyncManager`, `CalendarSyncManager`, and `TasksSyncManager`

- Remove `OnSuccessContext.uid` from `GeneratedResource`

* Minor changes

* Handle multiple events in a single iCalendar

- Rename `processVEvent` to `processICalendar`
- Add default alarm for non-full-day events again
- Prevent NPE on null flags (used for debug info)

* Fix tests
2025-10-30 14:28:05 +01:00
Ricki Hirner
a8c8a8d2e0 Refactor SEQUENCE and UID handling on successful uploads (#1785)
* Define interfaces

* [WIP] Refactor sequence and UID handling in event uploads

- Refactor `generateUpload` method to return `GeneratedResource`.
- Update `SyncManager` to handle `GeneratedResource`.
- Implement sequence and UID management in `CalendarSyncManager`.
- Remove redundant `prepareForUpload` method from `LocalEvent`.

* Refactor sequence handling in uploads

- Move UID validation to `DavUtils.isGoodFileBaseName`
- Update sequence directly in iCalendar for group-scheduled events
- Rename `resourceBaseName` to `suggestedBaseName` for clarity

* Refactor sequence / UID handling in contact uploads

- Update `LocalAddress` interface to include `updateUid` method
- Modify `ContactsSyncManager` to handle UID generation and update
- Remove redundant UID handling in `LocalContact` and `LocalGroup`
- Adjust code style settings for right margin

* Remove redundant UID update from `ContactsSyncManager` and `CalendarSyncManager`

* Implement UID handling in `TasksSyncManager` for uploads

* Update JtxSyncManager

- Update `JtxSyncManager.kt` to implement the `generateUpload` function.
- Update `LocalJtxICalObject.kt` to include the `updateUid` method for updating UIDs in the collection.

* Remove deprecated `prepareForUpload` method from `LocalResource`

- Remove `prepareForUpload` implementations from `LocalContact`, `LocalEvent`, `LocalGroup`, and `LocalTask`

* Rename `isGoodFileBaseName` to `isGoodFileName` in `DavUtils`

* Fix tests

* Move UID generation logic to `DavUtils.generateUidIfNecessary`

- Use `DavUtils.fileNameFromUid` for generating file names
- Update `ContactsSyncManager`, `CalendarSyncManager`, `TasksSyncManager`, and `JtxSyncManager` to use new utility methods

* Add tests for DavUtils

* Some tests

* Refactor onSuccessfulUpload

* Update KDoc

* Logging

* Remove unnecessary LocalEvent method

* KDoc
2025-10-29 17:57:34 +01:00
Ricki Hirner
b839cbfe7f Simplify LocalResource interface (#1784)
* Simplify LocalResource interface

- Remove generic parameter from LocalResource interface
- Update all implementations to reflect the change
- Adjust related test cases and exception handling

* Fix tests
2025-10-29 08:50:09 +01:00
Ricki Hirner
f0f9f58e49 Bump version to 4.5.5 2025-10-28 11:20:00 +01:00
Ricki Hirner
66f6e48e3b Fetch translations from Transifex 2025-10-28 11:18:54 +01:00
Ricki Hirner
d6feda1142 Fix OkHttp3 crash in release builds (bitfireAT/davx5#712)
- Add ProGuard rules to keep OkHttp3 IDN mapping classes
2025-10-28 11:12:51 +01:00
Ricki Hirner
05f058ab3f Version bump to 4.5.5-rc.1 2025-10-27 11:16:51 +01:00
Sunik Kupfer
4e4c0f5e31 Move debug info notification action to debug info screen button (#1730)
* Update view item on sync error string

* Remove view item action from notification

* Show button in debug info screen to jump to problematic event resource

* Move companion object to the end of activity class

* Add local resource dump to intent

* Add kdoc

* Add some comments for not yet implemented resources

* Don't export DebugInfoActivity

* Send intent instead of URI and launch from DebugInfoActivity

* Add option to view problematic contact

* Extract intent builder logic to another method

* Add option to view problematic contact

* Minor changes for readability

* Extract dump string creation to interface method

* Pass Uri instead of intent and create view local resource intent in DebugInfoActivity

* Use androids existing getContactLookupUri method

* Remove extra variable

* Remove obsolete val declaration

* Rename dump to summary

* Refactor code structure for local resource URI handling

* Update code structure to use getDebugSummary for local resource summaries

* Update exception handling in SyncNotificationManager

Change the catch block to handle all `Throwable` exceptions instead of just `OutOfMemoryError`. This ensures that any potential issues arising from providing information about the local resource are caught and ignored.

* Add "copy remote URL" action

* Use string resource

* Truncate contact, task, and event strings to 1000 characters

* Fix tests

* Minor changes

- Replace `ContactsContract.RawContacts` with `RawContacts` in `LocalContact.kt`
- Remove unnecessary newline in `LocalJtxICalObject.kt`

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-10-27 11:15:21 +01:00
Arnau Mora
0304d7168a Migrate to SecureTextField (#1191)
* Using `SecureTextField`

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

* `OutlinedSecureTextField` doesn't support `readOnly`

* fixed string conversions

* - Update AddWebdavMountScreen to use enabled instead of readOnly
- Ensure onKeyboardAction checks canContinue before proceeding
- Update UrlLogin and EmailLogin to ensure onKeyboardAction checks canContinue before proceeding
- Update InputDialogs to ensure confirmEnabled is checked before proceeding

* Use get() for deriving things from a mutable state

---------

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-10-27 11:15:11 +01:00
Ricki Hirner
19458aa95c Delete .github/workflows/dependent-issues.yml
Obsoleted by https://github.blog/changelog/2025-08-21-dependencies-on-issues/
2025-10-26 12:23:31 +01:00
dependabot[bot]
4412617079 Bump the app-dependencies group across 1 directory with 7 updates (#1778)
Bumps the app-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| androidx.work:work-runtime-ktx | `2.10.5` | `2.11.0` |
| androidx.work:work-testing | `2.10.5` | `2.11.0` |
| androidx.room:room-ktx | `2.8.2` | `2.8.3` |
| androidx.room:room-compiler | `2.8.2` | `2.8.3` |
| androidx.room:room-paging | `2.8.2` | `2.8.3` |
| androidx.room:room-runtime | `2.8.2` | `2.8.3` |
| androidx.room:room-testing | `2.8.2` | `2.8.3` |



Updates `androidx.work:work-runtime-ktx` from 2.10.5 to 2.11.0

Updates `androidx.work:work-testing` from 2.10.5 to 2.11.0

Updates `androidx.work:work-testing` from 2.10.5 to 2.11.0

Updates `androidx.room:room-ktx` from 2.8.2 to 2.8.3

Updates `androidx.room:room-compiler` from 2.8.2 to 2.8.3

Updates `androidx.room:room-paging` from 2.8.2 to 2.8.3

Updates `androidx.room:room-runtime` from 2.8.2 to 2.8.3

Updates `androidx.room:room-testing` from 2.8.2 to 2.8.3

Updates `androidx.room:room-compiler` from 2.8.2 to 2.8.3

Updates `androidx.room:room-paging` from 2.8.2 to 2.8.3

Updates `androidx.room:room-runtime` from 2.8.2 to 2.8.3

Updates `androidx.room:room-testing` from 2.8.2 to 2.8.3

---
updated-dependencies:
- dependency-name: androidx.work:work-runtime-ktx
  dependency-version: 2.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-testing
  dependency-version: 2.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-testing
  dependency-version: 2.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-ktx
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.3
  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>
2025-10-26 11:04:22 +01:00
Ricki Hirner
76277dbfd5 Update Kotlin and KSP versions (#1776)
- Update Kotlin version to 2.2.21
- Update KSP version to 2.3.0
- Remove specific Kotlin and KSP dependencies from Dependabot ignore list
2025-10-26 10:20:57 +01:00
Ricki Hirner
b0b99de56b Update Compose BOM and use PrimaryTabRow (#1772)
- Update Compose BOM version to 2025.10.01
- Replace `TabRow` with `PrimaryTabRow`
2025-10-25 11:15:01 +02:00
Ricki Hirner
b33c4750bb Bump version to 4.5.5-beta.1 2025-10-18 09:15:58 +02:00
Sunik Kupfer
25b749dd1b Rename account on background thread (#1727)
* Change context to default dispatcher on updating automatic sync when renaming account

* Minor changes
- add worker thread annotations
- use injected defaultDispatcher

* Add tests

* Apply withContext with default dispatcher on the whole rename method
2025-10-18 09:13:26 +02:00
Ricki Hirner
39a0fe3f98 Update AboutLibraries and other dependencies (#1760)
Update dependencies and modify AboutActivity to use dynamic library loading

- Remove outdated aboutlibraries.json
- Update AboutActivity to dynamically load libraries using LocalContext
- Replace InvalidRemoteResourceException with InvalidICalendarException in sync managers
- Bump dependency versions for various libraries including mikepenz-aboutLibraries, okhttp, and unifiedpush
- Adjust build.gradle.kts and gradle/libs.versions.toml for new plugin and library versions
2025-10-18 09:12:41 +02:00
Arnau Mora
dd798f8380 Update implementation to match guidelines (#1747) 2025-10-15 11:20:30 +02:00
Sunik Kupfer
47d380de62 Fix unreachable code possibly causing foreign key constraint violation exception (#1740)
* Add test

* Fix unreachable code possibly causing foreign key constraint violation exception

* Make code easier to understand

* Add comments

* Split up tests and add mockk verify
2025-10-14 10:57:15 +02:00
dependabot[bot]
019d32a9b7 [CI] Bump github/codeql-action from 3 to 4 in the ci-actions group (#1749)
Bumps the ci-actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3 to 4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  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-10-13 20:04:01 +02:00
Sunik Kupfer
0acabd9c80 Update http error message strings (#1745) 2025-10-13 10:41:00 +02:00
dependabot[bot]
aa23980a59 Bump the app-dependencies group across 1 directory with 13 updates (#1744)
* Bump the app-dependencies group across 1 directory with 13 updates

Bumps the app-dependencies group with 13 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| androidx.compose:compose-bom | `2025.09.01` | `2025.10.00` |
| [io.mockk:mockk](https://github.com/mockk/mockk) | `1.14.5` | `1.14.6` |
| [io.mockk:mockk-android](https://github.com/mockk/mockk) | `1.14.5` | `1.14.6` |
| [com.squareup.okhttp3:okhttp](https://github.com/square/okhttp) | `5.1.0` | `5.2.0` |
| [com.squareup.okhttp3:okhttp-brotli](https://github.com/square/okhttp) | `5.1.0` | `5.2.0` |
| [com.squareup.okhttp3:logging-interceptor](https://github.com/square/okhttp) | `5.1.0` | `5.2.0` |
| [com.squareup.okhttp3:mockwebserver](https://github.com/square/okhttp) | `5.1.0` | `5.2.0` |
| androidx.room:room-ktx | `2.8.1` | `2.8.2` |
| androidx.room:room-compiler | `2.8.1` | `2.8.2` |
| androidx.room:room-paging | `2.8.1` | `2.8.2` |
| androidx.room:room-runtime | `2.8.1` | `2.8.2` |
| androidx.room:room-testing | `2.8.1` | `2.8.2` |
| org.unifiedpush.android:connector | `3.0.10` | `3.1.0` |



Updates `androidx.compose:compose-bom` from 2025.09.01 to 2025.10.00

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

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

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

Updates `com.squareup.okhttp3:okhttp` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.1.0...parent-5.2.0)

Updates `com.squareup.okhttp3:okhttp-brotli` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.1.0...parent-5.2.0)

Updates `com.squareup.okhttp3:logging-interceptor` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.1.0...parent-5.2.0)

Updates `com.squareup.okhttp3:mockwebserver` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.1.0...parent-5.2.0)

Updates `com.squareup.okhttp3:okhttp-brotli` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.1.0...parent-5.2.0)

Updates `com.squareup.okhttp3:logging-interceptor` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.1.0...parent-5.2.0)

Updates `com.squareup.okhttp3:mockwebserver` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/square/okhttp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/square/okhttp/compare/parent-5.1.0...parent-5.2.0)

Updates `androidx.room:room-ktx` from 2.8.1 to 2.8.2

Updates `androidx.room:room-compiler` from 2.8.1 to 2.8.2

Updates `androidx.room:room-paging` from 2.8.1 to 2.8.2

Updates `androidx.room:room-runtime` from 2.8.1 to 2.8.2

Updates `androidx.room:room-testing` from 2.8.1 to 2.8.2

Updates `androidx.room:room-compiler` from 2.8.1 to 2.8.2

Updates `androidx.room:room-paging` from 2.8.1 to 2.8.2

Updates `androidx.room:room-runtime` from 2.8.1 to 2.8.2

Updates `androidx.room:room-testing` from 2.8.1 to 2.8.2

Updates `org.unifiedpush.android:connector` from 3.0.10 to 3.1.0

---
updated-dependencies:
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2025.10.00
  dependency-type: direct:production
  dependency-group: app-dependencies
- dependency-name: io.mockk:mockk
  dependency-version: 1.14.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: io.mockk:mockk-android
  dependency-version: 1.14.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: io.mockk:mockk-android
  dependency-version: 1.14.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:okhttp
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:okhttp-brotli
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:logging-interceptor
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:mockwebserver
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:okhttp-brotli
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:logging-interceptor
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: com.squareup.okhttp3:mockwebserver
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-ktx
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: org.unifiedpush.android:connector
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
...

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

* Downgrade mockk version to 1.14.5

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-10-09 13:10:31 +02:00
Sunik Kupfer
5674d6b954 ExceptionInfoDialog: show explanation on http 405 and others (#1739)
* Show explanation on http 405

* Also show message for http 5xx

* Correct spelling
2025-10-09 12:05:31 +02:00
dependabot[bot]
df56c8628a [CI] Bump gradle/actions from 4 to 5 in the ci-actions group (#1736)
Bumps the ci-actions group with 1 update: [gradle/actions](https://github.com/gradle/actions).


Updates `gradle/actions` from 4 to 5
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v4...v5)

---
updated-dependencies:
- dependency-name: gradle/actions
  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-10-06 17:08:39 +02:00
dependabot[bot]
af4ecd3a1d Bump the app-dependencies group with 12 updates (#1732)
Bumps the app-dependencies group with 12 updates:

| Package | From | To |
| --- | --- | --- |
| androidx.work:work-runtime-ktx | `2.10.4` | `2.10.5` |
| androidx.work:work-testing | `2.10.4` | `2.10.5` |
| androidx.compose:compose-bom | `2025.09.00` | `2025.09.01` |
| [com.google.dagger:hilt-android](https://github.com/google/dagger) | `2.57.1` | `2.57.2` |
| [com.google.dagger:hilt-android-compiler](https://github.com/google/dagger) | `2.57.1` | `2.57.2` |
| [com.google.dagger:hilt-android-testing](https://github.com/google/dagger) | `2.57.1` | `2.57.2` |
| [com.google.dagger.hilt.android](https://github.com/google/dagger) | `2.57.1` | `2.57.2` |
| androidx.room:room-ktx | `2.8.0` | `2.8.1` |
| androidx.room:room-compiler | `2.8.0` | `2.8.1` |
| androidx.room:room-paging | `2.8.0` | `2.8.1` |
| androidx.room:room-runtime | `2.8.0` | `2.8.1` |
| androidx.room:room-testing | `2.8.0` | `2.8.1` |


Updates `androidx.work:work-runtime-ktx` from 2.10.4 to 2.10.5

Updates `androidx.work:work-testing` from 2.10.4 to 2.10.5

Updates `androidx.work:work-testing` from 2.10.4 to 2.10.5

Updates `androidx.compose:compose-bom` from 2025.09.00 to 2025.09.01

Updates `com.google.dagger:hilt-android` from 2.57.1 to 2.57.2
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57.1...dagger-2.57.2)

Updates `com.google.dagger:hilt-android-compiler` from 2.57.1 to 2.57.2
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57.1...dagger-2.57.2)

Updates `com.google.dagger:hilt-android-testing` from 2.57.1 to 2.57.2
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57.1...dagger-2.57.2)

Updates `com.google.dagger.hilt.android` from 2.57.1 to 2.57.2
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57.1...dagger-2.57.2)

Updates `com.google.dagger:hilt-android-compiler` from 2.57.1 to 2.57.2
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57.1...dagger-2.57.2)

Updates `com.google.dagger:hilt-android-testing` from 2.57.1 to 2.57.2
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57.1...dagger-2.57.2)

Updates `androidx.room:room-ktx` from 2.8.0 to 2.8.1

Updates `androidx.room:room-compiler` from 2.8.0 to 2.8.1

Updates `androidx.room:room-paging` from 2.8.0 to 2.8.1

Updates `androidx.room:room-runtime` from 2.8.0 to 2.8.1

Updates `androidx.room:room-testing` from 2.8.0 to 2.8.1

Updates `androidx.room:room-compiler` from 2.8.0 to 2.8.1

Updates `androidx.room:room-paging` from 2.8.0 to 2.8.1

Updates `androidx.room:room-runtime` from 2.8.0 to 2.8.1

Updates `androidx.room:room-testing` from 2.8.0 to 2.8.1

Updates `com.google.dagger.hilt.android` from 2.57.1 to 2.57.2
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57.1...dagger-2.57.2)

---
updated-dependencies:
- dependency-name: androidx.work:work-runtime-ktx
  dependency-version: 2.10.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-testing
  dependency-version: 2.10.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-testing
  dependency-version: 2.10.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2025.09.01
  dependency-type: direct:production
  dependency-group: app-dependencies
- dependency-name: com.google.dagger:hilt-android
  dependency-version: 2.57.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.google.dagger:hilt-android-compiler
  dependency-version: 2.57.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.google.dagger:hilt-android-testing
  dependency-version: 2.57.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.google.dagger.hilt.android
  dependency-version: 2.57.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.google.dagger:hilt-android-compiler
  dependency-version: 2.57.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.google.dagger:hilt-android-testing
  dependency-version: 2.57.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-ktx
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.google.dagger.hilt.android
  dependency-version: 2.57.2
  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>
2025-09-29 22:15:39 +02:00
Arnau Mora
701e292ab5 Use IntentCompat for account fetching from intent extras (#1731) 2025-09-28 10:51:57 +02:00
Ricki Hirner
5da7d9e292 Add CODEOWNERS file (#1729) 2025-09-24 14:11:40 +02:00
Ricki Hirner
e6256764ac Bump version to 4.5.5-alpha.2 2025-09-24 14:00:30 +02:00
Ricki Hirner
e63f815416 Update synctools to work around eventStatus=null update problem (#1728)
Update synctools to work around eventStatus=null update problem; add synctools to Dependabot-ignore
2025-09-24 13:59:12 +02:00
Ricki Hirner
374dadfaaa Bump version to 4.5.5-alpha.1 2025-09-23 11:15:07 +02:00
Ricki Hirner
f6ef13f9fe Update synctools (new uses event builders/processors) and Kotlin/KSP (#1726) 2025-09-23 11:07:41 +02:00
dependabot[bot]
a88cfd2acf Bump the app-dependencies group with 4 updates (#1724)
Bumps the app-dependencies group with 4 updates: androidx.lifecycle:lifecycle-runtime-compose, androidx.lifecycle:lifecycle-viewmodel-ktx, androidx.lifecycle:lifecycle-viewmodel-compose and [com.google.guava:guava](https://github.com/google/guava).


Updates `androidx.lifecycle:lifecycle-runtime-compose` from 2.9.3 to 2.9.4

Updates `androidx.lifecycle:lifecycle-viewmodel-ktx` from 2.9.3 to 2.9.4

Updates `androidx.lifecycle:lifecycle-viewmodel-compose` from 2.9.3 to 2.9.4

Updates `androidx.lifecycle:lifecycle-viewmodel-ktx` from 2.9.3 to 2.9.4

Updates `androidx.lifecycle:lifecycle-viewmodel-compose` from 2.9.3 to 2.9.4

Updates `com.google.guava:guava` from 33.4.8-android to 33.5.0-android
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

---
updated-dependencies:
- dependency-name: androidx.lifecycle:lifecycle-runtime-compose
  dependency-version: 2.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-ktx
  dependency-version: 2.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-compose
  dependency-version: 2.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-ktx
  dependency-version: 2.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-compose
  dependency-version: 2.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: com.google.guava:guava
  dependency-version: 33.5.0-android
  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>
2025-09-22 18:40:14 +02:00
Michael Biebl
98fc946594 Remove duplicate lines in .gitignore (#1725) 2025-09-22 18:20:50 +02:00
Ricki Hirner
00523d9bc8 Fix state matching logic in AndroidSyncFrameworkTest (#1708)
* Fix state matching logic in AndroidSyncFrameworkTest

- Add `fullMatch` parameter to control whether all expected states must be present

* Ensure non-optional expected state matches actual state

* Remove unused rule / variable

* Adapt test

- Update `onStatusChanged` to override the interface method.
- Replace custom assertion with `assertTrue` for state comparison.
2025-09-16 10:42:11 +02:00
dependabot[bot]
53d338d03e Bump the app-dependencies group with 12 updates (#1714)
Bumps the app-dependencies group with 12 updates:

| Package | From | To |
| --- | --- | --- |
| androidx.activity:activity-compose | `1.10.1` | `1.11.0` |
| androidx.hilt:hilt-compiler | `1.2.0` | `1.3.0` |
| androidx.hilt:hilt-navigation-compose | `1.2.0` | `1.3.0` |
| androidx.hilt:hilt-work | `1.2.0` | `1.3.0` |
| androidx.work:work-runtime-ktx | `2.10.3` | `2.10.4` |
| androidx.work:work-testing | `2.10.3` | `2.10.4` |
| androidx.compose:compose-bom | `2025.08.01` | `2025.09.00` |
| androidx.room:room-ktx | `2.7.2` | `2.8.0` |
| androidx.room:room-compiler | `2.7.2` | `2.8.0` |
| androidx.room:room-paging | `2.7.2` | `2.8.0` |
| androidx.room:room-runtime | `2.7.2` | `2.8.0` |
| androidx.room:room-testing | `2.7.2` | `2.8.0` |


Updates `androidx.activity:activity-compose` from 1.10.1 to 1.11.0

Updates `androidx.hilt:hilt-compiler` from 1.2.0 to 1.3.0

Updates `androidx.hilt:hilt-navigation-compose` from 1.2.0 to 1.3.0

Updates `androidx.hilt:hilt-work` from 1.2.0 to 1.3.0

Updates `androidx.hilt:hilt-navigation-compose` from 1.2.0 to 1.3.0

Updates `androidx.hilt:hilt-work` from 1.2.0 to 1.3.0

Updates `androidx.work:work-runtime-ktx` from 2.10.3 to 2.10.4

Updates `androidx.work:work-testing` from 2.10.3 to 2.10.4

Updates `androidx.work:work-testing` from 2.10.3 to 2.10.4

Updates `androidx.compose:compose-bom` from 2025.08.01 to 2025.09.00

Updates `androidx.room:room-ktx` from 2.7.2 to 2.8.0

Updates `androidx.room:room-compiler` from 2.7.2 to 2.8.0

Updates `androidx.room:room-paging` from 2.7.2 to 2.8.0

Updates `androidx.room:room-runtime` from 2.7.2 to 2.8.0

Updates `androidx.room:room-testing` from 2.7.2 to 2.8.0

Updates `androidx.room:room-compiler` from 2.7.2 to 2.8.0

Updates `androidx.room:room-paging` from 2.7.2 to 2.8.0

Updates `androidx.room:room-runtime` from 2.7.2 to 2.8.0

Updates `androidx.room:room-testing` from 2.7.2 to 2.8.0

---
updated-dependencies:
- dependency-name: androidx.activity:activity-compose
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.hilt:hilt-compiler
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.hilt:hilt-navigation-compose
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.hilt:hilt-work
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.hilt:hilt-navigation-compose
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.hilt:hilt-work
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-runtime-ktx
  dependency-version: 2.10.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-testing
  dependency-version: 2.10.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.work:work-testing
  dependency-version: 2.10.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: app-dependencies
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2025.09.00
  dependency-type: direct:production
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-ktx
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-compiler
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-paging
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-runtime
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: app-dependencies
- dependency-name: androidx.room:room-testing
  dependency-version: 2.8.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>
2025-09-12 10:35:17 +02:00
Ricki Hirner
0424999225 Update dav4jvm (#1709)
- Replace `code` with `statusCode` in multiple files to align with the updated library
- Add `dav4jvm` to Dependabot ignores
2025-09-12 10:25:26 +02:00
Ricki Hirner
fa09a0560f Use SensitiveString for passwords (#1692)
* Use SensitiveString for passwords to prevent them from being logged by `toString()`

* Add test

* Fix other tests

* Credentials: equals / hashCode not needed anymore

* Add tests for equals
2025-09-10 10:31:24 +02:00
Ricki Hirner
f4aa55d482 Version bump to 4.5.4 2025-09-09 11:38:10 +02:00
Ricki Hirner
1bffd5efe1 Version bump to 4.5.4-rc.3 2025-09-06 13:14:48 +02:00
Ricki Hirner
4850f2a5a5 Update synctools to fix #1701 (#1702) 2025-09-06 13:01:56 +02:00
Ricki Hirner
53f38ce2ec Rename Dependabot dependency group 2025-09-05 11:27:06 +02:00
Ricki Hirner
7cf6e30577 Bump version to 4.5.4-rc.2 2025-09-05 10:31:21 +02:00
Ricki Hirner
cec77c33cb Fix KotlinNotImplementedError in LocalGroup update method (#1696)
- Remove scheduleTag assignment in LocalGroup update method
- Refactor LocalGroupTest to use @Before and @After annotations
- Add new test case for update method in LocalGroupTest
2025-09-05 10:27:10 +02:00
dependabot[bot]
6c98e6d501 Bump the lib-dependencies group across 1 directory with 9 updates (#1695)
Bumps the lib-dependencies group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| androidx.lifecycle:lifecycle-runtime-compose | `2.9.2` | `2.9.3` |
| androidx.lifecycle:lifecycle-viewmodel-ktx | `2.9.2` | `2.9.3` |
| androidx.lifecycle:lifecycle-viewmodel-compose | `2.9.2` | `2.9.3` |
| androidx.compose:compose-bom | `2025.08.00` | `2025.08.01` |
| [com.google.dagger:hilt-android](https://github.com/google/dagger) | `2.57` | `2.57.1` |
| [com.google.dagger:hilt-android-compiler](https://github.com/google/dagger) | `2.57` | `2.57.1` |
| [com.google.dagger:hilt-android-testing](https://github.com/google/dagger) | `2.57` | `2.57.1` |
| [com.google.dagger.hilt.android](https://github.com/google/dagger) | `2.57` | `2.57.1` |
| com.android.application | `8.12.1` | `8.13.0` |



Updates `androidx.lifecycle:lifecycle-runtime-compose` from 2.9.2 to 2.9.3

Updates `androidx.lifecycle:lifecycle-viewmodel-ktx` from 2.9.2 to 2.9.3

Updates `androidx.lifecycle:lifecycle-viewmodel-compose` from 2.9.2 to 2.9.3

Updates `androidx.lifecycle:lifecycle-viewmodel-ktx` from 2.9.2 to 2.9.3

Updates `androidx.lifecycle:lifecycle-viewmodel-compose` from 2.9.2 to 2.9.3

Updates `androidx.compose:compose-bom` from 2025.08.00 to 2025.08.01

Updates `com.google.dagger:hilt-android` from 2.57 to 2.57.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57...dagger-2.57.1)

Updates `com.google.dagger:hilt-android-compiler` from 2.57 to 2.57.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57...dagger-2.57.1)

Updates `com.google.dagger:hilt-android-testing` from 2.57 to 2.57.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57...dagger-2.57.1)

Updates `com.google.dagger.hilt.android` from 2.57 to 2.57.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57...dagger-2.57.1)

Updates `com.google.dagger:hilt-android-compiler` from 2.57 to 2.57.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57...dagger-2.57.1)

Updates `com.google.dagger:hilt-android-testing` from 2.57 to 2.57.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57...dagger-2.57.1)

Updates `com.android.application` from 8.12.1 to 8.13.0

Updates `com.google.dagger.hilt.android` from 2.57 to 2.57.1
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.57...dagger-2.57.1)

---
updated-dependencies:
- dependency-name: androidx.lifecycle:lifecycle-runtime-compose
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-ktx
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-compose
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-ktx
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: androidx.lifecycle:lifecycle-viewmodel-compose
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: androidx.compose:compose-bom
  dependency-version: 2025.08.01
  dependency-type: direct:production
  dependency-group: lib-dependencies
- dependency-name: com.google.dagger:hilt-android
  dependency-version: 2.57.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: com.google.dagger:hilt-android-compiler
  dependency-version: 2.57.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: com.google.dagger:hilt-android-testing
  dependency-version: 2.57.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: com.google.dagger.hilt.android
  dependency-version: 2.57.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: com.google.dagger:hilt-android-compiler
  dependency-version: 2.57.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: com.google.dagger:hilt-android-testing
  dependency-version: 2.57.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
- dependency-name: com.android.application
  dependency-version: 8.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: lib-dependencies
- dependency-name: com.google.dagger.hilt.android
  dependency-version: 2.57.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: lib-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-05 10:25:31 +02:00
Ricki Hirner
a7cd1cd49f Configure Dependabot for Gradle dependencies (#1680)
* Configure Dependabot for Gradle dependencies

Add Gradle dependency management configuration to Dependabot.

* Update dependabot.yml to ignore specific Kotlin and KSP dependencies
2025-09-05 09:26:30 +02:00
Ricki Hirner
0cc84dfd01 Update synctools (#1690) 2025-09-03 16:47:40 +02:00
Ricki Hirner
87239daaf6 Fix invalid translation 2025-09-03 15:53:43 +02:00
Ricki Hirner
81ceb57842 Version bump to 4.5.4-rc.1 2025-09-03 15:41:31 +02:00
Ricki Hirner
cd0b0c0804 Fetch translations from Transifex 2025-09-03 15:41:12 +02:00
Sunik Kupfer
48cbd4a05d Disable pending sync indicator on Android 14+ (#1689)
* Disable pending sync indicator on Android 14+

* Disable forever pending sync workaround on Android 14+
2025-09-03 15:32:29 +02:00
Ricki Hirner
beccc7a0d4 Version bump to 4.5.4-beta.1 2025-09-01 11:09:52 +02:00
Sunik Kupfer
2b629c8b18 Add comments on foreign key constraint enforcement (#1675)
* Add comments on foreign key constraint enforcement

* Fix grammar

* Add foreign key constraint comment to AppDatabase

* Update kdoc with better description
2025-08-28 11:47:49 +02:00
Arnau Mora
cd725479cd Handle null cases for Events.DIRTY (#1663)
* Handled nullity of `Calendar.dirty`

* Fixed ids

* Use multiline SQL queries

* Fix kdoc links

* Improve kdoc

* Fix capitalization

* Simplify kdoc

* Improved `LocalCalendarTest`
2025-08-28 11:25:57 +02:00
Ricki Hirner
44666d2138 Add groups section to dependabot configuration (#1677)
* Add groups section to dependabot configuration

* Fix patterns syntax in dependabot.yml
2025-08-27 15:56:34 +02:00
dependabot[bot]
8e67db7d54 [CI] Bump actions/setup-java from 4 to 5 (#1672)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 10:10:21 +02:00
Sunik Kupfer
a58e3b9036 Don't disable sync-ability of address books when set to manual only (#1662)
Only disable sync on content changes; but leave syncable
2025-08-25 10:15:56 +02:00
Ricki Hirner
d63918ff42 Update dependencies to latest versions (including AGP) (#1665) 2025-08-19 16:45:27 +02:00
dependabot[bot]
f21c3de94a [CI] Bump actions/checkout from 4 to 5 (#1664)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 13:54:15 +02:00
Ricki Hirner
24d4ba65e5 Fix NPE when Event._ID is larger than Integer.MAX_VALUE (#1661)
* Add test that reproduces NPE

* Fix NPE when Event ID is larger than Integer.MAX_VALUE
2025-08-14 17:19:04 +02:00
Arnau Mora
ae96f1ffbb Using new suggested icons for tabs (#1657) 2025-08-14 10:20:08 +02:00
Arnau Mora
a08ecae635 Change "Account doesn't exist" toast message to "Account has been removed" for deleting accounts (#1650)
* Finish activity after deleting

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

* Added proper toast for when the account is deleted

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

* Simplify logic

* Missing fix

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-08-13 08:55:26 +02:00
Ricki Hirner
eb4224780a Update to Gradle 9.0 (#1647) 2025-08-07 09:30:37 +02:00
Ricki Hirner
0240e67dab Bump version to 4.5.4-alpha.1 2025-08-06 16:42:10 +02:00
Ricki Hirner
0ccd9d5eb3 Update synctools for events as Entity and EventAndExceptions (#1605)
* [WIP] Refactor LocalResource interface and implementations to use immutable properties and add deleteLocal method

* Use `Optional` for `fileName` in `clearDirty` methods, update syncManagers

* Update synctools

* Refactor LocalCalendar to use AndroidRecurringCalendar for event operations

* Use AndroidCalendar.findEvent

* Update SyncManager to process fileName, eTag, scheduleTag and flags

* SyncManager: make ETag and Schedule-Tag processing more understandable

* Make upload handling more clear

* Update synctools
2025-08-06 15:38:38 +02:00
Sunik Kupfer
438f967152 [Sync Framework] Android 14+: Cancel forever pending address book account syncs (#1643)
* Also cancel sync for address books accounts

* Cancel known account directly

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-08-06 12:07:49 +02:00
Ricki Hirner
a093238864 Make "sync pending" work in AccountsScreen and AccountScreen (#1615)
* Add sync pending check for Android 14+

* Update sync pending UI logic to use selected authorities only

* Fix isSyncPending not handling multiple dataTypes

* Extract the accounts flow map to boolean flow logic

* Rename method

* Enhance kdoc

* Pass only one authority for pending check

* Update kdoc

* Update kdoc

* Update kdoc

* Fix whitespace

* Rename authority method to currentAuthority

* Update kdoc

---------

Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
2025-08-06 12:04:24 +02:00
Arnau Mora
293daf1e82 Fix bottom bar color on custom tabs (#1640)
* Fix bottom bar color on custom tabs

* Removed themind

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-08-06 11:45:29 +02:00
Sunik Kupfer
3b50747ce9 Rename CollectionListRefresher to CollectionsWithoutHomeSetRefresher (#1644)
* CollectionListRefresher to CollectionsWithoutHomeSetRefresher

* Adapt tests

* Unify class/method naming

* Use "without homeset" wording when applicable

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-08-06 10:45:43 +02:00
Arnau Mora
51d6ed279a Fetch translations from Transifex (#1638) 2025-08-04 11:50:15 +02:00
Arnau Mora
2c6842ac0c Version bump to 4.5.3 (#1637)
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-08-04 11:38:40 +02:00
Ricki Hirner
0e6644305a Bump version to 4.5.3-rc.2 2025-08-01 10:49:28 +02:00
Ricki Hirner
10e3b0a723 Update dependencies, including AGP (#1632) 2025-08-01 10:49:00 +02:00
Arnau Mora
2f45b705b3 Replaced Webcal tab icon in collections list (#1628)
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-08-01 10:43:09 +02:00
Ricki Hirner
be6c3311d7 WebDAV: remove notifications, timeout logic (#1630)
* Refactor openDocument operation to use OsConstants for mode parsing

* RandomAccessCallbackWrapper: refactor so that it's only purpose is to avoid the memory leak

* Use main looper instead of a new thread per RandomAccessCallback

* Remove WebDAV access notification

* Remove nsk90-kstatemachine dependency

* Simplify fileDescriptor() method

* Use dedicated I/O thread again; use Kotlin `copyTo` for copying
2025-08-01 10:38:24 +02:00
Ricki Hirner
755863778b Bump version to 4.5.3-rc.1 2025-07-30 10:49:15 +02:00
Ricki Hirner
0e81866d3a Fix ETag update logic in QueryChildDocumentsOperation (#1626) 2025-07-30 10:48:39 +02:00
Arnau Mora
93a256ee75 Icons next to CalDAV/CardDAV tab titles (#1599)
* Icons next to CalDAV/CardDAV tab titles

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

* Use shared element transitions

* Switch to `sharedBounds` to allow font size changes

* Update app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreen.kt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Minor changes

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-07-30 10:30:16 +02:00
Arnau Mora
61e9d60b7c Correctly handle SecurityException for acquireContentProvider (#1622)
* Handle `SecurityException` for `acquireContentProvider`

* Added optional `throwOnMissingPermissions` arg to `acquireContentProvider`

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

* Set `throwOnMissingPermissions` to `true`

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

* Adapt comments, remove now unnecessary try/catch in AccountSettingsMigration20

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-07-30 10:07:54 +02:00
Ricki Hirner
dc9fb7b608 AndroidSyncFrameworkTest: allow optional states (#1625)
* Update sync state verification to allow optional states

* Refactor AndroidSyncFrameworkTest to use stateEquals for comparison
2025-07-30 09:42:16 +02:00
Ricki Hirner
44b52f65a2 [WebDAV] Implement command pattern, streamline lifecycle, remove WebdavScope (#1617)
* Remove WebdavScope as it is no longer needed

* [WIP] DavDocumentsProviderImpl

* [WIP] Move DavDocumentsProvider to BaseDavDocumentsProvider and implementation to DavDocumentsProvider

* Adapt tests and DI

* [WIP] Implement Command pattern

* Finish Command pattern, add deprecation notices

* Unify DavDocumentsProvider with wrapper again

* Get rid of DavDocumentsActor

* Add notes about lifecycle, remove shutdown
2025-07-29 10:17:05 +02:00
Sunik Kupfer
e13c140554 Extract refreshHomesetsAndTheirCollections to HomeSetRefresher (#1606)
* Extract refreshHomesetsAndTheirCollections to HomeSetRefresher

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

* Add kdoc

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

* Minor changes

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-07-28 09:59:40 +02:00
Ricki Hirner
cdb50205f4 Don't warn on newer Commons versions 2025-07-24 18:12:06 +02:00
Sunik Kupfer
2ba4a2a510 Version bump to 4.5.3-alpha.1 2025-07-24 14:07:12 +02:00
Sunik Kupfer
38b2377760 Follow redirects when adding webdav mount (#1582)
* Follow redirects when adding webdav mount

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

* Update dav4jvm version

* Remove unused null

* Fix tests

* Add Kdoc

* Add Kdoc

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-07-24 14:02:50 +02:00
Sunik Kupfer
10f6356a6e Extract refreshPrincipals() to PrincipalsRefresher (#1607)
* Extract refreshPrincipals to PrincipalsRefresher

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

* Make method public

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-07-24 13:49:23 +02:00
Ricki Hirner
df4b6d3fbc Use early entry point for sync adapter services (#1610) 2025-07-24 13:19:56 +02:00
Ricki Hirner
dab948730e Choose real or fake (for tests) SyncAdapter over DI (#1608)
* Choose real or fake (for tests) SyncAdapter over DI

* Minor changes

* Rename SyncAdapterServicesTest.kt to RealSyncAdapterTest.kt

* Group sync adapter / sync framework classes into new package

* Cache SyncAdapter in SyncAdapterServices

* Add documentation to SyncAdapter interface and rename RealSyncAdapterTest to SyncAdapterImplTest
2025-07-24 12:19:54 +02:00
Sunik Kupfer
288583bfad Extract discoverHomesets() to ServiceRefresher (#1604)
* Extract discoverHomesets to ServiceRefresher

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

* Move hiltRule to top; Add space

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

* Log every request with method and path

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-07-24 11:33:48 +02:00
Sunik Kupfer
98c0b0c36a [Sync framework] Fix sync always pending on Android 14+ (#1463)
* Android 14 and 15 workaround: tell the sync framework to cancel any pending syncs

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

* Add test which documents wrong pending sync check behaviour

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

* Exclude android 13 and below

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

* Cancel only own sync request

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

* Cancel only after enqueuing sync worker

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

* Move test to AndroidSyncFrameworkTest

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

* Reset master sync state

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

* Remove limited parallelism and increase test timeout

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

* Rename test method

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

* Add assert message

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

* Update comment

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

* Add sdk suppress annotation

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

* Use runBlocking to be able to catch the timeout exception

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

* Extract pending sync cancellation to method

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

* Enhance sync framework tests for Android versions 9 to 16, verifying correct and incorrect pending states

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

* Add tests for sync always pending behavior on Android 10 and 11

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

* Remove obsolete unmockkAll call

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

* Make tests a bit more reliable

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

* Extract cancelSync method call to utils to stub the call more easily

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

* Remove some unnecessary calls and update stub

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

* Update expected states lists

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

* Move cancelSyncInSyncFramework to SyncFrameworkIntegration

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

* Pass the whole sync extras bundle when cancelling sync

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

* [WIP] Initialize pending sync state reporting wrong behaviour

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

* Optimize SyncAdapterServicesTest

* Remove unused property

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

* Reset master sync state

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

* Revert "Reset master sync state"

This reverts commit 4bfe73a25a.

* Revert "Remove unused property"

This reverts commit 7c0fdbf392.

* Reapply "Reset master sync state"

This reverts commit 5f7f0f9bce.

* Reapply "Remove unused property"

This reverts commit f1d5009f8a.

* Increase timeout to 2 min

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

* [WIP] Optimize tests

* Optimize sync framework tests

* SyncAdapterServices FakeSyncAdapter: support interrupting

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-07-23 13:03:02 +02:00
Sunik Kupfer
ed7a477d3f [UI] Use "(optional)" in labels to indicate optional textfields (#1571)
* Clarify optional fields in UI

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

* Add icons to add webdav mount screen

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

* Add title to group mount point address and name

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

* Use assistant composable

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-07-22 09:20:06 +02:00
Ricki Hirner
b0609fafb2 Update synctools for AndroidEvent2 (#1601)
* [WIP] Update synctools

* Refactor LocalEvent to use LegacyAndroidCalendar for event operations

* Refactor LocalEvent to use LegacyAndroidCalendar for event operations

* Update cert4android to get 16 kB page size support over Conscrypt 2.5.3 (#1581)

* Move SyncState to resource package because it's not in the database (#1585)

* Update dependencies, including dav4jvm that updates okhttp to 5.x (#1593)

* Update dependencies, including dav4jvm that updates okhttp to 5.x

* Update mockk and okhttp

* Bump version to 4.5.2-beta.1

* Move Insert/update to DAO (#1587)

* Move homeset insert/update logic from repository to DAO; add thread-safety test

* Rename insertOrUpdateByUrlRememberSync

* Fix exceptions being fetched as `Parcelable` instead of `Serializable` in `DebugInfoActivity` (#1597)

Typo: replace `getParcelableExtra` with `getSerializableExtra`

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

* Bump version to 4.5.2

* Fetch translations from Transifex

* Add documentation and handle missing event in LocalEvent

* Minor changes

* Rename `event` to `getCachedEvent()` to make it more clear what it does

* Update SEQUENCE after successful event upload more explicitly

* Update sequence after successful calendar event upload

* Remove deprecated add() method from LocalResource

* Update KDoc

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Arnau Mora <arnyminerz@proton.me>
2025-07-22 09:15:10 +02:00
Ricki Hirner
94a85833bc Bump version to 4.5.2.1 2025-07-21 18:13:31 +02:00
Ricki Hirner
4c5c8c3ed0 Don't disable AndroidX startup completely (#1603)
* Add test

* Update AndroidManifest.xml to configure Hilt/WorkManager integration and remove default WorkManagerInitializer
2025-07-21 18:09:37 +02:00
Ricki Hirner
4685ab6d0c Revert "Update synctools" (#1600)
Revert "Update synctools (#1579)"

This reverts commit 62a0ba3520.
2025-07-21 11:57:34 +02:00
Ricki Hirner
62a0ba3520 Update synctools (#1579)
* [WIP] Update synctools

* Refactor LocalEvent to use LegacyAndroidCalendar for event operations

* Refactor LocalEvent to use LegacyAndroidCalendar for event operations

* Update cert4android to get 16 kB page size support over Conscrypt 2.5.3 (#1581)

* Move SyncState to resource package because it's not in the database (#1585)

* Update dependencies, including dav4jvm that updates okhttp to 5.x (#1593)

* Update dependencies, including dav4jvm that updates okhttp to 5.x

* Update mockk and okhttp

* Bump version to 4.5.2-beta.1

* Move Insert/update to DAO (#1587)

* Move homeset insert/update logic from repository to DAO; add thread-safety test

* Rename insertOrUpdateByUrlRememberSync

* Fix exceptions being fetched as `Parcelable` instead of `Serializable` in `DebugInfoActivity` (#1597)

Typo: replace `getParcelableExtra` with `getSerializableExtra`

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

* Bump version to 4.5.2

* Fetch translations from Transifex

* Add documentation and handle missing event in LocalEvent

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Arnau Mora <arnyminerz@proton.me>
2025-07-21 11:54:20 +02:00
Ricki Hirner
71f3558b4b Fetch translations from Transifex 2025-07-21 10:50:15 +02:00
Ricki Hirner
22d933096f Bump version to 4.5.2 2025-07-21 10:24:22 +02:00
Arnau Mora
666b707854 Fix exceptions being fetched as Parcelable instead of Serializable in DebugInfoActivity (#1597)
Typo: replace `getParcelableExtra` with `getSerializableExtra`

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-07-21 10:03:19 +02:00
Ricki Hirner
39f6b82926 Move Insert/update to DAO (#1587)
* Move homeset insert/update logic from repository to DAO; add thread-safety test

* Rename insertOrUpdateByUrlRememberSync
2025-07-21 09:28:32 +02:00
Sunik Kupfer
b02fd23f0a Bump version to 4.5.2-beta.1 2025-07-17 11:26:40 +02:00
Ricki Hirner
ca56380c29 Update dependencies, including dav4jvm that updates okhttp to 5.x (#1593)
* Update dependencies, including dav4jvm that updates okhttp to 5.x

* Update mockk and okhttp
2025-07-17 11:06:04 +02:00
Ricki Hirner
ba9eb1446b Move SyncState to resource package because it's not in the database (#1585) 2025-07-16 12:06:34 +02:00
Ricki Hirner
055599c74f Update cert4android to get 16 kB page size support over Conscrypt 2.5.3 (#1581) 2025-07-15 09:50:39 +02:00
Ricki Hirner
62db3da579 Update Android Gradle Plugin to 8.11.1 and AboutLibraries to 12.2.4 2025-07-14 10:58:22 +02:00
Ricki Hirner
76d8d5acbf Sync davx5 / davx5-ose repos (#1575)
* Add custom certs UI build config field and use it in HttpClient; sync CollectionDao

* Backport max accounts setting
2025-07-10 16:49:07 +02:00
Ricki Hirner
dd294a4b03 Move external URIs from Constants to ExternalUris object (#1574)
* [WIP] Move external URIs from Constants to ExternalUris object

* Update external URIs to use class names or screen name for stat params
2025-07-10 16:14:09 +02:00
Sunik Kupfer
0efe6a7b9b Bump version to 4.5.2-alpha.1
Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-07-10 11:49:25 +02:00
Ricki Hirner
405b7abb39 [synctools] Use EventReader / EventWriter (#1573)
* Update synctools. Use Writer instead of ByteArrayOutputStream

* Update synctools
2025-07-10 10:57:45 +02:00
Ricki Hirner
4e2640ca01 Synctools: AndroidEvent companion object moved (#1572)
* Update synctools

* Refactor LocalCalendar to use Hilt

* Update synctools
2025-07-09 16:35:27 +02:00
Sunik Kupfer
904c8ba29b [Sync framework] Disable contacts content change triggered syncs if sync interval set to manual only (#1569)
* Fix lint error

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

* Show manual sync interval setting in UI

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

* Disable contacts content change triggered syncs if set to manual; update kdoc

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

* Update comments and kdoc

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

* Automatically close provider

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

* Explicitly handle special case

* Rename updateAutomaticSync to updateSyncFrameworkSetting; adjust comments

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-07-09 11:13:54 +02:00
Michael Biebl
62dc73c2a0 Use system defined line separator (#1562)
This fixes the test suite on Windows.
Windows uses CR LF, in contrast to Linux which uses LF. So do not
hard-code the value.
2025-07-07 17:42:24 +02:00
Ricki Hirner
58344099f7 AndroidCalendar refactoring (#1560)
* [WIP] Refactor calendar sync manager to use synctools library

* [WIP] Update synctools

* [WIP] Tests

* Remove test logger module and update calendar color methods

* Fix migrations

* Update libs.versions.toml
2025-07-03 22:12:21 +02:00
Ricki Hirner
b62c7eff0b Update to synctools version that uses explicit parsing / splitting (#1554)
* Update to synctools version that uses explicit parsing / splitting

* Update synctools
2025-07-02 15:33:13 +02:00
Ricki Hirner
12cedd4010 Fetch translations from Transifex 2025-07-02 14:30:57 +02:00
Ricki Hirner
3a0221c749 Update version to 4.5.1 2025-07-02 14:30:07 +02:00
Ricki Hirner
f78e7868e8 Revert "Update to synctools version that uses explicit parsing / splitting (#1554)"
This reverts commit 5dbaedfa60.

Reason: we don't want it in 4.5.1 yet
2025-07-02 14:28:57 +02:00
Ricki Hirner
5dbaedfa60 Update to synctools version that uses explicit parsing / splitting (#1554)
* Update to synctools version that uses explicit parsing / splitting

* Update synctools
2025-07-01 21:52:20 +02:00
Ricki Hirner
6187f92efd Bump version to 4.5.1-rc.2 2025-06-29 16:33:51 +02:00
Ricki Hirner
82ccf6a2f9 Update workflows to use build cache; don't reduce error level for configuration cache problems anymore 2025-06-29 16:23:56 +02:00
Ricki Hirner
0f9c5027d4 Fetch translations from Transifex 2025-06-29 15:42:40 +02:00
Ricki Hirner
7b76df3e70 Bump version to 4.5.1-rc.1 2025-06-29 15:41:36 +02:00
Ricki Hirner
80cfe1013d [CI] Don't generate a new discussion thread for every release
Updates will be posted in major release thread manually
2025-06-29 15:28:23 +02:00
Ricki Hirner
3e3c346019 Update dav4jvm 2025-06-29 14:58:12 +02:00
Ricki Hirner
1773dff8a4 Update synctools for R8 rules 2025-06-27 21:54:14 +02:00
Ricki Hirner
604b0aab98 Bump version to 4.5.1-alpha.1 2025-06-27 17:44:04 +02:00
Ricki Hirner
35cffa603b LocalCalendar: don't subclass AndroidCalendar (#1552)
* [WIP] Refactor LocalEvent to delegate to AndroidEvent

* Move tests

* Use test rules from synctools

* Add null check for content provider client in JtxSyncManagerTest

* Update dependencies, move OkhttpClientTest

* Refactor LocalCalendar to wrap AndroidCalendar

* Update bitfire-synctools to 1a613d5d3c
2025-06-27 17:43:14 +02:00
Ricki Hirner
89c3eacd36 LocalEvent: don't subclass AndroidEvent (#1551)
* [WIP] Refactor LocalEvent to delegate to AndroidEvent

* Move tests

* Use test rules from synctools

* Add null check for content provider client in JtxSyncManagerTest

* Update dependencies, move OkhttpClientTest
2025-06-27 17:06:41 +02:00
Ricki Hirner
4246ed65ac OAuth: Synchronize access token generation (#1547)
* OAuthInterceptor: synchronize refreshing of access token

* Remove sensitive logging

* [WIP] Logging

* [Google] Support custom client ID on re-authorization

* Statically synchronize acquisition of access token

* [WIP] OAuth: use callbacks for reading/writing AuthState

* Fix DavResourceFinderTest

* Move Credentials class to settings package; KDoc

* Simplify reauthorization
2025-06-27 11:24:02 +02:00
Ricki Hirner
789e7f3045 Set iCalendar PRODID in Constants and use it in respective classes (#1550)
* Set iCalendar PRODID in Constants and use it in respective classes

* Update bitfire-synctools
2025-06-26 15:38:37 +02:00
Michael Biebl
66f99f7362 Update README.md after switch to synctools (#1549) 2025-06-26 12:50:27 +02:00
Sunik Kupfer
90b04ddbdc [Google] Remove warning box on login screen (#1548)
* Remove the google login warning card

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

* Remove unused uri handler variable

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-06-26 12:47:53 +02:00
Ricki Hirner
a7f8ea8a48 Don't subclass AndroidEvent / AndroidCalendar populate / build methods anymore (#1544)
* Fix tests

* Update synctools; use AndroidCalendar SyncState

* Update synctools; move companion objects to end of class declarations
2025-06-25 14:17:58 +02:00
Sunik Kupfer
42cd8d8631 Update sync progress bar when pending in SAF (#1445)
* Provide flow to check if sync of account and authority is pending in sync framework

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

* Show if sync is pending in sync framework

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

* Show if sync is pending in sync framework

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

* Fix kdoc

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

* Cancel any pending SAF syncs on sync request

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

* Cancel sync adapter sync only after having enqueued our worker sync

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

* Improve accuracy by also checking isSyncActive

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

* Remove log statements

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

* Only query pending state

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

* Cancel sync adapter sync only on android 14 and 15

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

* Cancel sync adapter sync with authority

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

* Cancel sync adapter sync by using sync request instead of canceling directly

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

* Include android 16

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

* Include all versions after Android 14

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

* Add test which documents wrong pending sync check behaviour

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

* Revert "Add test which documents wrong pending sync check behaviour"

This reverts commit 8c538149ff2cb032d6355232c1736e103dcc9a18.

* Drop Android 14+ always pending sync work around

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

* Differentiate better between enqueued and pending syncs

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

* Refactor sync pending check to use flow for address book accounts

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

* Always return false for sync pending state check on Android 14+

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

* Refactor sync pending check to use flow for address book accounts

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

* Add comments

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

* Update comment

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

* Shorten variable name

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

* Update comments and variable name

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

* Remvoe obsolete call and add argument names as comments

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

* Remove sync active check from listener

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-06-25 13:54:44 +02:00
Ricki Hirner
a26847cf10 Update AGP, Kotlin, dependencies 2025-06-24 23:00:19 +02:00
Ricki Hirner
0e6c26aec6 Update synctools and use unified BatchOperation (#1543)
* Update synctools and use unified BatchOperation

* Fix tests
2025-06-24 16:26:40 +02:00
Arnau Mora
2204027993 Improve IME integration on forms (#1504)
* Fix focus accessibility issues

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

* Got rid of focus options. Improved IME integration

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

* Remove custom focus requesters

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

* Move url text to top of fields

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

* Add focus requester again

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

* Moved text

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-06-24 11:36:07 +02:00
Arnau Mora
e7189d66b0 Link to Managed DAVx5 in navigation drawer (bitfireAT/davx5#633)
Added managed drawer entry
2025-06-24 09:43:48 +02:00
Ricki Hirner
c517647819 Update dependencies 2025-06-24 09:40:21 +02:00
Ricki Hirner
0780b226ff Add F-Droid changelog 2025-06-23 13:20:54 +02:00
Ricki Hirner
8d9a417753 Use Android 14 for instrumented tests again (#1535) 2025-06-23 10:51:07 +02:00
Ricki Hirner
008c314b80 Version bump to 4.5 2025-06-23 09:34:10 +02:00
Ricki Hirner
f0019c54b1 Fetch translations from Transifex 2025-06-23 09:33:35 +02:00
Ricki Hirner
51ad6ee00a Intro / battery optimizations warning: remove Samsung; change wording from "probably" to "may" (#1532)
* Intro / battery optimizations warning: remove Samsung; change wording from "probably" to "may"

* Remove Huawei from list
2025-06-20 12:15:43 +02:00
Ricki Hirner
dd453a7837 Update version to 4.4.12-rc.1 2025-06-16 13:17:46 +02:00
Ricki Hirner
f91c968eb8 Update synctools, unify local storage exception handling 2025-06-16 13:16:18 +02:00
Ricki Hirner
968a43f9cc Support OAuth re-authorization (#1521)
* [WIP] Add OAuth re-authorization flow

* Refactor authorization contract to OAuthIntegration

* Add OAuth re-authentication UI and logic

* Update KDoc and UI strings
2025-06-12 09:37:59 +02:00
Sunik Kupfer
ac965b411b Delete local collection if missing remotely at delete request (#1512)
* Inject IO dispatcher

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

* Delete local collection on delete request when not remotely present anymore

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-06-11 11:13:35 +02:00
Michael Biebl
1608384418 Redact Authorization header from HTTP requests (#1520)
* Redact Authorization header from HTTP requests

https://github.com/square/okhttp/blob/master/okhttp-logging-interceptor/README.md

https://github.com/bitfireAT/davx5-ose/discussions/1438

* Use constant HttpHeaders.AUTHORIZATION from com.google.common.net.HttpHeaders

https://guava.dev/releases/19.0/api/docs/com/google/common/net/HttpHeaders.html#AUTHORIZATION

* Redact Cookie headers as well
2025-06-10 15:13:28 +02:00
Ricki Hirner
f25c22eba5 Replace ical4android and vcard4android with synctools (#1519) 2025-06-10 13:32:46 +02:00
Ricki Hirner
b1f742fb3a Update dependencies to latest versions 2025-06-05 20:10:09 +02:00
Ricki Hirner
47c8a0589d Bump version to 4.4.12-alpha.1 2025-06-05 20:08:16 +02:00
Arnau Mora
7c6474ce91 Fix edge to edge issue (#1515)
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-06-05 11:54:48 +02:00
Ricki Hirner
de8c1d160d Support Fastmail OAuth (#1509)
* Add Fastmail OAuth login implementation

* [CI] Run tests on API level 36, too

* Add Fastmail OAuth login support

* Remove logging and move companion object to bottom

* Remove FastmailLogin and GoogleLogin to OAuthLogin and OAuthGoogle

- Remove FastmailLogin class
- Refactor GoogleLogin class to OAuthGoogle object
- Update AndroidManifest.xml to use ${applicationId} for OAuth redirect URI
- Add OAuthFastmail object for Fastmail OAuth integration
- Update GoogleLoginModel and FastmailLoginModel to use OAuthGoogle and OAuthFastmail respectively
- Add OAuthIntegration object for shared OAuth functionality

* Update Fastmail authentication error message and add redirect URI documentation

* Add error handling for refresh token exception
2025-06-05 11:54:14 +02:00
Ricki Hirner
fa50fe4c30 [CI] Run tests on API level 35 2025-06-04 16:14:00 +02:00
Arnau Mora
ba4d3b2fd1 Increase SDK to 36 (#1513)
* Upgrade to SDK 36

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

* Enable on back invoked callback

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

* Remove `enableOnBackInvokedCallback`

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-06-04 15:42:57 +02:00
Sunik Kupfer
0fed85fdc3 Add app password hint under password field (#1507)
* Add app password hint under password field

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

* Change text to use prefer

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

* Append path encoded

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

* Update app password help URL and text

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-06-04 15:42:40 +02:00
Sunik Kupfer
6fbaea9487 [SyncManager]s Remove authority (#1491)
* Update sync stats to store sync data type instead of authority

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

* Use a real authority in the tests

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

* Replace authority with syncDataType in sync managers

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

* Minor changes
- import index
- edit comments

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

* Use lowercase localised strings for datatypes

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

* Pass sync data type extra as string

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

* Remove unknown datatype

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

* Move datatype name strings to collection screen section

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

* Update string usages

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

* Update test

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

* Add any type annotations to arrays

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-06-04 14:44:12 +02:00
Arnau Mora
fc2bc8aa47 Pass state to modal drawer for automatic back handler (#1495)
* Pass state to modal drawer for automatic back handler

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

* Fix deprecation: Use toUri instead of Uri.parse

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
2025-06-02 17:12:41 +02:00
Ricki Hirner
0321e4ab8f [Lint] Convert URIs to strings using toUri() (#1506) 2025-06-02 12:00:13 +02:00
Ricki Hirner
711543c5f1 Credentials / dav4jvm: store passwords as CharArray (#1483)
* Credentials / dav4jvm: store passwords as CharArray

* Fix tests
2025-05-30 17:37:14 +02:00
Ricki Hirner
5c485834e9 Update Gradle wrapper and Android Gradle Plugin versions 2025-05-30 17:36:10 +02:00
Ricki Hirner
f349f1fec8 Bump version to 4.4.11 2025-05-30 16:28:23 +02:00
Ricki Hirner
e6413506cb Fetch translations from Transifex 2025-05-30 16:26:53 +02:00
Michael Biebl
d4b5039297 Use lowercase GroupIDs as a workaround for jitpack.io issues (#1489)
Related discussion at

https://github.com/bitfireAT/davx5-ose/discussions/1236

and

https://github.com/jitpack/jitpack.io/issues/3295#issuecomment-2329134019

For example trying to access:
https://jitpack.io/com/github/bitfireAT/dav4jvm/b87d772e44/ I get:

File not found. Build ok

In comparison under
https://jitpack.io/com/github/bitfireat/dav4jvm/b87d772e44/ I get:

build.log
dav4jvm-b87d772e44-sources.jar
dav4jvm-b87d772e44.jar
dav4jvm-b87d772e44.module
dav4jvm-b87d772e44.pom
dav4jvm-b87d772e44.pom.md5
dav4jvm-b87d772e44.pom.sha1
2025-05-26 13:53:21 +02:00
Ricki Hirner
979f2257de Bump version to 4.4.11-rc.2 2025-05-22 19:43:03 +02:00
Ricki Hirner
3efb8d5c62 TaskSyncer: accept nullable ResyncType (#1487)
Fix TaskSyncer constructor to accept nullable ResyncType
2025-05-22 19:42:34 +02:00
Ricki Hirner
ec657519a9 Bump version to 4.4.11-rc.1 2025-05-22 17:33:24 +02:00
Sunik Kupfer
a835557b35 Handle when AccountActivity is started without account (#1481)
* Pass new account as non-nullable

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

* Move companion object

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

* Handle missing account in intent by logging and redirecting to accounts overview

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

* Fix deprecation

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

* Also check account exists

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

* Simplify toast message

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

* Add account name to toast message

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

* Don't check for invalid accounts in AccountActivity; handle InvalidAccountException in model

* Minor changes

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-05-22 17:22:31 +02:00
Arnau Mora
19f86670bf Implemented sort order for DAV documents (#1434)
* Implemented sort order

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

* Fixed column name

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

* Implemented sort order

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

* Fixed column name

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

* Improved issues

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

* Add test for WebDavDocumentDao.getChildren for ORDER BY

* Converted getChildren into a raw query

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

* Fix formatting of SQL query in WebDavDocumentDao

* Refactoring

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

* Drop comment

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

* Changed default sort to show directories first

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

* Fixed tests

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

* Switched to query constructor instead of in-place

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

* Changed log method

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

* Add DocumentSortByMapper to handle orderBy mapping for WebDavDocument queries

* Rename DavDocumentsProviderTest to DocumentSortByMapperTest and update test method name

* Refactor sorting and mapping

* Add "order by name" as last criterion

* Remove default sort order constant from DocumentSortByMapper and use WebDavDocumentDao.DEFAULT_ORDER instead

* Adapt comments

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-05-22 09:48:12 +02:00
Ricki Hirner
f74d14e2a2 Always append one-time syncs (#1482)
* Refactor sync worker manager to always append one-time syncs (but only once)

* Adapt KDoc
2025-05-21 15:57:10 +02:00
Ricki Hirner
57ef059099 [CI] Also run tests on push to main branch 2025-05-21 11:59:12 +02:00
Ricki Hirner
f157a819b7 Bump version to 4.4.11-beta.1 2025-05-21 11:55:51 +02:00
Ricki Hirner
9bbc4c096d Update Compose BOM version to 2025.05.01 2025-05-21 11:55:51 +02:00
Sunik Kupfer
b306219015 [Push] Append sync when already syncing (#1480)
* [WIP] Add delay to sync worker and append push syncs

* Enqueue sync work with append policy for push notifications

* Remove delay

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

* Also check for enqueued work

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-05-21 11:54:18 +02:00
Ricki Hirner
469c30b511 [CI] Run tests on pull requests instead of git pushes (#1479)
* [CI] Run tests on pull requests instead of git pushes

* Add comment to PR trigger in test-dev.yml

* Update concurrency group for development tests
2025-05-21 11:30:24 +02:00
Sunik Kupfer
05f6c7ab0b Reset subscription URLs on distributor change (#1467)
* Move distributor set/get to PushRegistrationManager

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

* Handle unsubscription after manual distributor change before resubscribing

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

* Minor changes

* Update distributor check to use method of the own class

* Use mutex and add KDoc

* UnifiedPush registration: add service type to "message for distributor"

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
Co-authored-by: Arnau Mora <arnyminerz@proton.me>
2025-05-19 16:42:40 +02:00
Ricki Hirner
eeb94d4039 [Sync] Replace extras Bundle by explicit arguments (#1475)
* Remove extras parameter from sync managers

* Refactor sync worker parameters and improve documentation

* Adapt tests

* Rename resyncType to resync in syncers, adapt KDoc

* Remove legacy sync result comment
2025-05-19 15:37:50 +02:00
Ricki Hirner
7bf9172bdc SyncManager: directly wrap HTTP calls with runInterruptible (#1472)
* SyncManager: Handle CancellationException in sync process

* Wrap remote resource operations with runInterruptible; provide suspending SyncException wrappers

* Update more sync methods to use runInterruptible only for the HTTP request itself

* Make downloadRemote methods suspendable and use runInterruptible for better concurrency handling

* Update code style

* Fix tests

* Refactor SyncException to use runBlocking for wrapping resources
2025-05-19 12:54:51 +02:00
Arnau Mora
dec5be5690 Display FCM instead of DAVx⁵ in distributor settings (#1468)
* Display FCM instead of DAVx⁵ in distributor settings

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

* Update app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsScreen.kt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-19 10:49:19 +02:00
Ricki Hirner
d8e8129d7b Bump version to 4.4.11-alpha.1 2025-05-14 13:49:05 +02:00
Ricki Hirner
c9fb7dc7a2 Update dependency 2025-05-14 13:46:47 +02:00
Arnau Mora
ededcb98e1 Added widget with just a sync icon (#1340)
* Renamed existing widget

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

* Added sync icon widget

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

* Added labels for widgets

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

* Added widget preview

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

* Removed max width restriction

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

* Changed widget description

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-05-14 08:50:50 +02:00
Arnau Mora
fc10a315d5 Added warning about credentials in debug info (#1457)
* Added warning about credentials in debug info

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

* Updated warning message

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

* Changed descriptive icon

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

* Replace "alert" by "notice"

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-05-13 15:45:52 +02:00
Ricki Hirner
cfeb6b3974 Make SyncManager suspending (first part) (#1451)
* Introduce coroutine dispatcher for sync operations

* Make `logSyncTimeBlocking` and `insertOrReplace` suspend functions

* Suspend first bunch of SyncManager methods; move sync dispatcher usage to SyncManager

* [WIP] Fix tests

TODO: extract test framework changes to separate PR

* Remove mainDispatcher from SyncManagerTest

* Remove explicit coroutineScope naming
2025-05-12 15:52:54 +02:00
Ricki Hirner
a15902e586 [DI] Make backing field of TestDispatcher + scheduler private 2025-05-10 09:17:22 +02:00
Ricki Hirner
47afddbd08 Simplify running suspending tests (#1452)
* Move LoggerModule to di package and add TestLoggerModule

* Remove main dispatcher injection and use runTest directly

* Add verbose logging module for tests

* Add test for UnifiedPushService.onUnregistered
2025-05-09 14:55:30 +02:00
Ricki Hirner
5f647b7403 Move GetDelayUntil tests to dav4jvm 2025-05-09 12:18:16 +02:00
Ricki Hirner
d460e4ca7b Update dav4jvm 2025-05-09 11:50:45 +02:00
Ricki Hirner
827a1b954f Update version code and name and increase max build metaspace size to 1GB 2025-05-09 10:59:34 +02:00
Ricki Hirner
54bcda1bb4 Update Android Gradle Plugin and Compose BOM versions 2025-05-08 13:27:13 +02:00
Ricki Hirner
7003c5f730 Bump version to 4.4.10-rc.1 2025-05-07 18:03:28 +02:00
Ricki Hirner
4ccf99ce23 Fetch translations from Transifex 2025-05-07 18:03:03 +02:00
Ricki Hirner
3fca4d60f1 Update dependencies 2025-05-07 18:02:57 +02:00
Ricki Hirner
2099f47d22 Use ical4android that removes ClassLoader checks; refactor SyncDispatcher (#1446)
* Use ical4android that removes ClassLoader checks; refactor SyncDispatcher

* [Tests] Add SyncDispatcher provider for tests

* Add sync dispatcher with fixed thread pool

* Replace fixed thread pool context with thread pool executor (as we had it previously, but without setting the contextClassLoader)

* Replace sync dispatcher with I/O dispatcher with limited parallelism
2025-05-07 17:57:46 +02:00
Sunik Kupfer
c12a723a52 Send Push-Dont-Notify header (#1444)
* Update dav4jvm

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

* Add push-dont-notify header to requests when push subscription has not expired

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

* Fix mocks in TestSyncManager

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

* Use relaxed mockk for collection

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

* Use lazy val for pushDontNotifyHeader

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

* Use null instead of empty map for pushDontNotifyHeader

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

* Get push subscription state as calculated collection property

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

* Add tests for active push subscription status in collection

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

* Rename test methods

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

* Return active subscription or null

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

* Use empty map instead of null for pushDontNotifyHeader

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

* Fix test

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

* Always send push-dont-notify header if subscription exists

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

* Fix copyright

* Send Push-Dont-Notify URL as quoted string

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-05-07 17:22:54 +02:00
Arnau Mora
eb0b75a9a7 [Push] Better title/description for Google FCM in UI (#1424)
* Added FCM indicator

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

* Changed text

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

* Added content description

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

* Add link to manual for WebDAV push in app settings

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-05-02 12:06:23 +02:00
Arnau Mora
94ca9cd871 Fixed deprecations with menu anchors (#1428)
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-04-30 11:58:01 +02:00
Ricki Hirner
9f697f06be Repositories: apply new naming scheme (#1423)
* DavHomeSetRepository, DavServiceRepository: apply new naming scheme for (non-)suspending calls

* AccountRepository: apply new naming scheme for (non-)suspending calls

* Update repository methods to new naming scheme
2025-04-30 10:58:00 +02:00
Ricki Hirner
ba7f95aad5 Clean up app/build.gradle (#1421)
* Remove packaging resources exclusion and configurations

* Update mikepenz-aboutLibraries to 12.1.0-rc03 and configure resource merging for LICENSE files
2025-04-27 11:10:20 +02:00
Ricki Hirner
993fffaa15 Fix UnifiedPushService tests (#1420) 2025-04-27 10:21:17 +02:00
Ricki Hirner
4b7f7ed45e Fetch translations from Transifex 2025-04-27 10:03:44 +02:00
Ricki Hirner
7e80607a34 Bump version to 4.4.10-beta.1 2025-04-27 10:01:41 +02:00
Ricki Hirner
d0389f13fc Move Hilt modules to .di package (bitfireAT/davx5#665)
Move Hilt module definitions to di package
2025-04-26 23:21:27 +02:00
Sunik Kupfer
993d0f66ec [Push] Update subscriptions when collection is (un)selected for sync (#1407)
* Update subscriptions when collection is (un)selected for sync

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

* Remove collections change listener and its hilt module

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

* Remove observer pattern to listen for collection changes

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

* Update push subscriptions when collection is (un)selected for sync

* Update collection selection listener to use lazy initialization

* Update push subscriptions and sync when collection is (un)selected for sync

* Update app/src/main/kotlin/at/bitfire/davdroid/ui/CollectionSelectedUseCase.kt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Move CollectionSelectedUseCase to account package

* Add test for CollectionSelectedUseCase

* Inject application scope instead of using a factory

* Update tests to run on our main dispatcher

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-26 22:13:40 +02:00
Arnau Mora
4c9ad959dd Upgrade about libs to 12.1.0-rc02 (#1416)
* Upgrade about libs to 12.1.0

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

* Update AboutLibraries version to 12.1.0-rc02

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-04-26 14:43:51 +02:00
Ricki Hirner
044a28138a [Push] Don't block main thread in UnifiedPushService (#1412)
* Refactor UnifiedPushService to use CoroutineScope so that it doesn't block on main thread

* Add KDoc why the scope is not cancelled
2025-04-25 10:36:02 +02:00
Ricki Hirner
e8ec98c257 Fix HttpClient.Builder.followRedirect 2025-04-25 10:30:53 +02:00
Ricki Hirner
fe0c1e67e7 Drop LiveData (#1414)
* Remove LiveData dependencies
2025-04-25 09:59:08 +02:00
Ricki Hirner
7261a8137d [IDE] Code style, width 2025-04-25 09:05:43 +02:00
Ricki Hirner
b1493f3f6a Bump version to 4.4.10-alpha.2 2025-04-24 20:08:30 +02:00
Ricki Hirner
d679dc5e97 [Push] Fix various subscription problems (#1411)
* Handle non-existing account gracefully when (un)subscribing collections

* Unsubscribe from all (subscribed) collections when no push distributor is selected
2025-04-24 20:03:39 +02:00
Ricki Hirner
b0f7196f2b Fix non-suspending DB access on main thread in accountRepository.delete() (#1409)
Make more database queries suspendable and run them in a blocking context
2025-04-24 19:02:22 +02:00
Ricki Hirner
77a6e5c5ab Bump version to 4.4.10-alpha.1 2025-04-24 13:12:38 +02:00
Ricki Hirner
845d979046 Update dependency versions 2025-04-24 13:12:14 +02:00
Arnau Mora
f62509ed80 [Push] Support UnifiedPush Connector 3.x, VAPID, Encryption, Google FCM (#1325)
* Upgrade UnifiedPush Connector to 3.0.4

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

* Updated overrides

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

* Added storing keys and auths

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

* Excluded tink

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

* Fixed deprecations and calls

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

* Integrate UnifiedPush 3.x connector and FCM distributor

* Integrate UnifiedPush connector 3.x, use VAPID and message encryption

* [WIP] Refactor push registration logic and remove deprecated methods

* [WIP] Remove PushRegistrationWorkerManager and refactor PushRegistrationManager

* Remove unused service repository dependency and update worker to suspend

* Add suspend modifier to DAO methods and repository methods

* Add runBlocking to getByService call in CollectionListRefresherTest

* Add documentation for UnifiedPushService and PushRegistrationManager

* Add fallback for push messages without topic

* [WIP] Add UnifiedPushService test with workaround for PushService binder

* Update UnifiedPush library version and clean up test code

* Refactor push message handling, synchronization and coroutines

* Add coroutine dispatchers for push registration and unregistration

* Add async support for push subscription updates

* Refactor unsubscribe logic into reusable method

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-04-24 13:09:51 +02:00
Ricki Hirner
e79c362f46 [WebDAV] Refactor coroutine usage (#1405)
* Inject coroutine scopes for cancellation and I/O

* Inject coroutine scopes for better cancellation and lifecycle management

* Inject coroutine scopes for WebDAV operations

* Fix tests

* More correct coroutine contexts
2025-04-24 12:12:17 +02:00
Arnau Mora
5c35741226 Update AboutLibraries to 12.0.0 (#1406)
* Upgrade aboutlibs to 12.0.0

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

* Migrated code

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

* Apply AboutLibraries after other gradle plugins

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-04-24 10:33:59 +02:00
Ricki Hirner
b90b8ce6a2 Update AGP to 8.9.2 2025-04-23 17:30:29 +02:00
Sunik Kupfer
321aeedd8f Sync after sync flag or forceReadOnly flag of a collection are changed (#1383)
* Perform delayed sync on a collection UI change

- sync state
- force-read-only state

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

* Use withContext to access DB on background thread

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

* Remove return comment

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

* Extract delay value to constant

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

* Use accountRepository to create account from name

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-04-23 17:25:12 +02:00
Arnau Mora
09f68a237b NPE when trying to log in without specifying an email (#1401)
* Added check for null baseUri

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

* Rollback

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

* Added check for IME next

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-04-22 10:44:21 +02:00
Ricki Hirner
93d715bb99 Test coroutines with runTest instead of runBlocking (#1399)
Add kotlinx-coroutines-test dependency and replace runBlocking with runTest in tests
2025-04-22 10:44:08 +02:00
Arnau Mora
04fe8e1aca Hide "keep permissions" in API 29 and below (#1400)
* Hide "keep permissions" for API 29 and below

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

* Fix deprecation

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-04-22 09:35:12 +02:00
Ricki Hirner
1f02f3cc27 Use R8 full mode (#1397)
* Use R8 full mode

* Add dav4jvm dependency (which includes an XmlPullParser) for testing
2025-04-20 12:53:17 +02:00
Ricki Hirner
a0acd4e929 Use ProGuard rules from libraries (#1387)
Update library versions and optimize ProGuard rules (most of them are now in the libraries consumer rules)
2025-04-20 12:46:46 +02:00
Ricki Hirner
3901e6ebe4 Update translation fetching script to use relative paths 2025-04-20 10:30:11 +02:00
Ricki Hirner
f229226521 Bump version to 4.4.9 2025-04-20 10:26:36 +02:00
Ricki Hirner
6644e4acd7 Fix 4.4.9 release compile error (#1386)
Update ical4android version and force Apache Commons dependencies
2025-04-17 16:39:23 +02:00
Ricki Hirner
d46f8056a5 Fetch translations from Transifex 2025-04-17 12:55:02 +02:00
Ricki Hirner
c3731ace88 Version bump to 4.4.9-rc.1 2025-04-17 12:53:21 +02:00
Ricki Hirner
038c2df524 Update dependencies 2025-04-17 12:52:20 +02:00
Arnau Mora
400318b390 Fixed paddings on horizontal mode (#1353)
* Fixed paddings

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

* Added padding to all screens

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

* Fixed edge to edge on accounts screen

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

* Fixed e2e issues

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

* Undo edge-to-edge

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

* Rollback

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

* Fixed paddings

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

* Fixed padding consumption

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

* Got rid of ime padding

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

* Fixed issues with paddings

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

* Fix UI elements partially obscured in landscape mode

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-04-17 12:47:59 +02:00
Arnau Mora
0d2e5a1f07 Avoid NPE when missing DTSTART for recurring events (#1336)
* Added handling of null dtstart

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

* Added JtxSyncManagerTest

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

* Test recurrence id without dtstart does not cause NPE

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

* Extract syncmanager creation from try-catch

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

* Add tests

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

* Assert RRULE remains in main vtodo

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

* Skip tests when jtx board not installed

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

* Correct annotation

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

* Simplify null checks

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

* Extract recurid definition

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

* Update ical4android

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

* Find recurrence instance without dtstart

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

* Rename method for clarity and update kdoc

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

* Use new method in test too

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

* Fix lint warnings

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

* Use a custom rule to wrap and ignore security exception if jtxboard is not installed

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

* Use existing permission utils

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

* Rename capture to catch exceptions rule

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

* Format code

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
2025-04-16 14:49:44 +02:00
Ricki Hirner
9835cd0d53 Refactor SharedPreferences edits to use Kotlin extension function (#1382) 2025-04-15 11:46:17 +02:00
Bernhard Stockmann
f6d8efcd26 Update Screenshots (#1380)
now reflects latest UI
2025-04-14 13:32:27 +02:00
Ricki Hirner
955de83b35 Add VAPID support to Collection entity (#1376) 2025-04-13 11:41:30 +02:00
Ricki Hirner
29a09f2038 Update dependencies, including dav4jvm (#1374)
* Update dependencies

* Remove unused import and simplify topic extraction

* Fix room issue
2025-04-13 10:26:34 +02:00
Ricki Hirner
fff332f31f Enable dependabot for Github Actions (#1363) 2025-04-01 17:28:24 +02:00
Ricki Hirner
b4d4a2fddd Update Android Gradle Plugin and Compose BOM versions 2025-04-01 10:40:56 +02:00
Arnau Mora
566a539a85 Debug Info does not contain actual error (#1362)
Fixed typo on intent extra

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-04-01 10:20:03 +02:00
Arnau Mora
e588ada891 Original string for webdav_add_mount_empty_more_info broken (#1354)
Got rid of extra `</string>` tag inside of CDATA

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-03-23 11:36:16 +01:00
Sunik Kupfer
0c89e3ba3b Version bump to 4.4.8 2025-03-13 12:44:38 +01:00
Sunik Kupfer
7b0e134c20 Fetch translations from Transifex 2025-03-13 12:40:43 +01:00
Ricki Hirner
8f7c285cb7 Move InvalidAccountException to account package (#1342)
Move InvalidAccountException
2025-03-13 11:21:44 +01:00
Arnau Mora
a2cddfc012 Release 4.4.8-rc.1 (bitfireAT/davx5#656)
* Increase version code and modify version name

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-03-13 11:21:44 +01:00
Sunik Kupfer
54eaecc6b5 Backport of bitfireAT/davx5#655: honor build config decision on whether users are allowed to accept custom certificates
* Honor customCertsUI build config value on whether users are allowed to accept custom certificates

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

* Add kdoc

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

* Update comment

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

* Extract config evaluation; Update kdoc

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

---------

Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
2025-03-11 12:37:55 +01:00
Sunik Kupfer
0012dec482 Track app foreground state via AppTheme using separate object (#1335)
* Track app in foreground state via AppTheme using separate object

* Make inForeground read only; add kdoc

* Remove type
2025-03-05 11:13:41 +01:00
Ricki Hirner
ced6abea3f Update AGP and dependencies 2025-03-05 10:41:35 +01:00
Sunik Kupfer
4a82baeaea Remove special chars in address book account names (#1334)
* Remove special chars from address book account names

* Add a test

* Update kdoc

* Only remove iso control and SQL problematic chars
2025-03-05 09:51:39 +01:00
Sunik Kupfer
f41b4fd59d Show a notification at sync when ContentProvider inaccessible (#1278)
* Extract notification handling to separate class

* Notify user at sync if content provider is missing

* Dismiss only content provider specific notification

* Remove title from notification text body

* Move sync warning strings into their own block

* Add KDoc, duplicate method for clarity

* Show message in notification for disabled tasks apps

* Pass authority through method calls

* Shorten method names

* Don't show content provider error notification when missing permission

* Rename methods and remove obsolete var

* Add spacing in content provider missing warning

* Improve kdoc

* Remove obsolete tasks provider error messages

* Syntactic sugar

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-03-04 13:36:29 +01:00
Ricki Hirner
f6bd4b0fc2 Update issue templates to support issue types and allow blank issues (for internal use) 2025-03-01 12:47:36 +01:00
Ricki Hirner
50879b6a0c Update AGP, Kotlin, dependencies 2025-02-27 13:14:47 +01:00
Arnau Mora
6be42d4ec3 Added timestamp to debug info (#1323)
* Added timestamp to debug info

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

* Added timestamp to `DebugInfoActivity.IntentBuilder`

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

* Show local time and UTC of timestamp

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-02-25 20:00:22 +01:00
Sunik Kupfer
a56d42d9a5 Provide test address books via DI (#1310)
* Add LocalTestAddressBookStore.kt

* Update tests acquiring local test address books

* Remove unused methods from LocalTestAddressBook

* Extract dirty check methods to the single test class using them

* Remove unused read only flag

* Drop obsolete context

* Reusables as properties

* Rename LocalTestAddressBookStore to LocalTestAddressBookProvider

* Minor changes

* Remove test address book after provision finishes and don't remove all before

* Fix more tests by removing address book accounts after run, not before

* Wrap provision method call in try-finally

* Rename provide methods anonymous function param for clarity

* Extract account recreation to variable

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-02-25 17:27:45 +01:00
Ricki Hirner
e34952bca9 Update dependencies 2025-02-19 12:58:36 +01:00
Arnau Mora
9d293a00e7 Added confirmation dialog for "Distrust System Certificates" (#1307)
* Added confirmation dialog for dsc

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

* Automatic dialog dismiss

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

* Added preview, and changed texts

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

* Updated dialog

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-02-15 23:04:32 +01:00
Arnau Mora
1ee41f8027 Use TaskStackBuilder instead of PendingIntent.getActivity (#1293)
* Use `TaskStackBuilder` instead of `PendingIntent.getActivity`

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

* Replaced usages for addNextIntentWithParentStack

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

* Remove unused import

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-02-13 18:53:10 +01:00
Sunik Kupfer
d3c1dbb5da DnsRecordResolverTest: Use seeded random number generator for deterministic test result (#1306)
* Use seeded random number generator for a 100% deterministic test result

* Fix typo

* Rename variable

* Mock random numbers and assert corresponding record is found
2025-02-12 17:50:50 +01:00
Sunik Kupfer
cd554d885b Skip login type selection when logging in via intent (#1267)
* Move companion object to the end of class

* Skip login type selection when logging in via intent

* Skip login type page if not default login type

* Add test for implicit email intent

* Fix test

* Update KDoc

* Refactor URI handling in LoginActivity and StandardLoginTypesProvider

* Skip login type page if intent is clear, but don't skip when using defaultLoginType

* Log unclear intents

* Use data class instead of pair

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-02-11 10:58:15 +01:00
Ricki Hirner
d9b4149d41 Refactor: replace deprecated getShowOnlyPersonalPair method and simplify removeAccount logic (#1304) 2025-02-07 11:55:16 +01:00
Arnau Mora
4af6165094 Backport login_credentials_lock for password change in account settings (bitfireAT/davx5#643)
---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-02-07 11:41:47 +01:00
Ricki Hirner
fb2023762d Add syncActive flag to control SyncAdapter behavior during tests (#1302) 2025-02-07 11:26:10 +01:00
Ricki Hirner
946c450036 SyncAdapter: Hilt error handling (#1299)
* SyncAdapter: Hilt error handling

* HiltTestRunner: enforce Android P requirement for MockK

* Update comment
2025-02-06 17:29:14 +01:00
Ricki Hirner
969d92d037 Bump version to 4.4.7 2025-02-02 18:30:35 +01:00
Ricki Hirner
6998f009c4 Fetch translations from Transifex 2025-02-02 14:57:15 +01:00
Ricki Hirner
cba1f01bdb Version bump to 4.4.7-rc.1 2025-02-02 14:56:29 +01:00
Ricki Hirner
5f80c8e779 Add migration for Syncer URL → ID change (#1285)
* Add AccountSettingsMigration20 to handle collection ID migration for syncer

* Add success path tests for address books and calendars

* Increase CURRENT_VERSION, fix task list store
2025-02-02 14:55:39 +01:00
Ricki Hirner
5ece438b3f Open-source intro page: don't select a "dont show for... months" option by default (#1287)
Refactor RadioButtons component to support initial selection and update parameter names for clarity
2025-02-02 14:46:50 +01:00
Ricki Hirner
acd4e41f8b Update cert4android to avoid Conscrypt crash in custom certificate handling (#1290) 2025-02-02 14:35:36 +01:00
Ricki Hirner
ae08093906 Syncer SecurityException: disable permission notification for now (#1284) 2025-02-02 14:28:07 +01:00
Ricki Hirner
dd456b41f1 Version bump to 4.4.7-beta.1 2025-01-31 22:52:19 +01:00
Ricki Hirner
30283f36a4 Update dependencies 2025-01-31 22:51:53 +01:00
Ricki Hirner
4858dd9229 Move authority and ContentProvider creation to LocalDataStore (#1272)
* Move authority and content provider acquire() to LocalDataStore implementations

* Fix tests

* Update LocalCalendarStore.kt

Reroder LocalCalendarStore delete
2025-01-30 12:42:11 +01:00
Sunik Kupfer
b910ba25ae Use ID to match DB collections with content provider collections (#1274)
* Update ical4android

* Match DB collections with content provider collections via ID

* Minor renaming and KDoc

* Move string constant to companion object

* Update KDoc

* Use getOrDefault to be more explicit

* Remove exception throw on missing collection ID

* Rewrite LocalAddressBookStoreTest

* Minor changes
- remove unused param
- make companion methods internal

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2025-01-29 16:47:51 +01:00
Arnau Mora
2a542210ca MKCALENDAR does not send valid calendar-timezone (#1251)
Fixed missing calendar properties

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2025-01-28 12:39:24 +01:00
Sunik Kupfer
eef85f1f7a Provide three options for donation reminder time (#1256)
* Provide three options for donation reminder time

* Move radio buttons to top card; Change strings

* Use padding instead of height for radio elements
2025-01-27 17:50:12 +01:00
Ricki Hirner
4f2d4e3a49 Further unmockk after tests 2025-01-27 16:47:31 +01:00
Ricki Hirner
feccb76ce8 MockK: use MockkRule if possible (#1269)
Unmockk all
2025-01-27 16:24:44 +01:00
Ricki Hirner
835689a4a6 Use DI for HttpClient.Builder (#1250)
* [WIP] Use Hilt for HttpClient.Builder

* Inject Provider<HttpClientBuilder> when necessary

* Proxy support

* [WIP] Tests

* Fix tests

* Minor changes, enable cache support again

* Update HttpClient to use modern TLS connection specification (disable TLS 1.0 and 1.1)

* Use real CredentialsStore
2025-01-27 13:44:33 +01:00
Ricki Hirner
50cbac147e [CI] Use if: !cancelled() instead of if: always() (#1266)
To make jobs cancellable,
see https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#always
2025-01-27 13:02:38 +01:00
635 changed files with 25551 additions and 15852 deletions

8
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,8 @@
# See https://docs.github.com/de/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# For combination with "Require review from code owners" for main-ose branch.
# Dependabot
gradle/** @bitfireAT/app-dev
# everything else
* @rfc2822

View File

@@ -1,4 +1,4 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: DAVx⁵ Community Support
url: https://github.com/bitfireAT/davx5-ose/discussions

View File

@@ -1,5 +1,6 @@
name: Qualified Bug Report
description: "[Developers only] For qualified bug reports. (Use Discussions if unsure.)"
description: "For qualified bug reports. (Use Discussions if unsure.)"
type: bug
labels: ["bug"]
body:
- type: checkboxes

View File

@@ -1,5 +1,6 @@
name: Qualified Feature Request
description: "[Developers only] For qualified feature requests. (Use Discussions if unsure.)"
description: "For qualified feature requests. (Use Discussions if unsure.)"
type: feature
labels: ["enhancement"]
body:
- type: checkboxes

32
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
version: 2
updates:
# Enable version updates for GitHub Actions
- package-ecosystem: "github-actions"
# Workflow files stored in the default location of `.github/workflows`
# You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "[CI] "
labels:
- "github_actions"
- "dependencies"
groups:
ci-actions:
patterns: ["*"]
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "weekly"
labels: # don't create "java" label (default for gradle ecosystem)
- "dependencies"
groups:
app-dependencies:
patterns: ["*"]
ignore:
# dependencies without semantic versioning
- dependency-name: "com.github.bitfireat:cert4android"
- dependency-name: "com.github.bitfireat:dav4jvm"
- dependency-name: "com.github.bitfireat:synctools"

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,38 +22,29 @@ jobs:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-java@v4
uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
cache-read-only: true # gradle user home cache is generated by test jobs
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
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 --configuration-cache-problems=warn --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@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"

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

@@ -1,55 +0,0 @@
name: Dependent Issues
on:
issues:
types:
- opened
- edited
- closed
- reopened
pull_request_target:
types:
- opened
- edited
- closed
- reopened
# Makes sure we always add status check for PRs. Useful only if
# this action is required to pass before merging. Otherwise, it
# can be removed.
- synchronize
# Schedule a daily check. Useful if you reference cross-repository
# issues or pull requests. Otherwise, it can be removed.
schedule:
- cron: '19 9 * * *'
permissions: write-all
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: z0al/dependent-issues@v1
env:
# (Required) The token to use to make API calls to GitHub.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# (Optional) The token to use to make API calls to GitHub for remote repos.
GITHUB_READ_TOKEN: ${{ secrets.DEPENDENT_ISSUES_READ_TOKEN }}
with:
# (Optional) The label to use to mark dependent issues
# label: dependent
# (Optional) Enable checking for dependencies in issues.
# Enable by setting the value to "on". Default "off"
check_issues: on
# (Optional) A comma-separated list of keywords. Default
# "depends on, blocked by"
keywords: depends on, blocked by
# (Optional) A custom comment body. It supports `{{ dependencies }}` token.
comment: >
This PR/issue depends on:
{{ dependencies }}

View File

@@ -19,19 +19,19 @@ jobs:
discussions: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v5
- name: Prepare keystore
run: echo ${{ secrets.android_keystore_base64 }} | base64 -d >$GITHUB_WORKSPACE/keystore.jks
- name: Build signed package
# Make sure that caches are disabled to generate reproducible release builds
run: ./gradlew --no-build-cache --no-configuration-cache --no-daemon app:assembleRelease
# Use build cache to speed up building of build variants, but clean caches from previous tests before
run: ./gradlew --build-cache --configuration-cache --no-daemon app:clean app:assembleRelease
env:
ANDROID_KEYSTORE: ${{ github.workspace }}/keystore.jks
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.android_keystore_password }}
@@ -45,4 +45,3 @@ jobs:
files: app/build/outputs/apk/ose/release/*.apk
fail_on_unmatched_files: true
generate_release_notes: true
discussion_category_name: Announcements

View File

@@ -2,80 +2,135 @@ name: Development tests
on:
push:
branches:
- '*'
- 'main-ose'
pull_request:
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@v4
- uses: actions/setup-java@v4
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
# See https://community.gradle.org/github-actions/docs/setup-gradle/ for more information
- uses: gradle/actions/setup-gradle@v4 # 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 --configuration-cache-problems=warn 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: ${{ always() }} # even if compile didn't run (because not on main branch)
name: Lint and unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
cache-read-only: true
- name: Run lint
run: ./gradlew --build-cache --configuration-cache --configuration-cache-problems=warn app:lintOseDebug
- name: Run unit tests
run: ./gradlew --build-cache --configuration-cache --configuration-cache-problems=warn 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: ${{ always() }} # even if compile didn't run (because not on main branch)
name: Instrumented tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v5
with:
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 --configuration-cache-problems=warn 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

4
.gitignore vendored
View File

@@ -16,10 +16,6 @@
bin/
gen/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties

9
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<option name="RIGHT_MARGIN" value="180" />
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

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, 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).
@@ -26,8 +28,7 @@ Parts of DAVx⁵ have been outsourced into these libraries:
* [cert4android](https://github.com/bitfireAT/cert4android) custom certificate management
* [dav4jvm](https://github.com/bitfireAT/dav4jvm) WebDAV/CalDav/CardDAV framework
* [ical4android](https://github.com/bitfireAT/ical4android) iCalendar processing and Calendar Provider access
* [vcard4android](https://github.com/bitfireAT/vcard4android) vCard processing and Contacts Provider access
* [synctools](https://github.com/bitfireAT/synctools) iCalendar/vCard/Tasks processing and content provider access
**If you want to support DAVx⁵, please consider [donating to DAVx⁵](https://www.davx5.com/donate)
or [purchasing it](https://www.davx5.com/download).**

View File

@@ -1,30 +1,30 @@
/***************************************************************************************************
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
*/
plugins {
alias(libs.plugins.mikepenz.aboutLibraries)
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)
}
// Android configuration
android {
compileSdk = 35
compileSdk = 36
defaultConfig {
applicationId = "at.bitfire.davdroid"
versionCode = 404060003
versionName = "4.4.6"
versionCode = 405090005
versionName = "4.5.9"
setProperty("archivesBaseName", "davx5-ose-$versionName")
base.archivesName = "davx5-$versionCode-$versionName"
minSdk = 24 // Android 7.0
targetSdk = 35 // Android 15
targetSdk = 36 // Android 16
testInstrumentationRunner = "at.bitfire.davdroid.HiltTestRunner"
}
@@ -48,7 +48,7 @@ android {
}
// Java namespace for our classes (not to be confused with Android package ID)
namespace = "at.bitfire.davdroid"
namespace = "com.davx5.ose"
flavorDimensions += "distribution"
productFlavors {
@@ -85,25 +85,28 @@ android {
}
lint {
disable += arrayOf("GoogleAppIndexingWarning", "ImpliedQuantity", "MissingQuantity", "MissingTranslation", "ExtraTranslation", "RtlEnabled", "RtlHardcoded", "Typos", "NullSafeMutableLiveData")
}
packaging {
resources {
excludes += arrayOf("META-INF/*.md")
}
disable += arrayOf("GoogleAppIndexingWarning", "ImpliedQuantity", "MissingQuantity", "MissingTranslation", "ExtraTranslation", "RtlEnabled", "RtlHardcoded", "Typos")
}
androidResources {
generateLocaleConfig = true
}
packaging {
resources {
// multiple (test) dependencies have LICENSE files at same location
merges += arrayOf("META-INF/LICENSE*")
}
}
@Suppress("UnstableApiUsage")
testOptions {
managedDevices {
localDevices {
create("virtual") {
device = "Pixel 3"
// TBD: API level 35 and higher causes network tests to fail sometimes, see https://github.com/bitfireAT/davx5-ose/issues/1525
// Suspected reason: https://developer.android.com/about/versions/15/behavior-changes-all#background-network-access
apiLevel = 34
systemImageSource = "aosp-atd"
}
@@ -117,22 +120,17 @@ ksp {
}
aboutLibraries {
excludeFields = arrayOf("generated")
}
configurations {
configureEach {
// exclude modules which are in conflict with system libraries
exclude(module="commons-logging")
exclude(group="org.json", module="json")
// Groovy requires SDK 26+, and it's not required, so exclude it
exclude(group="org.codehaus.groovy")
export {
// exclude timestamps for reproducible builds [https://github.com/bitfireAT/davx5-ose/issues/994]
excludeFields.add("generated")
}
}
dependencies {
// core
// app core
implementation(project(":core"))
// Kotlin / Android
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines)
coreLibraryDesugaring(libs.android.desugaring)
@@ -160,45 +158,62 @@ dependencies {
// Jetpack Compose
implementation(libs.compose.accompanist.permissions)
implementation(platform(libs.compose.bom))
implementation(libs.compose.material3)
implementation(libs.compose.materialIconsExtended)
implementation(libs.compose.runtime.livedata)
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.material3.adaptive)
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.material3)
// 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)
implementation(libs.bitfire.dav4jvm) {
exclude(group="junit")
exclude(group="org.ogce", module="xpp3") // Android has its own XmlPullParser implementation
}
implementation(libs.bitfire.synctools) {
exclude(group="androidx.test") // synctools declares test rules, but we don't want them in non-test code
exclude(group = "junit")
}
implementation(libs.bitfire.ical4android)
implementation(libs.bitfire.vcard4android)
// third-party libs
@Suppress("RedundantSuppression")
implementation(libs.conscrypt)
implementation(libs.dnsjava)
implementation(libs.guava)
implementation(libs.mikepenz.aboutLibraries)
implementation(libs.nsk90.kstatemachine)
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)
implementation(libs.okhttp.logging)
implementation(libs.openid.appauth)
implementation(libs.unifiedpush)
implementation(libs.unifiedpush) {
// UnifiedPush connector seems to be using a workaround by importing this library.
// Will be removed after https://github.com/tink-crypto/tink-java-apps/pull/5 is merged.
// See: https://codeberg.org/UnifiedPush/android-connector/src/commit/28cb0d622ed0a972996041ab9cc85b701abc48c6/connector/build.gradle#L56-L59
exclude(group = "com.google.crypto.tink", module = "tink")
}
implementation(libs.unifiedpush.fcm)
// force some versions for compatibility with our minSdk level (see version catalog for details)
implementation(libs.commons.codec)
implementation(libs.commons.lang)
// 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)
@@ -206,11 +221,13 @@ dependencies {
androidTestImplementation(libs.androidx.work.testing)
androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.mockk.android)
androidTestImplementation(libs.okhttp.mockwebserver)
androidTestImplementation(libs.room.testing)
testImplementation(libs.bitfire.dav4jvm)
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.okhttp.mockwebserver)
testImplementation(libs.robolectric)
}

View File

@@ -1,60 +0,0 @@
# R8 usage for DAVx⁵:
# shrinking yes (only in release builds)
# optimization yes (on by R8 defaults)
# full-mode no (see gradle.properties)
# obfuscation no (open-source)
-dontobfuscate
-printusage build/reports/r8-usage.txt
# ez-vcard: keep all vCard properties/parameters (used via reflection)
-keep class ezvcard.io.scribe.** { *; }
-keep class ezvcard.property.** { *; }
-keep class ezvcard.parameter.** { *; }
# ical4j: keep all iCalendar properties/parameters (used via reflection)
-keep class net.fortuna.ical4j.** { *; }
# XmlPullParser
-keep class org.xmlpull.** { *; }
# DAVx + libs
-keep class at.bitfire.** { *; } # all DAVx code is required
# AGP 8.2 and 8.3 seem to remove this class, but ezvcard.io uses it. See https://github.com/bitfireAT/davx5/issues/499
-keep class javax.xml.namespace.QName { *; }
# we use enum classes (https://www.guardsquare.com/en/products/proguard/manual/examples#enumerations)
-keepclassmembers,allowoptimization enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Additional rules which are now required since missing classes can't be ignored in R8 anymore.
# [https://developer.android.com/build/releases/past-releases/agp-7-0-0-release-notes#r8-missing-class-warning]
-dontwarn com.android.org.conscrypt.SSLParametersImpl
-dontwarn com.github.erosb.jsonsKema.** # ical4j
-dontwarn com.google.errorprone.annotations.**
-dontwarn com.sun.jna.** # dnsjava
-dontwarn groovy.**
-dontwarn java.beans.Transient
-dontwarn javax.cache.** # ical4j
-dontwarn javax.naming.NamingException # dnsjava
-dontwarn javax.naming.directory.** # dnsjava
-dontwarn junit.textui.TestRunner
-dontwarn lombok.** # dnsjava
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
-dontwarn org.bouncycastle.jsse.**
-dontwarn org.codehaus.groovy.**
-dontwarn org.joda.**
-dontwarn org.jparsec.** # ical4j
-dontwarn org.json.*
-dontwarn org.jsoup.**
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
-dontwarn org.xbill.DNS.spi.DnsjavaInetAddressResolverProvider # dnsjava
-dontwarn org.xmlpull.**
-dontwarn sun.net.spi.nameservice.NameService
-dontwarn sun.net.spi.nameservice.NameServiceDescriptor

1
app/src/.gitignore vendored
View File

@@ -1 +0,0 @@
espressoTest

View File

@@ -1,20 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid
import android.util.Xml
import at.bitfire.dav4jvm.XmlUtils
import org.junit.Assert.assertTrue
import org.junit.Test
class Dav4jvm {
@Test
fun test_Dav4jvm_XmlUtils_NewPullParser_RelaxedParsing() {
val parser = XmlUtils.newPullParser()
assertTrue(parser.getFeature(Xml.FEATURE_RELAXED))
}
}

View File

@@ -1,17 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader, name: String, context: Context): Application =
super.newApplication(cl, HiltTestApplication::class.java.name, context)
}

View File

@@ -1,123 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid
import android.Manifest
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.os.Build
import android.provider.CalendarContract
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalEvent
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.Event
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.RRule
import org.junit.Assert.assertNotNull
import org.junit.rules.ExternalResource
import org.junit.rules.RuleChain
import java.util.logging.Logger
/**
* JUnit ClassRule which initializes the AOSP CalendarProvider.
*
* It seems that the calendar provider unfortunately forgets the very first requests when it is used the very first time,
* maybe by some wrongly synchronized database initialization. So things like querying the instances
* fails in this case.
*
* So this rule is needed to allow tests which need the calendar provider to succeed even when the calendar provider
* is used the very first time (especially in CI tests / a fresh emulator).
*
* See [at.bitfire.davdroid.resource.LocalCalendarTest] for an example of how to use this rule.
*/
class InitCalendarProviderRule private constructor(): ExternalResource() {
companion object {
private var isInitialized = false
private val logger = Logger.getLogger(InitCalendarProviderRule::javaClass.name)
fun getInstance(): RuleChain = RuleChain
.outerRule(GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR))
.around(InitCalendarProviderRule())
}
override fun before() {
if (!isInitialized) {
logger.info("Initializing calendar provider")
if (Build.VERSION.SDK_INT < 31)
logger.warning("Calendar provider initialization may or may not work. See InitCalendarProviderRule")
val context = InstrumentationRegistry.getInstrumentation().targetContext
val client = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
assertNotNull("Couldn't acquire calendar provider", client)
client!!.use {
initCalendarProvider(client)
isInitialized = true
}
}
}
private fun initCalendarProvider(provider: ContentProviderClient) {
val account = Account("LocalCalendarTest", CalendarContract.ACCOUNT_TYPE_LOCAL)
// Sometimes, the calendar provider returns an ID for the created calendar, but then fails to find it.
var calendarOrNull: LocalCalendar? = null
for (i in 0..50) {
calendarOrNull = createAndVerifyCalendar(account, provider)
if (calendarOrNull != null)
break
else
Thread.sleep(100)
}
val calendar = calendarOrNull ?: throw IllegalStateException("Couldn't create calendar")
try {
// single event init
val normalEvent = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 1 instance"
}
val normalLocalEvent = LocalEvent(calendar, normalEvent, null, null, null, 0)
normalLocalEvent.add()
LocalEvent.numInstances(provider, account, normalLocalEvent.id!!)
// recurring event init
val recurringEvent = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event over 22 years"
rRules.add(RRule("FREQ=YEARLY;UNTIL=20740119T010203Z")) // year needs to be >2074 (not supported by Android <11 Calendar Storage)
}
val localRecurringEvent = LocalEvent(calendar, recurringEvent, null, null, null, 0)
localRecurringEvent.add()
LocalEvent.numInstances(provider, account, localRecurringEvent.id!!)
} finally {
calendar.delete()
}
}
private fun createAndVerifyCalendar(account: Account, provider: ContentProviderClient): LocalCalendar? {
val uri = AndroidCalendar.create(account, provider, ContentValues())
return try {
AndroidCalendar.findByID(
account,
provider,
LocalCalendar.Factory,
ContentUris.parseId(uri)
)
} catch (e: Exception) {
logger.warning("Couldn't find calendar after creation: $e")
null
}
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid
import android.content.Context
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.settings.SettingsManager
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import okhttp3.Request
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class OkhttpClientTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
@ApplicationContext
lateinit var context: Context
@Inject
lateinit var settingsManager: SettingsManager
@Before
fun inject() {
hiltRule.inject()
}
@Test
fun testIcloudWithSettings() {
val client = HttpClient.Builder(context).build()
client.okHttpClient.newCall(Request.Builder()
.get()
.url("https://icloud.com")
.build())
.execute()
}
}

View File

@@ -1,103 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.repository
import android.content.Context
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.settings.AccountSettings
import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class DavCollectionRepositoryTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var accountSettingsFactory: AccountSettings.Factory
@Inject
@ApplicationContext
lateinit var context: Context
@Inject
lateinit var db: AppDatabase
@Inject
lateinit var serviceRepository: DavServiceRepository
var service: Service? = null
@Before
fun setUp() {
hiltRule.inject()
service = createTestService(Service.TYPE_CARDDAV)!!
}
@After
fun cleanUp() {
db.close()
serviceRepository.deleteAll()
}
@Test
fun testOnChangeListener_setForceReadOnly() = runBlocking {
val collectionId = db.collectionDao().insertOrUpdateByUrl(
Collection(
serviceId = service!!.id,
type = Collection.TYPE_ADDRESSBOOK,
url = "https://example.com".toHttpUrl(),
forceReadOnly = false,
)
)
val testObserver = mockk<DavCollectionRepository.OnChangeListener>(relaxed = true)
val collectionRepository = DavCollectionRepository(
accountSettingsFactory,
context,
db,
object : Lazy<Set<DavCollectionRepository.OnChangeListener>> {
override fun get(): Set<DavCollectionRepository.OnChangeListener> {
return mutableSetOf(testObserver)
}
},
serviceRepository
)
assert(db.collectionDao().get(collectionId)?.forceReadOnly == false)
verify(exactly = 0) {
testObserver.onCollectionsChanged()
}
collectionRepository.setForceReadOnly(collectionId, true)
assert(db.collectionDao().get(collectionId)?.forceReadOnly == true)
verify(exactly = 1) {
testObserver.onCollectionsChanged()
}
}
// Test helpers and dependencies
private fun createTestService(serviceType: String) : Service? {
val service = Service(id=0, accountName="test", type=serviceType, principal = null)
val serviceId = serviceRepository.insertOrReplace(service)
return serviceRepository.get(serviceId)
}
}

View File

@@ -1,78 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.repository
import at.bitfire.davdroid.db.HomeSet
import at.bitfire.davdroid.db.Service
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class DavHomeSetRepositoryTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var repository: DavHomeSetRepository
@Inject
lateinit var serviceRepository: DavServiceRepository
@Before
fun setUp() {
hiltRule.inject()
}
@Test
fun testInsertOrUpdate() {
// should insert new row or update (upsert) existing row - without changing its key!
val serviceId = createTestService()
val entry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl())
val insertId1 = repository.insertOrUpdateByUrl(entry1)
assertEquals(1L, insertId1)
assertEquals(entry1.copy(id = 1L), repository.getById(1L))
val updatedEntry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl(), displayName="Updated Entry")
val updateId1 = repository.insertOrUpdateByUrl(updatedEntry1)
assertEquals(1L, updateId1)
assertEquals(updatedEntry1.copy(id = 1L), repository.getById(1L))
val entry2 = HomeSet(id=0, serviceId=serviceId, personal=true, url= "https://example.com/2".toHttpUrl())
val insertId2 = repository.insertOrUpdateByUrl(entry2)
assertEquals(2L, insertId2)
assertEquals(entry2.copy(id = 2L), repository.getById(2L))
}
@Test
fun testDelete() {
// should delete row with given primary key (id)
val serviceId = createTestService()
val entry1 = HomeSet(id=1, serviceId=serviceId, personal=true, url= "https://example.com/1".toHttpUrl())
val insertId1 = repository.insertOrUpdateByUrl(entry1)
assertEquals(1L, insertId1)
assertEquals(entry1, repository.getById(1L))
repository.delete(entry1)
assertEquals(null, repository.getById(1L))
}
private fun createTestService() : Long {
val service = Service(id=0, accountName="test", type= Service.TYPE_CALDAV, principal = null)
return serviceRepository.insertOrReplace(service)
}
}

View File

@@ -1,144 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.Manifest
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.Context
import android.provider.ContactsContract
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.LabeledProperty
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import ezvcard.property.Telephone
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import java.util.LinkedList
import javax.inject.Inject
@HiltAndroidTest
class LocalAddressBookTest {
@Inject @ApplicationContext
lateinit var context: Context
@get:Rule
val hiltRule = HiltAndroidRule(this)
val account = Account("Test Account", "Test Account Type")
lateinit var addressBook: LocalTestAddressBook
@Before
fun setUp() {
hiltRule.inject()
addressBook = LocalTestAddressBook.create(context, account, provider)
}
@After
fun tearDown() {
// remove address book
addressBook.remove()
}
/**
* Tests whether contacts are moved (and not lost) when an address book is renamed.
*/
@Test
fun test_renameAccount_retainsContacts() {
// insert contact with data row
val uid = "12345"
val contact = Contact(
uid = uid,
displayName = "Test Contact",
phoneNumbers = LinkedList(listOf(LabeledProperty(Telephone("1234567890"))))
)
val uri = LocalContact(addressBook, contact, null, null, 0).add()
val id = ContentUris.parseId(uri)
val localContact = addressBook.findContactById(id)
localContact.resetDirty()
assertFalse("Contact is dirty before moving", addressBook.isContactDirty(id))
// rename address book
val newName = "New Name"
addressBook.renameAccount(newName)
assertEquals(newName, addressBook.addressBookAccount.name)
// check whether contact is still here (including data rows) and not dirty
val result = addressBook.findContactById(id)
assertFalse("Contact is dirty after moving", addressBook.isContactDirty(id))
val contact2 = result.getContact()
assertEquals(uid, contact2.uid)
assertEquals("Test Contact", contact2.displayName)
assertEquals("1234567890", contact2.phoneNumbers.first().component1().text)
}
/**
* Tests whether groups are moved (and not lost) when an address book is renamed.
*/
@Test
fun test_renameAccount_retainsGroups() {
// insert group
val localGroup = LocalGroup(addressBook, Contact(displayName = "Test Group"), null, null, 0)
val uri = localGroup.add()
val id = ContentUris.parseId(uri)
// make sure it's not dirty
localGroup.clearDirty(null, null, null)
assertFalse("Group is dirty before moving", addressBook.isGroupDirty(id))
// rename address book
val newName = "New Name"
assertTrue(addressBook.renameAccount(newName))
assertEquals(newName, addressBook.addressBookAccount.name)
// check whether group is still here and not dirty
val result = addressBook.findGroupById(id)
assertFalse("Group is dirty after moving", addressBook.isGroupDirty(id))
val group = result.getContact()
assertEquals("Test Group", group.displayName)
}
companion object {
@JvmField
@ClassRule
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
private lateinit var provider: ContentProviderClient
@BeforeClass
@JvmStatic
fun connect() {
val context = InstrumentationRegistry.getInstrumentation().context
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
}
@AfterClass
@JvmStatic
fun disconnect() {
provider.close()
}
}
}

View File

@@ -1,151 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.provider.CalendarContract
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
import android.provider.CalendarContract.Events
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.InitCalendarProviderRule
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
import at.bitfire.ical4android.util.MiscUtils.closeCompat
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.RRule
import net.fortuna.ical4j.model.property.RecurrenceId
import net.fortuna.ical4j.model.property.Status
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.TestRule
class LocalCalendarTest {
companion object {
@JvmField
@ClassRule
val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance()
private lateinit var provider: ContentProviderClient
@BeforeClass
@JvmStatic
fun setUpClass() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
}
@AfterClass
@JvmStatic
fun tearDownClass() {
provider.closeCompat()
}
}
private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL)
private lateinit var calendar: LocalCalendar
@Before
fun setUp() {
val uri = AndroidCalendar.create(account, provider, ContentValues())
calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri))
}
@After
fun tearDown() {
calendar.delete()
}
@Test
fun testDeleteDirtyEventsWithoutInstances_NoInstances_CancelledExceptions() {
// create recurring event with only deleted/cancelled instances
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 3 instances"
rRules.add(RRule("FREQ=DAILY;COUNT=3"))
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220120T010203Z")
dtStart = DtStart("20220120T010203Z")
summary = "Cancelled exception on 1st day"
status = Status.VEVENT_CANCELLED
})
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220121T010203Z")
dtStart = DtStart("20220121T010203Z")
summary = "Cancelled exception on 2nd day"
status = Status.VEVENT_CANCELLED
})
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220122T010203Z")
dtStart = DtStart("20220122T010203Z")
summary = "Cancelled exception on 3rd day"
status = Status.VEVENT_CANCELLED
})
}
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
localEvent.add()
val eventId = localEvent.id!!
// set event as dirty
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
put(Events.DIRTY, 1)
}, null, null)
// this method should mark the event as deleted
calendar.deleteDirtyEventsWithoutInstances()
// verify that event is now marked as deleted
provider.query(
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
arrayOf(Events.DELETED), null, null, null
)!!.use { cursor ->
cursor.moveToNext()
assertEquals(1, cursor.getInt(0))
}
}
@Test
// Needs InitCalendarProviderRule
fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 3 instances"
rRules.add(RRule("FREQ=DAILY;COUNT=3"))
}
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
localEvent.add()
val eventId = localEvent.id!!
// set event as dirty
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
put(Events.DIRTY, 1)
}, null, null)
// this method should mark the event as deleted
calendar.deleteDirtyEventsWithoutInstances()
// verify that event is not marked as deleted
provider.query(
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
arrayOf(Events.DELETED), null, null, null
)!!.use { cursor ->
cursor.moveToNext()
assertEquals(0, cursor.getInt(0))
}
}
}

View File

@@ -1,485 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.os.Build
import android.provider.CalendarContract
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
import android.provider.CalendarContract.Events
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.InitCalendarProviderRule
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.MiscUtils.closeCompat
import at.techbee.jtx.JtxContract.asSyncAdapter
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateList
import net.fortuna.ical4j.model.parameter.Value
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.ExDate
import net.fortuna.ical4j.model.property.RRule
import net.fortuna.ical4j.model.property.RecurrenceId
import net.fortuna.ical4j.model.property.Status
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.TestRule
import java.util.UUID
class LocalEventTest {
private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL)
private lateinit var calendar: LocalCalendar
@Before
fun setUp() {
val uri = AndroidCalendar.create(account, provider, ContentValues())
calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri))
}
@After
fun removeCalendar() {
calendar.delete()
}
@Test
fun testNumDirectInstances_SingleInstance() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 1 instance"
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
assertEquals(1, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
}
@Test
fun testNumDirectInstances_Recurring() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 5 instances"
rRules.add(RRule("FREQ=DAILY;COUNT=5"))
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
assertEquals(5, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
}
@Test
fun testNumDirectInstances_Recurring_Endless() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event without end"
rRules.add(RRule("FREQ=DAILY"))
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
assertNull(LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
}
@Test
// flaky, needs InitCalendarProviderRule
fun testNumDirectInstances_Recurring_LateEnd() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 53 years"
rRules.add(RRule("FREQ=YEARLY;UNTIL=20740119T010203Z")) // year 2074 is not supported by Android <11 Calendar Storage
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
assertEquals(52, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
else
assertNull(LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
}
@Test
// flaky, needs InitCalendarProviderRule
fun testNumDirectInstances_Recurring_ManyInstances() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 2 years"
rRules.add(RRule("FREQ=DAILY;UNTIL=20240120T010203Z"))
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
val number = LocalEvent.numDirectInstances(provider, account, localEvent.id!!)
// Some android versions (i.e. <=Q and S) return 365*2 instances (wrong, 365*2+1 => correct),
// but we are satisfied with either result for now
assertTrue(number == 365*2 || number == 365*2+1)
}
@Test
// flaky, needs InitCalendarProviderRule
fun testNumDirectInstances_RecurringWithExdate() {
val event = Event().apply {
dtStart = DtStart(Date("20220120T010203Z"))
summary = "Event with 5 instances"
rRules.add(RRule("FREQ=DAILY;COUNT=5"))
exDates.add(ExDate(DateList("20220121T010203Z", Value.DATE_TIME)))
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
assertEquals(4, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
}
@Test
fun testNumDirectInstances_RecurringWithExceptions() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 5 instances"
rRules.add(RRule("FREQ=DAILY;COUNT=5"))
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220122T010203Z")
dtStart = DtStart("20220122T130203Z")
summary = "Exception on 3rd day"
})
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220124T010203Z")
dtStart = DtStart("20220122T160203Z")
summary = "Exception on 5th day"
})
}
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, 0)
localEvent.add()
assertEquals(5-2, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
}
@Test
// flaky, needs InitCalendarProviderRule
fun testNumInstances_SingleInstance() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 1 instance"
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
assertEquals(1, LocalEvent.numInstances(provider, account, localEvent.id!!))
}
@Test
fun testNumInstances_Recurring() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 5 instances"
rRules.add(RRule("FREQ=DAILY;COUNT=5"))
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
assertEquals(5, LocalEvent.numInstances(provider, account, localEvent.id!!))
}
@Test
fun testNumInstances_Recurring_Endless() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with infinite instances"
rRules.add(RRule("FREQ=YEARLY"))
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
assertNull(LocalEvent.numInstances(provider, account, localEvent.id!!))
}
@Test
// flaky, needs InitCalendarProviderRule
fun testNumInstances_Recurring_LateEnd() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event over 22 years"
rRules.add(RRule("FREQ=YEARLY;UNTIL=20740119T010203Z")) // year 2074 not supported by Android <11 Calendar Storage
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
assertEquals(52, LocalEvent.numInstances(provider, account, localEvent.id!!))
else
assertNull(LocalEvent.numInstances(provider, account, localEvent.id!!))
}
@Test
// flaky, needs InitCalendarProviderRule
fun testNumInstances_Recurring_ManyInstances() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event over two years"
rRules.add(RRule("FREQ=DAILY;UNTIL=20240120T010203Z"))
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
assertEquals(
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q)
365*2 // Android <10: does not include UNTIL (incorrect!)
else
365*2 + 1, // Android ≥10: includes UNTIL (correct)
LocalEvent.numInstances(provider, account, localEvent.id!!))
}
@Test
fun testNumInstances_RecurringWithExceptions() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 6 instances"
rRules.add(RRule("FREQ=DAILY;COUNT=6"))
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220122T010203Z")
dtStart = DtStart("20220122T130203Z")
summary = "Exception on 3rd day"
})
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220124T010203Z")
dtStart = DtStart("20220122T160203Z")
summary = "Exception on 5th day"
})
}
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, 0)
localEvent.add()
calendar.findById(localEvent.id!!)
assertEquals(6, LocalEvent.numInstances(provider, account, localEvent.id!!))
}
@Test
fun testMarkEventAsDeleted() {
// Create event
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "A fine event"
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add()
// Delete event
LocalEvent.markAsDeleted(provider, account, localEvent.id!!)
// Get the status of whether the event is deleted
provider.query(
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
arrayOf(Events.DELETED),
null,
null, null
)!!.use { cursor ->
cursor.moveToFirst()
assertEquals(1, cursor.getInt(0))
}
}
@Test
fun testPrepareForUpload_NoUid() {
// create event
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event without uid"
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add() // save it to calendar storage
// prepare for upload - this should generate a new random uuid, returned as filename
val fileNameWithSuffix = localEvent.prepareForUpload()
val fileName = fileNameWithSuffix.removeSuffix(".ics")
// throws an exception if fileName is not an UUID
UUID.fromString(fileName)
// UID in calendar storage should be the same as file name
provider.query(
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
arrayOf(Events.UID_2445), null, null, null
)!!.use { cursor ->
cursor.moveToFirst()
assertEquals(fileName, cursor.getString(0))
}
}
@Test
fun testPrepareForUpload_NormalUid() {
// create event
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with normal uid"
uid = "some-event@hostname.tld" // old UID format, UUID would be new format
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add() // save it to calendar storage
// prepare for upload - this should use the UID for the file name
val fileNameWithSuffix = localEvent.prepareForUpload()
val fileName = fileNameWithSuffix.removeSuffix(".ics")
assertEquals(event.uid, fileName)
// UID in calendar storage should still be set, too
provider.query(
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
arrayOf(Events.UID_2445), null, null, null
)!!.use { cursor ->
cursor.moveToFirst()
assertEquals(fileName, cursor.getString(0))
}
}
@Test
fun testPrepareForUpload_UidHasDangerousChars() {
// create event
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with funny uid"
uid = "https://www.example.com/events/asdfewfe-cxyb-ewrws-sadfrwerxyvser-asdfxye-"
}
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
localEvent.add() // save it to calendar storage
// prepare for upload - this should generate a new random uuid, returned as filename
val fileNameWithSuffix = localEvent.prepareForUpload()
val fileName = fileNameWithSuffix.removeSuffix(".ics")
// throws an exception if fileName is not an UUID
UUID.fromString(fileName)
// UID in calendar storage shouldn't have been changed
provider.query(
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
arrayOf(Events.UID_2445), null, null, null
)!!.use { cursor ->
cursor.moveToFirst()
assertEquals(event.uid, cursor.getString(0))
}
}
@Test
fun testDeleteDirtyEventsWithoutInstances_NoInstances_Exdate() {
// TODO
}
@Test
fun testDeleteDirtyEventsWithoutInstances_NoInstances_CancelledExceptions() {
// create recurring event with only deleted/cancelled instances
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 3 instances"
rRules.add(RRule("FREQ=DAILY;COUNT=3"))
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220120T010203Z")
dtStart = DtStart("20220120T010203Z")
summary = "Cancelled exception on 1st day"
status = Status.VEVENT_CANCELLED
})
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220121T010203Z")
dtStart = DtStart("20220121T010203Z")
summary = "Cancelled exception on 2nd day"
status = Status.VEVENT_CANCELLED
})
exceptions.add(Event().apply {
recurrenceId = RecurrenceId("20220122T010203Z")
dtStart = DtStart("20220122T010203Z")
summary = "Cancelled exception on 3rd day"
status = Status.VEVENT_CANCELLED
})
}
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
localEvent.add()
val eventId = localEvent.id!!
// set event as dirty
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
put(Events.DIRTY, 1)
}, null, null)
// this method should mark the event as deleted
calendar.deleteDirtyEventsWithoutInstances()
// verify that event is now marked as deleted
provider.query(
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
arrayOf(Events.DELETED), null, null, null
)!!.use { cursor ->
cursor.moveToNext()
assertEquals(1, cursor.getInt(0))
}
}
@Test
fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() {
val event = Event().apply {
dtStart = DtStart("20220120T010203Z")
summary = "Event with 3 instances"
rRules.add(RRule("FREQ=DAILY;COUNT=3"))
}
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
localEvent.add()
val eventId = localEvent.id!!
// set event as dirty
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
put(Events.DIRTY, 1)
}, null, null)
// this method should mark the event as deleted
calendar.deleteDirtyEventsWithoutInstances()
// verify that event is not marked as deleted
provider.query(
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
arrayOf(Events.DELETED), null, null, null
)!!.use { cursor ->
cursor.moveToNext()
assertEquals(0, cursor.getInt(0))
}
}
companion object {
@JvmField
@ClassRule
val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance()
private lateinit var provider: ContentProviderClient
@BeforeClass
@JvmStatic
fun setUpClass() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
}
@AfterClass
@JvmStatic
fun tearDownClass() {
provider.closeCompat()
}
}
}

View File

@@ -1,285 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.Manifest
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.CachedGroupMembership
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.GroupMethod
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class LocalGroupTest {
@Inject @ApplicationContext
lateinit var context: Context
@get:Rule
val hiltRule = HiltAndroidRule(this)
val account = Account("Test Account", "Test Account Type")
private lateinit var addressBookGroupsAsCategories: LocalTestAddressBook
private lateinit var addressBookGroupsAsVCards: LocalTestAddressBook
@Before
fun setup() {
hiltRule.inject()
addressBookGroupsAsCategories = LocalTestAddressBook.create(context, account, provider, GroupMethod.CATEGORIES)
addressBookGroupsAsVCards = LocalTestAddressBook.create(context, account, provider, GroupMethod.GROUP_VCARDS)
// clear contacts
addressBookGroupsAsCategories.clear()
addressBookGroupsAsVCards.clear()
}
@After
fun tearDown() {
addressBookGroupsAsCategories.remove()
addressBookGroupsAsVCards.remove()
}
@Test
fun testApplyPendingMemberships_addPendingMembership() {
val ab = addressBookGroupsAsVCards
val contact1 = LocalContact(ab, Contact().apply {
uid = "test1"
displayName = "Test"
}, "test1.vcf", null, 0)
contact1.add()
val group = newGroup(ab)
// set pending membership of contact1
ab.provider!!.update(
ContentUris.withAppendedId(ab.groupsSyncUri(), group.id!!),
ContentValues().apply {
put(LocalGroup.COLUMN_PENDING_MEMBERS, LocalGroup.PendingMemberships(setOf("test1")).toString())
},
null, null
)
// pending membership -> contact1 should be added to group
LocalGroup.applyPendingMemberships(ab)
// check group membership
ab.provider!!.query(
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID),
"${GroupMembership.MIMETYPE}=?", arrayOf(GroupMembership.CONTENT_ITEM_TYPE),
null
)!!.use { cursor ->
assertTrue(cursor.moveToNext())
assertEquals(group.id, cursor.getLong(0))
assertEquals(contact1.id, cursor.getLong(1))
assertFalse(cursor.moveToNext())
}
// check cached group membership
ab.provider!!.query(
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID),
"${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE),
null
)!!.use { cursor ->
assertTrue(cursor.moveToNext())
assertEquals(group.id, cursor.getLong(0))
assertEquals(contact1.id, cursor.getLong(1))
assertFalse(cursor.moveToNext())
}
}
@Test
fun testApplyPendingMemberships_removeMembership() {
val ab = addressBookGroupsAsVCards
val contact1 = LocalContact(ab, Contact().apply {
uid = "test1"
displayName = "Test"
}, "test1.vcf", null, 0)
contact1.add()
val group = newGroup(ab)
// add contact1 to group
val batch = BatchOperation(ab.provider!!)
contact1.addToGroup(batch, group.id!!)
batch.commit()
// no pending memberships -> membership should be removed
LocalGroup.applyPendingMemberships(ab)
// check group membership
ab.provider!!.query(
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID),
"${GroupMembership.MIMETYPE}=?", arrayOf(GroupMembership.CONTENT_ITEM_TYPE),
null
)!!.use { cursor ->
assertFalse(cursor.moveToNext())
}
// check cached group membership
ab.provider!!.query(
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID),
"${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE),
null
)!!.use { cursor ->
assertFalse(cursor.moveToNext())
}
}
@Test
fun testClearDirty_addCachedGroupMembership() {
val ab = addressBookGroupsAsCategories
val group = newGroup(ab)
val contact1 = LocalContact(ab, Contact().apply { displayName = "Test" }, "fn.vcf", null, 0)
contact1.add()
// insert group membership, but no cached group membership
ab.provider!!.insert(
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), ContentValues().apply {
put(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
put(GroupMembership.RAW_CONTACT_ID, contact1.id)
put(GroupMembership.GROUP_ROW_ID, group.id)
}
)
group.clearDirty(null, null)
// check cached group membership
ab.provider!!.query(
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID),
"${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE),
null
)!!.use { cursor ->
assertTrue(cursor.moveToNext())
assertEquals(group.id, cursor.getLong(0))
assertEquals(contact1.id, cursor.getLong(1))
assertFalse(cursor.moveToNext())
}
}
@Test
fun testClearDirty_removeCachedGroupMembership() {
val ab = addressBookGroupsAsCategories
val group = newGroup(ab)
val contact1 = LocalContact(ab, Contact().apply { displayName = "Test" }, "fn.vcf", null, 0)
contact1.add()
// insert cached group membership, but no group membership
ab.provider!!.insert(
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), ContentValues().apply {
put(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
put(CachedGroupMembership.RAW_CONTACT_ID, contact1.id)
put(CachedGroupMembership.GROUP_ID, group.id)
}
)
group.clearDirty(null, null)
// cached group membership should be gone
ab.provider!!.query(
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID),
"${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE),
null
)!!.use { cursor ->
assertFalse(cursor.moveToNext())
}
}
@Test
fun testMarkMembersDirty() {
val ab = addressBookGroupsAsCategories
val group = newGroup(ab)
val contact1 = LocalContact(ab, Contact().apply { displayName = "Test" }, "fn.vcf", null, 0)
contact1.add()
val batch = BatchOperation(ab.provider!!)
contact1.addToGroup(batch, group.id!!)
batch.commit()
assertEquals(0, ab.findDirty().size)
group.markMembersDirty()
assertEquals(contact1.id, ab.findDirty().first().id)
}
@Test
fun testPrepareForUpload() {
val group = newGroup()
assertNull(group.getContact().uid)
val fileName = group.prepareForUpload()
val newUid = group.getContact().uid
assertNotNull(newUid)
assertEquals("$newUid.vcf", fileName)
}
// helpers
private fun newGroup(addressBook: LocalAddressBook = addressBookGroupsAsCategories): LocalGroup =
LocalGroup(addressBook,
Contact().apply {
displayName = "Test Group"
}, null, null, 0
).apply {
add()
}
companion object {
@JvmField
@ClassRule
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
private lateinit var provider: ContentProviderClient
@BeforeClass
@JvmStatic
fun connect() {
val context = InstrumentationRegistry.getInstrumentation().context
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
}
@AfterClass
@JvmStatic
fun disconnect() {
provider.close()
}
}
}

View File

@@ -1,147 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.Context
import android.provider.ContactsContract
import at.bitfire.davdroid.R
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.SyncFrameworkIntegration
import at.bitfire.vcard4android.GroupMethod
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.junit.Assert.assertTrue
import java.io.FileNotFoundException
import java.util.Optional
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Logger
class LocalTestAddressBook @AssistedInject constructor(
@Assisted account: Account,
@Assisted("addressBook") addressBookAccount: Account,
@Assisted provider: ContentProviderClient,
@Assisted override val groupMethod: GroupMethod,
accountSettingsFactory: AccountSettings.Factory,
collectionRepository: DavCollectionRepository,
@ApplicationContext private val context: Context,
logger: Logger,
serviceRepository: DavServiceRepository,
syncFramework: SyncFrameworkIntegration
): LocalAddressBook(
account = account,
_addressBookAccount = addressBookAccount,
provider = provider,
accountSettingsFactory = accountSettingsFactory,
collectionRepository = collectionRepository,
context = context,
dirtyVerifier = Optional.empty(),
logger = logger,
serviceRepository = serviceRepository,
syncFramework = syncFramework
) {
@AssistedFactory
interface Factory {
fun create(account: Account, @Assisted("addressBook") addressBookAccount: Account, provider: ContentProviderClient, groupMethod: GroupMethod): LocalTestAddressBook
}
override var readOnly: Boolean
get() = false
set(_) = throw NotImplementedError()
fun clear() {
for (contact in queryContacts(null, null))
contact.delete()
for (group in queryGroups(null, null))
group.delete()
}
/**
* Returns the dirty flag of the given contact.
*
* @return true if the contact is dirty, false otherwise
*
* @throws FileNotFoundException if the contact can't be found
*/
fun isContactDirty(id: Long): Boolean {
val uri = ContentUris.withAppendedId(rawContactsSyncUri(), id)
provider!!.query(uri, arrayOf(ContactsContract.RawContacts.DIRTY), null, null, null)?.use { cursor ->
if (cursor.moveToFirst())
return cursor.getInt(0) != 0
}
throw FileNotFoundException()
}
/**
* Returns the dirty flag of the given contact group.
*
* @return true if the group is dirty, false otherwise
*
* @throws FileNotFoundException if the group can't be found
*/
fun isGroupDirty(id: Long): Boolean {
val uri = ContentUris.withAppendedId(groupsSyncUri(), id)
provider!!.query(uri, arrayOf(ContactsContract.Groups.DIRTY), null, null, null)?.use { cursor ->
if (cursor.moveToFirst())
return cursor.getInt(0) != 0
}
throw FileNotFoundException()
}
fun remove() {
val accountManager = AccountManager.get(context)
assertTrue(accountManager.removeAccountExplicitly(addressBookAccount))
}
companion object {
@dagger.hilt.EntryPoint
@InstallIn(SingletonComponent::class)
interface EntryPoint {
fun localTestAddressBookFactory(): Factory
}
val counter = AtomicInteger()
/**
* Creates a [at.bitfire.davdroid.resource.LocalTestAddressBook].
*
* Make sure to delete it with [at.bitfire.davdroid.resource.LocalTestAddressBook.remove] or [removeAll] after use.
*/
fun create(context: Context, account: Account, provider: ContentProviderClient, groupMethod: GroupMethod = GroupMethod.GROUP_VCARDS): LocalTestAddressBook {
// create new address book account
val addressBookAccount = Account("Test Address Book ${counter.incrementAndGet()}", context.getString(R.string.account_type_address_book))
val accountManager = AccountManager.get(context)
assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, null))
// return address book with this account
val entryPoint = EntryPointAccessors.fromApplication<EntryPoint>(context)
val factory = entryPoint.localTestAddressBookFactory()
return factory.create(account, addressBookAccount, provider, groupMethod)
}
fun removeAll(context: Context) {
val accountManager = AccountManager.get(context)
for (account in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)))
accountManager.removeAccountExplicitly(account)
}
}
}

View File

@@ -1,754 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.servicedetection
import android.content.Context
import android.security.NetworkSecurityPolicy
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.HomeSet
import at.bitfire.davdroid.db.Principal
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
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.mockkObject
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.logging.Logger
import javax.inject.Inject
@HiltAndroidTest
class CollectionListRefresherTest {
@Inject @ApplicationContext
lateinit var context: Context
@Inject
lateinit var db: AppDatabase
@Inject
lateinit var logger: Logger
@Inject
lateinit var refresherFactory: CollectionListRefresher.Factory
@Inject
lateinit var settings: SettingsManager
@get:Rule
var hiltRule = HiltAndroidRule(this)
private val mockServer = MockWebServer()
private lateinit var client: HttpClient
@Before
fun setup() {
hiltRule.inject()
// Start mock web server
mockServer.dispatcher = TestDispatcher(logger)
mockServer.start()
client = HttpClient.Builder(context).build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
}
@After
fun teardown() {
mockServer.shutdown()
db.close()
}
@Test
fun testDiscoverHomesets() {
val service = createTestService(Service.TYPE_CARDDAV)!!
val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)
// Query home sets
refresherFactory.create(service, client.okHttpClient).discoverHomesets(baseUrl)
// Check home set has been saved correctly to database
val savedHomesets = db.homeSetDao().getByService(service.id)
assertEquals(2, savedHomesets.size)
// Home set from current-user-principal
val personalHomeset = savedHomesets[1]
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL/"), personalHomeset.url)
assertEquals(service.id, personalHomeset.serviceId)
// personal should be true for homesets detected at first query of current-user-principal (Even if they occur in a group principal as well!!!)
assertEquals(true, personalHomeset.personal)
// Home set found in a group principal
val groupHomeset = savedHomesets[0]
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_NON_PERSONAL/"), groupHomeset.url)
assertEquals(service.id, groupHomeset.serviceId)
// personal should be false for homesets not detected at the first query of current-user-principal (IE. in groups)
assertEquals(false, groupHomeset.personal)
}
// refreshHomesetsAndTheirCollections
@Test
fun refreshHomesetsAndTheirCollections_addsNewCollection() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// save homeset in DB
val homesetId = db.homeSetDao().insert(
HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL"))
)
// Refresh
refresherFactory.create(service, client.okHttpClient).refreshHomesetsAndTheirCollections()
// Check the collection defined in homeset is now in the database
assertEquals(
Collection(
1,
service.id,
homesetId,
1, // will have gotten an owner too
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
displayName = "My Contacts",
description = "My Contacts Description"
),
db.collectionDao().getByService(service.id).first()
)
}
@Test
fun refreshHomesetsAndTheirCollections_updatesExistingCollection() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// save "old" collection in DB
val collectionId = db.collectionDao().insertOrUpdateByUrl(
Collection(
0,
service.id,
null,
null,
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
displayName = "My Contacts",
description = "My Contacts Description"
)
)
// Refresh
refresherFactory.create(service, client.okHttpClient).refreshHomesetsAndTheirCollections()
// Check the collection got updated
assertEquals(
Collection(
collectionId,
service.id,
null,
null,
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
displayName = "My Contacts",
description = "My Contacts Description"
),
db.collectionDao().get(collectionId)
)
}
@Test
fun refreshHomesetsAndTheirCollections_preservesCollectionFlags() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// save "old" collection in DB - with set flags
val collectionId = db.collectionDao().insertOrUpdateByUrl(
Collection(
0,
service.id,
null,
null,
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
displayName = "My Contacts",
description = "My Contacts Description",
forceReadOnly = true,
sync = true
)
)
// Refresh
refresherFactory.create(service, client.okHttpClient).refreshHomesetsAndTheirCollections()
// Check the collection got updated
assertEquals(
Collection(
collectionId,
service.id,
null,
null,
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
displayName = "My Contacts",
description = "My Contacts Description",
forceReadOnly = true,
sync = true
),
db.collectionDao().get(collectionId)
)
}
@Test
fun refreshHomesetsAndTheirCollections_marksRemovedCollectionsAsHomeless() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// save homeset in DB - which is empty (zero address books) on the serverside
val homesetId = db.homeSetDao().insert(
HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_EMPTY"))
)
// place collection in DB - as part of the homeset
val collectionId = db.collectionDao().insertOrUpdateByUrl(
Collection(
0,
service.id,
homesetId,
null,
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
)
)
// Refresh - should mark collection as homeless, because serverside homeset is empty.
refresherFactory.create(service, client.okHttpClient).refreshHomesetsAndTheirCollections()
// Check the collection, is now marked as homeless
assertEquals(null, db.collectionDao().get(collectionId)!!.homeSetId)
}
@Test
fun refreshHomesetsAndTheirCollections_addsOwnerUrls() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// save a homeset in DB
val homesetId = db.homeSetDao().insert(
HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL"))
)
// place collection in DB - as part of the homeset
val collectionId = db.collectionDao().insertOrUpdateByUrl(
Collection(
0,
service.id,
homesetId, // part of above home set
null,
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
)
)
// Refresh - homesets and their collections
assertEquals(0, db.principalDao().getByService(service.id).size)
refresherFactory.create(service, client.okHttpClient).refreshHomesetsAndTheirCollections()
// Check principal saved and the collection was updated with its reference
val principals = db.principalDao().getByService(service.id)
assertEquals(1, principals.size)
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url)
assertEquals(null, principals[0].displayName)
assertEquals(
principals[0].id,
db.collectionDao().get(collectionId)!!.ownerId
)
}
// refreshHomelessCollections
@Test
fun refreshHomelessCollections_updatesExistingCollection() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// place homeless collection in DB
val collectionId = db.collectionDao().insertOrUpdateByUrl(
Collection(
0,
service.id,
null,
null,
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
)
)
// Refresh
refresherFactory.create(service, client.okHttpClient).refreshHomelessCollections()
// Check the collection got updated - with display name and description
assertEquals(
Collection(
collectionId,
service.id,
null,
1, // will have gotten an owner too
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
displayName = "My Contacts",
description = "My Contacts Description"
),
db.collectionDao().get(collectionId)
)
}
@Test
fun refreshHomelessCollections_deletesInaccessibleCollections() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// place homeless collection in DB - it is also inaccessible
val collectionId = db.collectionDao().insertOrUpdateByUrl(
Collection(
0,
service.id,
null,
null,
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_INACCESSIBLE")
)
)
// Refresh - should delete collection
refresherFactory.create(service, client.okHttpClient).refreshHomelessCollections()
// Check the collection got deleted
assertEquals(null, db.collectionDao().get(collectionId))
}
@Test
fun refreshHomelessCollections_addsOwnerUrls() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// place homeless collection in DB
val collectionId = db.collectionDao().insertOrUpdateByUrl(
Collection(
0,
service.id,
null,
null,
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
)
)
// Refresh homeless collections
assertEquals(0, db.principalDao().getByService(service.id).size)
refresherFactory.create(service, client.okHttpClient).refreshHomelessCollections()
// Check principal saved and the collection was updated with its reference
val principals = db.principalDao().getByService(service.id)
assertEquals(1, principals.size)
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url)
assertEquals(null, principals[0].displayName)
assertEquals(
principals[0].id,
db.collectionDao().get(collectionId)!!.ownerId
)
}
// refreshPrincipals
@Test
fun refreshPrincipals_inaccessiblePrincipal() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// place principal without display name in db
val principalId = db.principalDao().insert(
Principal(
0,
service.id,
mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_INACCESSIBLE"), // no trailing slash
null // no display name for now
)
)
// add an associated collection - as the principal is rightfully removed otherwise
db.collectionDao().insertOrUpdateByUrl(
Collection(
0,
service.id,
null,
principalId, // create association with principal
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), // with trailing slash
)
)
// Refresh principals
refresherFactory.create(service, client.okHttpClient).refreshPrincipals()
// Check principal was not updated
val principals = db.principalDao().getByService(service.id)
assertEquals(1, principals.size)
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_INACCESSIBLE"), principals[0].url)
assertEquals(null, principals[0].displayName)
}
@Test
fun refreshPrincipals_updatesPrincipal() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// place principal without display name in db
val principalId = db.principalDao().insert(
Principal(
0,
service.id,
mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), // no trailing slash
null // no display name for now
)
)
// add an associated collection - as the principal is rightfully removed otherwise
db.collectionDao().insertOrUpdateByUrl(
Collection(
0,
service.id,
null,
principalId, // create association with principal
Collection.TYPE_ADDRESSBOOK,
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), // with trailing slash
)
)
// Refresh principals
refresherFactory.create(service, client.okHttpClient).refreshPrincipals()
// Check principal now got a display name
val principals = db.principalDao().getByService(service.id)
assertEquals(1, principals.size)
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url)
assertEquals("Mr. Wobbles", principals[0].displayName)
}
@Test
fun refreshPrincipals_deletesPrincipalsWithoutCollections() {
val service = createTestService(Service.TYPE_CARDDAV)!!
// place principal without collections in DB
db.principalDao().insert(
Principal(
0,
service.id,
mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS/")
)
)
// Refresh principals - detecting it does not own collections
refresherFactory.create(service, client.okHttpClient).refreshPrincipals()
// Check principal was deleted
val principals = db.principalDao().getByService(service.id)
assertEquals(0, principals.size)
}
// Others
@Test
fun shouldPreselect_none() {
val service = createTestService(Service.TYPE_CARDDAV)!!
mockkObject(settings) {
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_NONE
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns ""
val collection = Collection(
0,
service.id,
0,
type = Collection.TYPE_ADDRESSBOOK,
url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
)
val homesets = listOf(
HomeSet(0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL"))
)
val refresher = refresherFactory.create(service, client.okHttpClient)
assertFalse(refresher.shouldPreselect(collection, homesets))
}
}
@Test
fun shouldPreselect_all() {
val service = createTestService(Service.TYPE_CARDDAV)!!
mockkObject(settings) {
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns ""
val collection = Collection(
0,
service.id,
0,
type = Collection.TYPE_ADDRESSBOOK,
url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
)
val homesets = listOf(
HomeSet(0, service.id, false, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL"))
)
val refresher = refresherFactory.create(service, client.okHttpClient)
assertTrue(refresher.shouldPreselect(collection, homesets))
}
}
@Test
fun shouldPreselect_all_blacklisted() {
val service = createTestService(Service.TYPE_CARDDAV)!!
val url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
mockkObject(settings) {
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns url.toString()
val collection = Collection(
0,
service.id,
0,
type = Collection.TYPE_ADDRESSBOOK,
url = url
)
val homesets = listOf(
HomeSet(0, service.id, false, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL"))
)
val refresher = refresherFactory.create(service, client.okHttpClient)
assertFalse(refresher.shouldPreselect(collection, homesets))
}
}
@Test
fun shouldPreselect_personal_notPersonal() {
val service = createTestService(Service.TYPE_CARDDAV)!!
mockkObject(settings) {
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns ""
val collection = Collection(
0,
service.id,
0,
type = Collection.TYPE_ADDRESSBOOK,
url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
)
val homesets = listOf(
HomeSet(0, service.id, false, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL"))
)
val refresher = refresherFactory.create(service, client.okHttpClient)
assertFalse(refresher.shouldPreselect(collection, homesets))
}
}
@Test
fun shouldPreselect_personal_isPersonal() {
val service = createTestService(Service.TYPE_CARDDAV)!!
mockkObject(settings) {
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns ""
val collection = Collection(
0,
service.id,
0,
type = Collection.TYPE_ADDRESSBOOK,
url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
)
val homesets = listOf(
HomeSet(0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL"))
)
val refresher = refresherFactory.create(service, client.okHttpClient)
assertTrue(refresher.shouldPreselect(collection, homesets))
}
}
@Test
fun shouldPreselect_personal_isPersonalButBlacklisted() {
val service = createTestService(Service.TYPE_CARDDAV)!!
val collectionUrl = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
mockkObject(settings) {
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns collectionUrl.toString()
val collection = Collection(
0,
service.id,
0,
type = Collection.TYPE_ADDRESSBOOK,
url = collectionUrl
)
val homesets = listOf(
HomeSet(0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL"))
)
val refresher = refresherFactory.create(service, client.okHttpClient)
assertFalse(refresher.shouldPreselect(collection, homesets))
}
}
// Test helpers and dependencies
private fun createTestService(serviceType: String) : Service? {
val service = Service(id=0, accountName="test", type=serviceType, principal = null)
val serviceId = db.serviceDao().insertOrReplace(service)
return db.serviceDao().get(serviceId)
}
companion object {
private const val PATH_CALDAV = "/caldav"
private const val PATH_CARDDAV = "/carddav"
private const val SUBPATH_PRINCIPAL = "/principal"
private const val SUBPATH_PRINCIPAL_INACCESSIBLE = "/inaccessible-principal"
private const val SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS = "/principal2"
private const val SUBPATH_GROUPPRINCIPAL_0 = "/groups/0"
private const val SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL = "/addressbooks-homeset"
private const val SUBPATH_ADDRESSBOOK_HOMESET_NON_PERSONAL = "/addressbooks-homeset-non-personal"
private const val SUBPATH_ADDRESSBOOK_HOMESET_EMPTY = "/addressbooks-homeset-empty"
private const val SUBPATH_ADDRESSBOOK = "/addressbooks/my-contacts"
private const val SUBPATH_ADDRESSBOOK_INACCESSIBLE = "/addressbooks/inaccessible-contacts"
}
class TestDispatcher(
private val logger: Logger
): Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
val path = request.path!!.trimEnd('/')
if (request.method.equals("PROPFIND", true)) {
val properties = when (path) {
PATH_CALDAV,
PATH_CARDDAV ->
"<current-user-principal>" +
" <href>$path${SUBPATH_PRINCIPAL}</href>" +
"</current-user-principal>"
PATH_CARDDAV + SUBPATH_PRINCIPAL ->
"<resourcetype><principal/></resourcetype>" +
"<displayname>Mr. Wobbles</displayname>" +
"<CARD:addressbook-home-set>" +
" <href>${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL}</href>" +
"</CARD:addressbook-home-set>" +
"<group-membership>" +
" <href>${PATH_CARDDAV}${SUBPATH_GROUPPRINCIPAL_0}</href>" +
"</group-membership>"
PATH_CARDDAV + SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS ->
"<CARD:addressbook-home-set>" +
" <href>${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_EMPTY}</href>" +
"</CARD:addressbook-home-set>" +
"<displayname>Mr. Wobbles Jr.</displayname>"
PATH_CARDDAV + SUBPATH_GROUPPRINCIPAL_0 ->
"<resourcetype><principal/></resourcetype>" +
"<displayname>All address books</displayname>" +
"<CARD:addressbook-home-set>" +
" <href>${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL}</href>" +
" <href>${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_NON_PERSONAL}</href>" +
"</CARD:addressbook-home-set>"
PATH_CARDDAV + SUBPATH_ADDRESSBOOK,
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL ->
"<resourcetype>" +
" <collection/>" +
" <CARD:addressbook/>" +
"</resourcetype>" +
"<displayname>My Contacts</displayname>" +
"<CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>" +
"<owner>" +
" <href>${PATH_CARDDAV + SUBPATH_PRINCIPAL}</href>" +
"</owner>"
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET_NON_PERSONAL ->
"<resourcetype>" +
" <collection/>" +
" <CARD:addressbook/>" +
"</resourcetype>" +
"<displayname>Freds Contacts (not mine)</displayname>" +
"<CARD:addressbook-description>Not personal contacts</CARD:addressbook-description>" +
"<owner>" +
" <href>${PATH_CARDDAV + SUBPATH_PRINCIPAL}</href>" + // OK, user is allowed to own non-personal contacts
"</owner>"
PATH_CALDAV + SUBPATH_PRINCIPAL ->
"<CAL:calendar-user-address-set>" +
" <href>urn:unknown-entry</href>" +
" <href>mailto:email1@example.com</href>" +
" <href>mailto:email2@example.com</href>" +
"</CAL:calendar-user-address-set>"
SUBPATH_ADDRESSBOOK_HOMESET_EMPTY -> ""
else -> ""
}
var responseBody = ""
var responseCode = 207
when (path) {
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL ->
responseBody =
"<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav' xmlns:CAL='urn:ietf:params:xml:ns:caldav'>" +
"<response>" +
" <href>${PATH_CARDDAV + SUBPATH_ADDRESSBOOK}</href>" +
" <propstat><prop>" +
properties +
" </prop></propstat>" +
" <status>HTTP/1.1 200 OK</status>" +
"</response>" +
"</multistatus>"
PATH_CARDDAV + SUBPATH_PRINCIPAL_INACCESSIBLE,
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_INACCESSIBLE ->
responseCode = 404
else ->
responseBody =
"<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav' xmlns:CAL='urn:ietf:params:xml:ns:caldav'>" +
"<response>" +
" <href>$path</href>" +
" <propstat><prop>"+
properties +
" </prop></propstat>" +
"</response>" +
"</multistatus>"
}
logger.info("Queried: $path")
logger.info("Response: $responseBody")
return MockResponse()
.setResponseCode(responseCode)
.setBody(responseBody)
}
return MockResponse().setResponseCode(404)
}
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,332 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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"
android:installLocation="internalOnly">
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
<!-- normal permissions -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- other permissions -->
<!-- android.permission-group.CONTACTS -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<!-- android.permission-group.CALENDAR -->
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<!-- android.permission-group.LOCATION -->
<!-- getting the WiFi name (for "sync in Wifi only") requires
- coarse location (Android 8.1)
- fine location (Android 10) -->
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- required since Android 10 to get the WiFi name while in background (= while syncing) -->
<uses-permission-sdk-23 android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<!-- ical4android declares task access permissions -->
<!-- Disable GPS capability requirement, which is implicitly derived from ACCESS_FINE_LOCATION
permission and makes app unusable on some devices without GPS. We need location permissions only
to get the current WiFi SSID, and we don't need GPS for that. -->
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<application
android:name=".App"
android:allowBackup="false"
android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
android:resizeableActivity="true"
tools:ignore="UnusedAttribute"
android:supportsRtl="true">
<!-- required for Hilt/WorkManager integration -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
<application android:name=".App">
<!-- Remove the node added by AppAuth (remove only from net.openid.appauth library, not from our flavor manifest files) -->
<activity android:name="net.openid.appauth.RedirectUriReceiverActivity"
tools:node="remove" tools:selector="net.openid.appauth"/>
<activity android:name=".ui.intro.IntroActivity" />
<activity
android:name=".ui.AccountsActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ui.AboutActivity"
android:label="@string/navigation_drawer_about"
android:parentActivityName=".ui.AccountsActivity"/>
<activity
android:name=".ui.AppSettingsActivity"
android:label="@string/app_settings"
android:parentActivityName=".ui.AccountsActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.DebugInfoActivity"
android:parentActivityName=".ui.AppSettingsActivity"
android:exported="true"
android:label="@string/debug_info_title">
<intent-filter>
<action android:name="android.intent.action.BUG_REPORT"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.PermissionsActivity"
android:label="@string/app_settings_security_app_permissions"
android:parentActivityName=".ui.AppSettingsActivity" />
<activity
android:name=".ui.TasksActivity"
android:label="@string/intro_tasks_title"
android:parentActivityName=".ui.AppSettingsActivity" />
<activity
android:name=".ui.setup.LoginActivity"
android:parentActivityName=".ui.AccountsActivity"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="caldav"/>
<data android:scheme="caldavs"/>
<data android:scheme="carddav"/>
<data android:scheme="carddavs"/>
<data android:scheme="davx5"/>
</intent-filter>
<intent-filter>
<action android:name="loginFlow" /> <!-- Ensures this filter matches, even if the sending app is not defining an action -->
<category android:name="android.intent.category.DEFAULT" />
<data
tools:ignore="AppLinkUrlError"
android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
</activity>
<activity
android:name=".ui.account.AccountActivity"
android:parentActivityName=".ui.AccountsActivity"
android:exported="true">
</activity>
<activity
android:name=".ui.account.CollectionActivity"
android:parentActivityName=".ui.account.AccountActivity" />
<activity
android:name=".ui.account.CreateAddressBookActivity"
android:parentActivityName=".ui.account.AccountActivity" />
<activity
android:name=".ui.account.CreateCalendarActivity"
android:parentActivityName=".ui.account.AccountActivity" />
<activity
android:name=".ui.account.AccountSettingsActivity"
android:parentActivityName=".ui.account.AccountActivity" />
<activity
android:name=".ui.account.WifiPermissionsActivity"
android:parentActivityName=".ui.account.AccountSettingsActivity" />
<activity
android:name=".ui.webdav.WebdavMountsActivity"
android:exported="true"
android:parentActivityName=".ui.AccountsActivity" />
<activity
android:name=".ui.webdav.AddWebdavMountActivity"
android:parentActivityName=".ui.webdav.WebdavMountsActivity"
android:windowSoftInputMode="adjustResize" />
<!-- account type "DAVx⁵" -->
<service
android:name=".sync.account.AccountAuthenticatorService"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/account_authenticator"/>
</service>
<service
android:name=".sync.CalendarsSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_calendars"/>
</service>
<service
android:name=".sync.JtxSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_notes"/>
</service>
<service
android:name=".sync.OpenTasksSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_opentasks"/>
</service>
<service
android:name=".sync.TasksOrgSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_tasks_org"/>
</service>
<provider
android:authorities="@string/webdav_authority"
android:name=".webdav.DavDocumentsProvider"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<!-- account type "DAVx⁵ Address book" -->
<service
android:name=".sync.account.AddressBookAuthenticatorService"
android:exported="true"
tools:ignore="ExportedService"> <!-- Since Android 11, this must be true so that Google Contacts shows the address book accounts -->
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/account_authenticator_address_book"/>
</service>
<service
android:name=".sync.ContactsSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_contacts"/>
<meta-data
android:name="android.provider.CONTACTS_STRUCTURE"
android:resource="@xml/contacts"/>
</service>
<!-- provider to share debug info/logs -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/authority_debug_provider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/debug_paths" />
</provider>
<!-- UnifiedPush receiver -->
<receiver android:exported="true" android:enabled="true" android:name=".push.UnifiedPushReceiver" tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
</intent-filter>
</receiver>
<!-- Widgets -->
<receiver android:name=".ui.widget.SyncButtonWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info_sync_button" />
</receiver>
tools:node="remove" tools:selector="net.openid.appauth"/>
</application>
<!-- package visiblity which apps do we need to see? -->
<queries>
<!-- system providers (listing them is technically not required, but some apps like the
Huawei calendar take this as indication of whether these providers are accessed) -->
<provider android:authorities="com.android.calendar"/>
<provider android:authorities="com.android.contacts"/>
<!-- task providers -->
<package android:name="at.techbee.jtx" />
<package android:name="org.dmfs.tasks" />
<package android:name="org.tasks" />
<!-- ICSx5 for Webcal feeds -->
<package android:name="at.bitfire.icsdroid"/>
<!-- apps that interact with contact, calendar, task data (for debug info) -->
<intent>
<action android:name="*" />
<data android:scheme="content" android:host="com.android.contacts" />
</intent>
<intent>
<action android:name="*" />
<data android:scheme="content" android:host="com.android.calendar" />
</intent>
<!-- Open URLs in a browser or other app [https://developer.android.com/training/package-visibility/use-cases#open-urls-browser-or-other-app] -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<!-- Custom Tabs support (e.g. Nextcloud Login Flow) -->
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>

View File

@@ -1 +0,0 @@
{"ar_SA":["abdunnasir"],"bg":["dpa_transifex"],"ca":["Kintu","jordibrus","zagur"],"cs":["pavelb","svetlemodry","tomas.odehnal"],"da":["Tntdruid_","knutztar","mjjzf","twikedk"],"de":["Atalanttore","TheName","Wyrrrd","YvanM","amandablue","anestiskaci","corppneq","crit12","hammaschlach","maxkl","nicolas_git","owncube"],"el":["KristinaQejvanaj","anestiskaci","diamond_gr"],"es":["Ark74","Elhea","GranPC","aluaces","jcvielma","plaguna","polkhas","xphnx"],"eu":["Osoitz","Thadah","cockeredradiation"],"fa":["Numb","ahangarha","amiraliakbari","joojoojoo","maryambehzi","mtashackori","taranehsaei"],"fr":["AlainR","Amadeen","Floflr","JorisBodin","Llorc","LoiX07","Novick","Poussinou","Thecross","YvanM","alkino2","boutil","callmemagnus","chrcha","grenatrad","jokx","mathieugfortin","paullbn","vincen","ÉricB."],"fr_FR":["Llorc","Poussinou","chrcha"],"gl":["aluaces","pikamoku"],"hu":["Roshek","infeeeee","jtg"],"it":["Damtux","FranzMari","ed0","malaerba","noccio","nwandy","rickyroo","technezio"],"it_IT":["malaerba"],"ja":["Naofumi","yanorei32"],"nb_NO":["elonus"],"nl":["XtremeNova","davtemp","dehart","erikhubers","frankyboy1963"],"pl":["TORminator","TheName","Valdnet","gsz","mg6","oskarjakiela"],"pt":["amalvarenga","wanderlei.huttel"],"pt_BR":["wanderlei.huttel"],"ru":["aigoshin","anm","ashed","astalavister","nick.savin","vaddd"],"sk_SK":["brango67","tiborepcek"],"sl_SI":["MrLaaky","uroszor"],"sr":["daimonion"],"sv":["Mikaelb","campbelldavid"],"szl":["chlodny"],"tr_TR":["ooguz","pultars"],"uk":["androsua","olexn","twixi007"],"uk_UA":["astalavister"],"zh_CN":["anolir","jxj2zzz79pfp9bpo","linuxbckp","mofitt2016","oksjd","phy","spice2wolf"],"zh_TW":["linuxbckp","mofitt2016","phy","waiabsfabuloushk"]}

View File

@@ -1,53 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid
import android.net.Uri
import androidx.core.net.toUri
/**
* Brand-specific constants like (non-theme) colors, homepage URLs etc.
*/
object Constants {
const val DAVDROID_GREEN_RGBA = 0xFF8bc34a.toInt()
val HOMEPAGE_URL = "https://www.davx5.com".toUri()
const val HOMEPAGE_PATH_FAQ = "faq"
const val HOMEPAGE_PATH_FAQ_SYNC_NOT_RUN = "synchronization-is-not-run-as-expected"
const val HOMEPAGE_PATH_FAQ_LOCATION_PERMISSION = "wifi-ssid-restriction-location-permission"
const val HOMEPAGE_PATH_OPEN_SOURCE = "donate"
const val HOMEPAGE_PATH_PRIVACY = "privacy"
const val HOMEPAGE_PATH_TESTED_SERVICES = "tested-with"
val MANUAL_URL = "https://manual.davx5.com".toUri()
const val MANUAL_PATH_ACCOUNTS_COLLECTIONS = "accounts_collections.html"
const val MANUAL_FRAGMENT_SERVICE_DISCOVERY = "how-does-service-discovery-work"
const val MANUAL_PATH_SETTINGS = "settings.html"
const val MANUAL_FRAGMENT_APP_SETTINGS = "app-wide-settings"
const val MANUAL_FRAGMENT_ACCOUNT_SETTINGS = "account-settings"
const val MANUAL_PATH_WEBDAV_MOUNTS = "webdav_mounts.html"
val COMMUNITY_URL = "https://github.com/bitfireAT/davx5-ose/discussions".toUri()
val FEDIVERSE_HANDLE = "@davx5app@fosstodon.org"
val FEDIVERSE_URL = "https://fosstodon.org/@davx5app".toUri()
/**
* Appends query parameters for anonymized usage statistics (app ID, version).
* Can be used by the called Website to get an idea of which versions etc. are currently used.
*
* @param context optional info about from where the URL was opened (like a specific Activity)
*/
fun Uri.Builder.withStatParams(context: String? = null): Uri.Builder {
appendQueryParameter("pk_campaign", BuildConfig.APPLICATION_ID)
appendQueryParameter("app-version", BuildConfig.VERSION_NAME)
if (context != null)
appendQueryParameter("pk_kwd", context)
return this
}
}

View File

@@ -1,52 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.log
import android.os.Build
import android.util.Log
import at.bitfire.davdroid.BuildConfig
import com.google.common.base.Ascii
import java.util.logging.Handler
import java.util.logging.Level
import java.util.logging.LogRecord
/**
* Logging handler that logs to Android logcat.
*/
internal class LogcatHandler: Handler() {
init {
formatter = PlainTextFormatter.LOGCAT
}
override fun publish(r: LogRecord) {
val level = r.level.intValue()
val text = formatter.format(r)
// get class name that calls the logger (or fall back to package name)
val className = if (r.sourceClassName != null)
PlainTextFormatter.shortClassName(r.sourceClassName)
else
BuildConfig.APPLICATION_ID
// truncate class name to 23 characters on Android <8, see Log documentation
val tag = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
Ascii.truncate(className, 23, "")
else
className
when {
level >= Level.SEVERE.intValue() -> Log.e(tag, text, r.thrown)
level >= Level.WARNING.intValue() -> Log.w(tag, text, r.thrown)
level >= Level.CONFIG.intValue() -> Log.i(tag, text, r.thrown)
level >= Level.FINER.intValue() -> Log.d(tag, text, r.thrown)
else -> Log.v(tag, text, r.thrown)
}
}
override fun flush() {}
override fun close() {}
}

View File

@@ -1,111 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.log
import com.google.common.base.Ascii
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.logging.Formatter
import java.util.logging.LogRecord
class PlainTextFormatter(
private val withTime: Boolean,
private val withSource: Boolean,
private val padSource: Int = 30,
private val withException: Boolean,
private val lineSeparator: String?
): Formatter() {
companion object {
/**
* Formatter intended for logcat output.
*/
val LOGCAT = PlainTextFormatter(
withTime = false,
withSource = false,
withException = false,
lineSeparator = null
)
/**
* Formatter intended for file output.
*/
val DEFAULT = PlainTextFormatter(
withTime = true,
withSource = true,
withException = true,
lineSeparator = System.lineSeparator()
)
/**
* Maximum length of a log line (estimate).
*/
const val MAX_LENGTH = 10000
fun shortClassName(className: String) = className
.replace(Regex("^at\\.bitfire\\.(dav|cert4an|dav4an|ical4an|vcard4an)droid\\."), ".")
.replace(Regex("\\$.*$"), "")
private fun stackTrace(ex: Throwable): String {
val writer = StringWriter()
ex.printStackTrace(PrintWriter(writer))
return writer.toString()
}
}
private val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT)
override fun format(r: LogRecord): String {
val builder = StringBuilder()
if (withTime)
builder .append(timeFormat.format(Date(r.millis)))
.append(" ").append(r.threadID).append(" ")
if (withSource && r.sourceClassName != null) {
val className = shortClassName(r.sourceClassName)
if (className != r.loggerName) {
val classNameColumn = "[$className] ".padEnd(padSource)
builder.append(classNameColumn)
}
}
builder.append(truncate(r.message))
if (withException && r.thrown != null) {
val indentedStackTrace = stackTrace(r.thrown)
.replace("\n", "\n\t")
.removeSuffix("\t")
builder.append("\n\tEXCEPTION ").append(indentedStackTrace)
}
r.parameters?.let {
for ((idx, param) in it.withIndex()) {
builder.append("\n\tPARAMETER #").append(idx + 1).append(" = ")
val valStr = if (param == null)
"(null)"
else
truncate(param.toString())
builder.append(valStr)
}
}
if (lineSeparator != null)
builder.append(lineSeparator)
return builder.toString()
}
private fun truncate(s: String) =
Ascii.truncate(s, MAX_LENGTH, "[…]")
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.network
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationService
import okhttp3.Interceptor
import okhttp3.Response
import java.util.logging.Level
import java.util.logging.Logger
/**
* Sends an OAuth Bearer token authorization as described in RFC 6750.
*/
class BearerAuthInterceptor(
private val accessToken: String
): Interceptor {
companion object {
val logger: Logger
get() = Logger.getGlobal()
fun fromAuthState(authService: AuthorizationService, authState: AuthState, callback: AuthStateUpdateCallback? = null): BearerAuthInterceptor? {
return runBlocking {
val accessTokenFuture = CompletableDeferred<String>()
authState.performActionWithFreshTokens(authService) { accessToken: String?, _: String?, ex: AuthorizationException? ->
if (accessToken != null) {
// persist updated AuthState
callback?.onUpdate(authState)
// emit access token
accessTokenFuture.complete(accessToken)
}
else {
logger.log(Level.WARNING, "Couldn't obtain access token", ex)
accessTokenFuture.cancel()
}
}
// return value
try {
BearerAuthInterceptor(accessTokenFuture.await())
} catch (ignored: CancellationException) {
null
}
}
}
}
override fun intercept(chain: Interceptor.Chain): Response {
logger.finer("Authenticating request with access token")
val rq = chain.request().newBuilder()
.header("Authorization", "Bearer $accessToken")
.build()
return chain.proceed(rq)
}
fun interface AuthStateUpdateCallback {
fun onUpdate(authState: AuthState)
}
}

View File

@@ -1,91 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.network
import android.net.Uri
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.db.Credentials
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService
import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.ResponseTypeValues
import net.openid.appauth.TokenResponse
import java.net.URI
import java.util.logging.Logger
class GoogleLogin(
val authService: AuthorizationService
) {
private val logger: Logger = Logger.getGlobal()
companion object {
// davx5integration@gmail.com (for davx5-ose)
private const val CLIENT_ID = "1069050168830-eg09u4tk1cmboobevhm4k3bj1m4fav9i.apps.googleusercontent.com"
private val SCOPES = arrayOf(
"https://www.googleapis.com/auth/calendar", // CalDAV
"https://www.googleapis.com/auth/carddav" // CardDAV
)
/**
* Gets the Google CalDAV/CardDAV base URI. See https://developers.google.com/calendar/caldav/v2/guide;
* _calid_ of the primary calendar is the account name.
*
* This URL allows CardDAV (over well-known URLs) and CalDAV detection including calendar-homesets and secondary
* calendars.
*/
fun googleBaseUri(googleAccount: String): URI =
URI("https", "apidata.googleusercontent.com", "/caldav/v2/$googleAccount/user", null)
private val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse("https://accounts.google.com/o/oauth2/v2/auth"),
Uri.parse("https://oauth2.googleapis.com/token")
)
}
fun signIn(email: String, customClientId: String?, locale: String?): AuthorizationRequest {
val builder = AuthorizationRequest.Builder(
GoogleLogin.serviceConfig,
customClientId ?: CLIENT_ID,
ResponseTypeValues.CODE,
Uri.parse(BuildConfig.APPLICATION_ID + ":/oauth2/redirect")
)
return builder
.setScopes(*SCOPES)
.setLoginHint(email)
.setUiLocales(locale)
.build()
}
suspend fun authenticate(authResponse: AuthorizationResponse): Credentials {
val authState = AuthState(authResponse, null) // authorization code must not be stored; exchange it to refresh token
val credentials = CompletableDeferred<Credentials>()
withContext(Dispatchers.IO) {
authService.performTokenRequest(authResponse.createTokenExchangeRequest()) { tokenResponse: TokenResponse?, refreshTokenException: AuthorizationException? ->
logger.info("Refresh token response: ${tokenResponse?.jsonSerializeString()}")
if (tokenResponse != null) {
// success, save authState (= refresh token)
authState.update(tokenResponse, refreshTokenException)
credentials.complete(Credentials(authState = authState))
}
}
}
return credentials.await()
}
}

View File

@@ -1,338 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.network
import android.content.Context
import android.os.Build
import android.security.KeyChain
import at.bitfire.cert4android.CustomCertManager
import at.bitfire.dav4jvm.BasicDigestAuthHandler
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.Socket
import java.security.KeyStore
import java.security.Principal
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.logging.Level
import java.util.logging.Logger
import javax.net.ssl.KeyManager
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509TrustManager
import kotlinx.coroutines.flow.MutableStateFlow
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationService
import okhttp3.Cache
import okhttp3.ConnectionSpec
import okhttp3.CookieJar
import okhttp3.Interceptor
import okhttp3.OkHttp
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.brotli.BrotliInterceptor
import okhttp3.internal.tls.OkHostnameVerifier
import okhttp3.logging.HttpLoggingInterceptor
class HttpClient @AssistedInject constructor(
@Assisted val okHttpClient: OkHttpClient,
@Assisted private var authService: AuthorizationService? = null,
val settingsManager: SettingsManager
): AutoCloseable {
companion object {
/** max. size of disk cache (10 MB) */
const val DISK_CACHE_MAX_SIZE: Long = 10*1024*1024
/** Base Builder to build all clients from. Use rarely; [OkHttpClient]s should
* be reused as much as possible. */
fun baseBuilder() =
OkHttpClient.Builder()
// Set timeouts. According to [AbstractThreadedSyncAdapter], when there is no network
// traffic within a minute, a sync will be cancelled.
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.pingInterval(
45,
TimeUnit.SECONDS
) // avoid cancellation because of missing traffic; only works for HTTP/2
// keep TLS 1.0 and 1.1 for now; remove when major browsers have dropped it (probably 2020)
.connectionSpecs(
listOf(
ConnectionSpec.CLEARTEXT,
ConnectionSpec.COMPATIBLE_TLS
)
)
// don't allow redirects by default, because it would break PROPFIND handling
.followRedirects(false)
// add User-Agent to every request
.addInterceptor(UserAgentInterceptor)
}
@AssistedFactory
interface Factory {
fun create(okHttpClient: OkHttpClient, authService: AuthorizationService?): HttpClient
}
override fun close() {
authService?.dispose()
okHttpClient.cache?.close()
}
class Builder(
val context: Context,
accountSettings: AccountSettings? = null,
val logger: Logger = Logger.getGlobal(),
val loggerLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY
) {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface HttpClientBuilderEntryPoint {
fun authorizationService(): AuthorizationService
fun httpClientFactory(): Factory
fun settingsManager(): SettingsManager
}
private val entryPoint = EntryPointAccessors.fromApplication<HttpClientBuilderEntryPoint>(context)
fun interface CertManagerProducer {
fun certManager(): CustomCertManager
}
private var appInForeground: MutableStateFlow<Boolean>? =
MutableStateFlow(false)
private var authService: AuthorizationService? = null
private var certManagerProducer: CertManagerProducer? = null
private var certificateAlias: String? = null
private var offerCompression: Boolean = false
// default cookie store for non-persistent cookies (some services like Horde use cookies for session tracking)
private var cookieStore: CookieJar? = MemoryCookieStore()
private val orig = baseBuilder()
init {
// add network logging, if requested
if (logger.isLoggable(Level.FINEST)) {
val loggingInterceptor = HttpLoggingInterceptor { message -> logger.finest(message) }
loggingInterceptor.level = loggerLevel
orig.addNetworkInterceptor(loggingInterceptor)
}
val settings = entryPoint.settingsManager()
// custom proxy support
try {
val proxyTypeValue = settings.getInt(Settings.PROXY_TYPE)
if (proxyTypeValue != Settings.PROXY_TYPE_SYSTEM) {
// we set our own proxy
val address by lazy { // lazy because not required for PROXY_TYPE_NONE
InetSocketAddress(
settings.getString(Settings.PROXY_HOST),
settings.getInt(Settings.PROXY_PORT)
)
}
val proxy =
when (proxyTypeValue) {
Settings.PROXY_TYPE_NONE -> Proxy.NO_PROXY
Settings.PROXY_TYPE_HTTP -> Proxy(Proxy.Type.HTTP, address)
Settings.PROXY_TYPE_SOCKS -> Proxy(Proxy.Type.SOCKS, address)
else -> throw IllegalArgumentException("Invalid proxy type")
}
orig.proxy(proxy)
logger.log(Level.INFO, "Using proxy setting", proxy)
}
} catch (e: Exception) {
logger.log(Level.SEVERE, "Can't set proxy, ignoring", e)
}
customCertManager {
// by default, use a CustomCertManager that respects the "distrust system certificates" setting
val trustSystemCerts = !settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES)
CustomCertManager(context, trustSystemCerts, appInForeground)
}
// use account settings for authentication and cookies
if (accountSettings != null)
addAuthentication(null, accountSettings.credentials(), authStateCallback = { authState: AuthState ->
accountSettings.credentials(Credentials(authState = authState))
})
}
constructor(context: Context, host: String?, credentials: Credentials?) : this(context) {
if (credentials != null)
addAuthentication(host, credentials)
}
fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder {
if (credentials.username != null && credentials.password != null) {
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.username, credentials.password, insecurePreemptive)
orig.addNetworkInterceptor(authHandler)
.authenticator(authHandler)
}
if (credentials.certificateAlias != null)
certificateAlias = credentials.certificateAlias
credentials.authState?.let { authState ->
val newAuthService = entryPoint.authorizationService()
authService = newAuthService
BearerAuthInterceptor.fromAuthState(newAuthService, authState, authStateCallback)?.let { bearerAuthInterceptor ->
orig.addNetworkInterceptor(bearerAuthInterceptor)
}
}
return this
}
fun allowCompression(allow: Boolean): Builder {
offerCompression = allow
return this
}
fun cookieStore(store: CookieJar?): Builder {
cookieStore = store
return this
}
fun followRedirects(follow: Boolean): Builder {
orig.followRedirects(follow)
return this
}
fun customCertManager(producer: CertManagerProducer) {
certManagerProducer = producer
}
fun setForeground(foreground: Boolean): Builder {
appInForeground?.value = foreground
return this
}
fun withDiskCache(): Builder {
for (dir in arrayOf(context.externalCacheDir, context.cacheDir).filterNotNull()) {
if (dir.exists() && dir.canWrite()) {
val cacheDir = File(dir, "HttpClient")
cacheDir.mkdir()
logger.fine("Using disk cache: $cacheDir")
orig.cache(Cache(cacheDir, DISK_CACHE_MAX_SIZE))
break
}
}
return this
}
fun build(): HttpClient {
cookieStore?.let {
orig.cookieJar(it)
}
if (offerCompression)
// offer Brotli and gzip compression
orig.addInterceptor(BrotliInterceptor)
var keyManager: KeyManager? = null
certificateAlias?.let { alias ->
// get provider certificate and private key
val certs = KeyChain.getCertificateChain(context, alias) ?: return@let
val key = KeyChain.getPrivateKey(context, alias) ?: return@let
logger?.fine("Using provider certificate $alias for authentication (chain length: ${certs.size})")
// create KeyManager
keyManager = object : X509ExtendedKeyManager() {
override fun getServerAliases(p0: String?, p1: Array<out Principal>?): Array<String>? = null
override fun chooseServerAlias(p0: String?, p1: Array<out Principal>?, p2: Socket?) = null
override fun getClientAliases(p0: String?, p1: Array<out Principal>?) =
arrayOf(alias)
override fun chooseClientAlias(p0: Array<out String>?, p1: Array<out Principal>?, p2: Socket?) =
alias
override fun getCertificateChain(forAlias: String?) =
certs.takeIf { forAlias == alias }
override fun getPrivateKey(forAlias: String?) =
key.takeIf { forAlias == alias }
}
// HTTP/2 doesn't support client certificates (yet)
// see https://tools.ietf.org/html/draft-ietf-httpbis-http2-secondary-certs-04
orig.protocols(listOf(Protocol.HTTP_1_1))
}
if (certManagerProducer != null || keyManager != null) {
val manager = certManagerProducer?.certManager()
val trustManager = manager ?: /* fall back to system default trust manager */
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
.let { factory ->
factory.init(null as KeyStore?)
factory.trustManagers.first() as X509TrustManager
}
val hostnameVerifier =
if (manager != null)
manager.HostnameVerifier(OkHostnameVerifier)
else
OkHostnameVerifier
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(
if (keyManager != null) arrayOf(keyManager) else null,
arrayOf(trustManager),
null)
orig.sslSocketFactory(sslContext.socketFactory, trustManager)
orig.hostnameVerifier(hostnameVerifier)
}
return entryPoint.httpClientFactory().create(orig.build(), authService = authService)
}
}
object UserAgentInterceptor: Interceptor {
val userAgent = "DAVx5/${BuildConfig.VERSION_NAME} (dav4jvm; " +
"okhttp/${OkHttp.VERSION}) Android/${Build.VERSION.RELEASE}"
init {
Logger.getGlobal().info("Will set User-Agent: $userAgent")
}
override fun intercept(chain: Interceptor.Chain): Response {
val locale = Locale.getDefault()
val request = chain.request().newBuilder()
.header("User-Agent", userAgent)
.header("Accept-Language", "${locale.language}-${locale.country}, ${locale.language};q=0.7, *;q=0.5")
.build()
return chain.proceed(request)
}
}
}

View File

@@ -1,141 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.network
import android.content.Context
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.ui.setup.LoginInfo
import at.bitfire.davdroid.util.withTrailingSlash
import at.bitfire.vcard4android.GroupMethod
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URI
/**
* Implements Nextcloud Login Flow v2.
*
* See https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
*/
class NextcloudLoginFlow(
context: Context
): AutoCloseable {
companion object {
const val FLOW_V1_PATH = "index.php/login/flow"
const val FLOW_V2_PATH = "index.php/login/v2"
/** Path to DAV endpoint (e.g. `remote.php/dav`). Will be appended to the server URL returned by Login Flow. */
const val DAV_PATH = "remote.php/dav"
}
val httpClient = HttpClient.Builder(context)
.setForeground(true)
.build()
override fun close() {
httpClient.close()
}
// Login flow state
var loginUrl: HttpUrl? = null
var pollUrl: HttpUrl? = null
var token: String? = null
suspend fun initiate(baseUrl: HttpUrl): HttpUrl? {
loginUrl = null
pollUrl = null
token = null
val json = postForJson(initiateUrl(baseUrl), "".toRequestBody())
loginUrl = json.getString("login").toHttpUrlOrNull()
json.getJSONObject("poll").let { poll ->
pollUrl = poll.getString("endpoint").toHttpUrl()
token = poll.getString("token")
}
return loginUrl
}
fun initiateUrl(baseUrl: HttpUrl): HttpUrl {
val path = baseUrl.encodedPath
if (path.endsWith(FLOW_V2_PATH))
// already a Login Flow v2 URL
return baseUrl
if (path.endsWith(FLOW_V1_PATH))
// Login Flow v1 URL, rewrite to v2
return baseUrl.newBuilder()
.encodedPath(path.replace(FLOW_V1_PATH, FLOW_V2_PATH))
.build()
// other URL, make it a Login Flow v2 URL
return baseUrl.newBuilder()
.addPathSegments(FLOW_V2_PATH)
.build()
}
suspend fun fetchLoginInfo(): LoginInfo {
val pollUrl = pollUrl ?: throw IllegalArgumentException("Missing pollUrl")
val token = token ?: throw IllegalArgumentException("Missing token")
// send HTTP request to request server, login name and app password
val json = postForJson(pollUrl, "token=$token".toRequestBody("application/x-www-form-urlencoded".toMediaType()))
// make sure server URL ends with a slash so that DAV_PATH can be appended
val serverUrl = json.getString("server").withTrailingSlash()
return LoginInfo(
baseUri = URI(serverUrl).resolve(DAV_PATH),
credentials = Credentials(
username = json.getString("loginName"),
password = json.getString("appPassword")
),
suggestedGroupMethod = GroupMethod.CATEGORIES
)
}
private suspend fun postForJson(url: HttpUrl, requestBody: RequestBody): JSONObject = withContext(Dispatchers.IO) {
val postRq = Request.Builder()
.url(url)
.post(requestBody)
.build()
val response = runInterruptible {
httpClient.okHttpClient.newCall(postRq).execute()
}
if (response.code != HttpURLConnection.HTTP_OK)
throw HttpException(response)
response.body?.use { body ->
val mimeType = body.contentType() ?: throw DavException("Login Flow response without MIME type")
if (mimeType.type != "application" || mimeType.subtype != "json")
throw DavException("Invalid Login Flow response (not JSON)")
// decode JSON
return@withContext JSONObject(body.string())
}
throw DavException("Invalid Login Flow response (no body)")
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.push
import at.bitfire.dav4jvm.XmlReader
import at.bitfire.dav4jvm.XmlUtils
import at.bitfire.dav4jvm.property.push.PushMessage
import at.bitfire.dav4jvm.property.push.Topic
import org.xmlpull.v1.XmlPullParserException
import java.io.StringReader
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
class PushMessageParser @Inject constructor(
private val logger: Logger
) {
/**
* Parses a WebDAV-Push message and returns the `topic` that the message is about.
*
* @return topic of the modified collection, or `null` if the topic couldn't be determined
*/
operator fun invoke(message: String): String? {
var topic: String? = null
val parser = XmlUtils.newPullParser()
try {
parser.setInput(StringReader(message))
XmlReader(parser).processTag(PushMessage.NAME) {
val pushMessage = PushMessage.Factory.create(parser)
val properties = pushMessage.propStat?.properties ?: return@processTag
val pushTopic = properties.filterIsInstance<Topic>().firstOrNull()
topic = pushTopic?.topic
}
} catch (e: XmlPullParserException) {
logger.log(Level.WARNING, "Couldn't parse push message", e)
}
return topic
}
}

View File

@@ -1,192 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.push
import android.accounts.Account
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import at.bitfire.dav4jvm.DavCollection
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.HttpUtils
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.XmlUtils
import at.bitfire.dav4jvm.XmlUtils.insertTag
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.property.push.NS_WEBDAV_PUSH
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.repository.PreferenceRepository
import at.bitfire.davdroid.settings.AccountSettings
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.runInterruptible
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.io.StringWriter
import java.time.Duration
import java.time.Instant
import java.util.logging.Level
import java.util.logging.Logger
/**
* Worker that registers push for all collections that support it.
* To be run as soon as a collection that supports push is changed (selected for sync status
* changes, or collection is created, deleted, etc).
*/
@Suppress("unused")
@HiltWorker
class PushRegistrationWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParameters: WorkerParameters,
private val accountSettingsFactory: AccountSettings.Factory,
private val collectionRepository: DavCollectionRepository,
private val logger: Logger,
private val preferenceRepository: PreferenceRepository,
private val serviceRepository: DavServiceRepository
) : CoroutineWorker(context, workerParameters) {
override suspend fun doWork(): Result {
logger.info("Running push registration worker")
try {
registerSyncable()
unregisterNotSyncable()
} catch (_: IOException) {
return Result.retry() // retry on I/O errors
}
return Result.success()
}
private suspend fun registerPushSubscription(collection: Collection, account: Account, endpoint: String) {
val settings = accountSettingsFactory.create(account)
runInterruptible {
HttpClient.Builder(applicationContext, settings)
.setForeground(true)
.build()
.use { client ->
val httpClient = client.okHttpClient
// requested expiration time: 3 days
val requestedExpiration = Instant.now() + Duration.ofDays(3)
val serializer = XmlUtils.newSerializer()
val writer = StringWriter()
serializer.setOutput(writer)
serializer.startDocument("UTF-8", true)
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "push-register")) {
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "subscription")) {
// subscription URL
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "web-push-subscription")) {
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "push-resource")) {
text(endpoint)
}
}
}
// requested expiration
serializer.insertTag(Property.Name(NS_WEBDAV_PUSH, "expires")) {
text(HttpUtils.formatDate(requestedExpiration))
}
}
serializer.endDocument()
val xml = writer.toString().toRequestBody(DavResource.MIME_XML)
DavCollection(httpClient, collection.url).post(xml) { response ->
if (response.isSuccessful) {
val subscriptionUrl = response.header("Location")
val expires = response.header("Expires")?.let { expiresDate ->
HttpUtils.parseDate(expiresDate)
} ?: requestedExpiration
collectionRepository.updatePushSubscription(
id = collection.id,
subscriptionUrl = subscriptionUrl,
expires = expires?.epochSecond
)
} else
logger.warning("Couldn't register push for ${collection.url}: $response")
}
}
}
}
private suspend fun registerSyncable() {
val endpoint = preferenceRepository.unifiedPushEndpoint()
// register push subscription for syncable collections
if (endpoint != null)
for (collection in collectionRepository.getPushCapableAndSyncable()) {
val expires = collection.pushSubscriptionExpires
// calculate next run time, but use the duplicate interval for safety (times are not exact)
val nextRun = Instant.now() + Duration.ofDays(2*PushRegistrationWorkerManager.INTERVAL_DAYS)
if (expires != null && expires >= nextRun.epochSecond) {
logger.fine("Push subscription for ${collection.url} is still valid until ${collection.pushSubscriptionExpires}")
continue
}
// no existing subscription or expiring soon
logger.info("Registering push for ${collection.url}")
serviceRepository.get(collection.serviceId)?.let { service ->
val account = Account(service.accountName, applicationContext.getString(R.string.account_type))
try {
registerPushSubscription(collection, account, endpoint)
} catch (e: DavException) {
// catch possible per-collection exception so that all collections can be processed
logger.log(Level.WARNING, "Couldn't register push for ${collection.url}", e)
}
}
}
else
logger.info("No UnifiedPush endpoint configured")
}
private suspend fun unregisterPushSubscription(collection: Collection, account: Account, url: HttpUrl) {
val settings = accountSettingsFactory.create(account)
runInterruptible {
HttpClient.Builder(applicationContext, settings)
.setForeground(true)
.build()
.use { client ->
val httpClient = client.okHttpClient
try {
DavResource(httpClient, url).delete {
// deleted
}
} catch (e: DavException) {
logger.log(Level.WARNING, "Couldn't unregister push for ${collection.url}", e)
}
// remove registration URL from DB in any case
collectionRepository.updatePushSubscription(
id = collection.id,
subscriptionUrl = null,
expires = null
)
}
}
}
private suspend fun unregisterNotSyncable() {
for (collection in collectionRepository.getPushRegisteredAndNotSyncable()) {
logger.info("Unregistering push for ${collection.url}")
collection.pushSubscription?.toHttpUrlOrNull()?.let { url ->
serviceRepository.get(collection.serviceId)?.let { service ->
val account = Account(service.accountName, applicationContext.getString(R.string.account_type))
unregisterPushSubscription(collection, account, url)
}
}
}
}
}

View File

@@ -1,98 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.push
import android.content.Context
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import at.bitfire.davdroid.repository.DavCollectionRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit
import java.util.logging.Logger
import javax.inject.Inject
class PushRegistrationWorkerManager @Inject constructor(
@ApplicationContext val context: Context,
val collectionRepository: DavCollectionRepository,
val logger: Logger
) {
/**
* Determines whether there are any push-capable collections and updates the periodic worker accordingly.
*
* If there are push-capable collections, a unique periodic worker with an initial delay of 5 seconds is enqueued.
* A potentially existing worker is replaced, so that the first run should be soon.
*
* Otherwise, a potentially existing worker is cancelled.
*/
fun updatePeriodicWorker() {
val workerNeeded = runBlocking {
collectionRepository.anyPushCapable()
}
val workManager = WorkManager.getInstance(context)
if (workerNeeded) {
logger.info("Enqueuing periodic PushRegistrationWorker")
workManager.enqueueUniquePeriodicWork(
UNIQUE_WORK_NAME,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequest.Builder(PushRegistrationWorker::class, INTERVAL_DAYS, TimeUnit.DAYS)
.setInitialDelay(5, TimeUnit.SECONDS)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.build()
)
} else {
logger.info("Cancelling periodic PushRegistrationWorker")
workManager.cancelUniqueWork(UNIQUE_WORK_NAME)
}
}
companion object {
private const val UNIQUE_WORK_NAME = "push-registration"
const val INTERVAL_DAYS = 1L
}
/**
* Listener that enqueues a push registration worker when the collection list changes.
*/
class CollectionsListener @Inject constructor(
@ApplicationContext val context: Context,
val workerManager: PushRegistrationWorkerManager
): DavCollectionRepository.OnChangeListener {
override fun onCollectionsChanged() {
workerManager.updatePeriodicWorker()
}
}
/**
* Hilt module that registers [CollectionsListener] in [DavCollectionRepository].
*/
@Module
@InstallIn(SingletonComponent::class)
interface PushRegistrationWorkerModule {
@Binds
@IntoSet
fun listener(impl: CollectionsListener): DavCollectionRepository.OnChangeListener
}
}

View File

@@ -1,117 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.push
import android.content.Context
import at.bitfire.davdroid.db.Collection.Companion.TYPE_ADDRESSBOOK
import at.bitfire.davdroid.repository.AccountRepository
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.repository.PreferenceRepository
import at.bitfire.davdroid.sync.SyncDataType
import at.bitfire.davdroid.sync.TasksAppManager
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import dagger.Lazy
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.unifiedpush.android.connector.MessagingReceiver
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
@AndroidEntryPoint
class UnifiedPushReceiver: MessagingReceiver() {
@Inject
lateinit var accountRepository: AccountRepository
@Inject
lateinit var collectionRepository: DavCollectionRepository
@Inject
lateinit var logger: Logger
@Inject
lateinit var serviceRepository: DavServiceRepository
@Inject
lateinit var preferenceRepository: PreferenceRepository
@Inject
lateinit var parsePushMessage: PushMessageParser
@Inject
lateinit var pushRegistrationWorkerManager: PushRegistrationWorkerManager
@Inject
lateinit var tasksAppManager: Lazy<TasksAppManager>
@Inject
lateinit var syncWorkerManager: SyncWorkerManager
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
// remember new endpoint
preferenceRepository.unifiedPushEndpoint(endpoint)
// register new endpoint at CalDAV/CardDAV servers
pushRegistrationWorkerManager.updatePeriodicWorker()
}
override fun onUnregistered(context: Context, instance: String) {
// reset known endpoint
preferenceRepository.unifiedPushEndpoint(null)
}
override fun onMessage(context: Context, message: ByteArray, instance: String) {
CoroutineScope(Dispatchers.Default).launch {
val messageXml = message.toString(Charsets.UTF_8)
logger.log(Level.INFO, "Received push message", messageXml)
// parse push notification
val topic = parsePushMessage(messageXml)
// sync affected collection
if (topic != null) {
logger.info("Got push notification for topic $topic")
// Sync all authorities of account that the collection belongs to
// Later: only sync affected collection and authorities
collectionRepository.getSyncableByTopic(topic)?.let { collection ->
serviceRepository.get(collection.serviceId)?.let { service ->
val syncDataTypes = mutableSetOf<SyncDataType>()
// If the type is an address book, add the contacts type
if (collection.type == TYPE_ADDRESSBOOK)
syncDataTypes += SyncDataType.CONTACTS
// If the collection supports events, add the events type
if (collection.supportsVEVENT != false)
syncDataTypes += SyncDataType.EVENTS
// If the collection supports tasks, make sure there's a provider installed,
// and add the tasks type
if (collection.supportsVJOURNAL != false || collection.supportsVTODO != false)
if (tasksAppManager.get().currentProvider() != null)
syncDataTypes += SyncDataType.TASKS
// Schedule sync for all the types identified
val account = accountRepository.fromName(service.accountName)
for (syncDataType in syncDataTypes)
syncWorkerManager.enqueueOneTime(account, syncDataType, fromPush = true)
}
}
} else {
logger.warning("Got push message without topic, syncing all accounts")
for (account in accountRepository.getAll())
syncWorkerManager.enqueueOneTimeAllAuthorities(account, fromPush = true)
}
}
}
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.repository
import android.accounts.Account
import androidx.room.Transaction
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.HomeSet
import at.bitfire.davdroid.db.Service
import javax.inject.Inject
class DavHomeSetRepository @Inject constructor(
db: AppDatabase
) {
val dao = db.homeSetDao()
fun getAddressBookHomeSetsFlow(account: Account) =
dao.getBindableByAccountAndServiceTypeFlow(account.name, Service.TYPE_CARDDAV)
fun getBindableByServiceFlow(serviceId: Long) = dao.getBindableByServiceFlow(serviceId)
fun getById(id: Long) = dao.getById(id)
fun getByService(serviceId: Long) = dao.getByService(serviceId)
fun getCalendarHomeSetsFlow(account: Account) =
dao.getBindableByAccountAndServiceTypeFlow(account.name, Service.TYPE_CALDAV)
/**
* Tries to insert new row, but updates existing row if already present.
* This method preserves the primary key, as opposed to using "@Insert(onConflict = OnConflictStrategy.REPLACE)"
* which will create a new row with incremented ID and thus breaks entity relationships!
*
* @return ID of the row, that has been inserted or updated. -1 If the insert fails due to other reasons.
*/
@Transaction
fun insertOrUpdateByUrl(homeset: HomeSet): Long =
dao.getByUrl(homeset.serviceId, homeset.url.toString())?.let { existingHomeset ->
dao.update(homeset.copy(id = existingHomeset.id))
existingHomeset.id
} ?: dao.insert(homeset)
fun delete(homeSet: HomeSet) = dao.delete(homeSet)
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.repository
import android.content.Context
import android.content.pm.PackageManager
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.SyncStats
import dagger.hilt.android.qualifiers.ApplicationContext
import java.text.Collator
import java.util.logging.Logger
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class DavSyncStatsRepository @Inject constructor(
@ApplicationContext val context: Context,
db: AppDatabase,
private val logger: Logger
) {
private val dao = db.syncStatsDao()
data class LastSynced(
val appName: String,
val lastSynced: Long
)
fun getLastSyncedFlow(collectionId: Long): Flow<List<LastSynced>> =
dao.getByCollectionIdFlow(collectionId).map { list ->
val collator = Collator.getInstance()
list.map { stats ->
LastSynced(
appName = appNameFromAuthority(stats.authority),
lastSynced = stats.lastSync
)
}.sortedWith { a, b ->
collator.compare(a.appName, b.appName)
}
}
fun logSyncTime(collectionId: Long, authority: String, lastSync: Long = System.currentTimeMillis()) {
dao.insertOrReplace(SyncStats(
id = 0,
collectionId = collectionId,
authority = authority,
lastSync = lastSync
))
}
/**
* Tries to find the application name for given authority. Returns the authority if not
* found.
*
* @param authority authority to find the application name for (ie "at.techbee.jtx")
* @return the application name of authority (ie "jtx Board")
*/
private fun appNameFromAuthority(authority: String): String {
val packageManager = context.packageManager
val packageName = packageManager.resolveContentProvider(authority, 0)?.packageName ?: authority
return try {
val appInfo = packageManager.getPackageInfo(packageName, 0).applicationInfo
if (appInfo != null) {
packageManager.getApplicationLabel(appInfo).toString()
} else {
logger.warning("Package name ($packageName) not found for authority: $authority")
authority
}
} catch (e: PackageManager.NameNotFoundException) {
logger.warning("Application name not found for authority: $authority")
authority
}
}
}

View File

@@ -1,247 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import androidx.core.content.contentValuesOf
import at.bitfire.davdroid.db.SyncState
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidCalendarFactory
import at.bitfire.ical4android.BatchOperation
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
import java.util.LinkedList
import java.util.logging.Level
import java.util.logging.Logger
/**
* Application-specific subclass of [AndroidCalendar] for local calendars.
*
* [Calendars.NAME] is used to store the calendar URL.
*/
class LocalCalendar private constructor(
account: Account,
provider: ContentProviderClient,
id: Long
): AndroidCalendar<LocalEvent>(account, provider, LocalEvent.Factory, id), LocalCollection<LocalEvent> {
companion object {
private const val COLUMN_SYNC_STATE = Calendars.CAL_SYNC1
private val logger: Logger
get() = Logger.getGlobal()
}
override val collectionUrl: String?
get() = name
override val tag: String
get() = "events-${account.name}-$id"
override val title: String
get() = displayName ?: id.toString()
private var accessLevel: Int = Calendars.CAL_ACCESS_OWNER // assume full access if not specified
override val readOnly
get() = accessLevel <= Calendars.CAL_ACCESS_READ
override var lastSyncState: SyncState?
get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
return SyncState.fromString(cursor.getString(0))
else
null
}
set(state) {
val values = contentValuesOf(COLUMN_SYNC_STATE to state.toString())
provider.update(calendarSyncURI(), values, null, null)
}
override fun populate(info: ContentValues) {
super.populate(info)
accessLevel = info.getAsInteger(Calendars.CALENDAR_ACCESS_LEVEL) ?: Calendars.CAL_ACCESS_OWNER
}
override fun findDeleted() =
queryEvents("${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null)
override fun findDirty(): List<LocalEvent> {
val dirty = LinkedList<LocalEvent>()
/*
* RFC 5545 3.8.7.4. Sequence Number
* When a calendar component is created, its sequence number is 0. It is monotonically incremented by the "Organizer's"
* CUA each time the "Organizer" makes a significant revision to the calendar component.
*/
for (localEvent in queryEvents("${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null)) {
try {
val event = requireNotNull(localEvent.event)
val nonGroupScheduled = event.attendees.isEmpty()
val weAreOrganizer = localEvent.weAreOrganizer
val sequence = event.sequence
if (sequence == null)
// sequence has not been assigned yet (i.e. this event was just locally created)
event.sequence = 0
else if (nonGroupScheduled || weAreOrganizer) // increase sequence
event.sequence = sequence + 1
} catch(e: Exception) {
logger.log(Level.WARNING, "Couldn't check/increase sequence", e)
}
dirty += localEvent
}
return dirty
}
override fun findByName(name: String) =
queryEvents("${Events._SYNC_ID}=?", arrayOf(name)).firstOrNull()
override fun markNotDirty(flags: Int): Int {
val values = contentValuesOf(LocalEvent.COLUMN_FLAGS to flags)
return provider.update(Events.CONTENT_URI.asSyncAdapter(account), values,
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL",
arrayOf(id.toString()))
}
override fun removeNotDirtyMarked(flags: Int): Int {
var deleted = 0
// list all non-dirty events with the given flags and delete every row + its exceptions
provider.query(Events.CONTENT_URI.asSyncAdapter(account), arrayOf(Events._ID),
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${LocalEvent.COLUMN_FLAGS}=?",
arrayOf(id.toString(), flags.toString()), null)?.use { cursor ->
val batch = BatchOperation(provider)
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
// delete event and possible exceptions (content provider doesn't delete exceptions itself)
batch.enqueue(BatchOperation.CpoBuilder
.newDelete(Events.CONTENT_URI.asSyncAdapter(account))
.withSelection("${Events._ID}=? OR ${Events.ORIGINAL_ID}=?", arrayOf(id.toString(), id.toString())))
}
deleted = batch.commit()
}
return deleted
}
override fun forgetETags() {
val values = contentValuesOf(LocalEvent.COLUMN_ETAG to null)
provider.update(Events.CONTENT_URI.asSyncAdapter(account), values, "${Events.CALENDAR_ID}=?",
arrayOf(id.toString()))
}
fun processDirtyExceptions() {
// process deleted exceptions
logger.info("Processing deleted exceptions")
provider.query(
Events.CONTENT_URI.asSyncAdapter(account),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
"${Events.CALENDAR_ID}=? AND ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NOT NULL",
arrayOf(id.toString()), null)?.use { cursor ->
while (cursor.moveToNext()) {
logger.fine("Found deleted exception, removing and re-scheduling original event (if available)")
val id = cursor.getLong(0) // can't be null (by definition)
val originalID = cursor.getLong(1) // can't be null (by query)
val batch = BatchOperation(provider)
// get original event's SEQUENCE
provider.query(
ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(account),
arrayOf(LocalEvent.COLUMN_SEQUENCE),
null, null, null)?.use { cursor2 ->
if (cursor2.moveToNext()) {
// original event is available
val originalSequence = if (cursor2.isNull(0)) 0 else cursor2.getInt(0)
// re-schedule original event and set it to DIRTY
batch.enqueue(BatchOperation.CpoBuilder
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(account))
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1)
.withValue(Events.DIRTY, 1))
}
}
// completely remove deleted exception
batch.enqueue(BatchOperation.CpoBuilder.newDelete(ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(account)))
batch.commit()
}
}
// process dirty exceptions
logger.info("Processing dirty exceptions")
provider.query(
Events.CONTENT_URI.asSyncAdapter(account),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NOT NULL",
arrayOf(id.toString()), null)?.use { cursor ->
while (cursor.moveToNext()) {
logger.fine("Found dirty exception, increasing SEQUENCE to re-schedule")
val id = cursor.getLong(0) // can't be null (by definition)
val originalID = cursor.getLong(1) // can't be null (by query)
val sequence = if (cursor.isNull(2)) 0 else cursor.getInt(2)
val batch = BatchOperation(provider)
// original event to DIRTY
batch.enqueue(BatchOperation.CpoBuilder
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, originalID).asSyncAdapter(account))
.withValue(Events.DIRTY, 1))
// increase SEQUENCE and set DIRTY to 0
batch.enqueue(BatchOperation.CpoBuilder
.newUpdate(ContentUris.withAppendedId(Events.CONTENT_URI, id).asSyncAdapter(account))
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
.withValue(Events.DIRTY, 0))
batch.commit()
}
}
}
/**
* Marks dirty events (which are not already marked as deleted) which got no valid instances as "deleted"
*
* @return number of affected events
*/
fun deleteDirtyEventsWithoutInstances() {
provider.query(
Events.CONTENT_URI.asSyncAdapter(account),
arrayOf(Events._ID),
"${Events.DIRTY} AND NOT ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", // Get dirty main events (and no exception events)
null, null
)?.use { cursor ->
while (cursor.moveToNext()) {
val eventID = cursor.getLong(0)
// get number of instances
val numEventInstances = LocalEvent.numInstances(provider, account, eventID)
// delete event if there are no instances
if (numEventInstances == 0) {
logger.info("Marking event #$eventID without instances as deleted")
LocalEvent.markAsDeleted(provider, account, eventID)
}
}
}
}
object Factory: AndroidCalendarFactory<LocalCalendar> {
override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) =
LocalCalendar(account, provider, id)
}
}

View File

@@ -1,267 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.provider.CalendarContract
import android.provider.CalendarContract.Events
import androidx.core.content.contentValuesOf
import at.bitfire.davdroid.BuildConfig
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidEvent
import at.bitfire.ical4android.AndroidEventFactory
import at.bitfire.ical4android.BatchOperation
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.ICalendar
import at.bitfire.ical4android.Ical4Android
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
import net.fortuna.ical4j.model.property.ProdId
import java.util.UUID
class LocalEvent: AndroidEvent, LocalResource<Event> {
companion object {
init {
ICalendar.prodId = ProdId("DAVx5/${BuildConfig.VERSION_NAME} ical4j/" + Ical4Android.ical4jVersion)
}
const val COLUMN_ETAG = Events.SYNC_DATA1
const val COLUMN_FLAGS = Events.SYNC_DATA2
const val COLUMN_SEQUENCE = Events.SYNC_DATA3
const val COLUMN_SCHEDULE_TAG = Events.SYNC_DATA4
/**
* Marks the event as deleted
* @param eventID
*/
fun markAsDeleted(provider: ContentProviderClient, account: Account, eventID: Long) {
provider.update(
ContentUris.withAppendedId(
Events.CONTENT_URI,
eventID
).asSyncAdapter(account),
contentValuesOf(Events.DELETED to 1),
null, null
)
}
/**
* Finds the amount of direct instances this event has (without exceptions); used by [numInstances]
* to find the number of instances of exceptions.
*
* The number of returned instances may vary with the Android version.
*
* @return number of direct event instances (not counting instances of exceptions); *null* if
* the number can't be determined or if the event has no last date (recurring event without last instance)
*/
fun numDirectInstances(provider: ContentProviderClient, account: Account, eventID: Long): Int? {
// query event to get first and last instance
var first: Long? = null
var last: Long? = null
provider.query(
ContentUris.withAppendedId(
Events.CONTENT_URI,
eventID
),
arrayOf(Events.DTSTART, Events.LAST_DATE), null, null, null
)?.use { cursor ->
cursor.moveToNext()
if (!cursor.isNull(0))
first = cursor.getLong(0)
if (!cursor.isNull(1))
last = cursor.getLong(1)
}
// if this event doesn't have a last occurence, it's endless and always has instances
if (first == null || last == null)
return null
/* We can't use Long.MIN_VALUE and Long.MAX_VALUE because Android generates the instances
on the fly and it doesn't accept those values. So we use the first/last actual occurence
of the event (calculated by Android). */
val instancesUri = CalendarContract.Instances.CONTENT_URI.asSyncAdapter(account)
.buildUpon()
.appendPath(first.toString()) // begin timestamp
.appendPath(last.toString()) // end timestamp
.build()
var numInstances = 0
provider.query(
instancesUri, null,
"${CalendarContract.Instances.EVENT_ID}=?", arrayOf(eventID.toString()),
null
)?.use { cursor ->
numInstances += cursor.count
}
return numInstances
}
/**
* Finds the total number of instances this event has (including instances of exceptions)
*
* The number of returned instances may vary with the Android version.
*
* @return number of direct event instances (not counting instances of exceptions); *null* if
* the number can't be determined or if the event has no last date (recurring event without last instance)
*/
fun numInstances(provider: ContentProviderClient, account: Account, eventID: Long): Int? {
// num instances of the main event
var numInstances = numDirectInstances(provider, account, eventID) ?: return null
// add the number of instances of every main event's exception
provider.query(
Events.CONTENT_URI,
arrayOf(Events._ID),
"${Events.ORIGINAL_ID}=?", // get exception events of the main event
arrayOf("$eventID"), null
)?.use { exceptionsEventCursor ->
while (exceptionsEventCursor.moveToNext()) {
val exceptionEventID = exceptionsEventCursor.getLong(0)
val exceptionInstances = numDirectInstances(provider, account, exceptionEventID)
if (exceptionInstances == null)
// number of instances of exception can't be determined; so the total number of instances is also unclear
return null
numInstances += exceptionInstances
}
}
return numInstances
}
}
override var fileName: String? = null
private set
override var eTag: String? = null
override var scheduleTag: String? = null
override var flags: Int = 0
private set
var weAreOrganizer = false
private set
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int): super(calendar, event) {
this.fileName = fileName
this.eTag = eTag
this.scheduleTag = scheduleTag
this.flags = flags
}
private constructor(calendar: AndroidCalendar<*>, values: ContentValues): super(calendar, values) {
fileName = values.getAsString(Events._SYNC_ID)
eTag = values.getAsString(COLUMN_ETAG)
scheduleTag = values.getAsString(COLUMN_SCHEDULE_TAG)
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
}
override fun populateEvent(row: ContentValues, groupScheduled: Boolean) {
val event = requireNotNull(event)
event.sequence = row.getAsInteger(COLUMN_SEQUENCE)
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
weAreOrganizer = isOrganizer != null && isOrganizer != 0
super.populateEvent(row, groupScheduled)
}
override fun buildEvent(recurrence: Event?, builder: BatchOperation.CpoBuilder) {
val event = requireNotNull(event)
val buildException = recurrence != null
val eventToBuild = recurrence ?: event
builder .withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
.withValue(Events.DIRTY, 0)
.withValue(Events.DELETED, 0)
.withValue(COLUMN_FLAGS, flags)
if (buildException)
builder .withValue(Events.ORIGINAL_SYNC_ID, fileName)
else
builder .withValue(Events._SYNC_ID, fileName)
.withValue(COLUMN_ETAG, eTag)
.withValue(COLUMN_SCHEDULE_TAG, scheduleTag)
super.buildEvent(recurrence, builder)
}
/**
* Creates and sets a new UID in the calendar provider, if no UID is already set.
* It also returns the desired file name for the event for further processing in the sync algorithm.
*
* @return file name to use at upload
*/
override fun prepareForUpload(): String {
// make sure that UID is set
val uid: String = event!!.uid ?: run {
// generate new UID
val newUid = UUID.randomUUID().toString()
// update in calendar provider
val values = contentValuesOf(Events.UID_2445 to newUid)
calendar.provider.update(eventSyncURI(), values, null, null)
// update this event
event?.uid = newUid
newUid
}
val uidIsGoodFilename = uid.all { char ->
// see RFC 2396 2.2
char.isLetterOrDigit() || arrayOf( // allow letters and digits
';',':','@','&','=','+','$',',', // allow reserved characters except '/' and '?'
'-','_','.','!','~','*','\'','(',')' // allow unreserved characters
).contains(char)
}
return if (uidIsGoodFilename)
"$uid.ics" // use UID as file name
else
"${UUID.randomUUID()}.ics" // UID would be dangerous as file name, use random UUID instead
}
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
val values = ContentValues(5)
if (fileName != null)
values.put(Events._SYNC_ID, fileName)
values.put(COLUMN_ETAG, eTag)
values.put(COLUMN_SCHEDULE_TAG, scheduleTag)
values.put(COLUMN_SEQUENCE, event!!.sequence)
values.put(Events.DIRTY, 0)
calendar.provider.update(eventSyncURI(), values, null, null)
if (fileName != null)
this.fileName = fileName
this.eTag = eTag
this.scheduleTag = scheduleTag
}
override fun updateFlags(flags: Int) {
val values = contentValuesOf(COLUMN_FLAGS to flags)
calendar.provider.update(eventSyncURI(), values, null, null)
this.flags = flags
}
override fun resetDeleted() {
val values = contentValuesOf(Events.DELETED to 0)
calendar.provider.update(eventSyncURI(), values, null, null)
}
object Factory: AndroidEventFactory<LocalEvent> {
override fun fromProvider(calendar: AndroidCalendar<*>, values: ContentValues) =
LocalEvent(calendar, values)
}
}

View File

@@ -1,98 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.net.Uri
interface LocalResource<in TData: Any> {
companion object {
/**
* Resource is present on remote server. This flag is used to identify resources
* which are not present on the remote server anymore and can be deleted at the end
* of the synchronization.
*/
const val FLAG_REMOTELY_PRESENT = 1
}
/**
* Unique ID which identifies the resource in the local storage. May be null if the
* resource has not been saved yet.
*/
val id: Long?
/**
* Remote file name for the resource, for instance `mycontact.vcf`. Also used to determine whether
* a dirty record has just been created (in this case, [fileName] is *null*) or modified
* (in this case, [fileName] is the remote file name).
*/
val fileName: String?
/** remote ETag for the resource */
var eTag: String?
/** remote Schedule-Tag for the resource */
var scheduleTag: String?
/** bitfield of flags; currently either [FLAG_REMOTELY_PRESENT] or 0 */
val flags: Int
/**
* Prepares the resource for uploading:
*
* 1. If the resource doesn't have an UID yet, this method generates one and writes it to the content provider.
* 2. The new file name which can be used for the upload is derived from the UID and returned, but not
* saved to the content provider. The sync manager is responsible for saving the file name that
* was actually used.
*
* @return new file name of the resource (like "<uid>.vcf")
*/
fun prepareForUpload(): String
/**
* Unsets the /dirty/ field of the resource. Typically used after successfully uploading a
* locally modified resource.
*
* @param fileName If this argument is not *null*, [LocalResource.fileName] will be set to its value.
* @param eTag ETag of the uploaded resource as returned by the server (null if the server didn't return one)
* @param scheduleTag CalDAV Schedule-Tag of the uploaded resource as returned by the server (null if not applicable or if the server didn't return one)
*/
fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String? = null)
/**
* Sets (local) flags of the resource. At the moment, the only allowed values are
* 0 and [FLAG_REMOTELY_PRESENT].
*/
fun updateFlags(flags: Int)
/**
* Adds the data object to the content provider and ensures that the dirty flag is clear.
*
* @return content URI of the created row (e.g. event URI)
*/
fun add(): Uri
/**
* Updates the data object in the content provider and ensures that the dirty flag is clear.
*
* @return content URI of the updated row (e.g. event URI)
*/
fun update(data: TData): Uri
/**
* Deletes the data object from the content provider.
*
* @return number of affected rows
*/
fun delete(): Int
/**
* Undoes deletion of the data object from the content provider.
*/
fun resetDeleted()
}

View File

@@ -1,114 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.content.ContentValues
import androidx.core.content.contentValuesOf
import at.bitfire.ical4android.BatchOperation
import at.bitfire.ical4android.DmfsTask
import at.bitfire.ical4android.DmfsTaskFactory
import at.bitfire.ical4android.DmfsTaskList
import at.bitfire.ical4android.Task
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.util.UUID
class LocalTask: DmfsTask, LocalResource<Task> {
companion object {
const val COLUMN_ETAG = Tasks.SYNC1
const val COLUMN_FLAGS = Tasks.SYNC2
}
override var fileName: String? = null
override var scheduleTag: String? = null
override var eTag: String? = null
override var flags = 0
private set
constructor(taskList: DmfsTaskList<*>, task: Task, fileName: String?, eTag: String?, flags: Int)
: super(taskList, task) {
this.fileName = fileName
this.eTag = eTag
this.flags = flags
}
private constructor(taskList: DmfsTaskList<*>, values: ContentValues): super(taskList) {
id = values.getAsLong(Tasks._ID)
fileName = values.getAsString(Tasks._SYNC_ID)
eTag = values.getAsString(COLUMN_ETAG)
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
}
/* process LocalTask-specific fields */
override fun buildTask(builder: BatchOperation.CpoBuilder, update: Boolean) {
super.buildTask(builder, update)
builder .withValue(Tasks._SYNC_ID, fileName)
.withValue(COLUMN_ETAG, eTag)
.withValue(COLUMN_FLAGS, flags)
}
/* custom queries */
override fun prepareForUpload(): String {
val uid: String = task!!.uid ?: run {
// generate new UID
val newUid = UUID.randomUUID().toString()
// update in tasks provider
val values = contentValuesOf(Tasks._UID to newUid)
taskList.provider.update(taskSyncURI(), values, null, null)
// update this task
task!!.uid = newUid
newUid
}
return "$uid.ics"
}
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
if (scheduleTag != null)
logger.fine("Schedule-Tag for tasks not supported yet, won't save")
val values = ContentValues(4)
if (fileName != null)
values.put(Tasks._SYNC_ID, fileName)
values.put(COLUMN_ETAG, eTag)
values.put(Tasks.SYNC_VERSION, task!!.sequence)
values.put(Tasks._DIRTY, 0)
taskList.provider.update(taskSyncURI(), values, null, null)
if (fileName != null)
this.fileName = fileName
this.eTag = eTag
}
override fun updateFlags(flags: Int) {
if (id != null) {
val values = contentValuesOf(COLUMN_FLAGS to flags)
taskList.provider.update(taskSyncURI(), values, null, null)
}
this.flags = flags
}
override fun resetDeleted() {
throw NotImplementedError()
}
object Factory: DmfsTaskFactory<LocalTask> {
override fun fromProvider(taskList: DmfsTaskList<*>, values: ContentValues) =
LocalTask(taskList, values)
}
}

View File

@@ -1,130 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentValues
import androidx.core.content.contentValuesOf
import at.bitfire.davdroid.db.SyncState
import at.bitfire.ical4android.DmfsTaskList
import at.bitfire.ical4android.DmfsTaskListFactory
import at.bitfire.ical4android.TaskProvider
import org.dmfs.tasks.contract.TaskContract.TaskListColumns
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.util.logging.Level
import java.util.logging.Logger
/**
* App-specific implementation of a task list.
*
* [TaskLists._SYNC_ID] is used to store the task list URL.
*/
class LocalTaskList private constructor(
account: Account,
provider: ContentProviderClient,
providerName: TaskProvider.ProviderName,
id: Long
): DmfsTaskList<LocalTask>(account, provider, providerName, LocalTask.Factory, id), LocalCollection<LocalTask> {
private val logger = Logger.getGlobal()
private var accessLevel: Int = TaskListColumns.ACCESS_LEVEL_UNDEFINED
override val readOnly
get() =
accessLevel != TaskListColumns.ACCESS_LEVEL_UNDEFINED &&
accessLevel <= TaskListColumns.ACCESS_LEVEL_READ
override val collectionUrl: String?
get() = syncId
override val tag: String
get() = "tasks-${account.name}-$id"
override val title: String
get() = name ?: id.toString()
override var lastSyncState: SyncState?
get() {
try {
provider.query(taskListSyncUri(), arrayOf(TaskLists.SYNC_VERSION),
null, null, null)?.use { cursor ->
if (cursor.moveToNext())
cursor.getString(0)?.let {
return SyncState.fromString(it)
}
}
} catch (e: Exception) {
logger.log(Level.WARNING, "Couldn't read sync state", e)
}
return null
}
set(state) {
val values = contentValuesOf(TaskLists.SYNC_VERSION to state?.toString())
provider.update(taskListSyncUri(), values, null, null)
}
override fun populate(values: ContentValues) {
super.populate(values)
accessLevel = values.getAsInteger(TaskListColumns.ACCESS_LEVEL)
}
override fun findDeleted() = queryTasks(Tasks._DELETED, null)
override fun findDirty(): List<LocalTask> {
val tasks = queryTasks(Tasks._DIRTY, null)
for (localTask in tasks) {
try {
val task = requireNotNull(localTask.task)
val sequence = task.sequence
if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
task.sequence = 0
else // task was modified, increase sequence
task.sequence = sequence + 1
} catch(e: Exception) {
logger.log(Level.WARNING, "Couldn't check/increase sequence", e)
}
}
return tasks
}
override fun findByName(name: String) =
queryTasks("${Tasks._SYNC_ID}=?", arrayOf(name)).firstOrNull()
override fun markNotDirty(flags: Int): Int {
val values = contentValuesOf(LocalTask.COLUMN_FLAGS to flags)
return provider.update(tasksSyncUri(), values,
"${Tasks.LIST_ID}=? AND ${Tasks._DIRTY}=0",
arrayOf(id.toString()))
}
override fun removeNotDirtyMarked(flags: Int) =
provider.delete(tasksSyncUri(),
"${Tasks.LIST_ID}=? AND NOT ${Tasks._DIRTY} AND ${LocalTask.COLUMN_FLAGS}=?",
arrayOf(id.toString(), flags.toString()))
override fun forgetETags() {
val values = contentValuesOf(LocalEvent.COLUMN_ETAG to null)
provider.update(tasksSyncUri(), values, "${Tasks.LIST_ID}=?",
arrayOf(id.toString()))
}
object Factory: DmfsTaskListFactory<LocalTaskList> {
override fun newInstance(
account: Account,
provider: ContentProviderClient,
providerName: TaskProvider.ProviderName,
id: Long
) = LocalTaskList(account, provider, providerName, id)
}
}

View File

@@ -1,421 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.servicedetection
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.caldav.CalendarColor
import at.bitfire.dav4jvm.property.caldav.CalendarDescription
import at.bitfire.dav4jvm.property.caldav.CalendarHomeSet
import at.bitfire.dav4jvm.property.caldav.CalendarProxyReadFor
import at.bitfire.dav4jvm.property.caldav.CalendarProxyWriteFor
import at.bitfire.dav4jvm.property.caldav.CalendarTimezone
import at.bitfire.dav4jvm.property.caldav.CalendarTimezoneId
import at.bitfire.dav4jvm.property.caldav.Source
import at.bitfire.dav4jvm.property.caldav.SupportedCalendarComponentSet
import at.bitfire.dav4jvm.property.carddav.AddressbookDescription
import at.bitfire.dav4jvm.property.carddav.AddressbookHomeSet
import at.bitfire.dav4jvm.property.push.PushTransports
import at.bitfire.dav4jvm.property.push.Topic
import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet
import at.bitfire.dav4jvm.property.webdav.DisplayName
import at.bitfire.dav4jvm.property.webdav.GroupMembership
import at.bitfire.dav4jvm.property.webdav.HrefListProperty
import at.bitfire.dav4jvm.property.webdav.Owner
import at.bitfire.dav4jvm.property.webdav.ResourceType
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.HomeSet
import at.bitfire.davdroid.db.Principal
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavHomeSetRepository
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.util.DavUtils.parent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.util.logging.Level
import java.util.logging.Logger
/**
* Logic for refreshing the list of collections and home-sets and related information.
*/
class CollectionListRefresher @AssistedInject constructor(
@Assisted private val service: Service,
@Assisted private val httpClient: OkHttpClient,
private val db: AppDatabase,
private val collectionRepository: DavCollectionRepository,
private val homeSetRepository: DavHomeSetRepository,
private val logger: Logger,
private val settings: SettingsManager
) {
@AssistedFactory
interface Factory {
fun create(service: Service, httpClient: OkHttpClient): CollectionListRefresher
}
/**
* Principal properties to ask the server for.
*/
private val principalProperties = arrayOf(
DisplayName.NAME,
ResourceType.NAME
)
/**
* Home-set class to use depending on the given service type.
*/
private val homeSetClass: Class<out HrefListProperty> =
when (service.type) {
Service.TYPE_CARDDAV -> AddressbookHomeSet::class.java
Service.TYPE_CALDAV -> CalendarHomeSet::class.java
else -> throw IllegalArgumentException()
}
/**
* Home-set properties to ask for in a PROPFIND request to the principal URL,
* depending on the given service type.
*/
private val homeSetProperties: Array<Property.Name> =
arrayOf( // generic WebDAV properties
DisplayName.NAME,
GroupMembership.NAME,
ResourceType.NAME
) + when (service.type) { // service-specific CalDAV/CardDAV properties
Service.TYPE_CARDDAV -> arrayOf(
AddressbookHomeSet.NAME,
)
Service.TYPE_CALDAV -> arrayOf(
CalendarHomeSet.NAME,
CalendarProxyReadFor.NAME,
CalendarProxyWriteFor.NAME
)
else -> throw IllegalArgumentException()
}
/**
* Collection properties to ask for in a PROPFIND request on a collection.
*/
private val collectionProperties: Array<Property.Name> =
arrayOf( // generic WebDAV properties
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
Owner.NAME,
ResourceType.NAME,
PushTransports.NAME, // WebDAV-Push
Topic.NAME
) + when (service.type) { // service-specific CalDAV/CardDAV properties
Service.TYPE_CARDDAV -> arrayOf(
AddressbookDescription.NAME
)
Service.TYPE_CALDAV -> arrayOf(
CalendarColor.NAME,
CalendarDescription.NAME,
CalendarTimezone.NAME,
CalendarTimezoneId.NAME,
SupportedCalendarComponentSet.NAME,
Source.NAME
)
else -> throw IllegalArgumentException()
}
/**
* Starting at given principal URL, tries to recursively find and save all user relevant home sets.
*
* @param principalUrl URL of principal to query (user-provided principal or current-user-principal)
* @param level Current recursion level (limited to 0, 1 or 2):
* - 0: We assume found home sets belong to the current-user-principal
* - 1 or 2: We assume found home sets don't directly belong to the current-user-principal
* @param alreadyQueriedPrincipals The HttpUrls of principals which have been queried already, to avoid querying principals more than once.
* @param alreadySavedHomeSets The HttpUrls of home sets which have been saved to database already, to avoid saving home sets
* more than once, which could overwrite the already set "personal" flag with `false`.
*
* @throws java.io.IOException on I/O errors
* @throws HttpException on HTTP errors
* @throws at.bitfire.dav4jvm.exception.DavException on application-level or logical errors
*/
internal fun discoverHomesets(
principalUrl: HttpUrl,
level: Int = 0,
alreadyQueriedPrincipals: MutableSet<HttpUrl> = mutableSetOf(),
alreadySavedHomeSets: MutableSet<HttpUrl> = mutableSetOf()
) {
logger.fine("Discovering homesets of $principalUrl")
val relatedResources = mutableSetOf<HttpUrl>()
// Query the URL
val principal = DavResource(httpClient, principalUrl)
val personal = level == 0
try {
principal.propfind(0, *homeSetProperties) { davResponse, _ ->
alreadyQueriedPrincipals += davResponse.href
// If response holds home sets, save them
davResponse[homeSetClass]?.let { homeSets ->
for (homeSetHref in homeSets.hrefs)
principal.location.resolve(homeSetHref)?.let { homesetUrl ->
val resolvedHomeSetUrl = UrlUtils.withTrailingSlash(homesetUrl)
if (!alreadySavedHomeSets.contains(resolvedHomeSetUrl)) {
homeSetRepository.insertOrUpdateByUrl(
// HomeSet is considered personal if this is the outer recursion call,
// This is because we assume the first call to query the current-user-principal
// Note: This is not be be confused with the DAV:owner attribute. Home sets can be owned by
// other principals while still being considered "personal" (belonging to the current-user-principal)
// and an owned home set need not always be personal either.
HomeSet(0, service.id, personal, resolvedHomeSetUrl)
)
alreadySavedHomeSets += resolvedHomeSetUrl
}
}
}
// Add related principals to be queried afterwards
if (personal) {
val relatedResourcesTypes = listOf(
// current resource is a read/write-proxy for other principals
CalendarProxyReadFor::class.java,
CalendarProxyWriteFor::class.java,
// current resource is a member of a group (principal that can also have proxies)
GroupMembership::class.java
)
for (type in relatedResourcesTypes)
davResponse[type]?.let {
for (href in it.hrefs)
principal.location.resolve(href)?.let { url ->
relatedResources += url
}
}
}
// If current resource is a calendar-proxy-read/write, it's likely that its parent is a principal, too.
davResponse[ResourceType::class.java]?.let { resourceType ->
val proxyProperties = arrayOf(
ResourceType.CALENDAR_PROXY_READ,
ResourceType.CALENDAR_PROXY_WRITE,
)
if (proxyProperties.any { resourceType.types.contains(it) })
relatedResources += davResponse.href.parent()
}
}
} catch (e: HttpException) {
if (e.code/100 == 4)
logger.log(Level.INFO, "Ignoring Client Error 4xx while looking for ${service.type} home sets", e)
else
throw e
}
// query related resources
if (level <= 1)
for (resource in relatedResources)
if (alreadyQueriedPrincipals.contains(resource))
logger.warning("$resource already queried, skipping")
else
discoverHomesets(
principalUrl = resource,
level = level + 1,
alreadyQueriedPrincipals = alreadyQueriedPrincipals,
alreadySavedHomeSets = alreadySavedHomeSets
)
}
/**
* Refreshes home-sets and their collections.
*
* Each stored home-set URL is queried (`PROPFIND`) and its collections are either saved, updated
* or marked as homeless - in case a collection was removed from its home-set.
*
* If a home-set URL in fact points to a collection directly, the collection will be saved with this URL,
* and a null value for it's home-set. Refreshing of collections without home-sets is then handled by [refreshHomelessCollections].
*/
internal fun refreshHomesetsAndTheirCollections() {
val homesets = homeSetRepository.getByService(service.id).associateBy { it.url }.toMutableMap()
for((homeSetUrl, localHomeset) in homesets) {
logger.fine("Listing home set $homeSetUrl")
// To find removed collections in this homeset: create a queue from existing collections and remove every collection that
// is successfully rediscovered. If there are collections left, after processing is done, these are marked homeless.
val localHomesetCollections = db.collectionDao()
.getByServiceAndHomeset(service.id, localHomeset.id)
.associateBy { it.url }
.toMutableMap()
try {
DavResource(httpClient, homeSetUrl).propfind(1, *collectionProperties) { response, relation ->
// Note: This callback may be called multiple times ([MultiResponseCallback])
if (!response.isSuccess())
return@propfind
if (relation == Response.HrefRelation.SELF)
// this response is about the home set itself
homeSetRepository.insertOrUpdateByUrl(localHomeset.copy(
displayName = response[DisplayName::class.java]?.displayName,
privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind != false
))
// in any case, check whether the response is about a usable collection
var collection = Collection.fromDavResponse(response) ?: return@propfind
collection = collection.copy(
serviceId = service.id,
homeSetId = localHomeset.id,
sync = shouldPreselect(collection, homesets.values),
ownerId = response[Owner::class.java]?.href // save the principal id (collection owner)
?.let { response.href.resolve(it) }
?.let { principalUrl -> Principal.fromServiceAndUrl(service, principalUrl) }
?.let { principal -> db.principalDao().insertOrUpdate(service.id, principal) }
)
logger.log(Level.FINE, "Found collection", collection)
// save or update collection if usable (ignore it otherwise)
if (isUsableCollection(collection))
collectionRepository.insertOrUpdateByUrlAndRememberFlags(collection)
// Remove this collection from queue - because it was found in the home set
localHomesetCollections.remove(collection.url)
}
} catch (e: HttpException) {
// delete home set locally if it was not accessible (40x)
if (e.code in arrayOf(403, 404, 410))
homeSetRepository.delete(localHomeset)
}
// Mark leftover (not rediscovered) collections from queue as homeless (remove association)
for ((_, homelessCollection) in localHomesetCollections)
collectionRepository.insertOrUpdateByUrlAndRememberFlags(
homelessCollection.copy(homeSetId = null)
)
}
}
/**
* Refreshes collections which don't have a homeset.
*
* It queries each stored collection with a homeSetId of "null" and either updates or deletes (if inaccessible or unusable) them.
*/
internal fun refreshHomelessCollections() {
val homelessCollections = db.collectionDao().getByServiceAndHomeset(service.id, null).associateBy { it.url }.toMutableMap()
for((url, localCollection) in homelessCollections) try {
DavResource(httpClient, url).propfind(0, *collectionProperties) { response, _ ->
if (!response.isSuccess()) {
collectionRepository.delete(localCollection)
return@propfind
}
// Save or update the collection, if usable, otherwise delete it
Collection.fromDavResponse(response)?.let { collection ->
if (!isUsableCollection(collection))
return@let
collectionRepository.insertOrUpdateByUrlAndRememberFlags(collection.copy(
serviceId = localCollection.serviceId, // use same service ID as previous entry
ownerId = response[Owner::class.java]?.href // save the principal id (collection owner)
?.let { response.href.resolve(it) }
?.let { principalUrl -> Principal.fromServiceAndUrl(service, principalUrl) }
?.let { principal -> db.principalDao().insertOrUpdate(service.id, principal) }
))
} ?: collectionRepository.delete(localCollection)
}
} catch (e: HttpException) {
// delete collection locally if it was not accessible (40x)
if (e.code in arrayOf(403, 404, 410))
collectionRepository.delete(localCollection)
else
throw e
}
}
/**
* Refreshes the principals (get their current display names).
* Also removes principals which do not own any collections anymore.
*/
internal fun refreshPrincipals() {
// Refresh principals (collection owner urls)
val principals = db.principalDao().getByService(service.id)
for (oldPrincipal in principals) {
val principalUrl = oldPrincipal.url
logger.fine("Querying principal $principalUrl")
try {
DavResource(httpClient, principalUrl).propfind(0, *principalProperties) { response, _ ->
if (!response.isSuccess())
return@propfind
Principal.fromDavResponse(service.id, response)?.let { principal ->
logger.fine("Got principal: $principal")
db.principalDao().insertOrUpdate(service.id, principal)
}
}
} catch (e: HttpException) {
logger.info("Principal update failed with response code ${e.code}. principalUrl=$principalUrl")
}
}
// Delete principals which don't own any collections
db.principalDao().getAllWithoutCollections().forEach {principal ->
db.principalDao().delete(principal)
}
}
/**
* Finds out whether given collection is usable, by checking that either
* - CalDAV/CardDAV: service and collection type match, or
* - WebCal: subscription source URL is not empty
*/
private fun isUsableCollection(collection: Collection) =
(service.type == Service.TYPE_CARDDAV && collection.type == Collection.TYPE_ADDRESSBOOK) ||
(service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(collection.type)) ||
(collection.type == Collection.TYPE_WEBCAL && collection.source != null)
/**
* Whether to preselect the given collection for synchronisation, according to the
* settings [Settings.PRESELECT_COLLECTIONS] (see there for allowed values) and
* [Settings.PRESELECT_COLLECTIONS_EXCLUDED].
*
* A collection is considered _personal_ if it is found in one of the current-user-principal's home-sets.
*
* Before a collection is pre-selected, we check whether its URL matches the regexp in
* [Settings.PRESELECT_COLLECTIONS_EXCLUDED], in which case *false* is returned.
*
* @param collection the collection to check
* @param homeSets list of home-sets (to check whether collection is in a personal home-set)
* @return *true* if the collection should be preselected for synchronization; *false* otherwise
*/
internal fun shouldPreselect(collection: Collection, homeSets: Iterable<HomeSet>): Boolean {
val shouldPreselect = settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS)
val excluded by lazy {
val excludedRegex = settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED)
if (!excludedRegex.isNullOrEmpty())
Regex(excludedRegex).containsMatchIn(collection.url.toString())
else
false
}
return when (shouldPreselect) {
Settings.PRESELECT_COLLECTIONS_ALL ->
// preselect if collection url is not excluded
!excluded
Settings.PRESELECT_COLLECTIONS_PERSONAL ->
// preselect if is personal (in a personal home-set), but not excluded
homeSets
.filter { homeset -> homeset.personal }
.map { homeset -> homeset.id }
.contains(collection.homeSetId)
&& !excluded
else -> // don't preselect
false
}
}
}

View File

@@ -1,291 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.text.format.Formatter
import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.MultiResponseCallback
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.property.caldav.CalendarData
import at.bitfire.dav4jvm.property.caldav.GetCTag
import at.bitfire.dav4jvm.property.caldav.MaxResourceSize
import at.bitfire.dav4jvm.property.caldav.ScheduleTag
import at.bitfire.dav4jvm.property.webdav.GetETag
import at.bitfire.dav4jvm.property.webdav.SupportedReportSet
import at.bitfire.dav4jvm.property.webdav.SyncToken
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalEvent
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.ical4android.util.DateUtils
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import net.fortuna.ical4j.model.Component
import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.property.Action
import okhttp3.HttpUrl
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.ByteArrayOutputStream
import java.io.Reader
import java.io.StringReader
import java.time.Duration
import java.time.ZonedDateTime
import java.util.logging.Level
/**
* Synchronization manager for CalDAV collections; handles events (VEVENT).
*/
class CalendarSyncManager @AssistedInject constructor(
@Assisted account: Account,
@Assisted accountSettings: AccountSettings,
@Assisted extras: Array<String>,
@Assisted httpClient: HttpClient,
@Assisted authority: String,
@Assisted syncResult: SyncResult,
@Assisted localCalendar: LocalCalendar,
@Assisted collection: Collection
): SyncManager<LocalEvent, LocalCalendar, DavCalendar>(
account,
accountSettings,
httpClient,
extras,
authority,
syncResult,
localCalendar,
collection
) {
@AssistedFactory
interface Factory {
fun calendarSyncManager(
account: Account,
accountSettings: AccountSettings,
extras: Array<String>,
httpClient: HttpClient,
authority: String,
syncResult: SyncResult,
localCalendar: LocalCalendar,
collection: Collection
): CalendarSyncManager
}
override fun prepare(): Boolean {
davCollection = DavCalendar(httpClient.okHttpClient, collection.url)
// if there are dirty exceptions for events, mark their master events as dirty, too
localCollection.processDirtyExceptions()
// now find dirty events that have no instances and set them to deleted
localCollection.deleteDirtyEventsWithoutInstances()
return true
}
override fun queryCapabilities(): SyncState? =
SyncException.wrapWithRemoteResource(collection.url) {
var syncState: SyncState? = null
davCollection.propfind(0, MaxResourceSize.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[MaxResourceSize::class.java]?.maxSize?.let { maxSize ->
logger.info("Calendar accepts events up to ${Formatter.formatFileSize(context, maxSize)}")
}
response[SupportedReportSet::class.java]?.let { supported ->
hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
syncState = syncState(response)
}
}
logger.info("Calendar supports Collection Sync: $hasCollectionSync")
syncState
}
override fun syncAlgorithm() =
if (accountSettings.getTimeRangePastDays() != null || !hasCollectionSync)
SyncAlgorithm.PROPFIND_REPORT
else
SyncAlgorithm.COLLECTION_SYNC
override fun processLocallyDeleted(): Boolean {
if (localCollection.readOnly) {
var modified = false
for (event in localCollection.findDeleted()) {
logger.warning("Restoring locally deleted event (read-only calendar!)")
SyncException.wrapWithLocalResource(event) {
event.resetDeleted()
}
modified = true
}
// This is unfortunately ugly: When an event has been inserted to a read-only calendar
// it's not enough to force synchronization (by returning true),
// but we also need to make sure all events are downloaded again.
if (modified)
localCollection.lastSyncState = null
return modified
}
// mirror deletions to remote collection (DELETE)
return super.processLocallyDeleted()
}
override fun uploadDirty(): Boolean {
var modified = false
if (localCollection.readOnly) {
for (event in localCollection.findDirty()) {
logger.warning("Resetting locally modified event to ETag=null (read-only calendar!)")
SyncException.wrapWithLocalResource(event) {
event.clearDirty(null, null)
}
modified = true
}
// This is unfortunately ugly: When an event has been inserted to a read-only calendar
// it's not enough to force synchronization (by returning true),
// but we also need to make sure all events are downloaded again.
if (modified)
localCollection.lastSyncState = null
}
// generate UID/file name for newly created events
val superModified = super.uploadDirty()
// return true when any operation returned true
return modified or superModified
}
override fun generateUpload(resource: LocalEvent): RequestBody =
SyncException.wrapWithLocalResource(resource) {
val event = requireNotNull(resource.event)
logger.log(Level.FINE, "Preparing upload of event ${resource.fileName}", event)
val os = ByteArrayOutputStream()
event.write(os)
os.toByteArray().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8)
}
override fun listAllRemote(callback: MultiResponseCallback) {
// calculate time range limits
val limitStart = accountSettings.getTimeRangePastDays()?.let { pastDays ->
ZonedDateTime.now().minusDays(pastDays.toLong()).toInstant()
}
return SyncException.wrapWithRemoteResource(collection.url) {
logger.info("Querying events since $limitStart")
davCollection.calendarQuery(Component.VEVENT, limitStart, null, callback)
}
}
override fun downloadRemote(bunch: List<HttpUrl>) {
logger.info("Downloading ${bunch.size} iCalendars: $bunch")
SyncException.wrapWithRemoteResource(collection.url) {
davCollection.multiget(bunch) { response, _ ->
/*
* Real-world servers may return:
*
* - unrelated resources
* - the collection itself
* - the requested resources, but with a different collection URL (for instance, `/cal/1.ics` instead of `/shared-cal/1.ics`).
*
* So we:
*
* - ignore unsuccessful responses,
* - ignore responses without requested calendar data (should also ignore collections and hopefully unrelated resources), and
* - take the last segment of the href as the file name and assume that it's in the requested collection.
*/
SyncException.wrapWithRemoteResource(response.href) wrapResource@ {
if (!response.isSuccess()) {
logger.warning("Ignoring non-successful multi-get response for ${response.href}")
return@wrapResource
}
val iCal = response[CalendarData::class.java]?.iCalendar
if (iCal == null) {
logger.warning("Ignoring multi-get response without calendar-data")
return@wrapResource
}
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val scheduleTag = response[ScheduleTag::class.java]?.scheduleTag
processVEvent(
response.href.lastSegment,
eTag,
scheduleTag,
StringReader(iCal)
)
}
}
}
}
override fun postProcess() {}
// helpers
private fun processVEvent(fileName: String, eTag: String, scheduleTag: String?, reader: Reader) {
val events: List<Event>
try {
events = Event.eventsFromReader(reader)
} catch (e: InvalidCalendarException) {
logger.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e)
notifyInvalidResource(e, fileName)
return
}
if (events.size == 1) {
val event = events.first()
// set default reminder for non-full-day events, if requested
val defaultAlarmMinBefore = accountSettings.getDefaultAlarm()
if (defaultAlarmMinBefore != null && DateUtils.isDateTime(event.dtStart) && event.alarms.isEmpty()) {
val alarm = VAlarm(Duration.ofMinutes(-defaultAlarmMinBefore.toLong())).apply {
// Sets METHOD_ALERT instead of METHOD_DEFAULT in the calendar provider.
// Needed for calendars to actually show a notification.
properties += Action.DISPLAY
}
logger.log(Level.FINE, "${event.uid}: Adding default alarm", alarm)
event.alarms += alarm
}
// update local event, if it exists
val local = localCollection.findByName(fileName)
SyncException.wrapWithLocalResource(local) {
if (local != null) {
logger.log(Level.INFO, "Updating $fileName in local calendar", event)
local.eTag = eTag
local.scheduleTag = scheduleTag
local.update(event)
} else {
logger.log(Level.INFO, "Adding $fileName to local calendar", event)
val newLocal = LocalEvent(localCollection, event, fileName, eTag, scheduleTag, LocalResource.FLAG_REMOTELY_PRESENT)
SyncException.wrapWithLocalResource(newLocal) {
newLocal.add()
}
}
}
} else
logger.info("Received VCALENDAR with not exactly one VEVENT with UID and without RECURRENCE-ID; ignoring $fileName")
}
override fun notifyInvalidResourceTitle(): String =
context.getString(R.string.sync_invalid_event)
}

View File

@@ -1,191 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.accounts.AccountManager
import android.app.Service
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.os.Bundle
import android.os.IBinder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_COLLECTION_ID
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.worker.BaseSyncWorker
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
import javax.inject.Provider
abstract class SyncAdapterService: Service() {
@Inject
lateinit var syncAdapter: Provider<SyncAdapter>
override fun onBind(intent: Intent?): IBinder {
return syncAdapter.get().syncAdapterBinder
}
/**
* Entry point for the Sync Adapter Framework.
*
* Handles incoming sync requests from the Sync Adapter Framework.
*
* Although we do not use the sync adapter for syncing anymore, we keep this sole
* adapter to provide exported services, which allow android system components and calendar,
* contacts or task apps to sync via DAVx5.
*
* All Sync Adapter Framework related interaction should happen inside [SyncFrameworkIntegration].
*/
class SyncAdapter @Inject constructor(
private val accountSettingsFactory: AccountSettings.Factory,
private val collectionRepository: DavCollectionRepository,
private val serviceRepository: DavServiceRepository,
@ApplicationContext context: Context,
private val logger: Logger,
private val syncConditionsFactory: SyncConditions.Factory,
private val syncWorkerManager: SyncWorkerManager
): AbstractThreadedSyncAdapter(
context,
true // isSyncable shouldn't be -1 because DAVx5 (SyncFrameworkIntegration) sets it to 0 or 1.
// However, if it is -1 by accident, set it to 1 to avoid endless sync loops.
) {
/**
* Scope used to wait until the synchronization is finished. Will be cancelled when the sync framework
* requests cancellation.
*/
private val waitScope = CoroutineScope(Dispatchers.Default)
override fun onPerformSync(accountOrAddressBookAccount: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
// We have to pass this old SyncFramework extra for an Android 7 workaround
val upload = extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD)
logger.info("Sync request via sync framework for $accountOrAddressBookAccount $authority (upload=$upload)")
// If we should sync an address book account - find the account storing the settings
val account = if (accountOrAddressBookAccount.type == context.getString(R.string.account_type_address_book))
AccountManager.get(context)
.getUserData(accountOrAddressBookAccount, USER_DATA_COLLECTION_ID)
?.toLongOrNull()
?.let { collectionId ->
collectionRepository.get(collectionId)?.let { collection ->
serviceRepository.get(collection.serviceId)?.let { service ->
Account(service.accountName, context.getString(R.string.account_type))
}
}
}
else
accountOrAddressBookAccount
if (account == null) {
logger.warning("Address book account $accountOrAddressBookAccount doesn't have an associated collection")
return
}
val accountSettings = try {
accountSettingsFactory.create(account)
} catch (e: InvalidAccountException) {
logger.log(Level.WARNING, "Account doesn't exist anymore", e)
return
}
val syncConditions = syncConditionsFactory.create(accountSettings)
// Should we run the sync at all?
if (!syncConditions.wifiConditionsMet()) {
logger.info("Sync conditions not met. Aborting sync framework initiated sync")
return
}
logger.fine("Starting OneTimeSyncWorker for $account $authority and waiting for it")
val workerName = syncWorkerManager.enqueueOneTime(account, dataType = SyncDataType.fromAuthority(authority), upload = upload)
/* Because we are not allowed to observe worker state on a background thread, we can not
use it to block the sync adapter. Instead we use a Flow to get notified when the sync
has finished. */
val workManager = WorkManager.getInstance(context)
try {
val waitJob = waitScope.launch {
// wait for finished worker state
workManager.getWorkInfosForUniqueWorkFlow(workerName).collect { infoList ->
for (info in infoList)
if (info.state.isFinished) {
if (info.state == WorkInfo.State.FAILED) {
if (info.outputData.getBoolean(BaseSyncWorker.OUTPUT_TOO_MANY_RETRIES, false))
syncResult.tooManyRetries = true
else
syncResult.databaseError = true
}
cancel("$workerName has finished")
}
}
}
runBlocking {
withTimeout(10 * 60 * 1000) { // block max. 10 minutes
waitJob.join() // wait until worker has finished
}
}
} catch (_: CancellationException) {
// waiting for work was cancelled, either by timeout or because the worker has finished
logger.fine("Not waiting for OneTimeSyncWorker anymore.")
}
logger.log(Level.INFO, "Returning to sync framework.", syncResult)
}
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
logger.log(Level.WARNING, "Security exception for $account/$authority")
}
override fun onSyncCanceled() {
logger.info("Sync adapter requested cancellation won't cancel sync, but also won't block sync framework anymore")
// unblock sync framework
waitScope.cancel()
}
override fun onSyncCanceled(thread: Thread) = onSyncCanceled()
}
}
// exported sync adapter services; we need a separate class for each authority
@AndroidEntryPoint
class CalendarsSyncAdapterService: SyncAdapterService()
@AndroidEntryPoint
class ContactsSyncAdapterService: SyncAdapterService()
@AndroidEntryPoint
class JtxSyncAdapterService: SyncAdapterService()
@AndroidEntryPoint
class OpenTasksSyncAdapterService: SyncAdapterService()
@AndroidEntryPoint
class TasksOrgSyncAdapterService: SyncAdapterService()

View File

@@ -1,57 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync
import android.provider.CalendarContract
import android.provider.ContactsContract
import at.bitfire.ical4android.TaskProvider
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
enum class SyncDataType {
CONTACTS,
EVENTS,
TASKS;
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SyncDataTypeEntryPoint {
fun tasksAppManager(): TasksAppManager
}
fun possibleAuthorities(): List<String> =
when (this) {
CONTACTS -> listOf(
ContactsContract.AUTHORITY
)
EVENTS -> listOf(
CalendarContract.AUTHORITY
)
TASKS ->
TaskProvider.ProviderName.entries.map { it.authority }
}
companion object {
fun fromAuthority(authority: String): SyncDataType {
return when (authority) {
ContactsContract.AUTHORITY ->
CONTACTS
CalendarContract.AUTHORITY ->
EVENTS
TaskProvider.ProviderName.JtxBoard.authority,
TaskProvider.ProviderName.TasksOrg.authority,
TaskProvider.ProviderName.OpenTasks.authority ->
TASKS
else -> throw IllegalArgumentException("Unknown authority: $authority")
}
}
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
/**
* Creates a [CoroutineDispatcher] with multiple threads that guarantees that the threads
* have set their contextClassLoader to the application context's class loader.
*
* We use our own dispatcher to
*
* - make sure that all threads have [Thread.getContextClassLoader] set, which is required for ical4j (because it uses [ServiceLoader]),
* - control the global number of sync threads.
*/
@Singleton
class SyncDispatcher @Inject constructor(
@ApplicationContext context: Context
) {
val dispatcher = createDispatcher(context.classLoader)
private fun createDispatcher(classLoader: ClassLoader): CoroutineDispatcher =
ThreadPoolExecutor(
0, Runtime.getRuntime().availableProcessors(),
10, TimeUnit.SECONDS, LinkedBlockingQueue(),
object: ThreadFactory {
val group = ThreadGroup("sync-work")
override fun newThread(r: Runnable) =
Thread(group, r).apply {
contextClassLoader = classLoader
}
}
).asCoroutineDispatcher()
}

View File

@@ -1,137 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.ContentResolver
import android.provider.CalendarContract
import androidx.annotation.WorkerThread
import java.util.logging.Logger
import javax.inject.Inject
/**
* Handles all Sync Adapter Framework related interaction. Other classes should never call
* `ContentResolver.setIsSyncable()` or something similar themselves. Everything sync-framework
* related must be handled by this class.
*
* Sync requests from the Sync Adapter Framework are handled by [SyncAdapterService].
*/
class SyncFrameworkIntegration @Inject constructor(
private val logger: Logger
) {
/**
* Gets the global auto-sync setting that applies to all the providers and accounts. If this is
* false then the per-provider auto-sync setting is ignored.
*/
fun getMasterSyncAutomatically() =
ContentResolver.getMasterSyncAutomatically()
/**
* Check if this account/provider is syncable.
*/
fun isSyncable(account: Account, authority: String): Boolean =
ContentResolver.getIsSyncable(account, authority) > 0
/**
* Enable this account/provider to be syncable.
*/
fun enableSyncAbility(account: Account, authority: String) {
logger.fine("Enabling sync framework for account=$account, authority=$authority")
if (ContentResolver.getIsSyncable(account, authority) != 1)
ContentResolver.setIsSyncable(account, authority, 1)
}
/**
* Disable this account/provider to be syncable.
*/
fun disableSyncAbility(account: Account, authority: String) {
logger.fine("Disabling sync framework for account=$account, authority=$authority")
if (ContentResolver.getIsSyncable(account, authority) != 0)
ContentResolver.setIsSyncable(account, authority, 0)
}
/**
* Check if the provider should be synced when content (contact, calendar event or task) changes.
*/
fun syncsOnContentChange(account: Account, authority: String) =
ContentResolver.getSyncAutomatically(account, authority)
/**
* Enable syncing on content (contact, calendar event or task) changes.
*/
fun enableSyncOnContentChange(account: Account, authority: String) {
if (!isSyncable(account, authority))
enableSyncAbility(account, authority)
if (!ContentResolver.getSyncAutomatically(account, authority))
setSyncOnContentChange(account, authority, true)
}
/**
* Disable syncing on content (contact, calendar event or task) changes.
*/
fun disableSyncOnContentChange(account: Account, authority: String) {
if (ContentResolver.getSyncAutomatically(account, authority))
setSyncOnContentChange(account, authority, false)
}
/**
* Enables/disables sync adapter automatic sync (content triggered sync) for the given
* account and authority. Does *not* call [ContentResolver.setIsSyncable].
*
* We use the sync adapter framework only for the trigger, actual syncing is implemented
* with WorkManager. The trigger comes in through SyncAdapterService.
*
* Because there is no callback for when the sync status/interval has been updated, this method
* blocks until the sync-on-content-change has been enabled or disabled, so it should not be
* called from the UI thread.
*
* @param account account to enable/disable content change sync triggers for
* @param enable *true* enables automatic sync; *false* disables it
* @param authority sync authority (like [CalendarContract.AUTHORITY])
* @return whether the content triggered sync was enabled successfully
*/
@WorkerThread
private fun setSyncOnContentChange(account: Account, authority: String, enable: Boolean): Boolean {
logger.fine("Setting content-triggered syncs (sync framework) for account=$account, authority=$authority to enable=$enable")
// Try up to 10 times with 100 ms pause
repeat(10) {
if (setContentTrigger(account, authority, enable)) {
// Remove periodic syncs created by ContentResolver.setSyncAutomatically
ContentResolver.getPeriodicSyncs(account, authority).forEach { periodicSync ->
ContentResolver.removePeriodicSync(
periodicSync.account,
periodicSync.authority,
periodicSync.extras
)
}
// Set successfully
return true
}
Thread.sleep(100)
}
// Failed to set
return false
}
/**
* Enable or disable content change sync triggers of the Sync Adapter Framework.
*
* @param account account to enable/disable content change sync triggers for
* @param enable *true* enables automatic sync; *false* disables it
* @param authority sync authority (like [CalendarContract.AUTHORITY])
* @return whether the content triggered sync was enabled successfully
*/
private fun setContentTrigger(account: Account, authority: String, enable: Boolean): Boolean =
if (enable) {
ContentResolver.setSyncAutomatically(account, authority, true)
/* return */ ContentResolver.getSyncAutomatically(account, authority)
} else {
ContentResolver.setSyncAutomatically(account, authority, false)
/* return */ !ContentResolver.getSyncAutomatically(account, authority)
}
}

View File

@@ -1,189 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.text.format.Formatter
import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.MultiResponseCallback
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.property.caldav.CalendarData
import at.bitfire.dav4jvm.property.caldav.GetCTag
import at.bitfire.dav4jvm.property.caldav.MaxResourceSize
import at.bitfire.dav4jvm.property.webdav.GetETag
import at.bitfire.dav4jvm.property.webdav.SyncToken
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils.lastSegment
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.ical4android.Task
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import okhttp3.HttpUrl
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.ByteArrayOutputStream
import java.io.Reader
import java.io.StringReader
import java.util.logging.Level
/**
* Synchronization manager for CalDAV collections; handles tasks (VTODO)
*/
class TasksSyncManager @AssistedInject constructor(
@Assisted account: Account,
@Assisted accountSettings: AccountSettings,
@Assisted httpClient: HttpClient,
@Assisted extras: Array<String>,
@Assisted authority: String,
@Assisted syncResult: SyncResult,
@Assisted localCollection: LocalTaskList,
@Assisted collection: Collection
): SyncManager<LocalTask, LocalTaskList, DavCalendar>(
account,
accountSettings,
httpClient,
extras,
authority,
syncResult,
localCollection,
collection
) {
@AssistedFactory
interface Factory {
fun tasksSyncManager(
account: Account,
accountSettings: AccountSettings,
httpClient: HttpClient,
extras: Array<String>,
authority: String,
syncResult: SyncResult,
localCollection: LocalTaskList,
collection: Collection
): TasksSyncManager
}
override fun prepare(): Boolean {
davCollection = DavCalendar(httpClient.okHttpClient, collection.url)
return true
}
override fun queryCapabilities() =
SyncException.wrapWithRemoteResource(collection.url) {
var syncState: SyncState? = null
davCollection.propfind(0, MaxResourceSize.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[MaxResourceSize::class.java]?.maxSize?.let { maxSize ->
logger.info("Calendar accepts tasks up to ${Formatter.formatFileSize(context, maxSize)}")
}
syncState = syncState(response)
}
}
syncState
}
override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT
override fun generateUpload(resource: LocalTask): RequestBody =
SyncException.wrapWithLocalResource(resource) {
val task = requireNotNull(resource.task)
logger.log(Level.FINE, "Preparing upload of task ${resource.fileName}", task)
val os = ByteArrayOutputStream()
task.write(os)
os.toByteArray().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8)
}
override fun listAllRemote(callback: MultiResponseCallback) {
SyncException.wrapWithRemoteResource(collection.url) {
logger.info("Querying tasks")
davCollection.calendarQuery("VTODO", null, null, callback)
}
}
override fun downloadRemote(bunch: List<HttpUrl>) {
logger.info("Downloading ${bunch.size} iCalendars: $bunch")
// multiple iCalendars, use calendar-multi-get
SyncException.wrapWithRemoteResource(collection.url) {
davCollection.multiget(bunch) { response, _ ->
// See CalendarSyncManager for more information about the multi-get response
SyncException.wrapWithRemoteResource(response.href) wrapResource@ {
if (!response.isSuccess()) {
logger.warning("Ignoring non-successful multi-get response for ${response.href}")
return@wrapResource
}
val iCal = response[CalendarData::class.java]?.iCalendar
if (iCal == null) {
logger.warning("Ignoring multi-get response without calendar-data")
return@wrapResource
}
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
processVTodo(response.href.lastSegment, eTag, StringReader(iCal))
}
}
}
}
override fun postProcess() {
val touched = localCollection.touchRelations()
logger.info("Touched $touched relations")
}
// helpers
private fun processVTodo(fileName: String, eTag: String, reader: Reader) {
val tasks: List<Task>
try {
tasks = Task.tasksFromReader(reader)
} catch (e: InvalidCalendarException) {
logger.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e)
notifyInvalidResource(e, fileName)
return
}
if (tasks.size == 1) {
val newData = tasks.first()
// update local task, if it exists
val local = localCollection.findByName(fileName)
SyncException.wrapWithLocalResource(local) {
if (local != null) {
logger.log(Level.INFO, "Updating $fileName in local task list", newData)
local.eTag = eTag
local.update(newData)
} else {
logger.log(Level.INFO, "Adding $fileName to local task list", newData)
val newLocal = LocalTask(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)
SyncException.wrapWithLocalResource(newLocal) {
newLocal.add()
}
}
}
} else
logger.info("Received VCALENDAR with not exactly one VTODO; ignoring $fileName")
}
override fun notifyInvalidResourceTitle(): String =
context.getString(R.string.sync_invalid_task)
}

View File

@@ -1,358 +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.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
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.Home
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.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.composable.PixelBoxes
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.util.withJson
import dagger.BindsOptionalOf
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.text.Collator
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.LinkedList
import java.util.Locale
import java.util.Optional
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
@AndroidEntryPoint
class AboutActivity: AppCompatActivity() {
val model by viewModels<Model>()
@Inject
lateinit var licenseInfoProvider: Optional<AppLicenseInfoProvider>
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
val uriHandler = LocalUriHandler.current
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onSupportNavigateUp() }) {
Icon(
Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource(R.string.navigate_up)
)
}
},
title = {
Text(stringResource(R.string.navigation_drawer_about))
},
actions = {
IconButton(onClick = {
uriHandler.openUri(Constants.HOMEPAGE_URL
.buildUpon()
.withStatParams("AboutActivity")
.build().toString())
}) {
Icon(
Icons.Default.Home,
contentDescription = stringResource(R.string.navigation_drawer_website)
)
}
}
)
}
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
val scope = rememberCoroutineScope()
val state = rememberPagerState(pageCount = { 3 })
TabRow(state.currentPage) {
Tab(state.currentPage == 0, onClick = {
scope.launch { state.scrollToPage(0) }
}) {
Text(
stringResource(R.string.app_name),
modifier = Modifier.padding(8.dp)
)
}
Tab(state.currentPage == 1, onClick = {
scope.launch { state.scrollToPage(1) }
}) {
Text(
stringResource(R.string.about_translations),
modifier = Modifier.padding(8.dp)
)
}
Tab(state.currentPage == 2, onClick = {
scope.launch { state.scrollToPage(2) }
}) {
Text(
stringResource(R.string.about_libraries),
modifier = Modifier.padding(8.dp)
)
}
}
HorizontalPager(
state,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalAlignment = Alignment.Top
) { index ->
when (index) {
0 -> AboutApp(licenseInfoProvider = licenseInfoProvider.getOrNull())
1 -> {
val translations = model.translations.observeAsState(emptyList())
TranslatorsGallery(translations.value)
}
2 -> LibrariesContainer(Modifier.fillMaxSize(),
itemContentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
itemSpacing = 8.dp,
librariesBlock = { ctx ->
Libs.Builder()
.withJson(ctx, R.raw.aboutlibraries)
.build()
})
}
}
}
}
}
}
}
@HiltViewModel
class Model @Inject constructor(
@ApplicationContext val context: Context,
private val logger: Logger
): ViewModel() {
data class Translation(
val language: String,
val translators: Set<String>
)
val translations = MutableLiveData<List<Translation>>()
init {
viewModelScope.launch(Dispatchers.IO) {
loadTranslations()
}
}
private fun loadTranslations() {
try {
context.resources.assets.open("translators.json").use { stream ->
val jsonTranslations = JSONObject(stream.readBytes().decodeToString())
val result = LinkedList<Translation>()
for (langCode in jsonTranslations.keys()) {
val jsonTranslators = jsonTranslations.getJSONArray(langCode)
val translators = Array<String>(jsonTranslators.length()) { idx ->
jsonTranslators.getString(idx)
}
val langTag = langCode.replace('_', '-')
val language = Locale.forLanguageTag(langTag).displayName
result += Translation(language, translators.toSet())
}
// sort translations by localized language name
val collator = Collator.getInstance()
result.sortWith { o1, o2 ->
collator.compare(o1.language, o2.language)
}
translations.postValue(result)
}
} catch (e: Exception) {
logger.log(Level.WARNING, "Couldn't load translators", e)
}
}
}
interface AppLicenseInfoProvider {
@Composable
fun LicenseInfo()
}
@Module
@InstallIn(ActivityComponent::class)
interface AppLicenseInfoProviderModule {
@BindsOptionalOf
fun appLicenseInfoProvider(): AppLicenseInfoProvider
}
}
@Composable
fun AboutApp(licenseInfoProvider: AboutActivity.AppLicenseInfoProvider? = null) {
Column(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())) {
Image(
UiUtils.adaptiveIconPainterResource(R.mipmap.ic_launcher),
contentDescription = stringResource(R.string.app_name),
modifier = Modifier
.size(128.dp)
.align(Alignment.CenterHorizontally)
)
Text(
stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
Text(
stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
stringResource(R.string.about_copyright),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
Text(
stringResource(R.string.about_license_info_no_warranty),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
)
PixelBoxes(
arrayOf(Color(0xFFFCF434), Color.White, Color(0xFF9C59D1), Color.Black),
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp)
)
licenseInfoProvider?.LicenseInfo()
}
}
@Composable
@Preview
fun AboutApp_Preview() {
AboutApp(licenseInfoProvider = object : AboutActivity.AppLicenseInfoProvider {
@Composable
override fun LicenseInfo() {
Text("Some flavored License Info")
}
})
}
@Composable
fun TranslatorsGallery(
translations: List<AboutActivity.Model.Translation>
) {
val collator = Collator.getInstance()
LazyColumn(Modifier.padding(8.dp)) {
items(translations) { translation ->
Text(
translation.language,
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(vertical = 4.dp)
)
Text(
translation.translators
.sortedWith { a, b -> collator.compare(a, b) }
.joinToString(" · "),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
}
@Composable
@Preview
fun TranslatorsGallery_Sample() {
TranslatorsGallery(listOf(
AboutActivity.Model.Translation("Some Language", setOf("User 1", "User 2")),
AboutActivity.Model.Translation("Another Language", setOf("User 3", "User 4"))
))
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.LocalView
import at.bitfire.davdroid.ui.composable.SafeAndroidUriHandler
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val view = LocalView.current
SideEffect {
// If applicable, call Activity.enableEdgeToEdge to enable edge-to-edge layout on Android <15, too.
// When we have moved everything into one Activity with Compose navigation, we can call it there instead.
(view.context as? AppCompatActivity)?.enableEdgeToEdge(
navigationBarStyle = SystemBarStyle.auto(
lightScrim = M3ColorScheme.lightScheme.scrim.toArgb(),
darkScrim = M3ColorScheme.darkScheme.scrim.toArgb()
) { darkTheme }
)
}
// Apply SafeAndroidUriHandler to the composition
val uriHandler = SafeAndroidUriHandler(LocalContext.current)
CompositionLocalProvider(LocalUriHandler provides uriHandler) {
MaterialTheme(
colorScheme = if (!darkTheme)
M3ColorScheme.lightScheme
else
M3ColorScheme.darkScheme,
content = content
)
}
}

View File

@@ -1,72 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.account
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.map
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.CollectionType
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import javax.inject.Inject
/**
* Gets a list of collections for a service and type, optionally filtered by "show only personal" setting.
*
* Takes the "force read-only address books" setting into account: if set, all address books will have "forceReadOnly" set.
*/
class GetServiceCollectionPagerUseCase @Inject constructor(
val collectionRepository: DavCollectionRepository,
val settings: SettingsManager
) {
companion object {
const val PAGER_SIZE = 20
}
val forceReadOnlyAddressBooksFlow = settings.getBooleanFlow(Settings.FORCE_READ_ONLY_ADDRESSBOOKS, false)
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(
serviceFlow: Flow<Service?>,
@CollectionType collectionType: String,
showOnlyPersonalFlow: Flow<Boolean>
): Flow<PagingData<Collection>> =
combine(serviceFlow, showOnlyPersonalFlow, forceReadOnlyAddressBooksFlow) { service, onlyPersonal, forceReadOnlyAddressBooks ->
service?.let { service ->
val dataFlow = Pager(
config = PagingConfig(PAGER_SIZE),
pagingSourceFactory = {
if (onlyPersonal == true)
collectionRepository.pagePersonalByServiceAndType(service.id, collectionType)
else
collectionRepository.pageByServiceAndType(service.id, collectionType)
}
).flow
// set "forceReadOnly" for every address book if requested
if (forceReadOnlyAddressBooks && collectionType == Collection.TYPE_ADDRESSBOOK)
dataFlow.map { pagingData ->
pagingData.map { collection ->
collection.copy(forceReadOnly = true)
}
}
else
dataFlow
} ?: flowOf(PagingData.empty())
}.flatMapLatest { it }
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.composable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import at.bitfire.davdroid.R
@Composable
fun PasswordTextField(
password: String,
labelText: String?,
onPasswordChange: (String) -> Unit,
modifier: Modifier = Modifier,
leadingIcon: @Composable (() -> Unit)? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
keyboardActions: KeyboardActions = KeyboardActions.Default,
enabled: Boolean = true,
readOnly: Boolean = false,
isError: Boolean = false
) {
var passwordVisible by remember { mutableStateOf(false) }
OutlinedTextField(
value = password,
onValueChange = onPasswordChange,
label = labelText?.let { { Text(it) } },
leadingIcon = leadingIcon,
isError = isError,
singleLine = true,
enabled = enabled,
readOnly = readOnly,
modifier = modifier.focusGroup(),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(
enabled = enabled,
onClick = { passwordVisible = !passwordVisible }
) {
if (passwordVisible)
Icon(Icons.Default.VisibilityOff, stringResource(R.string.login_password_hide))
else
Icon(Icons.Default.Visibility, stringResource(R.string.login_password_show))
}
}
)
}
@Composable
@Preview
fun PasswordTextField_Sample() {
PasswordTextField(
password = "",
labelText = "labelText",
enabled = true,
isError = false,
onPasswordChange = {},
)
}
@Composable
@Preview
fun PasswordTextField_Sample_Filled() {
PasswordTextField(
password = "password",
labelText = "labelText",
enabled = true,
isError = false,
onPasswordChange = {},
)
}
@Composable
@Preview
fun PasswordTextField_Sample_Error() {
PasswordTextField(
password = "password",
labelText = "labelText",
enabled = true,
isError = true,
onPasswordChange = {},
)
}
@Composable
@Preview
fun PasswordTextField_Sample_Disabled() {
PasswordTextField(
password = "password",
labelText = "labelText",
enabled = false,
isError = false,
onPasswordChange = {},
)
}

View File

@@ -1,12 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.widget
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class SyncButtonWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = SyncButtonWidget()
}

View File

@@ -1,761 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.webdav
import android.app.AuthenticationRequiredException
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.res.AssetFileDescriptor
import android.database.Cursor
import android.database.MatrixCursor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Point
import android.media.ThumbnailUtils
import android.net.ConnectivityManager
import android.os.Build
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.os.storage.StorageManager
import android.provider.DocumentsContract.Document
import android.provider.DocumentsContract.Root
import android.provider.DocumentsContract.buildChildDocumentsUri
import android.provider.DocumentsContract.buildRootsUri
import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap
import androidx.annotation.WorkerThread
import androidx.core.content.getSystemService
import at.bitfire.dav4jvm.DavCollection
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet
import at.bitfire.dav4jvm.property.webdav.DisplayName
import at.bitfire.dav4jvm.property.webdav.GetContentLength
import at.bitfire.dav4jvm.property.webdav.GetContentType
import at.bitfire.dav4jvm.property.webdav.GetETag
import at.bitfire.dav4jvm.property.webdav.GetLastModified
import at.bitfire.dav4jvm.property.webdav.QuotaAvailableBytes
import at.bitfire.dav4jvm.property.webdav.QuotaUsedBytes
import at.bitfire.dav4jvm.property.webdav.ResourceType
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.WebDavDocument
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.network.MemoryCookieStore
import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity
import at.bitfire.davdroid.webdav.DavDocumentsProvider.DavDocumentsActor
import at.bitfire.davdroid.webdav.cache.ThumbnailCache
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withTimeout
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.logging.HttpLoggingInterceptor
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.net.HttpURLConnection
import java.util.concurrent.ConcurrentHashMap
import java.util.logging.Level
import java.util.logging.Logger
/**
* Provides functionality on WebDav documents.
*
* Actual implementation should go into [DavDocumentsActor].
*/
class DavDocumentsProvider: DocumentsProvider() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface DavDocumentsProviderEntryPoint {
fun appDatabase(): AppDatabase
fun logger(): Logger
fun randomAccessCallbackWrapperFactory(): RandomAccessCallbackWrapper.Factory
fun streamingFileDescriptorFactory(): StreamingFileDescriptor.Factory
fun webdavComponentBuilder(): WebdavComponentBuilder
}
@EntryPoint
@InstallIn(WebdavComponent::class)
interface DavDocumentsProviderWebdavEntryPoint {
fun credentialsStore(): CredentialsStore
fun thumbnailCache(): ThumbnailCache
}
companion object {
val DAV_FILE_FIELDS = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
GetETag.NAME,
GetContentType.NAME,
GetContentLength.NAME,
GetLastModified.NAME,
QuotaAvailableBytes.NAME,
QuotaUsedBytes.NAME,
)
const val MAX_NAME_ATTEMPTS = 5
const val THUMBNAIL_TIMEOUT_MS = 15000L
fun notifyMountsChanged(context: Context) {
context.contentResolver.notifyChange(buildRootsUri(context.getString(R.string.webdav_authority)), null)
}
}
private val ourContext by lazy { context!! } // requireContext() requires API level 30
private val authority by lazy { ourContext.getString(R.string.webdav_authority) }
private val globalEntryPoint by lazy { EntryPointAccessors.fromApplication<DavDocumentsProviderEntryPoint>(ourContext) }
private val webdavEntryPoint by lazy {
EntryPoints.get(
globalEntryPoint.webdavComponentBuilder().build(),
DavDocumentsProviderWebdavEntryPoint::class.java
)
}
private val logger by lazy { globalEntryPoint.logger() }
private val db by lazy { globalEntryPoint.appDatabase() }
private val mountDao by lazy { db.webDavMountDao() }
private val documentDao by lazy { db.webDavDocumentDao() }
private val credentialsStore by lazy { webdavEntryPoint.credentialsStore() }
private val cookieStore by lazy { mutableMapOf<Long, CookieJar>() }
private val thumbnailCache by lazy { webdavEntryPoint.thumbnailCache() }
private val connectivityManager by lazy { ourContext.getSystemService<ConnectivityManager>()!! }
private val storageManager by lazy { ourContext.getSystemService<StorageManager>()!! }
/** List of currently active [queryChildDocuments] runners.
*
* Key: document ID (directory) for which children are listed.
* Value: whether the runner is still running (*true*) or has already finished (*false*).
*/
private val runningQueryChildren = ConcurrentHashMap<Long, Boolean>()
private val actor by lazy { DavDocumentsActor(ourContext, db, logger, cookieStore, credentialsStore, authority) }
override fun onCreate() = true
/*** query ***/
override fun queryRoots(projection: Array<out String>?): Cursor {
logger.fine("WebDAV queryRoots")
val roots = MatrixCursor(projection ?: arrayOf(
Root.COLUMN_ROOT_ID,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_FLAGS,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_SUMMARY
))
for (mount in mountDao.getAll()) {
val rootDocument = documentDao.getOrCreateRoot(mount)
logger.info("Root ID: $rootDocument")
roots.newRow().apply {
add(Root.COLUMN_ROOT_ID, mount.id)
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
add(Root.COLUMN_TITLE, ourContext.getString(R.string.webdav_provider_root_title))
add(Root.COLUMN_DOCUMENT_ID, rootDocument.id.toString())
add(Root.COLUMN_SUMMARY, mount.name)
add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE or Root.FLAG_SUPPORTS_IS_CHILD)
val quotaAvailable = rootDocument.quotaAvailable
if (quotaAvailable != null)
add(Root.COLUMN_AVAILABLE_BYTES, quotaAvailable)
val quotaUsed = rootDocument.quotaUsed
if (quotaAvailable != null && quotaUsed != null)
add(Root.COLUMN_CAPACITY_BYTES, quotaAvailable + quotaUsed)
}
}
return roots
}
override fun queryDocument(documentId: String, projection: Array<out String>?): Cursor {
logger.fine("WebDAV queryDocument $documentId ${projection?.joinToString("+")}")
val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException()
val parent = doc.parentId?.let { parentId ->
documentDao.get(parentId)
}
return DocumentsCursor(projection ?: arrayOf(
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_ICON,
Document.COLUMN_SUMMARY
)).apply {
val bundle = doc.toBundle(parent)
logger.fine("queryDocument($documentId) = $bundle")
// override display names of root documents
if (parent == null) {
val mount = mountDao.getById(doc.mountId)
bundle.putString(Document.COLUMN_DISPLAY_NAME, mount.name)
}
addRow(bundle)
}
}
/**
* Gets old or new children of given parent.
*
* Dispatches a worker querying the server for new children of given parent, and instantly
* returns old children (or nothing, on initial call).
* Once the worker finishes its query, it notifies the [android.content.ContentResolver] about
* change, which calls this method again. The worker being done
*/
@Synchronized
override fun queryChildDocuments(parentDocumentId: String, projection: Array<out String>?, sortOrder: String?): Cursor {
logger.fine("WebDAV queryChildDocuments $parentDocumentId $projection $sortOrder")
val parentId = parentDocumentId.toLong()
val parent = documentDao.get(parentId) ?: throw FileNotFoundException()
val columns = projection ?: arrayOf(
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE,
Document.COLUMN_LAST_MODIFIED
)
// Register watcher
val result = DocumentsCursor(columns)
val notificationUri = buildChildDocumentsUri(authority, parentDocumentId)
result.setNotificationUri(ourContext.contentResolver, notificationUri)
// Dispatch worker querying for the children and keep track of it
val running = runningQueryChildren.getOrPut(parentId) {
CoroutineScope(Dispatchers.IO).launch {
actor.queryChildren(parent)
// Once the query is done, set query as finished (not running)
runningQueryChildren[parentId] = false
// .. and notify - effectively calling this method again
ourContext.contentResolver.notifyChange(notificationUri, null)
}
true
}
if (running) // worker still running
result.loading = true
else // remove worker from list if done
runningQueryChildren.remove(parentId)
// Regardless of whether the worker is done, return the children we already have
for (child in documentDao.getChildren(parentId)) {
val bundle = child.toBundle(parent)
result.addRow(bundle)
}
return result
}
override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean {
logger.fine("WebDAV isChildDocument $parentDocumentId $documentId")
val parent = documentDao.get(parentDocumentId.toLong()) ?: throw FileNotFoundException()
var iter: WebDavDocument? = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException()
while (iter != null) {
val currentParentId = iter.parentId
if (currentParentId == parent.id)
return true
iter = if (currentParentId != null)
documentDao.get(currentParentId)
else
null
}
return false
}
/*** copy/create/delete/move/rename ***/
override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String): String {
logger.fine("WebDAV copyDocument $sourceDocumentId $targetParentDocumentId")
val srcDoc = documentDao.get(sourceDocumentId.toLong()) ?: throw FileNotFoundException()
val dstFolder = documentDao.get(targetParentDocumentId.toLong()) ?: throw FileNotFoundException()
val name = srcDoc.name
if (srcDoc.mountId != dstFolder.mountId)
throw UnsupportedOperationException("Can't COPY between WebDAV servers")
val dstDocId: String
actor.httpClient(srcDoc.mountId).use { client ->
val dav = DavResource(client.okHttpClient, srcDoc.toHttpUrl(db))
try {
val dstUrl = dstFolder.toHttpUrl(db).newBuilder()
.addPathSegment(name)
.build()
dav.copy(dstUrl, false) {
// successfully copied
}
dstDocId = documentDao.insertOrReplace(WebDavDocument(
mountId = dstFolder.mountId,
parentId = dstFolder.id,
name = name,
isDirectory = srcDoc.isDirectory,
displayName = srcDoc.displayName,
mimeType = srcDoc.mimeType,
size = srcDoc.size
)).toString()
actor.notifyFolderChanged(targetParentDocumentId)
} catch (e: HttpException) {
if (e.code == HttpURLConnection.HTTP_NOT_FOUND)
throw FileNotFoundException()
throw e
}
}
return dstDocId
}
override fun createDocument(parentDocumentId: String, mimeType: String, displayName: String): String? {
logger.fine("WebDAV createDocument $parentDocumentId $mimeType $displayName")
val parent = documentDao.get(parentDocumentId.toLong()) ?: throw FileNotFoundException()
val parentUrl = parent.toHttpUrl(db)
val createDirectory = mimeType == Document.MIME_TYPE_DIR
var docId: Long? = null
actor.httpClient(parent.mountId).use { client ->
for (attempt in 0..MAX_NAME_ATTEMPTS) {
val newName = displayNameToMemberName(displayName, attempt)
val newLocation = parentUrl.newBuilder()
.addPathSegment(newName)
.build()
val doc = DavResource(client.okHttpClient, newLocation)
try {
if (createDirectory)
doc.mkCol(null) {
// directory successfully created
}
else
doc.put("".toRequestBody(null), ifNoneMatch = true) {
// document successfully created
}
docId = documentDao.insertOrReplace(WebDavDocument(
mountId = parent.mountId,
parentId = parent.id,
name = newName,
mimeType = mimeType.toMediaTypeOrNull(),
isDirectory = createDirectory
))
actor.notifyFolderChanged(parentDocumentId)
break
} catch (e: HttpException) {
e.throwForDocumentProvider(true)
}
}
}
return docId?.toString()
}
override fun deleteDocument(documentId: String) {
logger.fine("WebDAV removeDocument $documentId")
val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException()
actor.httpClient(doc.mountId).use { client ->
val dav = DavResource(client.okHttpClient, doc.toHttpUrl(db))
try {
dav.delete {
// successfully deleted
}
logger.fine("Successfully removed")
documentDao.delete(doc)
actor.notifyFolderChanged(doc.parentId)
} catch (e: HttpException) {
e.throwForDocumentProvider()
}
}
}
override fun moveDocument(sourceDocumentId: String, sourceParentDocumentId: String, targetParentDocumentId: String): String {
logger.fine("WebDAV moveDocument $sourceDocumentId $sourceParentDocumentId $targetParentDocumentId")
val doc = documentDao.get(sourceDocumentId.toLong()) ?: throw FileNotFoundException()
val dstParent = documentDao.get(targetParentDocumentId.toLong()) ?: throw FileNotFoundException()
if (doc.mountId != dstParent.mountId)
throw UnsupportedOperationException("Can't MOVE between WebDAV servers")
val newLocation = dstParent.toHttpUrl(db).newBuilder()
.addPathSegment(doc.name)
.build()
actor.httpClient(doc.mountId).use { client ->
val dav = DavResource(client.okHttpClient, doc.toHttpUrl(db))
try {
dav.move(newLocation, false) {
// successfully moved
}
documentDao.update(doc.copy(parentId = dstParent.id))
actor.notifyFolderChanged(sourceParentDocumentId)
actor.notifyFolderChanged(targetParentDocumentId)
} catch (e: HttpException) {
e.throwForDocumentProvider()
}
}
return doc.id.toString()
}
override fun renameDocument(documentId: String, displayName: String): String? {
logger.fine("WebDAV renameDocument $documentId $displayName")
val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException()
val oldUrl = doc.toHttpUrl(db)
actor.httpClient(doc.mountId).use { client ->
for (attempt in 0..MAX_NAME_ATTEMPTS) {
val newName = displayNameToMemberName(displayName, attempt)
val newLocation = oldUrl.newBuilder()
.removePathSegment(oldUrl.pathSegments.lastIndex)
.addPathSegment(newName)
.build()
try {
val dav = DavResource(client.okHttpClient, oldUrl)
dav.move(newLocation, false) {
// successfully renamed
}
documentDao.update(doc.copy(name = newName))
actor.notifyFolderChanged(doc.parentId)
return doc.id.toString()
} catch (e: HttpException) {
e.throwForDocumentProvider(true)
}
}
}
return null
}
private fun displayNameToMemberName(displayName: String, appendNumber: Int = 0): String {
val safeName = displayName.filterNot { it.isISOControl() }
if (appendNumber != 0) {
val extension: String? = MimeTypeMap.getFileExtensionFromUrl(displayName)
if (extension != null) {
val baseName = safeName.removeSuffix(".$extension")
return "${baseName}_$appendNumber.$extension"
} else
return "${safeName}_$appendNumber"
} else
return safeName
}
/*** read/write ***/
private fun headRequest(client: HttpClient, url: HttpUrl, signal: CancellationSignal?): HeadResponse {
val response = CoroutineScope(Dispatchers.IO).async {
runInterruptible {
HeadResponse.fromUrl(client, url)
}
}
signal?.setOnCancelListener {
response.cancel("Cancelled by signal")
}
return runBlocking {
response.await()
}
}
override fun openDocument(documentId: String, mode: String, signal: CancellationSignal?): ParcelFileDescriptor {
logger.fine("WebDAV openDocument $documentId $mode $signal")
val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException()
val url = doc.toHttpUrl(db)
val client = actor.httpClient(doc.mountId, logBody = false)
val modeFlags = ParcelFileDescriptor.parseMode(mode)
val readAccess = when (mode) {
"r" -> true
"w", "wt" -> false
else -> throw UnsupportedOperationException("Mode $mode not supported by WebDAV")
}
val fileInfo = headRequest(client, url, signal)
logger.fine("Received file info: $fileInfo")
// RandomAccessCallback.Wrapper / StreamingFileDescriptor are responsible for closing httpClient
return if (
Build.VERSION.SDK_INT >= 26 && // openProxyFileDescriptor exists since Android 8.0
readAccess && // WebDAV doesn't support random write access natively
fileInfo.size != null && // file descriptor must return a useful value on getFileSize()
(fileInfo.eTag != null || fileInfo.lastModified != null) && // we need a method to determine whether the document has changed during access
fileInfo.supportsPartial == true // WebDAV server must support random access
) {
logger.fine("Creating RandomAccessCallback for $url")
val factory = globalEntryPoint.randomAccessCallbackWrapperFactory()
val accessor = factory.create(client, url, doc.mimeType, fileInfo, signal)
storageManager.openProxyFileDescriptor(modeFlags, accessor, accessor.workerHandler)
} else {
logger.fine("Creating StreamingFileDescriptor for $url")
val factory = globalEntryPoint.streamingFileDescriptorFactory()
val fd = factory.create(client, url, doc.mimeType, signal) { transferred ->
// called when transfer is finished
val now = System.currentTimeMillis()
if (!readAccess /* write access */) {
// write access, update file size
documentDao.update(doc.copy(size = transferred, lastModified = now))
}
actor.notifyFolderChanged(doc.parentId)
}
if (readAccess)
fd.download()
else
fd.upload()
}
}
override fun openDocumentThumbnail(documentId: String, sizeHint: Point, signal: CancellationSignal?): AssetFileDescriptor? {
logger.info("openDocumentThumbnail documentId=$documentId sizeHint=$sizeHint signal=$signal")
if (connectivityManager.isActiveNetworkMetered)
// don't download the large images just to create a thumbnail on metered networks
return null
if (signal == null) {
// see https://github.com/zhanghai/MaterialFiles/issues/588
logger.warning("openDocumentThumbnail without cancellationSignal causes too much problems, please fix calling app")
return null
}
val doc = documentDao.get(documentId.toLong()) ?: throw FileNotFoundException()
val docCacheKey = doc.cacheKey()
if (docCacheKey == null) {
logger.warning("openDocumentThumbnail won't generate thumbnails when document state (ETag/Last-Modified) is unknown")
return null
}
val thumbFile = thumbnailCache.get(docCacheKey, sizeHint) {
// create thumbnail
val job = CoroutineScope(Dispatchers.IO).async {
withTimeout(THUMBNAIL_TIMEOUT_MS) {
actor.httpClient(doc.mountId, logBody = false).use { client ->
val url = doc.toHttpUrl(db)
val dav = DavResource(client.okHttpClient, url)
var result: ByteArray? = null
runInterruptible {
dav.get("image/*", null) { response ->
response.body?.byteStream()?.use { data ->
BitmapFactory.decodeStream(data)?.let { bitmap ->
val thumb = ThumbnailUtils.extractThumbnail(bitmap, sizeHint.x, sizeHint.y)
val baos = ByteArrayOutputStream()
thumb.compress(Bitmap.CompressFormat.JPEG, 95, baos)
result = baos.toByteArray()
}
}
}
}
result
}
}
}
signal.setOnCancelListener {
logger.fine("Cancelling thumbnail for ${doc.name}")
job.cancel("Cancelled by signal")
}
try {
runBlocking {
job.await()
}
} catch (e: Exception) {
logger.log(Level.WARNING, "Couldn't generate thumbnail", e)
null
}
}
if (thumbFile != null)
return AssetFileDescriptor(
ParcelFileDescriptor.open(thumbFile, ParcelFileDescriptor.MODE_READ_ONLY),
0, thumbFile.length()
)
return null
}
/**
* Acts on behalf of [DavDocumentsProvider].
*
* Encapsulates functionality to make it easily testable without generating lots of
* DocumentProviders during the tests.
*
* By containing the actual implementation logic of [DavDocumentsProvider], it adds a layer of separation
* to make the methods of [DavDocumentsProvider] more easily testable.
* [DavDocumentsProvider]s methods should do nothing more, but to call [DavDocumentsActor]s methods.
*/
class DavDocumentsActor(
private val context: Context,
private val db: AppDatabase,
private val logger: Logger,
private val cookieStore: MutableMap<Long, CookieJar>,
private val credentialsStore: CredentialsStore,
private val authority: String
) {
private val documentDao = db.webDavDocumentDao()
/**
* Finds children of given parent [WebDavDocument]. After querying, it
* updates existing children, adds new ones or removes deleted ones.
*
* There must never be more than one running instance per [parent]!
*
* @param parent folder to search for children
*/
@WorkerThread
internal fun queryChildren(parent: WebDavDocument) {
val oldChildren = documentDao.getChildren(parent.id).associateBy { it.name }.toMutableMap() // "name" of file/folder must be unique
val newChildrenList = hashMapOf<String, WebDavDocument>()
httpClient(parent.mountId).use { client ->
val parentUrl = parent.toHttpUrl(db)
val folder = DavCollection(client.okHttpClient, parentUrl)
try {
folder.propfind(1, *DAV_FILE_FIELDS) { response, relation ->
logger.fine("$relation $response")
val resource: WebDavDocument =
when (relation) {
Response.HrefRelation.SELF -> // it's about the parent
parent
Response.HrefRelation.MEMBER -> // it's about a member
WebDavDocument(mountId = parent.mountId, parentId = parent.id, name = response.hrefName())
else -> {
// we didn't request this; log a warning and ignore it
logger.warning("Ignoring unexpected $response $relation in $parentUrl")
return@propfind
}
}
val updatedResource = resource.copy(
isDirectory = response[ResourceType::class.java]?.types?.contains(ResourceType.COLLECTION) ?: resource.isDirectory,
displayName = response[DisplayName::class.java]?.displayName,
mimeType = response[GetContentType::class.java]?.type,
eTag = response[GetETag::class.java]?.takeIf { !it.weak }?.let { resource.eTag },
lastModified = response[GetLastModified::class.java]?.lastModified?.toEpochMilli(),
size = response[GetContentLength::class.java]?.contentLength,
mayBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind,
mayUnbind = response[CurrentUserPrivilegeSet::class.java]?.mayUnbind,
mayWriteContent = response[CurrentUserPrivilegeSet::class.java]?.mayWriteContent,
quotaAvailable = response[QuotaAvailableBytes::class.java]?.quotaAvailableBytes,
quotaUsed = response[QuotaUsedBytes::class.java]?.quotaUsedBytes,
)
if (resource == parent)
documentDao.update(updatedResource)
else {
documentDao.insertOrUpdate(updatedResource)
newChildrenList[resource.name] = updatedResource
}
// remove resource from known child nodes, because not found on server
oldChildren.remove(resource.name)
}
} catch (e: Exception) {
logger.log(Level.WARNING, "Couldn't query children", e)
}
}
// Delete child nodes which were not rediscovered (deleted serverside)
for ((_, oldChild) in oldChildren)
documentDao.delete(oldChild)
}
// helpers
/**
* Creates a HTTP client that can be used to access resources in the given mount.
*
* @param mountId ID of the mount to access
* @param logBody whether to log the body of HTTP requests (disable for potentially large files)
*/
internal fun httpClient(mountId: Long, logBody: Boolean = true): HttpClient {
val builder = HttpClient.Builder(
context = context,
loggerLevel = if (logBody) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.HEADERS
).cookieStore(
cookieStore.getOrPut(mountId) { MemoryCookieStore() }
)
credentialsStore.getCredentials(mountId)?.let { credentials ->
builder.addAuthentication(null, credentials, true)
}
return builder.build()
}
internal fun notifyFolderChanged(parentDocumentId: Long?) {
if (parentDocumentId != null)
context.contentResolver.notifyChange(buildChildDocumentsUri(authority, parentDocumentId.toString()), null)
}
internal fun notifyFolderChanged(parentDocumentId: String) {
context.contentResolver.notifyChange(buildChildDocumentsUri(authority, parentDocumentId), null)
}
}
private fun HttpException.throwForDocumentProvider(ignorePreconditionFailed: Boolean = false) {
when (code) {
HttpURLConnection.HTTP_UNAUTHORIZED -> {
if (Build.VERSION.SDK_INT >= 26) {
// TODO edit mount
val intent = Intent(ourContext, WebdavMountsActivity::class.java)
throw AuthenticationRequiredException(this, PendingIntent.getActivity(ourContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
}
}
HttpURLConnection.HTTP_NOT_FOUND ->
throw FileNotFoundException()
HttpURLConnection.HTTP_PRECON_FAILED ->
if (ignorePreconditionFailed)
return
}
// re-throw
throw this
}
}

View File

@@ -1,239 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.webdav
import android.annotation.TargetApi
import android.content.Context
import android.os.CancellationSignal
import android.os.ProxyFileDescriptorCallback
import android.system.ErrnoException
import android.system.OsConstants
import android.text.format.Formatter
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.HttpUtils
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.ui.NotificationRegistry
import at.bitfire.davdroid.util.DavUtils
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runInterruptible
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.MediaType
import java.io.InterruptedIOException
import java.net.HttpURLConnection
import java.util.logging.Level
import java.util.logging.Logger
@TargetApi(26)
class RandomAccessCallback @AssistedInject constructor(
@Assisted val httpClient: HttpClient,
@Assisted val url: HttpUrl,
@Assisted val mimeType: MediaType?,
@Assisted headResponse: HeadResponse,
@Assisted private val cancellationSignal: CancellationSignal?,
@ApplicationContext val context: Context,
private val logger: Logger,
private val notificationRegistry: NotificationRegistry
): ProxyFileDescriptorCallback() {
companion object {
/**
* WebDAV resources will be read in chunks of this size (or less at the end of the file).
*/
const val MAX_PAGE_SIZE = 2 * 1024*1024 // 2 MB
}
@AssistedFactory
interface Factory {
fun create(httpClient: HttpClient, url: HttpUrl, mimeType: MediaType?, headResponse: HeadResponse, cancellationSignal: CancellationSignal?): RandomAccessCallback
}
data class PageIdentifier(
val offset: Long,
val size: Int
)
private val dav = DavResource(httpClient.okHttpClient, url)
private val fileSize = headResponse.size ?: throw IllegalArgumentException("Can only be used with given file size")
private val documentState = headResponse.toDocumentState() ?: throw IllegalArgumentException("Can only be used with ETag/Last-Modified")
private val notificationManager = NotificationManagerCompat.from(context)
private val notification = NotificationCompat.Builder(context, notificationRegistry.CHANNEL_STATUS)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setContentTitle(context.getString(R.string.webdav_notification_access))
.setContentText(dav.fileName())
.setSubText(Formatter.formatFileSize(context, fileSize))
.setSmallIcon(R.drawable.ic_storage_notify)
.setOngoing(true)
private val notificationTag = url.toString()
private val pageLoader = PageLoader()
private val pageCache: LoadingCache<PageIdentifier, ByteArray> = CacheBuilder.newBuilder()
.maximumSize(10) // don't cache more than 10 entries (MAX_PAGE_SIZE each)
.softValues() // use SoftReference for the page contents so they will be garbage collected if memory is needed
.build(pageLoader) // fetch actual content using pageLoader
private val pagingReader = PagingReader(fileSize, MAX_PAGE_SIZE, pageCache)
init {
cancellationSignal?.let {
logger.fine("Cancelling random access to $url")
pageLoader.cancelAll()
}
}
override fun onFsync() { /* not used */ }
override fun onGetSize(): Long {
logger.fine("onGetFileSize $url")
throwIfCancelled("onGetFileSize")
return fileSize
}
override fun onRead(offset: Long, size: Int, data: ByteArray): Int {
logger.fine("onRead $url $offset $size")
throwIfCancelled("onRead")
try {
return pagingReader.read(offset, size, data)
} catch (e: Exception) {
logger.log(Level.WARNING, "Couldn't read from WebDAV resource", e)
throw e.toErrNoException("onRead")
}
}
override fun onWrite(offset: Long, size: Int, data: ByteArray): Int {
logger.fine("onWrite $url $offset $size")
// ranged write requests not supported by WebDAV (yet)
throw ErrnoException("onWrite", OsConstants.EROFS)
}
override fun onRelease() {
logger.fine("onRelease")
notificationManager.cancel(notificationTag, NotificationRegistry.NOTIFY_WEBDAV_ACCESS)
}
private fun throwIfCancelled(functionName: String) {
if (cancellationSignal?.isCanceled == true) {
logger.warning("Random file access cancelled, throwing ErrnoException(EINTR)")
throw ErrnoException(functionName, OsConstants.EINTR)
}
}
private fun Exception.toErrNoException(functionName: String) =
ErrnoException(
functionName,
when (this) {
is HttpException ->
when (code) {
HttpURLConnection.HTTP_FORBIDDEN -> OsConstants.EPERM
HttpURLConnection.HTTP_NOT_FOUND -> OsConstants.ENOENT
else -> OsConstants.EIO
}
is IndexOutOfBoundsException -> OsConstants.ENXIO // no such [device or] address, see man lseek (2)
is InterruptedIOException -> OsConstants.EINTR
is PartialContentNotSupportedException -> OsConstants.EOPNOTSUPP
else -> OsConstants.EIO
},
this
)
/**
* Responsible for loading (= downloading) a single page from the WebDAV resource.
*/
inner class PageLoader: CacheLoader<PageIdentifier, ByteArray>() {
private val jobs = mutableSetOf<Deferred<ByteArray>>()
fun cancelAll() {
for (job in jobs)
job.cancel()
}
override fun load(key: PageIdentifier): ByteArray {
val offset = key.offset
val size = key.size
logger.fine("Loading page $url $offset/$size")
// update notification
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_WEBDAV_ACCESS, tag = notificationTag) {
val progress =
if (fileSize == 0L) // avoid division by zero
100
else
(offset * 100 / fileSize).toInt()
notification.setProgress(100, progress, false).build()
}
val ifMatch: Headers =
documentState.eTag?.let { eTag ->
Headers.headersOf("If-Match", "\"$eTag\"")
} ?: documentState.lastModified?.let { lastModified ->
Headers.headersOf("If-Unmodified-Since", HttpUtils.formatDate(lastModified))
} ?: throw DavException("ETag/Last-Modified required for random access")
// create async job that can be cancelled (and cancellation interrupts I/O)
val job = CoroutineScope(Dispatchers.IO).async {
runInterruptible {
var result: ByteArray? = null
dav.getRange(
DavUtils.acceptAnything(preferred = mimeType),
offset,
size,
ifMatch
) { response ->
if (response.code == 200) // server doesn't support ranged requests
throw PartialContentNotSupportedException()
else if (response.code != 206)
throw HttpException(response)
result = response.body?.bytes()
}
return@runInterruptible result ?: throw DavException("No response body")
}
}
try {
// register job in set so that it can be cancelled
jobs += job
// wait for result
return runBlocking {
job.await()
}
} finally {
jobs -= job
}
}
}
class PartialContentNotSupportedException: Exception()
}

View File

@@ -1,189 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.webdav
import android.content.Context
import android.os.Build
import android.os.CancellationSignal
import android.os.Handler
import android.os.HandlerThread
import android.os.ProxyFileDescriptorCallback
import androidx.annotation.RequiresApi
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.webdav.RandomAccessCallbackWrapper.Companion.TIMEOUT_INTERVAL
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.HttpUrl
import okhttp3.MediaType
import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.state.State
import ru.nsk.kstatemachine.state.finalState
import ru.nsk.kstatemachine.state.initialState
import ru.nsk.kstatemachine.state.onEntry
import ru.nsk.kstatemachine.state.onExit
import ru.nsk.kstatemachine.state.onFinished
import ru.nsk.kstatemachine.state.state
import ru.nsk.kstatemachine.state.transitionOn
import ru.nsk.kstatemachine.statemachine.StateMachine
import ru.nsk.kstatemachine.statemachine.createStdLibStateMachine
import ru.nsk.kstatemachine.statemachine.processEventBlocking
import java.util.Timer
import java.util.TimerTask
import java.util.logging.Logger
import kotlin.concurrent.schedule
/**
* (2021/12/02) Currently Android's `StorageManager.openProxyFileDescriptor` has a memory leak:
* the given callback is registered in `com.android.internal.os.AppFuseMount` (which adds it to
* a [Map]), but is not unregistered anymore. So it stays in the memory until the whole mount
* is unloaded. See https://issuetracker.google.com/issues/208788568
*
* Use this wrapper to
*
* - ensure that all memory is released as soon as [onRelease] is called,
* - provide timeout functionality: [RandomAccessCallback] will be closed when not
*
* used for more than [TIMEOUT_INTERVAL] ms and re-created when necessary.
*
* @param httpClient HTTP client [RandomAccessCallbackWrapper] is responsible to close it
*/
@RequiresApi(Build.VERSION_CODES.O)
class RandomAccessCallbackWrapper @AssistedInject constructor(
@Assisted val httpClient: HttpClient,
@Assisted val url: HttpUrl,
@Assisted val mimeType: MediaType?,
@Assisted val headResponse: HeadResponse,
@Assisted val cancellationSignal: CancellationSignal?,
@ApplicationContext val context: Context,
private val logger: Logger,
private val callbackFactory: RandomAccessCallback.Factory
): ProxyFileDescriptorCallback() {
companion object {
const val TIMEOUT_INTERVAL = 15000L
}
@AssistedFactory
interface Factory {
fun create(httpClient: HttpClient, url: HttpUrl, mimeType: MediaType?, headResponse: HeadResponse, cancellationSignal: CancellationSignal?): RandomAccessCallbackWrapper
}
sealed class Events {
object Transfer : Event
object NowIdle : Event
object GoStandby : Event
object Close : Event
}
/* We don't use a sealed class for states here because the states would then be singletons, while we can have
multiple instances of the state machine (which require multiple instances of the states, too). */
private val machine = createStdLibStateMachine {
lateinit var activeIdleState: State
lateinit var activeTransferringState: State
lateinit var standbyState: State
lateinit var closedState: State
initialState("active") {
onEntry {
_callback = callbackFactory.create(httpClient, url, mimeType, headResponse, cancellationSignal)
}
onExit {
_callback?.onRelease()
_callback = null
}
transitionOn<Events.GoStandby> { targetState = { standbyState } }
transitionOn<Events.Close> { targetState = { closedState } }
// active has two nested states: transferring (I/O running) and idle (starts timeout timer)
activeIdleState = initialState("idle") {
val timer: Timer = Timer(true)
var timeout: TimerTask? = null
onEntry {
timeout = timer.schedule(TIMEOUT_INTERVAL) {
machine.processEventBlocking(Events.GoStandby)
}
}
onExit {
timeout?.cancel()
timeout = null
}
onFinished {
timer.cancel()
}
transitionOn<Events.Transfer> { targetState = { activeTransferringState } }
}
activeTransferringState = state("transferring") {
transitionOn<Events.NowIdle> { targetState = { activeIdleState } }
}
}
standbyState = state("standby") {
transitionOn<Events.Transfer> { targetState = { activeTransferringState } }
transitionOn<Events.NowIdle> { targetState = { activeIdleState } }
transitionOn<Events.Close> { targetState = { closedState } }
}
closedState = finalState("closed")
onFinished {
shutdown()
}
logger = StateMachine.Logger { message ->
this@RandomAccessCallbackWrapper.logger.finer(message())
}
}
private val workerThread = HandlerThread(javaClass.simpleName).apply { start() }
val workerHandler: Handler = Handler(workerThread.looper)
private var _callback: RandomAccessCallback? = null
fun<T> requireCallback(block: (callback: RandomAccessCallback) -> T): T {
machine.processEventBlocking(Events.Transfer)
try {
return block(_callback ?: throw IllegalStateException())
} finally {
machine.processEventBlocking(Events.NowIdle)
}
}
/// states ///
@Synchronized
private fun shutdown() {
httpClient.close()
workerThread.quit()
}
/// delegating implementation of ProxyFileDescriptorCallback ///
@Synchronized
override fun onFsync() { /* not used */ }
@Synchronized
override fun onGetSize() =
requireCallback { it.onGetSize() }
@Synchronized
override fun onRead(offset: Long, size: Int, data: ByteArray) =
requireCallback { it.onRead(offset, size, data) }
@Synchronized
override fun onWrite(offset: Long, size: Int, data: ByteArray) =
requireCallback { it.onWrite(offset, size, data) }
@Synchronized
override fun onRelease() {
machine.processEventBlocking(Events.Close)
}
}

View File

@@ -1,207 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.webdav
import android.content.Context
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.text.format.Formatter
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.ui.NotificationRegistry
import at.bitfire.davdroid.util.DavUtils
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runInterruptible
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.RequestBody
import okhttp3.internal.headersContentLength
import okio.BufferedSink
import java.io.IOException
import java.util.logging.Level
import java.util.logging.Logger
/**
* @param client HTTP client [StreamingFileDescriptor] is responsible to close it
*/
class StreamingFileDescriptor @AssistedInject constructor(
@Assisted val client: HttpClient,
@Assisted val url: HttpUrl,
@Assisted val mimeType: MediaType?,
@Assisted val cancellationSignal: CancellationSignal?,
@Assisted val finishedCallback: OnSuccessCallback,
@ApplicationContext val context: Context,
private val logger: Logger,
private val notificationRegistry: NotificationRegistry
) {
companion object {
/** 1 MB transfer buffer */
private const val BUFFER_SIZE = 1024*1024
}
@AssistedFactory
interface Factory {
fun create(client: HttpClient, url: HttpUrl, mimeType: MediaType?, cancellationSignal: CancellationSignal?, finishedCallback: OnSuccessCallback): StreamingFileDescriptor
}
val dav = DavResource(client.okHttpClient, url)
var transferred: Long = 0
private val notificationManager = NotificationManagerCompat.from(context)
private val notification = NotificationCompat.Builder(context, notificationRegistry.CHANNEL_STATUS)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setContentText(dav.fileName())
.setSmallIcon(R.drawable.ic_storage_notify)
.setOngoing(true)
val notificationTag = url.toString()
fun download() = doStreaming(false)
fun upload() = doStreaming(true)
private fun doStreaming(upload: Boolean): ParcelFileDescriptor {
val (readFd, writeFd) = ParcelFileDescriptor.createReliablePipe()
val result = CoroutineScope(Dispatchers.IO).async {
try {
if (upload)
uploadNow(readFd)
else
downloadNow(writeFd)
} catch (e: HttpException) {
logger.log(Level.WARNING, "HTTP error when opening remote file", e)
writeFd.closeWithError("${e.code} ${e.message}")
} catch (e: Exception) {
logger.log(Level.INFO, "Couldn't serve file (not necessesarily an error)", e)
writeFd.closeWithError(e.message)
} finally {
client.close()
}
try {
readFd.close()
writeFd.close()
} catch (ignored: IOException) {}
notificationManager.cancel(notificationTag, NotificationRegistry.NOTIFY_WEBDAV_ACCESS)
finishedCallback.onSuccess(transferred)
}
cancellationSignal?.setOnCancelListener {
logger.fine("Cancelling transfer of $url")
result.cancel()
}
return if (upload)
writeFd
else
readFd
}
@WorkerThread
private suspend fun downloadNow(writeFd: ParcelFileDescriptor) = runInterruptible {
dav.get(DavUtils.acceptAnything(preferred = mimeType), null) { response ->
response.body?.use { body ->
if (response.isSuccessful) {
val length = response.headersContentLength()
notification.setContentTitle(context.getString(R.string.webdav_notification_download))
if (length == -1L)
// unknown file size, show notification now (no updates on progress)
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_WEBDAV_ACCESS, notificationTag) {
notification
.setProgress(100, 0, true)
.build()
}
else
// known file size
notification.setSubText(Formatter.formatFileSize(context, length))
ParcelFileDescriptor.AutoCloseOutputStream(writeFd).use { output ->
val buffer = ByteArray(BUFFER_SIZE)
body.byteStream().use { source ->
// read first chunk
var bytes = source.read(buffer)
while (bytes != -1) {
// update notification (if file size is known)
if (length > 0)
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_WEBDAV_ACCESS, notificationTag) {
val progress = (transferred*100/length).toInt()
notification
.setProgress(100, progress, false)
.build()
}
// write chunk
output.write(buffer, 0, bytes)
transferred += bytes
// read next chunk
bytes = source.read(buffer)
}
logger.finer("Downloaded $transferred byte(s) from $url")
}
}
} else
writeFd.closeWithError("${response.code} ${response.message}")
}
}
}
@WorkerThread
private suspend fun uploadNow(readFd: ParcelFileDescriptor) = runInterruptible {
val body = object: RequestBody() {
override fun contentType(): MediaType? = mimeType
override fun isOneShot() = true
override fun writeTo(sink: BufferedSink) {
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_WEBDAV_ACCESS, notificationTag) {
notification
.setContentTitle(context.getString(R.string.webdav_notification_upload))
.build()
}
ParcelFileDescriptor.AutoCloseInputStream(readFd).use { input ->
val buffer = ByteArray(BUFFER_SIZE)
// read first chunk
var size = input.read(buffer)
while (size != -1) {
// write chunk
sink.write(buffer, 0, size)
transferred += size
// read next chunk
size = input.read(buffer)
}
logger.finer("Uploaded $transferred byte(s) to $url")
}
}
}
DavResource(client.okHttpClient, url).put(body) {
// upload successful
}
}
fun interface OnSuccessCallback {
fun onSuccess(transferred: Long)
}
}

View File

@@ -1,22 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.webdav
import dagger.hilt.DefineComponent
import dagger.hilt.components.SingletonComponent
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.BINARY)
annotation class WebdavScoped
@WebdavScoped
@DefineComponent(parent = SingletonComponent::class)
interface WebdavComponent
@DefineComponent.Builder
interface WebdavComponentBuilder {
fun build(): WebdavComponent
}

View File

@@ -1,3 +0,0 @@
{"metadata":{"generated":"2023-12-03T12:08:52.214Z"},"libraries":[
{"uniqueId":"com.example:sample","funding":[],"developers":[{"name":"Sample Developer"}],"artifactVersion":"1.0","description":"This list has to be updated at release build time by explicitly writing to R.raw.aboutlibraries.","name":"Sample Dependency","licenses":["Sample-License"]}
], "licenses":{}}

View File

@@ -1,449 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="account_invalid">Kasutajakontot ei leidu (enam)</string>
<string name="account_title_address_book">DAVx⁵ aadressiraamat</string>
<string name="account_prefs_use_app">Palun ära muuda kasutajakontot siin! Selle asemel pruugi kasutajakontode halduseks otseselt rakendust.</string>
<string name="dialog_delete">Kustuta</string>
<string name="dialog_remove">Eemalda</string>
<string name="dialog_deny">Katkesta</string>
<string name="field_required">See väli on kohustuslik</string>
<string name="help">Abiteave</string>
<string name="navigate_up">Liigu üles</string>
<string name="optional_label">* valikuline</string>
<string name="options_menu">Valikute menüü</string>
<string name="share">Jaga</string>
<string name="sync_started">Sünkroniseerimine algas või on tööde järjekorras</string>
<string name="database_destructive_migration_title">Andmebaas on vigane</string>
<string name="database_destructive_migration_text">Kõik kasutajakontod on kohalikust seadmest eemaldatud</string>
<string name="notification_channel_debugging">Silumine ja veaotsing</string>
<string name="notification_channel_general">Muud olulised sõnumid</string>
<string name="notification_channel_status">Väheolulised olekuteated</string>
<string name="notification_channel_sync">Sünkroniseerimine</string>
<string name="notification_channel_sync_errors">Sünkroniseerimisvead</string>
<string name="notification_channel_sync_errors_desc">Olulised vead, mis peatavad sünkroniseerimise, nagu näiteks ootamatud päringuvastused serverist</string>
<string name="notification_channel_sync_warnings">Sünkroniseerimishoiatused</string>
<string name="notification_channel_sync_warnings_desc">Vähetõsised sünkroniseerimisteated näiteks vigaste failide kohta</string>
<string name="notification_channel_sync_io_errors">Võrgu- ja sisend/väljundvead</string>
<string name="notification_channel_sync_io_errors_desc">Ühenduste aegumine ja muud sarnased probleemid (tihti ajutised)</string>
<!--IntroActivity-->
<string name="intro_slogan1">Sinu andmed. Sinu valik.</string>
<string name="intro_slogan2">Sina otsustad.</string>
<string name="intro_battery_title">Regulaarne sünkroniseerimisvälp</string>
<string name="intro_battery_text">Selleks, et sünkroniseerimine soovitud ajavahemike järel toimiks taustateenusena, vajab %s õigust töötada taustal. Vastasel juhul võib Android igal ajal sünkroniseerimise peatada.</string>
<string name="intro_battery_dont_show">Ma ei soovi kasutada regulaarset sünkroniseerimisvälpa. *</string>
<string name="intro_autostart_title">%s ühilduvus</string>
<string name="intro_autostart_text">Ilmselt see nutiseade blokeerib sünkroniseerimist. Kui see sinu tegevust mõjutab, siis saad olukorra lahendada käsitsi.</string>
<string name="intro_autostart_dont_show">Ma juba kasutan nõutavaid seadistusi. Ära enam tuleta seda mulle meelde.*</string>
<string name="intro_leave_unchecked">* Kui soovid hilisemat meeldetuletust, jäta see märkimata. Lisaks saad seada muuta rakenduse seadistustest / %s.</string>
<string name="intro_more_info">Lisateave</string>
<string name="intro_tasks_jtx">jtx Board</string>
<string name="intro_tasks_jtx_info"><![CDATA[Toetab ülesannete, märkmete ja päevikute sünkroniseerimist.]]></string>
<string name="intro_tasks_title">Ülesannete tugi</string>
<string name="intro_tasks_text1">Kui sinu kasutatav server toetab ülesannete haldust, siis nende sünkroniseerimine on võimalik toetatud ülesannete rakendusega:</string>
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Tundub, et arendus on lõppenud ja seega pole kasutamine enam mõistlik.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Mõned funktsionaalsused <a href="https://www.davx5.com/faq/tasks/advanced-task-features">pole toetatud</a>.]]></string>
<string name="intro_tasks_no_app_store">Rakendustepoodi pole saadaval</string>
<string name="intro_tasks_dont_show">Ma ei vaja ülesannete tuge.*</string>
<string name="intro_open_source_title">Avatud lähtekoodiga tarkvara</string>
<string name="intro_open_source_text">Me oleme rõõmsad, et kasutad avatud lähtekoodil põhinevat rakendust %s. Selle arendus, hooldus ja kasutajatugi nõuavad märgatavat tööd. Palun kaalu erinevaid võimalusi osalemiseks või rahalist toetamist. Me hindaksime seda väga!</string>
<string name="intro_open_source_details">Võimalused kaastööks või rahaliseks toetamiseks</string>
<string name="intro_open_source_dont_show">Ära näita seda lähiajal uuesti</string>
<string name="intro_next">Järgmine</string>
<!--PermissionsActivity-->
<string name="permissions_title">Õigused</string>
<string name="permissions_text">%s vajab korralikuks toimimiseks õigusi.</string>
<string name="permissions_all_title">Kõik alljärgnev</string>
<string name="permissions_all_status_off">Kasuta seda valikut kõikide funktsionaalsuste sisselülitamiseks (soovitatav)</string>
<string name="permissions_all_status_on">Rakenduse õigused on olemas</string>
<string name="permissions_contacts_title">Kontaktide õigused</string>
<string name="permissions_contacts_status_off">Kontaktide sünkroniseerimine puudub (pole soovitatud)</string>
<string name="permissions_contacts_status_on">Kontaktide sünkroniseerimine on võimalik</string>
<string name="permissions_calendar_title">Kalendri õigused</string>
<string name="permissions_calendar_status_off">Kalendri sünkroniseerimine puudub (pole soovitatud)</string>
<string name="permissions_calendar_status_on">Kalendri sünkroniseerimine on võimalik</string>
<string name="permissions_notification_title">Teavituste õigused</string>
<string name="permissions_notification_status_off">Teavitused pole kasutusel (pole soovitatav)</string>
<string name="permissions_notification_status_on">Teavitused on kasutusel</string>
<string name="permissions_jtx_title">Õigused - jtx Board</string>
<string name="permissions_opentasks_title">Õigused - OpenTasks</string>
<string name="permissions_tasksorg_title">Ülesannete õigused</string>
<string name="permissions_tasks_status_off">Ülesannete sünkroniseerimine puudub</string>
<string name="permissions_tasks_status_on">Ülesannete sünkroniseerimine on võimalik</string>
<string name="permissions_autoreset_title">Säilita õigused</string>
<string name="permissions_autoreset_status_off">Õigusi võib muuta automaatselt (pole soovitatud)</string>
<string name="permissions_autoreset_status_on">Õigused ei saa olema automaatselt muudetud</string>
<string name="permissions_autoreset_instruction">Klõpsi Õigused ja eemalda valik „Eemalda load, kui rakendust ei kasutata“</string>
<string name="permissions_app_settings_hint">Kui muutmine ei toimi, siis kasuta rakenduse õiguste seadistusi.</string>
<string name="permissions_app_settings">Rakenduse seadistused</string>
<!--WifiPermissionsActivity-->
<string name="wifi_permissions_label">WiFi SSID õigused</string>
<string name="wifi_permissions_intro">Selleks, et toimiks ligipääs hetkel kasutatavale WiFi võrgunimele (SSID), peavad olema täidetud järgnevad tingimused:</string>
<string name="wifi_permissions_location_permission">Õigused täpse asukoha tuvastamiseks</string>
<string name="wifi_permissions_location_permission_on">Õigused asukoha tuvastamiseks on olemas</string>
<string name="wifi_permissions_location_permission_off">Õigused asukoha tuvastamiseks on keelatud</string>
<string name="wifi_permissions_background_location_permission">Õigused asukoha tuvastamiseks taustal</string>
<string name="wifi_permissions_background_location_permission_label">Luba alati</string>
<string name="wifi_permissions_background_location_permission_on">Asukohaõigused on: %s</string>
<string name="wifi_permissions_background_location_permission_off">Asukohaõiguseid pole: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s kasutab asukohaandmeid (vaid WiFi SSID võrgutunnust) vaid sünkroniseerimise tagamiseks konkreetse WiFi-võrgu piires. See kehtib ka siis, kui sünkroniseerimine on seadistatud töötama taustal.</string>
<string name="wifi_permissions_background_location_disclaimer2">Kõik asukohaandmed (vaid WiFi SSId võrgutunnus) on kasutusel kohalikus nutiseadmes ega saadeta mitte kuhugile mujale.</string>
<string name="wifi_permissions_location_enabled">Asukohateenus on alati kasutusel</string>
<string name="wifi_permissions_location_enabled_on">Asukohateenus on lubatud</string>
<string name="wifi_permissions_location_enabled_off">Asukohateenus pole lubatud</string>
<!--AboutActivity-->
<string name="about_translations">Tõlked</string>
<string name="about_libraries">Teegid</string>
<string name="about_version">Versioon %1$s (%2$d)</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) ja kaasautorid</string>
<string name="about_license_info_no_warranty">Selle rakenduse kasutamisega EI KAASNE MITTE ÜHTEGI GARANTIID. Tegemist on vaba ja avatud tarkvaraga ning sa võid seda levitada kindlate tingimuste alusel.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Logifaili loomine ei õnnestunud</string>
<string name="logging_notification_text">Nüüd logime kõiki %s rakenduse tegevusi</string>
<string name="logging_notification_view_share">Vaata/jaga</string>
<string name="logging_notification_disable">Lülita välja</string>
<!--AccountsScreen-->
<string name="navigation_drawer_subtitle">CalDAV/CardDAV sünkroniseerimise sobitaja</string>
<string name="navigation_drawer_about">Teave / litsents</string>
<string name="navigation_drawer_beta_feedback">Beetaversiooni tagasiside</string>
<string name="install_browser">Palun paigalda veebibrauser</string>
<string name="navigation_drawer_settings">Seadistused</string>
<string name="navigation_drawer_news_updates">Uudised ja uuendused</string>
<string name="navigation_drawer_tools">Tarvikud</string>
<string name="navigation_drawer_external_links">Välised lingid</string>
<string name="navigation_drawer_website">Veebisait</string>
<string name="navigation_drawer_manual">Käsiraamat</string>
<string name="navigation_drawer_faq">KKK</string>
<string name="navigation_drawer_community">Kogukond</string>
<string name="navigation_drawer_support_project">Toeta projekti</string>
<string name="navigation_drawer_contribute">Osalemise viisid</string>
<string name="navigation_drawer_privacy_policy">Privaatsuspoliitika</string>
<string name="account_list_no_notification_permission">Teavitused on väljalülitatud ja seega sünkroniseerimisvigade infot sa ei näe.</string>
<string name="account_list_manage_connections">Halda ühendusi</string>
<string name="account_list_datasaver_enabled">Andmemahu piiraja on kasutusel. Taustal sünkroniseerimine võib toimida piirangutega.</string>
<string name="account_list_manage_datasaver">Halda andmemahu piirajat</string>
<string name="account_list_battery_saver_enabled">Akukasutuse piiraja on kasutusel. Taustal sünkroniseerimine võib toimida piirangutega.</string>
<string name="account_list_manage_battery_saver">Halda akukasutuse piirajat</string>
<string name="account_list_low_storage">Vaba andmeruumi napib. Android ei sünkroniseeri kohalikke muudatusi kohe, vaid järgmise regulaarse sünkroniseerimise ajal.</string>
<string name="account_list_manage_storage">Halda andmeruumi</string>
<string name="account_list_welcome">Tere tulemast kasutama rakendust DAVx⁵!</string>
<string name="account_list_empty">Loo ühendus oma serveriga ja hoia kalendrid ning kontaktid sünkroniseerituna.</string>
<string name="accounts_sync_all">Sünkroniseeri kõik kasutajakontod</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Teenuse tuvastamine ei õnnestunud</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Kogumike loendi uuendamine ei õnnestunud</string>
<!--Foreground service used by WorkManager on Android <12-->
<string name="foreground_service_notify_title">Töötame esiplaanil</string>
<string name="foreground_service_notify_text">See eelistus on vajalik sünkroniseerimiseks mõnedes seadmetes.</string>
<!--AppSettingsActivity-->
<string name="app_settings">Seadistused</string>
<string name="app_settings_debug">Silumine ja veaotsing</string>
<string name="app_settings_show_debug_info">Näita silumisteavet</string>
<string name="app_settings_show_debug_info_details">Vaata/jaga seadistuse üksikasju ja logisid</string>
<string name="app_settings_logging">Väga üksikasjalik logimine</string>
<string name="app_settings_logging_on">Logimine on kasutusel. Silumisteabe osana saad vaadata logisid.</string>
<string name="app_settings_logging_off">Logimine pole kasutusel</string>
<string name="app_settings_battery_optimization">Akukasutuse optimeerimine</string>
<string name="app_settings_battery_optimization_exempted">See rakendus ei allu akukasutuse optimeerimisele (soovitatav valik)</string>
<string name="app_settings_battery_optimization_optimized">Akukasutuse optimeerimise piirangud on kasutusel (mittesoovitatav valik)</string>
<string name="app_settings_connection">Ühendus</string>
<string name="app_settings_proxy">Proksiserveri tüüp</string>
<string-array name="app_settings_proxy_types">
<item>Süsteemi proksiserver</item>
<item>Proksiserver puudub</item>
<item>HTTP</item>
<item>SOCKS (Orboti jaoks)</item>
</string-array>
<string name="app_settings_proxy_host">Proksiserveri hostinimi</string>
<string name="app_settings_proxy_port">Proksiserveri port</string>
<string name="app_settings_security">Turvalisus</string>
<string name="app_settings_security_app_permissions">Rakenduse õigused</string>
<string name="app_settings_security_app_permissions_summary">Täpsusta sünkroniseerimiseks vajalike õigusi</string>
<string name="app_settings_distrust_system_certs">Ära usalda nutiseadme süsteemseid sertifikaate</string>
<string name="app_settings_distrust_system_certs_on">Süsteemsed ja kasutaja lisatud sertifitseerimiskeskused ei ole usaldatud</string>
<string name="app_settings_distrust_system_certs_off">Süsteemsed ja kasutaja lisatud sertifitseerimiskeskused on usaldatud (soovitatav valik)</string>
<string name="app_settings_reset_certificates">Lähtesta (mitte)usaldatud sertifikaatide loend</string>
<string name="app_settings_reset_certificates_summary">Selle valikuga eemaldatakse kõik sinu lisatud sertifikaatide usaldusmärked</string>
<string name="app_settings_reset_certificates_success">Kõik sinu lisatud sertifikaatide usaldusmärked on eemaldatud</string>
<string name="app_settings_user_interface">Kasutajaliides</string>
<string name="app_settings_notification_settings">Teavituste seadistused</string>
<string name="app_settings_notification_settings_summary">Halda teavituskanaleid ja nende seadistusi</string>
<string name="app_settings_theme_title">Vali kujundus</string>
<string-array name="app_settings_theme_names">
<item>Süsteemi kujundus</item>
<item>Hele kujundus</item>
<item>Tume kujundus</item>
</string-array>
<string name="app_settings_reset_hints">Lähtesta vihjed</string>
<string name="app_settings_reset_hints_summary">Lülitab varem väljalülitatud vihtjete kuvamise uuesti sisse</string>
<string name="app_settings_reset_hints_success">Näitame jälle kõiki vihjeid</string>
<string name="app_settings_integration">Lõimimine</string>
<string name="app_settings_tasks_provider">Ülesannete rakendus</string>
<string name="app_settings_tasks_provider_none">Ühilduvat ülesannete rakendust ei leidu</string>
<string name="app_settings_unifiedpush">UnifiedPush (katseline)</string>
<string name="app_settings_unifiedpush_disable">Puudub (tõuketeenuseid pole)</string>
<string name="app_settings_unifiedpush_choose_distributor">Vali levitaja</string>
<string name="app_settings_unifiedpush_no_distributor">Ühtegi tõukesõnumite levitajat pole paigaldatud</string>
<string name="app_settings_unifiedpush_no_endpoint">Otspunkt on seadistamata</string>
<string name="app_settings_unifiedpush_ready">Valmis tõuketeadete vastuvõtmiseks %s vahendusel</string>
<!--AccountScreen-->
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_missing_permissions">Nende kogumike sünkroniseerimiseks on vajalikud täiendavad õigused.</string>
<string name="account_manage_permissions">Halda õigusi</string>
<string name="account_synchronize_now">Sünkroniseeri nüüd</string>
<string name="account_settings">Kasutajakonto seadistused</string>
<string name="account_rename">Muuda kasutajakonto nime</string>
<string name="account_rename_new_name_description">Salvestamata kohalik teave võib vahele jääda. Peale nime muutmist palun sünkroniseeri uuesti.</string>
<string name="account_rename_new_name">Kasutajakonto uus nimi</string>
<string name="account_rename_rename">Muuda nime</string>
<string name="account_rename_exists_already">Selline nimi on juba kasutusel</string>
<string name="account_rename_couldnt_rename">Kasutajakonto nime muutmine ei õnnestunud</string>
<string name="account_delete">Kustuta kasutajakonto</string>
<string name="account_delete_confirmation_title">Kas tõesti kustutame kasutajakonto?</string>
<string name="account_delete_confirmation_text">Sellega kustutame ka kõik aadresside, kalendrite ja ülesannete kohalikud koopiad.</string>
<string name="account_synchronize_this_collection">sünkroniseeri see kogumik</string>
<string name="account_read_only">ainult lugemisõigus</string>
<string name="account_calendar">kalender</string>
<string name="account_contacts">kontaktid</string>
<string name="account_journal">päevik</string>
<string name="account_task_list">ülesanded</string>
<string name="account_only_personal">Näita vaid isiklikke</string>
<string name="account_refresh_collections">Uuenda loendit</string>
<string name="account_webcal_external_app">Webcali tellimusi on võimalik sünkroniseerida väliste rakendustega.</string>
<string name="account_no_webcal_handler_found">Webcaliga ühilduvaid rakendusi ei leidu</string>
<string name="account_install_icsx5">Paigalda ICSx⁵</string>
<!--AddAccountActivity-->
<string name="login_title">Lisa kasutajakonto</string>
<string name="login_privacy_hint"><![CDATA[Kõik andmed liiguvad vaid sinu serveri ja sinu nutiseadme vahel. %1$s ei saada neid mitte kuhugile mujale. Lisateavet leiad <a href="%2$s">meie privaatsuspoliitikast</a>.]]></string>
<string name="login_generic_login">Üldine sisselogimine</string>
<string name="login_provider_login">Teenusepakkujakohane sisselogimine</string>
<string name="login_continue">Jätka</string>
<string name="login_login">Logi sisse</string>
<string name="login_type_email">Logi sisse e-posti aadressiga</string>
<string name="login_email_address">E-posti aadress</string>
<string name="login_email_address_error">Nõutav on korrektne e-posti aadress</string>
<string name="login_email_address_info"><![CDATA[E-posti aadressi domeeni alusel leiame alustuseks mõeldud võrguaadressi. <a href="%s">Teenused tuvastame</a> nimeserveri kirjete ning „.well-known“ tunnusaadresside abil.]]></string>
<string name="login_password">Salasõna</string>
<string name="login_password_hide">Peida salasõna</string>
<string name="login_password_show">Näita salasõna</string>
<string name="login_password_optional">Salasõna*</string>
<string name="login_type_url">Logi sisse võrguaadressi ja kasutajanimega</string>
<string name="login_user_name">Kasutajanimi</string>
<string name="login_user_name_optional">Kasutajanimi*</string>
<string name="login_base_url">Alustuseks mõeldud võrguaadress</string>
<string name="login_base_url_info"><![CDATA[Kontrollime alustuseks mõeldud võrguaadressi ka, aga lisaks <a href="%s">tuvastame teenuseid</a> nimeserveri kirjete ning „.well-known“ tunnusaadresside abil.]]></string>
<string name="login_select_certificate">Vali sertifikaat</string>
<string name="login_add_account">Lisa kasutajakonto</string>
<string name="login_account_name">Kasutajakonto nimi</string>
<string name="login_account_avoid_apostrophe">Ülakomade (\') kasutamine tundub mõnedes seadmetes tekitama probleeme.</string>
<string name="login_account_name_info">Kuna Android pruugib kasutajakonto nime sinu loodavate ürituste Korraldaja ehk ORGANIZER välja väärtustamiseks, siis soovitame, et sinu kasutajakonto nimi on sinu e-posti aadress. Palun arvesta, et sul ei saa olla kahte samanimelist kasutajakontot.</string>
<string name="login_account_contact_group_method">Kontaktgrupi meetod:</string>
<string name="login_account_name_required">Kasutajakonto nimi on nõutav</string>
<string name="login_account_name_already_taken">Selline nimi on juba kasutusel</string>
<string name="login_account_not_added">Kasutajakonto lisamine ei õnnestunud</string>
<string name="login_finish">Lõpeta</string>
<string name="login_type_advanced">Täiendavad sisselogimise seadistused</string>
<string name="login_no_client_certificate_optional">Kliendi sertifikaat puudub*</string>
<string name="login_client_certificate_selected">Kliendi sertifikaat: %s</string>
<string name="login_no_certificate_found">Kliendisertifikaati ei leidunud</string>
<string name="login_install_certificate">Paigalda sertifikaat</string>
<string name="login_type_google">Google\'i Kontaktid / Kalender</string>
<string name="login_google_see_tested_with">Uuendatud teavet leiad meie „Tested with Google“ lehelt.</string>
<string name="login_google_unexpected_warnings">Võib tekkida ootamatuid vigu ja/või sa pead looma oma klienditunnuse.</string>
<string name="login_google_account">Google\'i kasutajakonto</string>
<string name="login_google">Logi sisse Google\'i kasutajakontoga</string>
<string name="login_google_client_id">Klienditunnus (kui soovid lisada)</string>
<string name="login_google_client_privacy_policy"><![CDATA[%1$s teisaldab sinu Google\'i kontaktide ja kalendri andmeid vaid sünkroniseerimiseks selles seadmes. Lisateavet leiad meie <a href="%2$s">Privaatsuspoliitikast</a>.]]></string>
<string name="login_google_client_limited_use"><![CDATA[%1$s järgib <a href="%2$s">Google\'i API teenuste kasutajaandmete poliitikat</a>, sealhulgas piiratud kasutuse nõudeid.]]></string>
<string name="login_oauth_couldnt_obtain_auth_code">Autoriseerimiskoodi saamine polnud võimalik</string>
<string name="login_type_nextcloud">Nextcloud</string>
<string name="login_nextcloud_login_with_nextcloud">Logi sisse Nextcloudi kontoga</string>
<string name="login_nextcloud_login_flow_text">Selle eelistusega käivitad Nextcloudi sisselogimise veebibrauseris.</string>
<string name="login_nextcloud_login_flow_server_address">Nextcloudi serveri aadress</string>
<string name="login_nextcloud_login_flow_sign_in">Logi sisse</string>
<string name="login_nextcloud_login_flow_no_login_url">Sisselogimise võrguaadressi tuvastamine polnud võimalik</string>
<string name="login_nextcloud_login_flow_no_login_data">Sisselogimisandmete tuvastamine polnud võimalik</string>
<string name="login_configuration_detection">Seadistuste tuvastamine</string>
<string name="login_querying_server">Palun oota, pärime andmeid serverist…</string>
<string name="login_no_service">Ei õnnestunud leida CalDAV või CardDAV teenust.</string>
<string name="login_no_service_info">Antud võrguaadress ei tundu olema ligipääsetav CalDAVi/CardDAVi võrguaadress ja teenuse tuvastamine ei õnnestunud.</string>
<string name="login_see_tested_services"><![CDATA[Lisateavet leidad oma teenusepakkuja juhendist ja <a href="%s">meie poolt testitud teenuste loendist</a> koos toimivate võrguaadressidega.]]></string>
<string name="login_check_credentials">Palun samuti topeltkontrolli autentimist (tavaliselt kasutajanimi ja salasõna)</string>
<string name="login_logs_available">Täiendav tehniline teade leidub logides.</string>
<string name="login_view_logs">Vaata logisid</string>
<!--AccountSettingsActivity-->
<string name="settings_sync">Sünkroniseerimine</string>
<string name="settings_sync_interval_contacts">Kontaktide sünkroniseerimise välp</string>
<string name="settings_sync_summary_manually">Vaid käsitsi</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Iga %d minuti järel + kohalikud muudatused koheselt</string>
<string name="settings_sync_interval_calendars">Kalendrite sünkroniseerimise välp</string>
<string name="settings_sync_interval_tasks">Ülesannete sünkroniseerimise välp</string>
<string-array name="settings_sync_interval_names">
<item>Vaid käsitsi</item>
<item>Iga 15 minuti järel</item>
<item>Iga 30 minuti järel</item>
<item>Kord tunnis</item>
<item>Iga 2 tunni järel</item>
<item>Iga 4 tunni järel</item>
<item>Kord päevas</item>
</string-array>
<string name="settings_sync_wifi_only">Sünkroniseeri vaid WiFi ühendusega</string>
<string name="settings_sync_wifi_only_on">Sünkroniseerimine on lubatud vaid WiFi ühendusega</string>
<string name="settings_sync_wifi_only_off">Ühenduse liik pole oluline</string>
<string name="settings_sync_wifi_only_ssids">WiFi SSID piirangud</string>
<string name="settings_sync_wifi_only_ssids_on">Sünkroniseeri vaid %s võrgus</string>
<string name="settings_sync_wifi_only_ssids_off">Kasuta kõiki WiFi ühendusi</string>
<string name="settings_sync_wifi_only_ssids_message">Lubatud WiFi võrgunimede (SSID) komadega eraldatud loend (kui jätad tühjaks on kõik lubatud)</string>
<string name="settings_sync_wifi_only_ssids_permissions_required">WiFi SSID piirang vajab täiendavat saedistamist</string>
<string name="settings_sync_wifi_only_ssids_permissions_action">Halda</string>
<string name="settings_ignore_vpns">VPNi kasutamine eeldab, et võrguühendus toimib</string>
<string name="settings_ignore_vpns_on">VPN ilma toimiva ja kontrollitud internetiühenduseta pole piisav sünkroniseerimiseks (soovitatud)</string>
<string name="settings_ignore_vpns_off">VPN ilma toimiva ja kontrollitud internetiühenduseta on sünkroniseerimiseks piisav</string>
<string name="settings_authentication">Autentimine</string>
<string name="settings_username">Kasutajanimi</string>
<string name="settings_password">Salasõna</string>
<string name="settings_new_password">Uus salasõna</string>
<string name="settings_password_summary">Uuenda salasõna vastavalt oma serveri juhendile.</string>
<string name="settings_certificate_alias">Kliendi sertifikaat</string>
<string name="settings_certificate_alias_empty">Sertifikaati pole saadaval või paigaldatud</string>
<string name="settings_certificate_install">Paigalda sertifikaat</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Möödunud sündmuste ajapiir</string>
<string name="settings_sync_time_range_past_none">Kõik sündmused kuuluvad sünkroniseerimisele</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Eira enam kui üks päev vanu sündmuseid</item>
<item quantity="other">Eira enam kui %d päeva vanu sündmuseid</item>
</plurals>
<string name="settings_sync_time_range_past_message">Sündmused, mis on vanemad, kui siin märgitud päevade arv, jäävad sünkroniseerimata (võib olla ka 0). Kõikide sündmuste sünkroniseerimiseks jäta tühjaks.</string>
<string name="settings_default_alarm">Vaikimisi meeldetuletus</string>
<plurals name="settings_default_alarm_on">
<item quantity="one">Vaikimisi meeldetuletus üks minutit enne sündmust</item>
<item quantity="other">Vaikimisi meeldetuletus %d minutit enne sündmust</item>
</plurals>
<string name="settings_default_alarm_off">Vaikimisi meeldetuletused puuduvad</string>
<string name="settings_default_alarm_message">Eelistus määrab, kas kasutame vaikimisi meeldetuletust sündmuste puhul, kus eraldi meeldetuletus on seadistamata. Aktiveerimiseks sisesta vaikimisi meeldetuletuse aeg minutites. Väljalülitamiseks jäta tühjaks.</string>
<string name="settings_manage_calendar_colors">Halda kalendrivärve</string>
<string name="settings_manage_calendar_colors_on">Kalendri värvid lähtestatakse igal sünkroniseerimisel</string>
<string name="settings_manage_calendar_colors_off">Muud rakendused võivad kalendrivärve seadistada</string>
<string name="settings_event_colors">Sündmuste värvide tugi</string>
<string name="settings_event_colors_on">Sündmuste värvid kuuluvad sünkroniseerimisele</string>
<string name="settings_event_colors_off">Sündmuste värvid ei kuulu sünkroniseerimisele</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Kontaktgrupi meetod</string>
<string-array name="settings_contact_group_method_entries">
<item>Grupid on eraldi vCard-kirjed</item>
<item>Grupid on kontaktikohased kategooriad</item>
</string-array>
<!--CreateAddressBookScreen, CreateCalendarScreen-->
<string name="create_addressbook">Loo aadressiraamat</string>
<string name="create_addressbook_maybe_not_supported">See server ei pruugi toetada aadressiraamatu loomist CardDAVi ühenduse abil.</string>
<string name="create_calendar">Loo kalender</string>
<string name="create_calendar_time_zone_optional">Vaikimisi ajavöönd*</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">Võimalikud kalendrikirjed</string>
<string name="create_calendar_type_vevent">Sündmused</string>
<string name="create_calendar_type_vtodo">Ülesanded</string>
<string name="create_calendar_type_vjournal">Märkmed / päevik</string>
<string name="create_calendar_maybe_not_supported">See server ei pruugi toetada kalendri loomist CalDAVi ühenduse abil.</string>
<string name="create_collection_color">Värv</string>
<string name="create_collection_display_name">Pealkiri</string>
<string name="create_collection_home_set">Andmeruumi asukoht</string>
<string name="create_collection_description_optional">Kirjeldus*</string>
<string name="create_collection_create">Loo</string>
<string name="create_collection_optional">* valikuline</string>
<!--CollectionScreen-->
<string name="collection_delete">Kustuta kogumik</string>
<string name="collection_delete_warning">See kogumik (%s) koos oma kõikide andmetega kustutatakse nüüd jäädavalt nii serverist, kui kohalikust nutiseadmest.</string>
<string name="collection_synchronization">Sünkroniseerimine</string>
<string name="collection_synchronization_on">Sünkroniseerimine on kasutusel</string>
<string name="collection_synchronization_off">Sünkroniseerimine pole kasutusel</string>
<string name="collection_read_only">Ainult lugemisõigus</string>
<string name="collection_read_only_by_server">Ainult lugemisõigus (serveri poolt)</string>
<string name="collection_read_only_by_setting">Ainult lugemisõigus (reeglite alusel)</string>
<string name="collection_read_only_forced">Ainult lugemisõigus (ainult kohalikus nutiseadmes)</string>
<string name="collection_read_write">Lugemis- ja kirjutamisõigus</string>
<string name="collection_title">Pealkiri</string>
<string name="collection_description">Kirjeldus</string>
<string name="collection_owner">Omanik</string>
<string name="collection_push_support">Tõuketeenuse tugi</string>
<string name="collection_push_web_push">Server teavitab tõuketeenuse toe olemasolust</string>
<string name="collection_push_subscribed_at">Tellitud %1$s, aegub %2$s</string>
<string name="collection_last_sync">Viimane sünkroniseerimine (%s)</string>
<string name="collection_url">Aadress (võrguaadress)</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_title">Silumisteave</string>
<string name="debug_info_archive_caption">ZIP-arhiivifail</string>
<string name="debug_info_archive_subtitle">Sisaldab silumisteavet ja logisid</string>
<string name="debug_info_archive_text">Tõsta arhiiv uurimiseks arvutisse, saada huvilisele e-postiga või lisa veateatele meie veahalduses.</string>
<string name="debug_info_archive_share">Jaga arhiivi</string>
<string name="debug_info_attached">Sõnumile lisatud silumisteave (eeldab, et vastuvõttev rakendus oskab manuseid käsitleda).</string>
<string name="debug_info_http_error">HTTP-viga</string>
<string name="debug_info_server_error">Serveri viga</string>
<string name="debug_info_webdav_error">WebDAVi viga</string>
<string name="debug_info_io_error">Sisend-/väljundviga</string>
<string name="debug_info_http_403_description">Osapool keeldus päringule vastamast. Kontrolli asjaomaseid teenuseid ja tarvikuid ning üksikasjalikku teavet võid leida silumislogidest.</string>
<string name="debug_info_http_404_description">Soovitud teenust või tarvikud pole (enam) olemas. Kontrolli asjaomaseid teenuseid ja tarvikuid ning üksikasjalikku teavet võid leida silumislogidest.</string>
<string name="debug_info_http_5xx_description">Tekkis serveripoolne viga. Palun võta ühendust serveri haldajaga.</string>
<string name="debug_info_unexpected_error">Tekkis ootamatu viga. Lisainfot leiad silumisteabest.</string>
<string name="debug_info_view_details">Vaata üksikasju</string>
<string name="debug_info_subtitle">Silumisteave on kogutud</string>
<string name="debug_info_involved_caption">Seotud teenused ja tarvikud</string>
<string name="debug_info_involved_subtitle">Probleemi või veaga seotud teave</string>
<string name="debug_info_involved_remote">Serveris asuvad teenused ja tarvikud:</string>
<string name="debug_info_involved_local">Kohalikus nutiseadmes teenused ja tarvikud:</string>
<string name="debug_info_logs_caption">Logid</string>
<string name="debug_info_logs_subtitle">Saadaval on üksikasjalikud logid</string>
<string name="debug_info_logs_view">Vaata logisid</string>
<!--ExceptionInfoFragment-->
<string name="exception">Tekkis viga.</string>
<string name="exception_httpexception">Tekkis http-viga.</string>
<string name="exception_ioexception">Tekkis sisend-väljundviga.</string>
<string name="exception_show_details">Näita üksikasju</string>
<!--WebDAV accounts-->
<string name="webdav_mounts_title">WebDAVi haakepunktid</string>
<string name="webdav_mounts_quota_used_available">Kasutatud mahukvoot: %1$s / saadaval: %2$s</string>
<string name="webdav_mounts_share_content">Jaga sisu</string>
<string name="webdav_mounts_unmount">Eemalda haakimine</string>
<string name="webdav_add_mount_title">Lisa WebDAVi haakepunkt</string>
<string name="webdav_mounts_empty">Otseligipääs sinu failidele WebDAVi haakepunktist!</string>
<string name="webdav_add_mount_empty_more_info"><![CDATA[Vaata juhendit <a href="%1$s">kuidas WebDAVi haakepunktid toimivad</a>.</string>]]></string>
<string name="webdav_add_mount_display_name">Kuvatav nimi</string>
<string name="webdav_add_mount_url">WebDAVi võrguaadress</string>
<string name="webdav_add_mount_url_invalid">Vigane võrguaadress</string>
<string name="webdav_add_mount_authentication">Autentimine (kui on vaja)</string>
<string name="webdav_add_mount_username">Kasutajanimi</string>
<string name="webdav_add_mount_password">Salasõna</string>
<string name="webdav_add_mount_add">Lisa haakepunkt</string>
<string name="webdav_add_mount_no_support">Sellel võrguaadressil ei leidu WebDAVi teenust</string>
<string name="webdav_remove_mount_title">Eemalda haakepunkt</string>
<string name="webdav_remove_mount_text">Ühenduse andmed lähevad kaotsi, aga ühtegi faili ei kustutata.</string>
<string name="webdav_notification_access">Ligipääs WebDAVi failile</string>
<string name="webdav_notification_download">Laadime WebDAVi faili alla</string>
<string name="webdav_notification_upload">Laadime WebDAVi faili üles</string>
<string name="webdav_provider_root_title">WebDAVi haakepunkt</string>
<!--sync-->
<string name="sync_error_permissions">DAVx⁵ õigused</string>
<string name="sync_error_permissions_text">Vajalikud on täiendavad õigused</string>
<string name="sync_error_tasks_too_old">%s on liiga vana</string>
<string name="sync_error_tasks_required_version">Väikseim nõutav versioon: %1$s</string>
<string name="sync_error_authentication_failed">Autentimine ei õnnestunud (kontrolli, et kasutajanimi/salasõna oleksid õiged)</string>
<string name="sync_error_io">Võrgu- või sisend/väljundviga %s</string>
<string name="sync_error_http_dav">HTTP serveri viga %s</string>
<string name="sync_error_local_storage">Kohaliku salvestusruumi viga %s</string>
<string name="sync_error_retry_limit_reached">Pehme viga (korduspäringute arvu ülempiir on käes)</string>
<string name="sync_error_view_item">Vaata objekti</string>
<string name="sync_invalid_contact">Saime serverist vigase kontaktikirje</string>
<string name="sync_invalid_event">Saime serverist vigase sündmusekirje</string>
<string name="sync_invalid_task">Saime serverist vigase ülesandekirje</string>
<string name="sync_invalid_resources_ignoring">Eirame ühte või enamat teenust või tarvikut</string>
<string name="sync_notification_pending_push_title">Sünkroniseerimine on ootel</string>
<string name="sync_notification_pending_push_message">Serveris olevad andmed on muutunud</string>
<!--widgets-->
<string name="widget_sync_all">Sünkroniseeri kõik</string>
<string name="widget_sync_all_accounts">Sünkroniseeri kõik kasutajakontod</string>
<!--cert4android-->
</resources>

View File

@@ -1,42 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVx⁵</string>
<string name="account_title_address_book">DAVx⁵ Osoitekirja</string>
<string name="address_books_authority_title">Osoitekirjat</string>
<string name="help">Apua</string>
<string name="manage_accounts">Hallitse tilejä</string>
<string name="notification_channel_debugging">Debuggaus</string>
<string name="notification_channel_general">Muut tärkeät viestit</string>
<string name="notification_channel_sync">Synkronointi</string>
<string name="notification_channel_sync_errors">Synkronoinnin virheet</string>
<string name="notification_channel_sync_errors_desc">Huomattavat virheet jotka estävät synkronoinnin kuten palvelimen odottamattomat vastaukset </string>
<string name="notification_channel_sync_warnings">Synkronoinnin varoitukset</string>
<string name="notification_channel_sync_warnings_desc">Ei-kohtalokkaat synkronoinnin ongelmat kuten tietyt virheelliset tiedostot </string>
<string name="notification_channel_sync_io_errors">Verkko ja I/O virheet</string>
<string name="notification_channel_sync_io_errors_desc">Aikakatkaisut, yhteysvirheet, yms. (usein väliaikaisia)</string>
<!--IntroActivity-->
<!--PermissionsActivity-->
<!--WifiPermissionsActivity-->
<!--AboutActivity-->
<!--global settings-->
<!--AccountsActivity-->
<!--DavService-->
<!--ForegroundService-->
<!--AppSettingsActivity-->
<!--AccountActivity-->
<!--AddAccountActivity-->
<string name="login_type_email">Kirjaudu sähköpostilla</string>
<string name="login_email_address">Sähköpostiosoite</string>
<string name="login_password">Salasana</string>
<string name="login_type_url">Kirjaudu verkko-osoitteella ja käyttäjänimellä</string>
<string name="login_user_name">Käyttäjänimi</string>
<!--AccountSettingsActivity-->
<string name="settings_username">Käyttäjänimi</string>
<string name="settings_password">Salasana</string>
<!--collection management-->
<!--debugging and DebugInfoActivity-->
<!--ExceptionInfoFragment-->
<!--sync adapters-->
<!--cert4android-->
</resources>

View File

@@ -1,418 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="account_invalid">Account inesistente (o cancellato)</string>
<string name="account_title_address_book">Rubrica DAVx⁵</string>
<string name="dialog_delete">Cancella</string>
<string name="dialog_remove">Elimina</string>
<string name="dialog_deny">Annulla</string>
<string name="field_required">Questo campo è necessario</string>
<string name="help">Aiuto</string>
<string name="optional_label">* opzionale</string>
<string name="options_menu">Menu opzioni</string>
<string name="share">Condividi</string>
<string name="sync_started">Sincronizzazione avviata</string>
<string name="database_destructive_migration_title">Database danneggiato</string>
<string name="database_destructive_migration_text">Tutti gli account sono stati rimossi localmente.</string>
<string name="notification_channel_debugging">Debugging</string>
<string name="notification_channel_general">Altri messaggi importanti</string>
<string name="notification_channel_status">Messaggi di stato a bassa priorità</string>
<string name="notification_channel_sync">Sincronizzazione</string>
<string name="notification_channel_sync_errors">Errori di sincronizzazione</string>
<string name="notification_channel_sync_errors_desc">Errori importanti che bloccano la sincronizzazione, come risposte inattese del server</string>
<string name="notification_channel_sync_warnings">Avvisi di sincronizzazione</string>
<string name="notification_channel_sync_warnings_desc">Problemi di sincronizzazione non gravi come alcuni file non validi</string>
<string name="notification_channel_sync_io_errors">Errori di Rete e di I/O</string>
<string name="notification_channel_sync_io_errors_desc">Timeouts, problemi di connessione, ecc. (spesso temporanei)</string>
<!--IntroActivity-->
<string name="intro_slogan1">Tuoi i dati. Tua la scelta.</string>
<string name="intro_slogan2">Riprendi il controllo.</string>
<string name="intro_battery_title">Intervalli di sincronizzazione regolari.</string>
<string name="intro_battery_text">Per sincronizzare i dati a intervalli regolari, %s deve essere autorizzato a girare in background. Altrimenti Android può mettere in pausa gli aggiornamenti in qualunque momento.</string>
<string name="intro_battery_dont_show">Non ho bisogno di sincronizzare a intervalli di tempo regolari.*</string>
<string name="intro_autostart_title">%s compatibilità</string>
<string name="intro_autostart_text">Questo dispositivo probabilmente impedisce la sincronizzazione. In questo caso puoi risolvere solo manualmente.</string>
<string name="intro_autostart_dont_show">Ho settato le impostazioni richieste. Non ricordarmelo più.</string>
<string name="intro_leave_unchecked">* Lascia smarcato per fartelo ricordare dopo. Può essere reimpostato nelle impostazione dell\'app %s.</string>
<string name="intro_more_info">Maggiori informazioni</string>
<string name="intro_tasks_jtx_info"><![CDATA[Supporta la sincronizzazione di Attività, Diari e Note.]]></string>
<string name="intro_tasks_title">Supporto per le attività</string>
<string name="intro_tasks_text1">Se le attività sono supportate dal tuo server, possono essere sincronizzate con una app per attività supportata:</string>
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Non sembra essere più sviluppato - non raccomandato.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_no_app_store">Nessun app store disponibile</string>
<string name="intro_tasks_dont_show">Non ho bisogno del supporto alle attività.*</string>
<string name="intro_open_source_title">Software open-source</string>
<string name="intro_open_source_text">Siamo felici che tu usi %s, che è un software open source. Lo sviluppo, la manutenzione e il supporto sono compiti duri. Per piacere prendi in considerazione di dare una mano (puoi farlo in molti modi) o una donazione. Sarebbe davvero apprezzato!</string>
<string name="intro_open_source_details">Come aiutare/donare</string>
<string name="intro_open_source_dont_show">Non mostrare nell\'immediato futuro</string>
<!--PermissionsActivity-->
<string name="permissions_title">Autorizzazioni</string>
<string name="permissions_text">%s richiede autorizzazioni per funzionare correttamente.</string>
<string name="permissions_all_title">Tutti i seguenti</string>
<string name="permissions_all_status_off">Usare questo per abilitare tutte le funzioni (consigliato)</string>
<string name="permissions_all_status_on">Concedi tutte le autorizzazioni</string>
<string name="permissions_contacts_title">Autorizzazioni per i contatti</string>
<string name="permissions_contacts_status_off">Non sincronizzare i contatti (sconsigliato)</string>
<string name="permissions_contacts_status_on">Possibilità di sincronizzare i contatti</string>
<string name="permissions_calendar_title">Autorizzazioni per il calendario</string>
<string name="permissions_calendar_status_off">Non sincronizzare il calendario (sconsigliato)</string>
<string name="permissions_calendar_status_on">Permette di sincronizzare il calendario</string>
<string name="permissions_notification_title">Autorizza notifiche</string>
<string name="permissions_notification_status_off">Notifiche disabilitate (non consigliato)</string>
<string name="permissions_notification_status_on">Notifiche attive</string>
<string name="permissions_opentasks_title">Autorizzazioni di OpenTasks</string>
<string name="permissions_tasksorg_title">Autorizzazioni delle attività</string>
<string name="permissions_tasks_status_on">Permette di sincronizzare le attività</string>
<string name="permissions_autoreset_title">Mantieni autorizzazioni</string>
<string name="permissions_autoreset_status_off">Le autorizzazioni possono essere reimpostate automaticamente (sconsigliato)</string>
<string name="permissions_autoreset_status_on">Le autorizzazioni non si reimposteranno automaticamente</string>
<string name="permissions_autoreset_instruction">Fai click su Autorizzazioni &gt; deseleziona \"Rimuovi autorizzazioni se l\'app non è in uso\"</string>
<string name="permissions_app_settings_hint">Se uno slider non funziona, vai a impostazioni app/ autorizzazioni.</string>
<string name="permissions_app_settings">Impostazioni app</string>
<!--WifiPermissionsActivity-->
<string name="wifi_permissions_label">Autorizzazioni per WiFi SSID</string>
<string name="wifi_permissions_intro">Per poter accedere al nome dell\'attuale nome del WIFI (SSID), devono essere soddfsfatte queste condizioni:</string>
<string name="wifi_permissions_location_permission">Autorizzazione precisa della localizzazione</string>
<string name="wifi_permissions_location_permission_on">Garantire l\'autorizzazione della posizione</string>
<string name="wifi_permissions_location_permission_off">Negare l\'autorizzazione della posizione</string>
<string name="wifi_permissions_background_location_permission">Autorizzazione della posizione in background</string>
<string name="wifi_permissions_background_location_permission_label">Permettere sempre</string>
<string name="wifi_permissions_background_location_permission_on">Permessi di localizzazione impostati a: %s</string>
<string name="wifi_permissions_background_location_permission_off">Permessi di localizzazione non impostati a: %s</string>
<string name="wifi_permissions_location_enabled">Posizione sempre disabilitata</string>
<string name="wifi_permissions_location_enabled_on">Servizio di posizione abiltato</string>
<string name="wifi_permissions_location_enabled_off">Servizio di posizione disabilitato</string>
<!--AboutActivity-->
<string name="about_translations">Traduzioni</string>
<string name="about_libraries">Librerie</string>
<string name="about_version">Versione %1$s (%2$d)</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) e contibutori</string>
<string name="about_license_info_no_warranty">Il programma è distribuito SENZA ALCUNA GARANZIA. È software libero e può essere redistribuito sotto alcune condizioni.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Impossibile creare il file di log</string>
<string name="logging_notification_text">Adesso l\'accesso all\' %s delle attività </string>
<string name="logging_notification_view_share">Visualizza/condividi</string>
<string name="logging_notification_disable">Disabilita</string>
<!--AccountsScreen-->
<string name="navigation_drawer_subtitle">CalDAV/CardDAV adattatore di sincronizzazione</string>
<string name="navigation_drawer_about">Informazioni / Licenza</string>
<string name="navigation_drawer_beta_feedback">Feedback sulla beta</string>
<string name="install_browser">Installare un browser Web</string>
<string name="navigation_drawer_settings">Impostazioni</string>
<string name="navigation_drawer_news_updates">Notizie &amp; aggiornamenti</string>
<string name="navigation_drawer_tools">Strumenti</string>
<string name="navigation_drawer_external_links">Link esterni</string>
<string name="navigation_drawer_website">Sito web</string>
<string name="navigation_drawer_manual">Manuale</string>
<string name="navigation_drawer_faq">Domande Frequenti</string>
<string name="navigation_drawer_community">Comunità</string>
<string name="navigation_drawer_support_project">Supporta il progetto</string>
<string name="navigation_drawer_contribute">Come contribuire</string>
<string name="navigation_drawer_privacy_policy">Politica sulla riservatezza</string>
<string name="account_list_no_notification_permission">Notifiche non attive. Non sarai avvisato di eventuali errori di sincronizzazione</string>
<string name="account_list_manage_connections">Gestione connessioni</string>
<string name="account_list_datasaver_enabled">Risparmio dati attivo. La sincronizzazione in background è limitata,</string>
<string name="account_list_battery_saver_enabled">Risparmio energetico attivo. La sincronizzazione in background è limitata,</string>
<string name="account_list_manage_battery_saver">Gestisci risparmio energetico</string>
<string name="account_list_low_storage">Spazio di memorizzazione scarso. Androin non salverà immediatamente i cambiamente, ma alla prossima sincronizzazione programmata.</string>
<string name="account_list_manage_storage">Gestisci spazio di memorizzazione</string>
<string name="accounts_sync_all">Sincronizzazione di tutti gli account</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Impossibile trovare il servizio</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Impossibile aggiornare la lista delle raccolte</string>
<!--Foreground service used by WorkManager on Android <12-->
<string name="foreground_service_notify_title">Esecuzione in primo piano</string>
<string name="foreground_service_notify_text">Su alcuni dispositivi, questo è necessario per la sincronizzazione automatica.</string>
<!--AppSettingsActivity-->
<string name="app_settings">Impostazioni</string>
<string name="app_settings_debug">Debug</string>
<string name="app_settings_show_debug_info">Mostra informazioni di debug</string>
<string name="app_settings_logging">Log completo</string>
<string name="app_settings_logging_off">Log disabilitato</string>
<string name="app_settings_battery_optimization">Ottimizzazione batteria</string>
<string name="app_settings_connection">Connessione</string>
<string name="app_settings_proxy">Tipo di proxy</string>
<string-array name="app_settings_proxy_types">
<item>Predefinito di sistema</item>
<item>Nessun proxy</item>
<item>HTTP</item>
<item>SOCKS (per Orbot)</item>
</string-array>
<string name="app_settings_proxy_host">Nome host proxy</string>
<string name="app_settings_proxy_port">Porta proxy</string>
<string name="app_settings_security">Sicurezza</string>
<string name="app_settings_security_app_permissions">Autorizzazioni app</string>
<string name="app_settings_security_app_permissions_summary">Controlla le autorizzazioni per la sincronizzazione</string>
<string name="app_settings_distrust_system_certs">Non ti fidare dei certificati di sistema</string>
<string name="app_settings_distrust_system_certs_on">Le CA di sistema e quelle aggiunte dall\'utente non sono affidabili</string>
<string name="app_settings_distrust_system_certs_off">Le CA di sistema e quelle aggiunte dall\'utente sono affidabili (raccomandato)</string>
<string name="app_settings_reset_certificates">Reimposta la fiducia in tutti i certificati</string>
<string name="app_settings_reset_certificates_summary">Reimposta la fiducia nei certificati aggiunti</string>
<string name="app_settings_reset_certificates_success">Sono stati cancellati tutti i certificati aggiunti</string>
<string name="app_settings_user_interface">Interfaccia utente</string>
<string name="app_settings_notification_settings">Impostazioni di notifica</string>
<string name="app_settings_notification_settings_summary">Gestisci i canali di notifica e le loro impostazioni</string>
<string name="app_settings_theme_title">Seleziona il tema</string>
<string-array name="app_settings_theme_names">
<item> Sistema predefinito </item>
<item> Luce </item>
<item> Buio </item>
</string-array>
<string name="app_settings_reset_hints">Reimposta i suggerimenti</string>
<string name="app_settings_reset_hints_summary">Riabilita i suggerimenti precedentemente disabilitati</string>
<string name="app_settings_reset_hints_success">I suggerimenti verranno mostrati</string>
<string name="app_settings_integration">Integrazione</string>
<string name="app_settings_tasks_provider">Funzioni dell\'applicazione</string>
<string name="app_settings_tasks_provider_none">Nessuna applicazione compatibile con e funzionalità trovata</string>
<!--AccountScreen-->
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_missing_permissions">Per sincronizzare questi dati sono richiesti permessi aggiuntivi.</string>
<string name="account_manage_permissions">Gestisci permessi</string>
<string name="account_synchronize_now">Sincronizza adesso</string>
<string name="account_settings">Impostazioni account</string>
<string name="account_rename">Rinomina account</string>
<string name="account_rename_new_name_description">Dati locali non salvati potrebbero venir persi. Dopo il cambio nome è necessaria la ri-sincronizzazione.</string>
<string name="account_rename_new_name">Nuovo nome account</string>
<string name="account_rename_rename">Rinomina</string>
<string name="account_rename_exists_already">Nome account già usato</string>
<string name="account_rename_couldnt_rename">Impossibile rinominare l\'account</string>
<string name="account_delete">Elimina account</string>
<string name="account_delete_confirmation_title">Cancellare l\'account?</string>
<string name="account_delete_confirmation_text">Tutte le copie locali delle rubriche, dei calendari e degli elenchi attività verranno eliminate.</string>
<string name="account_synchronize_this_collection">Sincronizza questa raccolta</string>
<string name="account_read_only">sola lettura</string>
<string name="account_calendar">calendario</string>
<string name="account_contacts">contatti</string>
<string name="account_journal">diario</string>
<string name="account_task_list">attività</string>
<string name="account_only_personal">Mostra solo personale</string>
<string name="account_refresh_collections">Aggiorna lista</string>
<string name="account_webcal_external_app">Sottoscrizioni al Webcal possono essere sincronizzate con applicazioni esterne.</string>
<string name="account_no_webcal_handler_found">Non ho trovato nessuna applicazione abilitata per Webcal</string>
<string name="account_install_icsx5">Installa ICSx⁵</string>
<!--AddAccountActivity-->
<string name="login_title">Aggiungi account</string>
<string name="login_generic_login">Login generico</string>
<string name="login_provider_login">Login del Provider</string>
<string name="login_continue">Continua</string>
<string name="login_login">Login</string>
<string name="login_type_email">Accedi con indirizzo email</string>
<string name="login_email_address">Indirizzo email</string>
<string name="login_email_address_error">È necessario un indirizzo email valido</string>
<string name="login_email_address_info"><![CDATA[Viene usato il dominio dell\'email come URL base. <a href="%s">I servizi sono individuati </a> usando record DNS e le URL well-known.]]></string>
<string name="login_password">Password</string>
<string name="login_password_hide">Nascondi password</string>
<string name="login_password_show">Mostra password</string>
<string name="login_password_optional">Password*</string>
<string name="login_type_url">Accedi con URL e nome utente</string>
<string name="login_user_name">Nome utente</string>
<string name="login_user_name_optional">Nome utente*</string>
<string name="login_base_url">Base URL</string>
<string name="login_base_url_info"><![CDATA[La URL base viene controllata direttamente, ma <a href="%s">i servizi sono individuati anche </a> usando record DNS records e le URL well-known.]]></string>
<string name="login_select_certificate">Seleziona certificato</string>
<string name="login_add_account">Aggiungi account</string>
<string name="login_account_name">Nome account</string>
<string name="login_account_avoid_apostrophe">L\'uso degli apostrofi (\') potrebbe causare problemi su alcuni dispositivi.</string>
<string name="login_account_name_info">Inserisci il tuo indirizzo email come nome dell\'account in quanto Android userà il nome dell\'account nel campo ORGANIZER degli eventi creati. Non è possibile avere due account con nome uguale.</string>
<string name="login_account_contact_group_method">Metodo del contact group:</string>
<string name="login_account_name_required">Richiesto il nome dell\'account</string>
<string name="login_account_name_already_taken">Nome account già usato</string>
<string name="login_type_advanced">Login avanzato</string>
<string name="login_no_client_certificate_optional">Nessun certificato client*</string>
<string name="login_client_certificate_selected">Certificato client: %s</string>
<string name="login_no_certificate_found">Nessun certificato trovato</string>
<string name="login_install_certificate">Installa il certificato</string>
<string name="login_type_google">Contatti Google / Calendario</string>
<string name="login_google_see_tested_with">Consultare la nostra pagina \"Tested with Google\" per informazioni aggiornate.</string>
<string name="login_google_account">Account Google</string>
<string name="login_google">Accedi con Google</string>
<string name="login_google_client_id">ID Client (facoltativo)</string>
<string name="login_google_client_limited_use"><![CDATA[%1$s è conforme alla <a href="%2$s">Google API Services User Data Policy</a>, incluso il Limited Use requirements.]]></string>
<string name="login_oauth_couldnt_obtain_auth_code">Non posso ottenere il codice di autorizzazione</string>
<string name="login_type_nextcloud">Nextcloud</string>
<string name="login_nextcloud_login_with_nextcloud">Accedi con Nextcloud</string>
<string name="login_nextcloud_login_flow_text">Questo aprirà la pagina di login di Nextcloud nel browser.</string>
<string name="login_nextcloud_login_flow_server_address">Indirizzo del server Nextcloud</string>
<string name="login_nextcloud_login_flow_sign_in">Iscriviti</string>
<string name="login_nextcloud_login_flow_no_login_url">Non posso ottenere l\'URL di login</string>
<string name="login_nextcloud_login_flow_no_login_data">Non posso ottenere i dati di login</string>
<string name="login_configuration_detection">Rilevazione configurazione</string>
<string name="login_querying_server">Attendere, invio richiesta al server…</string>
<string name="login_no_service">Impossibile trovare servizi CalDAV o CardDAV.</string>
<string name="login_no_service_info">L\'URL base non sembra essere un URL CalDAV/CardDAV accessibile e i servizi di individuazione hanno fallito.</string>
<string name="login_check_credentials">Controlla attentamente i dati di autenticazione (normalmente username e password).</string>
<string name="login_logs_available">Informazioni tecniche aggiuntive sono reperibili nei log.</string>
<string name="login_view_logs">Vedi i registri</string>
<!--AccountSettingsActivity-->
<string name="settings_sync">Sincronizzazione</string>
<string name="settings_sync_interval_contacts">Intervallo sincr. Contatti</string>
<string name="settings_sync_summary_manually">Solo manualmente</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Ogni %d minuti e a seguito di ogni cambiamento locale</string>
<string name="settings_sync_interval_calendars">Intervallo sincr. calendari</string>
<string name="settings_sync_interval_tasks">Intervallo sincr. attività</string>
<string-array name="settings_sync_interval_names">
<item>Solo manualmente</item>
<item>Ogni 15 minuti</item>
<item>Ogni 30 minuti</item>
<item>Ogni ora</item>
<item>Ogni 2 ore</item>
<item>Ogni 4 ore</item>
<item>Una volta al giorno</item>
</string-array>
<string name="settings_sync_wifi_only">Sincr. solo tramite WiFi</string>
<string name="settings_sync_wifi_only_on">La sincronizzazione è limitata alle connessioni WiFi</string>
<string name="settings_sync_wifi_only_off">Il tipo di connessione non è preso in considerazione</string>
<string name="settings_sync_wifi_only_ssids">Restrizione SSID WiFi</string>
<string name="settings_sync_wifi_only_ssids_on">Sincronizzeremo solo oltre %s</string>
<string name="settings_sync_wifi_only_ssids_off">Verranno utilizzate tutte le connessioni WIFI </string>
<string name="settings_sync_wifi_only_ssids_message">Nomi (SSID) delle reti WiFi autorizzate separati da virgola (lascia vuoto per autorizzarle tutte)</string>
<string name="settings_sync_wifi_only_ssids_permissions_required">Le restrizioni del SSID WIFI richiedono ulteriori impostazioni</string>
<string name="settings_sync_wifi_only_ssids_permissions_action">Riuscire</string>
<string name="settings_ignore_vpns">La VPN richiede connessione internet</string>
<string name="settings_ignore_vpns_on">La VPN senza una connessione internet validata non è sufficiente per lanciare la sincronizzazione (raccomandato)</string>
<string name="settings_ignore_vpns_off">La VPN senza una connessione internet validata è sufficiente per lanciare la sincronizzazione</string>
<string name="settings_authentication">Autenticazione</string>
<string name="settings_username">Nome utente</string>
<string name="settings_password">Password</string>
<string name="settings_new_password">Nuova password</string>
<string name="settings_password_summary">Aggiorna la password come sul tuo server.</string>
<string name="settings_certificate_alias">Certificato client</string>
<string name="settings_certificate_alias_empty">Nessun certificato disponibile o selezionato</string>
<string name="settings_certificate_install">Installa il certificato</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Limite di tempo per gli eventi trascorsi</string>
<string name="settings_sync_time_range_past_none">Verranno sincronizzati tutti gli eventi</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Eventi più vecchi di un giorno saranno ignorati</item>
<item quantity="many">Eventi più vecchi di %d giorni saranno ignorati</item>
<item quantity="other">Eventi più vecchi di %d giorni saranno ignorati</item>
</plurals>
<string name="settings_sync_time_range_past_message">Eventi più vecchi di questo numero di giorni verranno ignorati(può anche essere 0). Lasciare in bianco per sincronizzare tutti gli eventi.</string>
<string name="settings_default_alarm">Promemoria predefinito</string>
<plurals name="settings_default_alarm_on">
<item quantity="one">Promemoria predefinito un minuto prima dell\'evento</item>
<item quantity="many">Promemoria predefinito %d minuti prima dell\'evento</item>
<item quantity="other">Promemoria predefinito %d minuti prima dell\'evento</item>
</plurals>
<string name="settings_default_alarm_off">Nessun promemoria di default creato</string>
<string name="settings_default_alarm_message">Indicare il numero di minuti che si desidera per il promemoria predefinito.
Lasciare vuoto per non creare un promemoria predefinito.</string>
<string name="settings_manage_calendar_colors">Cambia il colore del calendario</string>
<string name="settings_manage_calendar_colors_on">I colori del calendario sono resettati ad ogni sincronizzazione</string>
<string name="settings_manage_calendar_colors_off">I colori del calendario possono essere scelti da altre applicazioni</string>
<string name="settings_event_colors">Supporto colore dell\'evento</string>
<string name="settings_event_colors_on">I colori degli eventi sono sincronizzati</string>
<string name="settings_event_colors_off">I colori degli eventi non sono sicnronizzati</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Organizzazione dei gruppi di contatto</string>
<string-array name="settings_contact_group_method_entries">
<item>I gruppi sono vCards separate</item>
<item>I gruppi sono categorie per ogni contatto</item>
</string-array>
<!--CreateAddressBookScreen, CreateCalendarScreen-->
<string name="create_addressbook">Crea rubrica</string>
<string name="create_addressbook_maybe_not_supported">La creazione di rubriche tramitte CardDAV potrebbe non essere supportata dal server.</string>
<string name="create_calendar">Crea calendario</string>
<string name="create_calendar_time_zone_optional">Fuso orario di default*</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">Possibili voci del calendario</string>
<string name="create_calendar_type_vevent">Eventi</string>
<string name="create_calendar_type_vtodo">Attività</string>
<string name="create_calendar_type_vjournal">Note / diario</string>
<string name="create_calendar_maybe_not_supported">La creazione do calendari tramite CalDAV potrebbe non essere supportata dal server.</string>
<string name="create_collection_color">Colore</string>
<string name="create_collection_display_name">Titolo</string>
<string name="create_collection_home_set">Percorso di archiviazione</string>
<string name="create_collection_description_optional">Descrizione*</string>
<string name="create_collection_create">Crea</string>
<string name="create_collection_optional">* opzionale</string>
<!--CollectionScreen-->
<string name="collection_delete">Elimina raccolta</string>
<string name="collection_delete_warning">Questa raccolta (%s) e tutti i suoi dati saranno rimossi definitivamente, sia localmente che sul server.</string>
<string name="collection_synchronization">Sincronizzazione</string>
<string name="collection_synchronization_on">Sincronizzazione attivata</string>
<string name="collection_synchronization_off">Sincronizzazione disattivata</string>
<string name="collection_read_only">Sola lettura</string>
<string name="collection_read_only_by_server">Sola lettura (dal server)</string>
<string name="collection_read_only_forced">Sola lettura (locale)</string>
<string name="collection_read_write">Lettura/scrittura</string>
<string name="collection_title">Titolo</string>
<string name="collection_description">Descrizione</string>
<string name="collection_owner">Proprietario</string>
<string name="collection_push_support">Supporto push</string>
<string name="collection_last_sync">Ultima sincronizzazione %s</string>
<string name="collection_url">Indirizzo (URL)</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_title">Informazioni di debug</string>
<string name="debug_info_archive_caption">Archivio ZIP</string>
<string name="debug_info_archive_subtitle">Contiene informazioni sui debug e sugli accessi</string>
<string name="debug_info_archive_text">Condividi l\'archivio per trasferirlo ad un computer, per inviarlo tramite email o per fissarlo ad un ticket di supporto.</string>
<string name="debug_info_archive_share">Condividi l\'archivio</string>
<string name="debug_info_attached">Informazioni sul debug fissate a questo messaggio (richiede un supporto di fissaggio dell\'applicazione di supporto). </string>
<string name="debug_info_http_error">Errore HTTP</string>
<string name="debug_info_server_error">Errore del Server</string>
<string name="debug_info_webdav_error">Errore WebDAV</string>
<string name="debug_info_io_error">Errore I/O</string>
<string name="debug_info_http_403_description">La richiesta è stata negata. Controlla le fonti coinvolte e le informazioni debug per dettagli.</string>
<string name="debug_info_http_404_description">La fonte richiesta non esiste (più). Controlla le fonti coinvolte e le informazioni debug per dettagli.</string>
<string name="debug_info_http_5xx_description">Si è verificato un problema del server. Per favore contatta il tuo server di supporto.</string>
<string name="debug_info_unexpected_error">Si è verificato un errore inaspettato. Vedi le informazioni di debug per maggiori dettagli.</string>
<string name="debug_info_view_details">Vedi dettagli</string>
<string name="debug_info_subtitle">Sono state raccolte informazioni di debug</string>
<string name="debug_info_involved_caption">Fonti coinvolte</string>
<string name="debug_info_involved_subtitle">Collegate con il problema</string>
<string name="debug_info_involved_remote">Fonti remote:</string>
<string name="debug_info_involved_local">Fonti locali:</string>
<string name="debug_info_logs_caption">Registri</string>
<string name="debug_info_logs_subtitle">Sono disponibili registri verbali</string>
<string name="debug_info_logs_view">Vedi i registri</string>
<!--ExceptionInfoFragment-->
<string name="exception">Si è verificato un errore.</string>
<string name="exception_httpexception">Si è verificato un errore HTTP.</string>
<string name="exception_ioexception">Si è verificato un errore di I/O.</string>
<string name="exception_show_details">Mostra dettagli</string>
<!--WebDAV accounts-->
<string name="webdav_mounts_title">Installazioni WebDAV</string>
<string name="webdav_mounts_quota_used_available">Quantità utilizzata: %1$s / disponibile: %2$s</string>
<string name="webdav_mounts_share_content">Condividi i contenuti</string>
<string name="webdav_mounts_unmount">Disinstallazioni</string>
<string name="webdav_add_mount_title">Aggiungi installazioni WedDAV</string>
<string name="webdav_mounts_empty">Accedi direttamente ai tuoi file nel cloud aggiungendo un supporto WebDAV!</string>
<string name="webdav_add_mount_empty_more_info"><![CDATA[Leggi il manuale per vedere <a href="%1$s">come funzionano i supporti WebDAV</a>.</string>]]></string>
<string name="webdav_add_mount_display_name">Nome del display</string>
<string name="webdav_add_mount_url">URL WebDVA</string>
<string name="webdav_add_mount_url_invalid">URL non valido</string>
<string name="webdav_add_mount_authentication">Autenticazione (facoltativa)</string>
<string name="webdav_add_mount_username">Nome utente</string>
<string name="webdav_add_mount_password">Password</string>
<string name="webdav_add_mount_add">Aggiungi installazioni</string>
<string name="webdav_add_mount_no_support">Nessun servizio WebDAV a questo URL</string>
<string name="webdav_remove_mount_title">Rimuovi punto di mont</string>
<string name="webdav_remove_mount_text">I dettagli della connessione saranno perduti, ma nessun file verrà cancellato.</string>
<string name="webdav_notification_access">File di accesso WebDAV</string>
<string name="webdav_notification_download">File di download WebDAV</string>
<string name="webdav_notification_upload">Caricare file WebDAV</string>
<string name="webdav_provider_root_title">Installazione WebDAV</string>
<!--sync-->
<string name="sync_error_permissions">Autorizzazioni DAVx⁵</string>
<string name="sync_error_permissions_text">Autorizzazioni addizionali richieste</string>
<string name="sync_error_tasks_too_old">%s troppo vecchio</string>
<string name="sync_error_tasks_required_version">Versione minima richiesta %1$s</string>
<string name="sync_error_authentication_failed">Autenticazione fallita (controlla credenziali login)</string>
<string name="sync_error_io">Errore di rete o di I/O %s</string>
<string name="sync_error_http_dav">Errore server HTTP %s</string>
<string name="sync_error_local_storage">Errore di archiviazione locale %s</string>
<string name="sync_error_view_item">Visualizza oggetto</string>
<string name="sync_invalid_contact">Contatto non valido ricevuto dal server</string>
<string name="sync_invalid_event">Evento non valido ricevuto dal server</string>
<string name="sync_invalid_task">Attività non valida ricevuta dal server</string>
<string name="sync_invalid_resources_ignoring">Una o più risorse non valide ignorate</string>
<!--widgets-->
<string name="widget_sync_all">Sincronizza tutto</string>
<string name="widget_sync_all_accounts">Sincronizzazione di tutti gli account</string>
<!--cert4android-->
</resources>

View File

@@ -1,427 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="account_invalid">ანგარიში (აღარ) არსებობს</string>
<string name="account_title_address_book">DAVx⁵ მისამართთა წიგნაკი</string>
<string name="dialog_delete">წაშლა</string>
<string name="dialog_remove">ამოშლა</string>
<string name="dialog_deny">გაუქმება</string>
<string name="field_required">ეს ველი სავალდებულოა</string>
<string name="help">დახმარება</string>
<string name="navigate_up">ზემოთ გადასვლა</string>
<string name="optional_label">* არასავალდებულო</string>
<string name="options_menu">ოპციების მენიუ</string>
<string name="share">გაზიარება</string>
<string name="sync_started">სინქრონიზაცია დაიწყა/დადგა რიგში</string>
<string name="database_destructive_migration_title">მონაცემთა ბაზა კორუმპირებულია</string>
<string name="database_destructive_migration_text">ყველა ანგარიში წაშლილ იქნა ადგილობრივად.</string>
<string name="notification_channel_debugging">დებაგი</string>
<string name="notification_channel_general">სხვა მნიშვნელოვანი შეტყობინებები</string>
<string name="notification_channel_status">დაბალი პრიორიტეტის სტატუსის შეტყობინებები</string>
<string name="notification_channel_sync">სინქრონიზაცია</string>
<string name="notification_channel_sync_errors">სინქრონიზაციის შეცდომები</string>
<string name="notification_channel_sync_errors_desc">მნიშვნელოვანი შეცდომები, რომლებიც აჩერებს სინქრონიზაციას, მაგ., მოულოდნელი სერვერის პასუხები</string>
<string name="notification_channel_sync_warnings">სინქრონიზაციის გაფრთხილებები</string>
<string name="notification_channel_sync_warnings_desc">არა-ლეტალური სინქრონიზაციის პრობლემები, როგორც ზოგი არასწორი ფაილი</string>
<string name="notification_channel_sync_io_errors">ქსელის ან ჩაწერა/წაკითხვის შეცდომები</string>
<string name="notification_channel_sync_io_errors_desc">ვადის გასვლა, კავშირის პრობლემები, სხვა (ხშირად დროებითი)</string>
<!--IntroActivity-->
<string name="intro_slogan1">თქვენი მონაცემები. თქვენი არჩევანი.</string>
<string name="intro_slogan2">აიღეთ კონტროლი.</string>
<string name="intro_battery_title">რეგულარული სინქრონიზაციის ინტერვალები</string>
<string name="intro_battery_text">რეგულარული ინტერვალი სინქრონიზაციისთვის, %s-ს უნდა ჰქონდეს უფლება გაეშვას ფონურ რეჟიმში. სხვაგვარად, Android-მა შეიძლება ნებისმიერ მომენტში შეაჩეროს სინქრონიზაცია.</string>
<string name="intro_battery_dont_show">მე არ მჭირდება რეგულარული სინქრონიზაციის ინტერვალები.*</string>
<string name="intro_autostart_title">%s თავსებადობა</string>
<string name="intro_autostart_text">ეს მოწყობილობა სავარაუდოდ ბლოკავს სინქრონიზაცია. თუ ეს გეხებათ, ამის გამოსწორება მხოლოდ ხელით შეიძლება.</string>
<string name="intro_autostart_dont_show">მე შევცვალე საჭირო პარამეტრები. აღარ შემახსენოთ.*</string>
<string name="intro_leave_unchecked">* დატოვეთ მოუნიშნელად მოგვიანებით შესახსენებლად. შეიძლება ჩამოგდებულ იქნას აპის პარამეტრებში /%s.</string>
<string name="intro_more_info">მეტი ინფორმაცია</string>
<string name="intro_tasks_jtx">jtx Board</string>
<string name="intro_tasks_jtx_info"><![CDATA[Supports sync of Tasks, Journals and Notes.]]></string>
<string name="intro_tasks_title">დავალებების მხარდაჭერა</string>
<string name="intro_tasks_text1">თუ დავალებები მხარდაჭერილია თქვენი სერვერის მიერ, მათი სინქრონიზირება შეიძლება მხარდაჭერილი დავალებათა აპით:</string>
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">აღარ მიმდინარეობს განვითარება - არ არის რეკომენდებული.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_no_app_store">აპების მაღაზია ხელმიუწვდომია</string>
<string name="intro_tasks_dont_show">მე არ მჭირდება დავალებების მხარდაჭერა.*</string>
<string name="intro_open_source_title">ღია კოდის პროგრამული უზრუნველყოფა</string>
<string name="intro_open_source_text">კმაყოფილები ვართ, რომ იყენებთ %s-ს, რომელიც ღია კოდის პროგრამული უზრუნველყოფაა. განვითარება და მხარდაჭერა რთული სამუშაო. გთხოვთ, გაითვალისწინოთ წილის შეტანა (მრავალი გზა არსებობს) ან ფულის ჩუქბეა. ძალიან მადლობელი ვიქნებით!</string>
<string name="intro_open_source_details">როგორ შევიტანო წვლილი/დაგეხმაროთ</string>
<string name="intro_open_source_dont_show">არ მაჩვენოთ ახლო მომავალში</string>
<!--PermissionsActivity-->
<string name="permissions_title">უფლებები</string>
<string name="permissions_text">%s-ს სჭირდება უფლებები სწორად სამუშაოდ.</string>
<string name="permissions_all_title">ყველა ქვემოთ მოცემული</string>
<string name="permissions_all_status_off">გამოიყენეთ ეს ყველა ფუნქციის ჩასართავად (რეკომენდებული)</string>
<string name="permissions_all_status_on">ყველა უფლება დართულია</string>
<string name="permissions_contacts_title">კონტაქტების უფლებები</string>
<string name="permissions_contacts_status_off">კონტაქტის სინქრონიზაციის გარეშე (არა რეკომენდებული)</string>
<string name="permissions_contacts_status_on">კონტაქტის სინქრონიზაცია შესაძლებელია</string>
<string name="permissions_calendar_title">კალენდარის უფლებები</string>
<string name="permissions_calendar_status_off">კალენდარის სინქრონიზაციის გარეშე (არა რეკომენდებული)</string>
<string name="permissions_calendar_status_on">კალენდარის სინქრონიზაცია შესაძლებელია</string>
<string name="permissions_notification_title">შეტყობინებების უფლება</string>
<string name="permissions_notification_status_off">შეტყობინებები გათიშულია (არა რეკომენდებული)</string>
<string name="permissions_notification_status_on">შეტყობინებები ჩართლია</string>
<string name="permissions_jtx_title">jtx Board-ის უფლებები</string>
<string name="permissions_opentasks_title">OpenTasks-ის უფლებები</string>
<string name="permissions_tasksorg_title">დავალებების უფლებები</string>
<string name="permissions_tasks_status_off">დავალებების სინქრონიზაციის გარეშე</string>
<string name="permissions_tasks_status_on">დავალებების სინქრონიზაცია შესაძლებელია</string>
<string name="permissions_autoreset_title">Keep-ის უფლებები</string>
<string name="permissions_autoreset_status_off">უფლებები შეიძლება ავტომატურად ჩამოიყაროს (არა რეკომენდებული)</string>
<string name="permissions_autoreset_status_on">უფლებები ავტომატურად არ ჩამოიყრება</string>
<string name="permissions_autoreset_instruction">შეამოწმეთ უფლებები &gt; მოხსენით \"უფლებების ამოშლა, თუ აპი არ გამოიყენება\"-ს მონიშვნა</string>
<string name="permissions_app_settings_hint">თუ გადამრთველი არ მუშაობს, გამოიყენეთ აპის პარამეტრები / უფლებები.</string>
<string name="permissions_app_settings">აპის პარამეტრები</string>
<!--WifiPermissionsActivity-->
<string name="wifi_permissions_label">WiFi SSID-ს უფლებები</string>
<string name="wifi_permissions_intro">რათა მიწვდეთ მიმდინარე WiFi-ს სახელს (SSID), ეს პირობები უნდა შესრულდეს:</string>
<string name="wifi_permissions_location_permission">ზუსტი ადგილმდებარეობის უფლება</string>
<string name="wifi_permissions_location_permission_on">ადგილმდებარეობის უფლება დართულია</string>
<string name="wifi_permissions_location_permission_off">ადგილმდებარეობის უფლება უარყოფილია</string>
<string name="wifi_permissions_background_location_permission">ფონური ადგილმდებარეობის უფლება</string>
<string name="wifi_permissions_background_location_permission_label">ყოველთვის დაშვება</string>
<string name="wifi_permissions_background_location_permission_on">ადგილმდებარეობის უფლების მნიშვნელობა: %s</string>
<string name="wifi_permissions_background_location_permission_off">ადგილმდებარეობის უფლება არ არის შემდეგი: %s</string>
<string name="wifi_permissions_location_enabled">ადგილმდებარეობა ყოველთვის ჩართულია</string>
<string name="wifi_permissions_location_enabled_on">ადგილმდებარეობის სერვისი ჩართულია</string>
<string name="wifi_permissions_location_enabled_off">ადგილმდებარეობის სერვისი გათიშულია</string>
<!--AboutActivity-->
<string name="about_translations">თარგმანი</string>
<string name="about_libraries">ბიბლიოთეკები</string>
<string name="about_version">ვერსია %1$s (%2$d)</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) და მონაწილეები</string>
<string name="about_license_info_no_warranty">ამ პროგრამას არ აქვს არანაირი გარანტია. იგი არის უფასო პროგრამული უზრუნველყოფა, ხოლო თქვენ შეგეძლეიათ იგი გაავრცელოთ გარკვეული პირობების გათვალისწინებით.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">ჟურნალის ფაილი ვერ შეიქმნა</string>
<string name="logging_notification_text">აწი მიმდინარეობს ყველა %s აქტივობის ჟურნალში ჩაწერა</string>
<string name="logging_notification_view_share">ნახვა/გაზიარება</string>
<string name="logging_notification_disable">გათიშვა</string>
<!--AccountsScreen-->
<string name="navigation_drawer_subtitle">CalDAV/CardDAV სინქრონიზაციის ადაპტერი</string>
<string name="navigation_drawer_about">შესახებ / ლიცენზია</string>
<string name="navigation_drawer_beta_feedback">ბეტას უკუკავშირი</string>
<string name="install_browser">გთხოვთ, დააყენოთ ვებ ბრაუზერი</string>
<string name="navigation_drawer_settings">პარამეტრები</string>
<string name="navigation_drawer_news_updates">ახალი ამბები &amp; განახლებები</string>
<string name="navigation_drawer_tools">ხელსაწყოები</string>
<string name="navigation_drawer_external_links">გარე ბმულები</string>
<string name="navigation_drawer_website">ვებ საიტი</string>
<string name="navigation_drawer_manual">ინსტრუქცია</string>
<string name="navigation_drawer_faq">ხდკ</string>
<string name="navigation_drawer_community">საზოგადოება</string>
<string name="navigation_drawer_support_project">პროექტის მხარდაჭერა</string>
<string name="navigation_drawer_contribute">როგორ შევიტანო ღვაწლი</string>
<string name="navigation_drawer_privacy_policy">პირადულობის პოლიტიკა</string>
<string name="account_list_no_notification_permission">შეტყობინებები გათიშული. თქვენ არ მიიღებთ შეტყობინებებს სიქნრონიზაციის შეცდომების შესახებ.</string>
<string name="account_list_manage_connections">კავშირების მართვა</string>
<string name="account_list_datasaver_enabled">გააქტიურებულია მონაცემთა შემნახველი. ფონური სინქრონიზაცია შეზღუდულია.</string>
<string name="account_list_manage_datasaver">მონაცემთა შემნახველის მართვა</string>
<string name="account_list_battery_saver_enabled">გფააქტიურებულია კვების ელემენტის შემნახველი. სინქრონიზაცია შეიძლება შეზღუდულ იქნას.</string>
<string name="account_list_manage_battery_saver">კვების ელემენტის შემნახველის მართვა</string>
<string name="account_list_low_storage">მეხსიერება ცოტა დარჩა. Android არ დაასინქრონიზირებს ადგილობრივ ცვლილებებს დაუყონებლივ, ხოლო დაასინქრონიზირებს შემდეგი რეგულარული სინქრონიზაციის დროს.</string>
<string name="account_list_manage_storage">მეხსიერების მართვა</string>
<string name="accounts_sync_all">ყველა ანგარიშის სინქრონიზაცია</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">სერვისის აღმოჩენა ჩაიშალა</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">კოლექციათა სიის განახლება ვერ მოხერხდა</string>
<!--Foreground service used by WorkManager on Android <12-->
<string name="foreground_service_notify_title">მუშაობს ფონში</string>
<string name="foreground_service_notify_text">ზოგ მოწყობილობაზე, ეს საჭიროა ავტომატური სინქრონიზაციისთვის.</string>
<!--AppSettingsActivity-->
<string name="app_settings">პარამეტრები</string>
<string name="app_settings_debug">დებაგი</string>
<string name="app_settings_show_debug_info">დებაგის ინფორმაციის ჩვენება</string>
<string name="app_settings_logging">დეტალური ჟურნალში ჩაწერა</string>
<string name="app_settings_logging_off">ჟურნალში ჩაწერა გათიშულია</string>
<string name="app_settings_battery_optimization">კვების ელემენტის ოპტიმიზაცია</string>
<string name="app_settings_battery_optimization_exempted">აპი გამორიცხულია (რეკომენდებულია)</string>
<string name="app_settings_battery_optimization_optimized">გამოიყენება კვების ელემენტის შეზღუდვები (არა რეკომენდებულია)</string>
<string name="app_settings_connection">კავშირი</string>
<string name="app_settings_proxy">პროქსის ტიპი</string>
<string-array name="app_settings_proxy_types">
<item>ნაგულისხმევი სისტემის მიერ</item>
<item>პროქსის გარეშე</item>
<item>HTTP3</item>
<item>SOCKS (Orbot-სთვის)</item>
</string-array>
<string name="app_settings_proxy_host">პროქსის ჰოსტის სახელი</string>
<string name="app_settings_proxy_port">პროქსის პორტი</string>
<string name="app_settings_security">უსაფრთხოება</string>
<string name="app_settings_security_app_permissions">აპის ეფლებები</string>
<string name="app_settings_security_app_permissions_summary">გადახედეთ სინქრონიზაციისთვის საჭირო ეფლებებს</string>
<string name="app_settings_distrust_system_certs">სისტემური სერთიფიკატების ნდობის გაუქმება</string>
<string name="app_settings_distrust_system_certs_on">სისტემური და მომხმარებლის მიერ დამატებული სერთიფიცირების ავტორიტეტების ნდობა არ იქნება</string>
<string name="app_settings_distrust_system_certs_off">სისტემური და მომხმარებლის მიერ დამატებული სერთიფიცირების ავტორიტეტების ნდობა იქნება (რეკომენდებული)</string>
<string name="app_settings_reset_certificates">(არა) ნდობითი სერთიფიკატების ჩამოყრა</string>
<string name="app_settings_reset_certificates_summary">ნდობის ჩამოყრა ყველა კერძო სერთიფიკატზე</string>
<string name="app_settings_reset_certificates_success">ყველა კერძო სერთიფიკატი გასუფთავდა</string>
<string name="app_settings_user_interface">მომხმარებლის ინტერფეისი</string>
<string name="app_settings_notification_settings">შეტყობინებების პარამეტრები</string>
<string name="app_settings_notification_settings_summary">შეტყობინებების არხების და პარამეტრების მართვა</string>
<string name="app_settings_theme_title">აირჩიეთ თემა</string>
<string-array name="app_settings_theme_names">
<item>სისტემის მიერ ნაგულისხმევი</item>
<item>ღია</item>
<item>მუქი</item>
</string-array>
<string name="app_settings_reset_hints">მითითებების ჩამოყრა</string>
<string name="app_settings_reset_hints_summary">თავიდან ააქტიურებს მითითებებს, რომლებიც დამალულ იქნა წარსულში</string>
<string name="app_settings_reset_hints_success">ყველა მითითება თავიდან იქნება ნაჩვენები</string>
<string name="app_settings_integration">ინტეგრაცია</string>
<string name="app_settings_tasks_provider">დავალებათა აპი</string>
<string name="app_settings_tasks_provider_none">თავსებადი დავალებათა აპი ვერ მოიძებნა</string>
<!--AccountScreen-->
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_missing_permissions">საჭიროა დამატებითი უფლებები ამ კოლექციების სინქრონიზაციისთვის.</string>
<string name="account_manage_permissions">უფლებების მართვა</string>
<string name="account_synchronize_now">ახლავე სინქრონიზირება</string>
<string name="account_settings">ანგარიშის პარამეტრები</string>
<string name="account_rename">ანგარიშის სახელის შეცვლა</string>
<string name="account_rename_new_name_description">შეუნახავი ადგილობრივი მონაცემები შეიძლება გაუქმებულ იქნას. საჭიროა თავიდან სინქრონიზირება სახელის შეცვლის შემდეგ.</string>
<string name="account_rename_new_name">ახალი ანგარიშის სახელი</string>
<string name="account_rename_rename">სახელის შეცვლა</string>
<string name="account_rename_exists_already">ანგარიშის სახელი უკვე დაკავებულია</string>
<string name="account_rename_couldnt_rename">ანგარიშის სახელის შეცვლა ვერ მოხერხდა</string>
<string name="account_delete">ანგარიშის წაშლა</string>
<string name="account_delete_confirmation_title">მართლა წაიშალოს ანგარიში?</string>
<string name="account_delete_confirmation_text">წაიშლება მისამართთა წიგნაკების, კალენდრების და დავალებათა სიების ყველა ადგილობრივი ასლი.</string>
<string name="account_synchronize_this_collection">ამ კოლექციის სინქრონიზირება</string>
<string name="account_read_only">მხოლოდ წაკითხვადი</string>
<string name="account_calendar">კალენდარი</string>
<string name="account_contacts">კონტაქტები</string>
<string name="account_journal">ჟურნალი</string>
<string name="account_task_list">დავალებები</string>
<string name="account_only_personal">მხოლოდ პირადის ჩვენება</string>
<string name="account_refresh_collections">სიის განახლება</string>
<string name="account_webcal_external_app">Webcal გამოწერები შეიძ₾ება სინქრონიზირებულ იქნას გარე აპებთან.</string>
<string name="account_no_webcal_handler_found">Webcal-თან თავსებადი აპი ვერ მოიძებნა</string>
<string name="account_install_icsx5">ICSx⁵-ს დაყენება</string>
<!--AddAccountActivity-->
<string name="login_title">ანგარიშის დამატება</string>
<string name="login_generic_login">ზოგადი შესვლა</string>
<string name="login_provider_login">პროვაიდერის შესვლა</string>
<string name="login_continue">გაგრძელება</string>
<string name="login_login">შესვლა</string>
<string name="login_type_email">ელ. ფოსტის მისამართით შესვლა</string>
<string name="login_email_address">ელ. ფოსტის მისამართი</string>
<string name="login_email_address_error">საჭიროა სწორი ელ. ფოსტის მისამართი</string>
<string name="login_email_address_info"><![CDATA[ელ. ფოსტის დომენი გამოიყენება საბაზო URL-ად. <a href="%s">აღმოჩენილია სერვისები</a> DNS ჩანაწერების და კარგად ცნობილი URL-ების მეშვეობით.]]></string>
<string name="login_password">პაროლი</string>
<string name="login_password_hide">პაროლის დამალვა</string>
<string name="login_password_show">პაროლის ჩვენება</string>
<string name="login_password_optional">პაროლი*</string>
<string name="login_type_url">URL-ით და მომხმარებლის სახელით შესვლა</string>
<string name="login_user_name">მომხმარებლის სახელი</string>
<string name="login_user_name_optional">მოხმარებლის სახელი*</string>
<string name="login_base_url">საბაზო URL</string>
<string name="login_base_url_info"><![CDATA[საბაზო URL-ი პირადპირ იქნება შემოწმებული, მაგრამ <a href="%s">ასევე აღმოჩენილია სერვისები</a> DNS ჩანაწერების და კარგად ცნობილი URL-ების მეშვეობით.]]></string>
<string name="login_select_certificate">სერტიფიკატის არჩევა</string>
<string name="login_add_account">ანგარიშის დამატება</string>
<string name="login_account_name">ანგარიშის სახელი</string>
<string name="login_account_avoid_apostrophe">აპოსტროფების (\') გამოყენება იწვევს პრობლემებს ზოგ მოწყობილობაზე.</string>
<string name="login_account_name_info">გამოიყენეთ თქვენი ელ. ფოსტის მსიამართი ანგარიშის სახელად, რადგან Android გამოიყენებს ანგარიშის სახელს ორგანიზატორის ველში თქვენს მიერ შექმნილ ღონისძიებებისთვის. თქვენ არ შეიძლება გქონდეთ ორი ანგარიში იგივე სახელით.</string>
<string name="login_account_contact_group_method">კონტაქტების დაჯგუფების მეთოდი:</string>
<string name="login_account_name_required">საჭიროა ანგარიშის სახელი</string>
<string name="login_account_name_already_taken">ანგარიშის სახელი უკვე დაკავებულია</string>
<string name="login_type_advanced">გაფართოებული შესვლა</string>
<string name="login_no_client_certificate_optional">კლიენტის სერტიფიკატი არ არის*</string>
<string name="login_client_certificate_selected">კლიენტის სერტიფიკატი: %s</string>
<string name="login_no_certificate_found">სერტიფიკატი ვერ მოიძებნა</string>
<string name="login_install_certificate">სერტიფიკატის დაყენება</string>
<string name="login_type_google">Google კონტაქტები / კალენდარი</string>
<string name="login_google_see_tested_with">გთხოვთ, იხილოთ ჩვენი \"ტესტირებულია Google-თან\" გვერდი ბოლო ინფორმაციისთვის.</string>
<string name="login_google_unexpected_warnings">შეიძლება გქონდეთ მოულოდნელი გაფრთხილებები ან/და მოგიწიოთ შექმნათ თქვენი პირადი კლიენტის ID.</string>
<string name="login_google_account">Google ანგარიში</string>
<string name="login_google">Google-ით შესვლა</string>
<string name="login_google_client_id">კლიენტის ID (aრასავალდებულო)</string>
<string name="login_google_client_privacy_policy"><![CDATA[%1$sგადასცემს თქვენს Google კონტაქტებისა და კალენდარის მონაცემებს მხოლოდ სინქრონიზაციისთვის ამ მოწყობილობასთან. იხილეთ ჩვენი <a href="%2$s">პირადულობის პოლიტიკა</a> დეტალებისთვის.]]></string>
<string name="login_google_client_limited_use"><![CDATA[%1$s ექვემდებარება <a href="%2$s">Google API სერვისების მომხმარებელთა მონაცემების პოლიტიკას</a>, მათ შორის, შეზღუდული გამოყენების მოთხოვნებს.]]></string>
<string name="login_oauth_couldnt_obtain_auth_code">ავტორიზაციის კოდის მიღება ვერ მოხერხდა</string>
<string name="login_type_nextcloud">Nextcloud</string>
<string name="login_nextcloud_login_with_nextcloud">შესვლა Nextcloud-ისთ</string>
<string name="login_nextcloud_login_flow_text">ეს დაიწყებს Nextcloud-ის შესვლის პროცესს ვებ ბრაუზერში.</string>
<string name="login_nextcloud_login_flow_server_address">Nextcloud-ის სერვერის მისამართი</string>
<string name="login_nextcloud_login_flow_sign_in">შესვლა</string>
<string name="login_nextcloud_login_flow_no_login_url">შესვლის URL-ის მიღება ვერ მოხერხდა</string>
<string name="login_nextcloud_login_flow_no_login_data">შესვლის მონაცემების მიღება ვერ მოხერხდა</string>
<string name="login_configuration_detection">კონფიგურაციის აღმოჩენა</string>
<string name="login_querying_server">გთხოვთ, დაელოდოთ, მიმდინარეობს სერვერის გამოკითხვა...</string>
<string name="login_no_service">CalDAV-ის ან CardDAV-ის სერვისის მოძებნა ვერ მოხერხდა.</string>
<string name="login_no_service_info">საბაზო URL არ არის წვდომადი CalDAV/CardDAV URL და სერვერისის აღმოჩენა არ იყო წარმატებული.</string>
<string name="login_see_tested_services"><![CDATA[გთხოვთ, იხილოთ თქვენი მომსახურების მომწოდებლის ინსტრუქცია და <a href="%s">ჩვენს მიერ ტესტირებული სერვისების სია</a> და მათი საბაზო URL.]]></string>
<string name="login_check_credentials">გთხოვთ, ასევე გადაამოწმოთ აუთენტიფიკაცია (ზოგადად, მომხმარებლის სახელი დაპაროლი).</string>
<string name="login_logs_available">დამატებითი ტექნიკური ინფორმაცია ხელმისაწვდომია ჟურნალებში.</string>
<string name="login_view_logs">ჟურნალების ნახვა</string>
<!--AccountSettingsActivity-->
<string name="settings_sync">სინქრონიზაცია</string>
<string name="settings_sync_interval_contacts">კონტაქტების სინქრონიზაციის ინტერვალი</string>
<string name="settings_sync_summary_manually">მხოლოდ ხელით</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">ყოველ %d წუთში + დაუყონებლივ ადგილობრივი ცვლილებებისას</string>
<string name="settings_sync_interval_calendars">კალენდრების სინქრონიზაციის ინტერვალი</string>
<string name="settings_sync_interval_tasks">დავალებვათა სინქრონიზაციის ინტერვალი</string>
<string-array name="settings_sync_interval_names">
<item>მხოლოდ ხელით</item>
<item>ყოველ 15 წუთში</item>
<item>ყოველ 30 წუთში</item>
<item>ყოველ 1 საათში</item>
<item>ყოველ 2 საათში</item>
<item>ყოველ 4 საათში</item>
<item>ყოველდღე</item>
</string-array>
<string name="settings_sync_wifi_only">მხოლოდ WiFi-ით სინქრონიზაცია</string>
<string name="settings_sync_wifi_only_on">სინქრონიზაცია შეზღუდულია WiFi კავშირზე</string>
<string name="settings_sync_wifi_only_off">კავშირის ტიპი არ გაითვალისწინება</string>
<string name="settings_sync_wifi_only_ssids">WiFi SSID-ს შეზღუდვა</string>
<string name="settings_sync_wifi_only_ssids_on">დასინქრონიზირდება მხოლო %s-ით</string>
<string name="settings_sync_wifi_only_ssids_off">გამოიყენება ყველა WiFi კავშირი</string>
<string name="settings_sync_wifi_only_ssids_message">დაშვებული WiFi ქსელების მძიმეთი დაყოფილი სახელები (SSID) (დატოვეთ ცარიელად ყველასთვის)</string>
<string name="settings_sync_wifi_only_ssids_permissions_required">WiFi SSID-ს შეზღუდვას სჭირდება დამატებითი პარამეტრები</string>
<string name="settings_sync_wifi_only_ssids_permissions_action">მართვა</string>
<string name="settings_ignore_vpns">VPN-ს სჭირდება არსებული ინტერნეტ-კავშირი</string>
<string name="settings_ignore_vpns_on">VPN არსებული დადასტურებული ინტერნეტ-კავშირის გარეშე არ არის საკმარისი სინქრონიზაციის გასაშვებად (რეკომენდებული)</string>
<string name="settings_ignore_vpns_off">VPN არსებული დადასტურებული ინტერნეტ-კავშირის გარეშე არ არის საკმარისი სინქრონიზაციის გასაშვებად</string>
<string name="settings_authentication">აუთენტიფიკაცია</string>
<string name="settings_username">მომხმარებლის სახელი</string>
<string name="settings_password">პაროლი</string>
<string name="settings_new_password">ახალი პაროლი</string>
<string name="settings_password_summary">პაროლის განახლება თქვენი სერვერის მიხედვით</string>
<string name="settings_certificate_alias">კლიენტის სერთიფიკატი</string>
<string name="settings_certificate_alias_empty">სერთიფიკატი ხელმიუწვდომია ან არ არის არჩეული</string>
<string name="settings_certificate_install">სერტიფიკატის დაყენება</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">გასული ღონისძიების დროის შეზღუდვა</string>
<string name="settings_sync_time_range_past_none">დასინქრონიზირდება ყველა ღონისძიება</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">ერთ დღეზე უფრო ძველი ღონისძიებები იქნება იგნორირებული</item>
<item quantity="other">%d დღეზე უფრო ძველი ღონისძიებები იქნება იგნორირებული</item>
</plurals>
<string name="settings_sync_time_range_past_message">ღონისძიებები, რომლებიც უფრო ძველია, ვიდრე დღეთა მითითებული რაოდენობა, იქნება იგნორირებული (შეიძლება იყოს 0). დატოვეთ ცარიელად ყველას სინქრონიზებისთვის.</string>
<string name="settings_default_alarm">ნაგულისხმევა შეხსენება</string>
<plurals name="settings_default_alarm_on">
<item quantity="one">ნაგულისხმევი შეხსენება ღონისძიებამდე ერთი წუთით ადრე</item>
<item quantity="other">ნაგულისხმევი შეხსენება ღონისძიებამდე %d წუთით ადრე</item>
</plurals>
<string name="settings_default_alarm_off">ნაგულისხმევი შეხსენება არ არის შექმნილი</string>
<string name="settings_default_alarm_message">თუ ნაგულისხმევი შეხსენება უნდა შეიქმნას შეხსენების გარეშე ღონისძიებებისთვის: ღონისძიებამდე წუთების სასურველი რიცხვი. დატოვეთ ცარიელად ნაგულისხმევი შეხსენებების გასათიშად.</string>
<string name="settings_manage_calendar_colors">კალენდარის ფერების მართვა</string>
<string name="settings_manage_calendar_colors_on">კალენდარის ფერები ჩამოიყრება ყოველ სინქრონიზაციაზე</string>
<string name="settings_manage_calendar_colors_off">კალენდარის ფერები შეიძლება დაყენებულ იქნას სხვა აპების მიერ</string>
<string name="settings_event_colors">ღონისძიების ფერის მხარდაჭერა</string>
<string name="settings_event_colors_on">ღონისძიების ფერები არის სინქრონიზირებული</string>
<string name="settings_event_colors_off">ღონისძიების ფერები არ არის სინქრონიზირებული</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">კონტაქტების დაჯგუფების მეთოდი</string>
<string-array name="settings_contact_group_method_entries">
<item>ჯგუფები ცალკე vCard-ებია</item>
<item>ჯგუფები არის კონტაქტთა კატეგორია</item>
</string-array>
<!--CreateAddressBookScreen, CreateCalendarScreen-->
<string name="create_addressbook">მისამართთა წიგნაკის შექმნა</string>
<string name="create_addressbook_maybe_not_supported">მისამართთა წიგნაკის შექმნა CardDAV-ით შეიძლება არ იყოს მხარდაჭერილი სერვერის მიერ.</string>
<string name="create_calendar">კალენდარის შექმნა</string>
<string name="create_calendar_time_zone_optional">ნაგულისხმევი საათის სარტყელი*</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">დაშვებული კალენდარის ჩანაწერები</string>
<string name="create_calendar_type_vevent">ღონისძიებები</string>
<string name="create_calendar_type_vtodo">დავალებები</string>
<string name="create_calendar_type_vjournal">შენიშვნები / ჟურნალი</string>
<string name="create_calendar_maybe_not_supported">კალენდრის შექმნა CalDAV-ით შეიძლება არ იყოს მხარდაჭერილი სერვერის მიერ.</string>
<string name="create_collection_color">ფერი</string>
<string name="create_collection_display_name">სათაური</string>
<string name="create_collection_home_set">მეხსიერების ადგილმდებარეობა</string>
<string name="create_collection_description_optional">აღწერა*</string>
<string name="create_collection_create">შექმნა</string>
<string name="create_collection_optional">* არასავალდებულო</string>
<!--CollectionScreen-->
<string name="collection_delete">კოლექციის წაშლა</string>
<string name="collection_delete_warning">ეს კოლექცია (%s) და მისი ყველა მონაცემი სამუდამოდ წაიშლება, როგორც ადგილობრივად, ისე სერვერზეც.</string>
<string name="collection_synchronization">სინქრონიზაცია</string>
<string name="collection_synchronization_on">სინქრონიზაცია ჩართულია</string>
<string name="collection_synchronization_off">სინქრონიზაცია გამორთულია</string>
<string name="collection_read_only">მხოლოდ წაკითხვადი</string>
<string name="collection_read_only_by_server">მხოლოდ წაკითხვადი (სერვერის მიერ)</string>
<string name="collection_read_only_forced">მხოლოდ წაკითხვადი (მხოლოდ ადგილობრივად)</string>
<string name="collection_read_write">წაკითხვა/ჩაწერა</string>
<string name="collection_title">სათაური</string>
<string name="collection_description">აღწერა</string>
<string name="collection_owner">მფლობელი</string>
<string name="collection_push_support">Push-ის მხარდაჭერა</string>
<string name="collection_push_web_push">სერვერი გადმოსცემს Push-ის მხარდაჭერას</string>
<string name="collection_last_sync">ბოლო სინქრონიზაცია (%s)</string>
<string name="collection_url">მისამართი (URL)</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_title">დებაგის ინფო</string>
<string name="debug_info_archive_caption">ZIP არქივი</string>
<string name="debug_info_archive_subtitle">შეიცავს დებაგის ინფოს და ჟურნალებს</string>
<string name="debug_info_archive_text">გააზიარეთ არქივი მისი კომპიუტერზე გადასაგზავნად, ელ. ფოსტით გასაგზავნად ან მისი მხარდაჭერის ბილეთზე მისაბმელად.</string>
<string name="debug_info_archive_share">არქივის გაზიარება</string>
<string name="debug_info_attached">დებაგის ინფო მიბმულია ამ შეტყობინებაზე (სჭირდება მიბმის მხარდაჭერა მიმღებ აპში).</string>
<string name="debug_info_http_error">HTTP შეცდომა</string>
<string name="debug_info_server_error">სერვერის შეცდომა</string>
<string name="debug_info_webdav_error">WebDAV შეცდომა</string>
<string name="debug_info_io_error">წაკითხვა/ჩაწერის შეცდომა</string>
<string name="debug_info_http_403_description">ეს მოთხოვნა იქნა უარყოფილი. შეამოწმეთ შესაბამისი რესურსები და დებაგის ინფო დეტალებისთვის.</string>
<string name="debug_info_http_404_description">მოთხოვნილი რესურსი (აღარ) არსებობს. შეამოწმეთ შესაბამისი რესურსები და დებაგის ინფო დეტალებისთვის.</string>
<string name="debug_info_http_5xx_description">მოხდა პრობლემა სერვერის მხარეს. გთხოვთ, დაუკავშირდეთ თქვენს სერვერის მხარდაჭერას.</string>
<string name="debug_info_unexpected_error">მოხდა მოულოდნელი შეცდომა. იხილეთ დებაგის ინფო დეტალებისთვის.</string>
<string name="debug_info_view_details">დეტალების ნახვა</string>
<string name="debug_info_subtitle">დებაგის ინფო შეგროვდა</string>
<string name="debug_info_involved_caption">შესაბამისი რესურსები</string>
<string name="debug_info_involved_subtitle">დაკავშირებული პრობლემასთან</string>
<string name="debug_info_involved_remote">დაშორებული რესურსი:</string>
<string name="debug_info_involved_local">ადგილობრივი რესურსი:</string>
<string name="debug_info_logs_caption">ჟურნალები</string>
<string name="debug_info_logs_subtitle">ხელმისაწვდომია დეტალური ჟურნალები</string>
<string name="debug_info_logs_view">ჟურნალების ნახვა</string>
<!--ExceptionInfoFragment-->
<string name="exception">მოხდა შეცდომა.</string>
<string name="exception_httpexception">მოხდა HTTP შეცდომა.</string>
<string name="exception_ioexception">მოხდა წაკითხვა/ჩაწერის შეცდომა.</string>
<string name="exception_show_details">დეტალების ჩვენება.</string>
<!--WebDAV accounts-->
<string name="webdav_mounts_title">WebDAV-ის მიბმები</string>
<string name="webdav_mounts_quota_used_available">გამოყენებული კვოტა: %1$s / ხელმისაწვდომი: %2$s</string>
<string name="webdav_mounts_share_content">შიგთავსის გაზიარება</string>
<string name="webdav_mounts_unmount">მიბმის გათიშვა</string>
<string name="webdav_add_mount_title">WebDAV-ის მიბმის დამატება</string>
<string name="webdav_mounts_empty">პირდაპირ იქონიეთ წვდომა თქვენი ღრუბლის ფაილებზე WebDAV-ის მიბმის დამატებით!</string>
<string name="webdav_add_mount_empty_more_info"><![CDATA[იხილეთ ინსტრუქცია, თუ <a href="%1$s">როგორ მოქმედებს WebDAV-ის მიბმები</a>.</string>]]></string>
<string name="webdav_add_mount_display_name">ნაჩვენები სახელი</string>
<string name="webdav_add_mount_url">WebDAV URL</string>
<string name="webdav_add_mount_url_invalid">არასწორი URL</string>
<string name="webdav_add_mount_authentication">აუთენტიფიკაცია (არასავალდებულო)</string>
<string name="webdav_add_mount_username">მომხმარებლის სახელი</string>
<string name="webdav_add_mount_password">პაროლი</string>
<string name="webdav_add_mount_add">მიბმის დამატება</string>
<string name="webdav_add_mount_no_support">WebDAV სერვისი ამ URL-ზე არ არის</string>
<string name="webdav_remove_mount_title">მიბმის წერტილის ამოშლა</string>
<string name="webdav_remove_mount_text">კავშირის დეტალები დაიკარგება, მაგრამ ფაილები არ წაიშლება.</string>
<string name="webdav_notification_access">მიმდინარეობს WebDAV ფაილზე წვდომა</string>
<string name="webdav_notification_download">მიმდინარეობს WebDAV ფაილის გადმოტვირთვა</string>
<string name="webdav_notification_upload">მიმდინარეობს WebDAV ფაილის ატვირთვა</string>
<string name="webdav_provider_root_title">WebDAV-iს მიბმა</string>
<!--sync-->
<string name="sync_error_permissions">DAVx⁵-ის უფლებები</string>
<string name="sync_error_permissions_text">საჭიროა დამატებითი უფლებები</string>
<string name="sync_error_tasks_too_old">%s ნამეტანი ძველია</string>
<string name="sync_error_tasks_required_version">მინიმალური საჭირო ვერსია: %1$s</string>
<string name="sync_error_authentication_failed">აუთენტიფიკაცია ჩაიშალა (შეამოწმეთ შევლის იდენტიფიკატორები)</string>
<string name="sync_error_io">ქსელური ან ჩაწერა/წაკითხვის შეცდომა - %s</string>
<string name="sync_error_http_dav">HTTP სერვერის შეცდომა - %s</string>
<string name="sync_error_local_storage">ადგილობრივი მეხსიერების შეცდომა - %s</string>
<string name="sync_error_retry_limit_reached">რბილის შეცდომა (მიღწეულია თავიდან ცდის მაწსიმუმი)</string>
<string name="sync_error_view_item">ჩანაწერის ნახვა</string>
<string name="sync_invalid_contact">მიღებულია არასწორი კონტაქტი სერვერიდან</string>
<string name="sync_invalid_event">მიღებულია არასწორი ღონისძიება სერვერიდან</string>
<string name="sync_invalid_task">მიღებული არასწორი დავალება სერვერიდან</string>
<string name="sync_invalid_resources_ignoring">ერთი ან მეტი არასწორი რესურსის იგნორირება</string>
<!--widgets-->
<string name="widget_sync_all">ყველაფრის სინქრონიზირება</string>
<string name="widget_sync_all_accounts">ყველა ანგარიშის სინქრონიზაცია</string>
<!--cert4android-->
</resources>

View File

@@ -1,449 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="account_invalid">Account bestaat niet (of niet meer)</string>
<string name="account_title_address_book">DAVx⁵ Adresboek</string>
<string name="account_prefs_use_app">Verander hier niet van account! Gebruik in plaats daarvan direct de app om accounts te beheren.</string>
<string name="dialog_delete">Verwijderen</string>
<string name="dialog_remove">Verwijder</string>
<string name="dialog_deny">Annuleer</string>
<string name="field_required">Dit veld is verplicht</string>
<string name="help">Hulp</string>
<string name="navigate_up">Navigeer omhoog</string>
<string name="optional_label">*optioneel</string>
<string name="options_menu">Opties menu</string>
<string name="share">Delen</string>
<string name="sync_started">Synchronisatie begonnen/in wachtrij geplaatst</string>
<string name="database_destructive_migration_title">Database beschadigd</string>
<string name="database_destructive_migration_text">Alle accounts zijn lokaal verwijderd.</string>
<string name="notification_channel_debugging">Debuggen</string>
<string name="notification_channel_general">Andere belangrijke berichten</string>
<string name="notification_channel_status">Statusberichten met lage prioriteit</string>
<string name="notification_channel_sync">Synchroniseren</string>
<string name="notification_channel_sync_errors">Synchronisatiefouten</string>
<string name="notification_channel_sync_errors_desc">Belangrijke fouten die het synchroniseren stoppen, zoals onverwachte server antwoorden</string>
<string name="notification_channel_sync_warnings">Synchronisatie waarschuwingen</string>
<string name="notification_channel_sync_warnings_desc">Niet-fatale problemen bij het synchroniseren zoals bepaalde ongeldige bestanden</string>
<string name="notification_channel_sync_io_errors">Netwerk en I/O fouten</string>
<string name="notification_channel_sync_io_errors_desc">Timeouts, connectie problemen, etc. (vaak tijdelijk).</string>
<!--IntroActivity-->
<string name="intro_slogan1">Jouw gegevens. Jouw keuze.</string>
<string name="intro_slogan2">Houd zelf de controle</string>
<string name="intro_battery_title">regelmatige sync-intervallen</string>
<string name="intro_battery_text">Om op gezette tijden te synchroniseren moet %s zonder beperking op de achtergrond kunnen draaien. Anders kan Android het synchroniseren op elk moment onderbreken.</string>
<string name="intro_battery_dont_show">Synchroniseren op gezette tijden is niet nodig.*</string>
<string name="intro_autostart_title">%s compatibiliteit</string>
<string name="intro_autostart_text">Waarschijnlijk blokkeert dit toestel het synchroniseren. In dat geval is dit alleen handmatig op te lossen.</string>
<string name="intro_autostart_dont_show">De vereiste instellingen zijn verricht. Er aan herinneren is niet meer nodig.*</string>
<string name="intro_leave_unchecked">* Niet aanvinken om later herinnerd te worden. Kan teruggezet in app instellingen / %s.</string>
<string name="intro_more_info">Meer informatie</string>
<string name="intro_tasks_jtx">jtx Board</string>
<string name="intro_tasks_jtx_info"><![CDATA[Synchroniseert taken, agenda\'s en notities met elke geschikte CalDAV-server.]]></string>
<string name="intro_tasks_title">Ondersteunt taken</string>
<string name="intro_tasks_text1">Als de server taken ondersteunt, synchroniseert een geschikte taken-app ze:</string>
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Schijnt niet meer ontwikkeld te worden - niet aanbevolen.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Enkele functies <a href="https://www.davx5.com/faq/tasks/advanced-task-features">worden niet ondersteund</a>.]]></string>
<string name="intro_tasks_no_app_store">Geen app-store beschikbaar</string>
<string name="intro_tasks_dont_show">Ik hoef geen ondersteuning van taken.*</string>
<string name="intro_open_source_title">Open-source software</string>
<string name="intro_open_source_text">We zijn blij dat de keuze valt op open source software %s. Ontwikkelen, onderhouden en ondersteunen is veel werk. Overweeg daarom bij te dragen (kan op vele manieren) of een donatie. Wij waarderen het zeer!</string>
<string name="intro_open_source_details">Hoe bijdragen/doneren</string>
<string name="intro_open_source_dont_show">In de nabije toekomst niet weergeven</string>
<string name="intro_next">Volgende</string>
<!--PermissionsActivity-->
<string name="permissions_title">Rechten toestaan</string>
<string name="permissions_text">%s heeft rechten nodig om goed te werken.</string>
<string name="permissions_all_title">Alle onderstaande</string>
<string name="permissions_all_status_off">Gebruik dit om alle functies in te schakelen (aanbevolen)</string>
<string name="permissions_all_status_on">Alle rechten toegekend</string>
<string name="permissions_contacts_title">Contacten toestaan</string>
<string name="permissions_contacts_status_off">Geen contacten synchroniseren (niet aanbevolen)</string>
<string name="permissions_contacts_status_on">Contacten synchroniseren mogelijk</string>
<string name="permissions_calendar_title">Kalender machtigingen</string>
<string name="permissions_calendar_status_off">Geen kalenders synchroniseren (niet aanbevolen)</string>
<string name="permissions_calendar_status_on"> Kalenders synchroniseren mogelijk</string>
<string name="permissions_notification_title">Toestemming voor meldingen</string>
<string name="permissions_notification_status_off">Meldingen uitgeschakeld (niet aanbevolen)</string>
<string name="permissions_notification_status_on">Meldingen ingeschakeld</string>
<string name="permissions_jtx_title">jtx Board-rechten</string>
<string name="permissions_opentasks_title">OpenTasks rechten</string>
<string name="permissions_tasksorg_title">Rechten voor taken</string>
<string name="permissions_tasks_status_off">Geen taak-sync</string>
<string name="permissions_tasks_status_on">Taak-sync mogelijk</string>
<string name="permissions_autoreset_title">Rechten behouden</string>
<string name="permissions_autoreset_status_off">Rechten kunnen automatisch worden teruggezet (niet aanbevolen)</string>
<string name="permissions_autoreset_status_on">Rechten worden niet automatisch teruggezet</string>
<string name="permissions_autoreset_instruction">Klik op App Rechten &gt; vinkje uit bij \"Rechten intrekken\"</string>
<string name="permissions_app_settings_hint">Als een schakeloptie niet werkt, gebruik dan App-info / Rechten.</string>
<string name="permissions_app_settings">App instellingen</string>
<!--WifiPermissionsActivity-->
<string name="wifi_permissions_label">WiFi SSID rechten</string>
<string name="wifi_permissions_intro">Voor toegang tot de huidige WiFi-naam (SSID), moet aan deze voorwaarden worden voldaan:</string>
<string name="wifi_permissions_location_permission">Recht van toegang tot exacte locatie</string>
<string name="wifi_permissions_location_permission_on">Toegang tot locatie verleend</string>
<string name="wifi_permissions_location_permission_off">Toegang tot locatie geweigerd</string>
<string name="wifi_permissions_background_location_permission">Toegang tot locatie op de achtergrond</string>
<string name="wifi_permissions_background_location_permission_label">Onbeperkt toestaan</string>
<string name="wifi_permissions_background_location_permission_on">Locatietoestemming ingesteld op: %s</string>
<string name="wifi_permissions_background_location_permission_off">Locatietoestemming niet ingesteld op: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s gebruikt locatiegegevens (alleen WiFi SSID) uitsluitend om de synchronisatie te beperken tot een specifieke WiFi SSID. Dit gebeurt zelfs als de synchronisatie op de achtergrond wordt uitgevoerd.</string>
<string name="wifi_permissions_background_location_disclaimer2">Alle locatiegegevens (alleen WiFi SSID) worden alleen lokaal gebruikt en worden nergens naartoe verzonden.</string>
<string name="wifi_permissions_location_enabled">Toegang tot locatie altijd ingeschakeld</string>
<string name="wifi_permissions_location_enabled_on">Toegang tot locatie is ingeschakeld</string>
<string name="wifi_permissions_location_enabled_off">Toegang tot locatie is uitgeschakeld</string>
<!--AboutActivity-->
<string name="about_translations">Vertalingen</string>
<string name="about_libraries">Bibliotheken</string>
<string name="about_version">Versie%1$s (%2$d)</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) en bijdragers</string>
<string name="about_license_info_no_warranty">Dit programma wordt geleverd met ABSOLUUT GEEN GARANTIE. Het is gratis software, en mag opnieuw worden verspreid onder bepaalde voorwaarden.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Kon geen logbestand aanmaken</string>
<string name="logging_notification_text">Logt nu alle %s activiteiten</string>
<string name="logging_notification_view_share">Bekijken/delen</string>
<string name="logging_notification_disable">Uitschakelen</string>
<!--AccountsScreen-->
<string name="navigation_drawer_subtitle">CalDAV/CardDAV Sync adapter</string>
<string name="navigation_drawer_about">Over / Licentie</string>
<string name="navigation_drawer_beta_feedback">Beta terugkoppeling</string>
<string name="install_browser">Webbrowser is vereist</string>
<string name="navigation_drawer_settings">Instellingen</string>
<string name="navigation_drawer_news_updates">Nieuws &amp; updates</string>
<string name="navigation_drawer_tools">Gereedschap</string>
<string name="navigation_drawer_external_links">Externe links</string>
<string name="navigation_drawer_website">Website</string>
<string name="navigation_drawer_manual">Handleiding</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_community">Community</string>
<string name="navigation_drawer_support_project">Ondersteun het project</string>
<string name="navigation_drawer_contribute">Hoe bijdragen</string>
<string name="navigation_drawer_privacy_policy">Privacybeleid</string>
<string name="account_list_no_notification_permission">Meldingen uitgeschakeld. U krijgt geen meldingen over synchronisatiefouten.</string>
<string name="account_list_manage_connections">Verbindingen beheren</string>
<string name="account_list_datasaver_enabled">Gegevensbesparing ingeschakeld. Synchronisatie op de achtergrond is beperkt.</string>
<string name="account_list_manage_datasaver">Beheer van gegevensbesparing</string>
<string name="account_list_battery_saver_enabled">Batterijbesparing ingeschakeld. Synchronisatie kan beperkt zijn.</string>
<string name="account_list_manage_battery_saver">Batterijbesparing beheren</string>
<string name="account_list_low_storage">Weinig opslagruimte. Android zal lokale wijzigingen niet onmiddellijk synchroniseren, maar tijdens de volgende reguliere synchronisatie.</string>
<string name="account_list_manage_storage">Opslag beheren</string>
<string name="account_list_welcome">Welkom bij DAVx⁵!</string>
<string name="account_list_empty">Maak verbinding met je server en houd je agenda\'s en contactpersonen gesynchroniseerd.</string>
<string name="accounts_sync_all">Alle accounts synchroniseren</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Service herkenning is mislukt</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">De collectielijst is niet bijgewerkt</string>
<!--Foreground service used by WorkManager on Android <12-->
<string name="foreground_service_notify_title">Draait op de voorgrond</string>
<string name="foreground_service_notify_text">Op sommige toestellen is dit nodig voor automatische synchronisatie.</string>
<!--AppSettingsActivity-->
<string name="app_settings">Instellingen</string>
<string name="app_settings_debug">Debuggen</string>
<string name="app_settings_show_debug_info">Debug-info</string>
<string name="app_settings_show_debug_info_details">Configuratiedetails en logbestanden bekijken/delen</string>
<string name="app_settings_logging">Uitgebreid loggen</string>
<string name="app_settings_logging_on">Loggen is actief. Je kunt de logs bekijken als onderdeel van de debug-info.</string>
<string name="app_settings_logging_off">Loggen is niet actief</string>
<string name="app_settings_battery_optimization">Batterijoptimalisatie</string>
<string name="app_settings_battery_optimization_exempted">App is vrijgesteld (aanbevolen)</string>
<string name="app_settings_battery_optimization_optimized">Batterijbeperkingen van toepassing (niet aanbevolen)</string>
<string name="app_settings_connection">Verbinding</string>
<string name="app_settings_proxy">Proxy type</string>
<string-array name="app_settings_proxy_types">
<item>Systeem standaard</item>
<item>Geen proxy</item>
<item>HTTP</item>
<item>SOCKS (voor Orbot)</item>
</string-array>
<string name="app_settings_proxy_host">Proxy host naam</string>
<string name="app_settings_proxy_port">Proxy poort</string>
<string name="app_settings_security">Beveiliging</string>
<string name="app_settings_security_app_permissions">App rechten</string>
<string name="app_settings_security_app_permissions_summary">De vereiste rechten om te synchroniseren controleren</string>
<string name="app_settings_distrust_system_certs">Wantrouw systeemcertificaten</string>
<string name="app_settings_distrust_system_certs_on">Door systeem en gebruiker toegevoegde CA certificaten niet vertrouwen</string>
<string name="app_settings_distrust_system_certs_off">Door systeem en gebruiker toegevoegde CA certificaten vertrouwen (aanbevolen)</string>
<string name="app_settings_reset_certificates">(Niet-)vertrouwde certificaten terugzetten</string>
<string name="app_settings_reset_certificates_summary">Herstelt het vertrouwen van alle aangepaste certificaten</string>
<string name="app_settings_reset_certificates_success">Alle aangepaste certificaten zijn gewist</string>
<string name="app_settings_user_interface">Gebruikersinterface</string>
<string name="app_settings_notification_settings">App-meldingen</string>
<string name="app_settings_notification_settings_summary">Meldingskanalen en hun instellingen beheren</string>
<string name="app_settings_theme_title">Thema selecteren</string>
<string-array name="app_settings_theme_names">
<item>Systeem standaard</item>
<item>Licht</item>
<item>Donker</item>
</string-array>
<string name="app_settings_reset_hints">Hints opnieuw instellen</string>
<string name="app_settings_reset_hints_summary">Hints die al gezien zijn opnieuw weergeven</string>
<string name="app_settings_reset_hints_success">Alle hints opnieuw weergeven</string>
<string name="app_settings_integration">Integratie</string>
<string name="app_settings_tasks_provider">Taken app</string>
<string name="app_settings_tasks_provider_none">Geen compatibele taken app gevonden</string>
<string name="app_settings_unifiedpush">UnifiedPush (experimenteel)</string>
<string name="app_settings_unifiedpush_disable">Geen (push uitschakelen)</string>
<string name="app_settings_unifiedpush_choose_distributor">Kies een distributeur</string>
<string name="app_settings_unifiedpush_no_distributor">Geen push distributeur geïnstalleerd</string>
<string name="app_settings_unifiedpush_no_endpoint">Geen eindpunt geconfigureerd</string>
<string name="app_settings_unifiedpush_ready">Klaar om pushberichten te ontvangen via %s</string>
<!--AccountScreen-->
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_missing_permissions">Er zijn extra rechten nodig om deze collecties te synchroniseren.</string>
<string name="account_manage_permissions">Machtigingen beheren</string>
<string name="account_synchronize_now">Nu synchroniseren</string>
<string name="account_settings">Account instellingen</string>
<string name="account_rename">Naam account wijzigen</string>
<string name="account_rename_new_name_description">Niet opgeslagen lokale gegevens kunnen worden verwijderd. Na het hernoemen is opnieuw synchroniseren vereist.</string>
<string name="account_rename_new_name">Nieuwe accountnaam</string>
<string name="account_rename_rename">Naam wijzigen</string>
<string name="account_rename_exists_already">Accountnaam is al in gebruik</string>
<string name="account_rename_couldnt_rename">Naam account is niet gewijzigd</string>
<string name="account_delete">Account verwijderen</string>
<string name="account_delete_confirmation_title">Account echt verwijderen?</string>
<string name="account_delete_confirmation_text">Alle lokale kopieën van adresboeken, kalenders en takenlijsten worden verwijderd.</string>
<string name="account_synchronize_this_collection">deze collectie synchroniseren</string>
<string name="account_read_only">alleen-lezen</string>
<string name="account_calendar">kalender</string>
<string name="account_contacts">contacten</string>
<string name="account_journal">logboek</string>
<string name="account_task_list">taken</string>
<string name="account_only_personal">Alleen persoonlijk tonen</string>
<string name="account_refresh_collections">Lijst verversen</string>
<string name="account_webcal_external_app">Webcal abonnementen kunnen worden gesynchroniseerd met externe apps.</string>
<string name="account_no_webcal_handler_found">Geen Webcal-app gevonden</string>
<string name="account_install_icsx5">ICSx⁵ installeren</string>
<!--AddAccountActivity-->
<string name="login_title">Account toevoegen</string>
<string name="login_privacy_hint"><![CDATA[Alle gegevens worden alleen overgedragen tussen je server en je apparaat. %1$s zal ze nergens anders naartoe sturen. Zie<a href="%2$s">privacybeleid</a>.]]></string>
<string name="login_generic_login">Algemeen inloggen</string>
<string name="login_provider_login">Aanbieder-specifieke login</string>
<string name="login_continue">Ga verder</string>
<string name="login_login">Login</string>
<string name="login_type_email">Inloggen met e-mailadres</string>
<string name="login_email_address">E-mailadres</string>
<string name="login_email_address_error">Geldig e-mailadres vereist</string>
<string name="login_email_address_info"><![CDATA[Het e-maildomein wordt gebruikt als basis-URL. <a href="%s">Diensten worden ontdekt</a> met behulp van DNS-records en bekende URL\'s.]]></string>
<string name="login_password">Wachtwoord</string>
<string name="login_password_hide">Verberg wachtwoord</string>
<string name="login_password_show">Wachtwoord tonen</string>
<string name="login_password_optional">Wachtwoord*</string>
<string name="login_type_url">Inloggen met URL en gebruikersnaam</string>
<string name="login_user_name">Gebruikersnaam</string>
<string name="login_user_name_optional">Gebruikersnaam*</string>
<string name="login_base_url">Basis-URL</string>
<string name="login_base_url_info"><![CDATA[De basis URL wordt direct gecontroleerd, maar <a href="%s">services worden ook ontdekt</a> met behulp van DNS records en bekende URL\'s.]]></string>
<string name="login_select_certificate">Certificaat selecteren</string>
<string name="login_add_account">Account toevoegen</string>
<string name="login_account_name">Accountnaam</string>
<string name="login_account_avoid_apostrophe">Het gebruik van apostrofs (\') lijkt problemen te veroorzaken op sommige apparaten.</string>
<string name="login_account_name_info">Gebruik het eigen e-mailadres als accountnaam, want Android gebruikt het als ORGANIZER veld voor gebeurtenissen. Twee accounts met hetzelfde adres kan niet.</string>
<string name="login_account_contact_group_method">Methode voor contact-groepen:</string>
<string name="login_account_name_required">Accountnaam verplicht</string>
<string name="login_account_name_already_taken">Accountnaam is al in gebruik</string>
<string name="login_account_not_added">Account kon niet worden toegevoegd</string>
<string name="login_finish">afwerken</string>
<string name="login_type_advanced">Geavanceerd inloggen</string>
<string name="login_no_client_certificate_optional">Geen cliëntcertificaat*</string>
<string name="login_client_certificate_selected">Cliëntcertificaat: %s</string>
<string name="login_no_certificate_found">Geen certificaat gevonden</string>
<string name="login_install_certificate">Certificaat installeren</string>
<string name="login_type_google">Google Contacten / Kalender</string>
<string name="login_google_see_tested_with">Raadpleeg onze pagina \"Getest met Google\" voor actuele informatie.</string>
<string name="login_google_unexpected_warnings">Het kan zijn dat je onverwachte waarschuwingen krijgt en/of je eigen client-ID moet aanmaken.</string>
<string name="login_google_account">Google account</string>
<string name="login_google">Inloggen met Google</string>
<string name="login_google_client_id">Client ID (optioneel)</string>
<string name="login_google_client_privacy_policy"><![CDATA[%1$s draagt uw Google Contacten en Agenda gegevens uitsluitend over voor synchronisatie met dit apparaat. Zie ons Privacybeleid voor meer informatie. Zie ons <a href="%2$s">Privacybeleid</a> voor meer informatie.]]></string>
<string name="login_google_client_limited_use"><![CDATA[%1$s voldoet aan het <a href="%2$s">beleid voor gebruikersgegevens van Google API Services</a>, met inbegrip van de vereisten voor beperkt gebruik.]]></string>
<string name="login_oauth_couldnt_obtain_auth_code">Kon geen autorisatiecode verkrijgen</string>
<string name="login_type_nextcloud">Nextcloud</string>
<string name="login_nextcloud_login_with_nextcloud">Inloggen met Nextcloud</string>
<string name="login_nextcloud_login_flow_text">Hiermee wordt de Nextcloud Flow-aanmelding in een webbrowser gestart.</string>
<string name="login_nextcloud_login_flow_server_address">Nextcloud serveradres</string>
<string name="login_nextcloud_login_flow_sign_in">Aanmelden</string>
<string name="login_nextcloud_login_flow_no_login_url">Kan inlog-URL niet verkrijgen</string>
<string name="login_nextcloud_login_flow_no_login_data">Kan inlog-URL niet verkrijgen</string>
<string name="login_configuration_detection">Configuratie detecteren</string>
<string name="login_querying_server">Even geduld, verzoek naar server…</string>
<string name="login_no_service">Geen CalDAV- of CardDAV-service gevonden.</string>
<string name="login_no_service_info">De basis URL lijkt geen toegankelijke CalDAV/CardDAV URL te zijn en de detectie van de service was niet succesvol.</string>
<string name="login_see_tested_services"><![CDATA[Raadpleeg de handleiding van uw serviceprovider en <a href="%s">onze lijst met geteste services</a> en hun basis URL\'s.]]></string>
<string name="login_check_credentials">Controleer ook de authenticatie (meestal gebruikersnaam en wachtwoord).</string>
<string name="login_logs_available">Meer technische informatie is beschikbaar in de logboeken.</string>
<string name="login_view_logs">Details bekijken</string>
<!--AccountSettingsActivity-->
<string name="settings_sync">Synchronisatie</string>
<string name="settings_sync_interval_contacts">Contacten synchronisatie interval</string>
<string name="settings_sync_summary_manually">Alleen handmatig</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Elke %d minuten + direct bij lokale veranderingen</string>
<string name="settings_sync_interval_calendars">Kalenders synchronisatie-interval</string>
<string name="settings_sync_interval_tasks">Taken synchronisatie-interval</string>
<string-array name="settings_sync_interval_names">
<item>Handmatig </item>
<item>Elke 15 minuten </item>
<item>Elke 30 minuten</item>
<item>Elk uur</item>
<item>Elke 2 uur</item>
<item>Elke 4 uur</item>
<item>Eenmaal daags</item>
</string-array>
<string name="settings_sync_wifi_only">Synchronisatie beperken tot WiFi</string>
<string name="settings_sync_wifi_only_on">Alleen verbinden via WiFi</string>
<string name="settings_sync_wifi_only_off">Type verbinding is niet relevant</string>
<string name="settings_sync_wifi_only_ssids">Tot bepaalde WiFi-SSID beperken</string>
<string name="settings_sync_wifi_only_ssids_on">Synchronisatie alleen via %s</string>
<string name="settings_sync_wifi_only_ssids_off">Elke WiFI-SSID toestaan</string>
<string name="settings_sync_wifi_only_ssids_message">Door komma\'s gescheiden namen (SSID\'s) van toegestane WiFi-netwerken (laat leeg voor alle)</string>
<string name="settings_sync_wifi_only_ssids_permissions_required">Beperking WiFi-SSID vereist verdere instellingen</string>
<string name="settings_sync_wifi_only_ssids_permissions_action">Beheren</string>
<string name="settings_ignore_vpns">VPN vereist onderliggend internet</string>
<string name="settings_ignore_vpns_on">VPN zonder onderliggende gevalideerde internetverbinding is niet voldoende om synchronisatie uit te voeren (aanbevolen)</string>
<string name="settings_ignore_vpns_off">VPN zonder onderliggende gevalideerde internetverbinding is voldoende om synchronisatie uit te voeren</string>
<string name="settings_authentication">Authenticatie</string>
<string name="settings_username">Gebruikersnaam</string>
<string name="settings_password">Wachtwoord</string>
<string name="settings_new_password">Nieuw wachtwoord</string>
<string name="settings_password_summary">Gebruik het zelfde wachtwoord als op de server.</string>
<string name="settings_certificate_alias">Cliëntcertificaat</string>
<string name="settings_certificate_alias_empty">Geen certificaat beschikbaar of geselecteerd</string>
<string name="settings_certificate_install">Certificaat installeren</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Gebeurtenissen in verleden tijd</string>
<string name="settings_sync_time_range_past_none">Worden alle gesynchroniseerd</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Afspraken ouder dan een dag worden genegeerd</item>
<item quantity="other">Ouder dan %d dagen worden genegeerd</item>
</plurals>
<string name="settings_sync_time_range_past_message">Gebeurtenissen ouder dan ingevuld aantal dagen worden genegeerd (mag 0 zijn). Veld leeg laten om alle te synchroniseren.</string>
<string name="settings_default_alarm">Standaardherinnering</string>
<plurals name="settings_default_alarm_on">
<item quantity="one">Standaardherinnering één minut voor het evenement</item>
<item quantity="other"> %d minuten voor aanvang gebeurtenis</item>
</plurals>
<string name="settings_default_alarm_off">Wordt niet aangemaakt</string>
<string name="settings_default_alarm_message">Vul het gewenste aantal minuten in. Leeg laten om herinneringen uit te schakelen.</string>
<string name="settings_manage_calendar_colors">Kalender kleuren beheren</string>
<string name="settings_manage_calendar_colors_on">Worden bij elke sync teruggezet</string>
<string name="settings_manage_calendar_colors_off">Kunnen door andere apps worden ingesteld</string>
<string name="settings_event_colors">Gebeurtenis kleuren ondersteunen</string>
<string name="settings_event_colors_on">Worden gesynchroniseerd</string>
<string name="settings_event_colors_off">Worden niet gesynchroniseerd</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Methode voor contact-groepen:</string>
<string-array name="settings_contact_group_method_entries">
<item>Groepen zijn afzonderlijke vCards</item>
<item>Groepen zijn categorieën per contact</item>
</string-array>
<!--CreateAddressBookScreen, CreateCalendarScreen-->
<string name="create_addressbook">Adresboek aanmaken</string>
<string name="create_addressbook_maybe_not_supported">Het aanmaken van een adresboek via CardDAV wordt mogelijk niet ondersteund door de server.</string>
<string name="create_calendar">Kalender aanmaken</string>
<string name="create_calendar_time_zone_optional">Standaard tijdzone*</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">Mogelijke kalender-items</string>
<string name="create_calendar_type_vevent">Gebeurtenissen</string>
<string name="create_calendar_type_vtodo">Taken</string>
<string name="create_calendar_type_vjournal">Notities / Dagboek</string>
<string name="create_calendar_maybe_not_supported">Het aanmaken van een kalender via CalDAV wordt mogelijk niet ondersteund door de server.</string>
<string name="create_collection_color">Kleur</string>
<string name="create_collection_display_name">Titel</string>
<string name="create_collection_home_set">Opslaglocatie</string>
<string name="create_collection_description_optional">Beschrijving*</string>
<string name="create_collection_create">Aanmaken</string>
<string name="create_collection_optional">*optioneel</string>
<!--CollectionScreen-->
<string name="collection_delete">Collectie verwijderen</string>
<string name="collection_delete_warning">Deze collectie (%s) en alle gegevens worden permanent verwijderd, zowel lokaal als op de server.</string>
<string name="collection_synchronization">Synchroniseren</string>
<string name="collection_synchronization_on">Synchronisatie ingeschakeld</string>
<string name="collection_synchronization_off">Synchronisatie uitgeschakeld</string>
<string name="collection_read_only">Alleen-lezen</string>
<string name="collection_read_only_by_server">Alleen-lezen (door server)</string>
<string name="collection_read_only_by_setting">Alleen-lezen (volgens beleid)</string>
<string name="collection_read_only_forced">Alleen-lezen (alleen lokaal)</string>
<string name="collection_read_write">Lezen/schrijven</string>
<string name="collection_title">Titel</string>
<string name="collection_description">Beschrijving</string>
<string name="collection_owner">Eigenaar</string>
<string name="collection_push_support">Push-ondersteuning</string>
<string name="collection_push_web_push">Server adverteert Push-ondersteuning</string>
<string name="collection_push_subscribed_at">Ingeschreven op %1$s, vervalt op %2$s</string>
<string name="collection_last_sync">Laatste gesynchroniseerd (%s)</string>
<string name="collection_url">Adres (URL)</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_title">Debug informatie</string>
<string name="debug_info_archive_caption">ZIP archief</string>
<string name="debug_info_archive_subtitle">Bevat debuginformatie en logbestanden</string>
<string name="debug_info_archive_text">Deel het archief om over te zetten naar een computer, per e-mail te verzenden of als bijlage bij een supportticket te voegen..</string>
<string name="debug_info_archive_share">Archief delen</string>
<string name="debug_info_attached">Debug info als bijlage bij dit bericht (vereist ondersteuning voor bijlagen van de ontvangende app).</string>
<string name="debug_info_http_error">HTTP-fout</string>
<string name="debug_info_server_error">Serverfout</string>
<string name="debug_info_webdav_error">WebDAV fout</string>
<string name="debug_info_io_error">I/O-fout</string>
<string name="debug_info_http_403_description">Het verzoek is afgewezen. Controleer de betrokken bronnen en debug-info voor details.</string>
<string name="debug_info_http_404_description">De gevraagde bron bestaat niet (meer). Controleer de betrokken bronnen en debug-info voor details.</string>
<string name="debug_info_http_5xx_description">Er is bij de server een probleem opgetreden. Neem contact op met de server-ondersteuning.</string>
<string name="debug_info_unexpected_error">Er is een onverwachte fout opgetreden. Bekijk debug-info voor details.</string>
<string name="debug_info_view_details">Details bekijken</string>
<string name="debug_info_subtitle">Debug-info is verzameld</string>
<string name="debug_info_involved_caption">Betrokken bronnen</string>
<string name="debug_info_involved_subtitle">Gerelateerd aan het probleem</string>
<string name="debug_info_involved_remote">Externe bron:</string>
<string name="debug_info_involved_local">Lokale bron:</string>
<string name="debug_info_logs_caption">Logboeken</string>
<string name="debug_info_logs_subtitle">Uitgebreide logboeken zijn beschikbaar</string>
<string name="debug_info_logs_view">Details bekijken</string>
<!--ExceptionInfoFragment-->
<string name="exception">Er is een fout opgetreden.</string>
<string name="exception_httpexception">Een HTTP-fout is opgetreden.</string>
<string name="exception_ioexception">Een I/O fout is opgetreden.</string>
<string name="exception_show_details">Details weergeven</string>
<!--WebDAV accounts-->
<string name="webdav_mounts_title">WebDAV-koppelingen</string>
<string name="webdav_mounts_quota_used_available">Quotum gebruikt: %1$s / Beschikbaar: %2$s</string>
<string name="webdav_mounts_share_content">Inhoud delen</string>
<string name="webdav_mounts_unmount">Ontkoppelen</string>
<string name="webdav_add_mount_title">WebDAV-koppeling toevoegen</string>
<string name="webdav_mounts_empty">Verkrijg directe toegang tot cloudbestanden met een WebDAV-koppeling!</string>
<string name="webdav_add_mount_empty_more_info"><![CDATA[Zie de handleiding over <a href="%1$s"> het koppelen van WebDAV</a>.</string>]]></string>
<string name="webdav_add_mount_display_name">Weergavenaam</string>
<string name="webdav_add_mount_url">WebDAV-URL</string>
<string name="webdav_add_mount_url_invalid">Ongeldige URL</string>
<string name="webdav_add_mount_authentication">Authenticatie (optioneel)</string>
<string name="webdav_add_mount_username">Gebruikersnaam</string>
<string name="webdav_add_mount_password">Wachtwoord</string>
<string name="webdav_add_mount_add">Koppeling toevoegen</string>
<string name="webdav_add_mount_no_support">Geen WebDAV-service op deze URL</string>
<string name="webdav_remove_mount_title">Verwijder het koppelpunt</string>
<string name="webdav_remove_mount_text">Verbindingsgegevens gaan verloren, maar er worden geen bestanden gewist.</string>
<string name="webdav_notification_access">WebDAV-bestand openen</string>
<string name="webdav_notification_download">WebDAV-bestand downloaden</string>
<string name="webdav_notification_upload">WebDAV-bestand uploaden</string>
<string name="webdav_provider_root_title">WebDAV-koppeling</string>
<!--sync-->
<string name="sync_error_permissions">DAVx⁵ rechten</string>
<string name="sync_error_permissions_text">Aanvullende rechten vereist</string>
<string name="sync_error_tasks_too_old">%ste oud</string>
<string name="sync_error_tasks_required_version">Minimaal vereiste versie: %1$s</string>
<string name="sync_error_authentication_failed">Verificatie mislukt (controleer aanmeldingsgegevens)</string>
<string name="sync_error_io">Netwerk of I/O error - %s</string>
<string name="sync_error_http_dav">HTTP-server fout - %s</string>
<string name="sync_error_local_storage">Lokale opslag fout - %s</string>
<string name="sync_error_retry_limit_reached">Soft error (max. aantal pogingen bereikt)</string>
<string name="sync_error_view_item">Item bekijken</string>
<string name="sync_invalid_contact">Ongeldig contact ontvangen van server</string>
<string name="sync_invalid_event">Ongeldige gebeurtenis ontvangen van server</string>
<string name="sync_invalid_task">Ongeldige taak ontvangen van server</string>
<string name="sync_invalid_resources_ignoring">Een of meer ongeldige bronnen negeren</string>
<string name="sync_notification_pending_push_title">Synchronisatie in afwachting</string>
<string name="sync_notification_pending_push_message">De gegevens op afstand zijn veranderd</string>
<!--widgets-->
<string name="widget_sync_all">Alles synchroniseren</string>
<string name="widget_sync_all_accounts">Alle accounts synchroniseren</string>
<!--cert4android-->
</resources>

View File

@@ -1,446 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="account_invalid">Contul nu (mai) există</string>
<string name="account_title_address_book">Agenda DAVx⁵</string>
<string name="dialog_delete">Șterge</string>
<string name="dialog_remove">Elimină</string>
<string name="dialog_deny">Anulează</string>
<string name="field_required">Acest câmp este obligatoriu</string>
<string name="help">Ajutor</string>
<string name="navigate_up">Navigare în sus</string>
<string name="optional_label">* opțional</string>
<string name="options_menu">Meniul Opțiuni</string>
<string name="share">Distribuie</string>
<string name="sync_started">Sincronizare începută/pusă în coadă</string>
<string name="database_destructive_migration_title">Bază de date deteriorată</string>
<string name="database_destructive_migration_text">Toate conturile au fost eliminate local.</string>
<string name="notification_channel_debugging">Depanare</string>
<string name="notification_channel_general">Alte mesaje importante</string>
<string name="notification_channel_status">Mesaje de stare cu prioritate redusă</string>
<string name="notification_channel_sync">Sincronizare</string>
<string name="notification_channel_sync_errors">Erori de sincronizare</string>
<string name="notification_channel_sync_errors_desc">Erori importante care opresc sincronizarea, cum ar fi răspunsurile neașteptate ale serverului</string>
<string name="notification_channel_sync_warnings">Avertismente de sincronizare</string>
<string name="notification_channel_sync_warnings_desc">Probleme de sincronizare non-fatale, cum ar fi anumite fișiere nevalide</string>
<string name="notification_channel_sync_io_errors">Erori de rețea și I/O</string>
<string name="notification_channel_sync_io_errors_desc">Expirare, probleme de conexiune etc. (adesea temporare)</string>
<!--IntroActivity-->
<string name="intro_slogan1">Datele tale. Alegerea ta.</string>
<string name="intro_slogan2">Preia controlul.</string>
<string name="intro_battery_title">Intervale regulate de sincronizare</string>
<string name="intro_battery_text">Pentru sincronizare la intervale regulate, %s trebuie să aibă voie să ruleze în fundal. În caz contrar, Android poate întrerupe sincronizarea în orice moment.</string>
<string name="intro_battery_dont_show">Nu am nevoie de intervale regulate de sincronizare.*</string>
<string name="intro_autostart_title">Compatibilitate %s </string>
<string name="intro_autostart_text">Acest dispozitiv probabil blochează sincronizarea. Dacă ești afectat, poți rezolva acest lucru numai manual.</string>
<string name="intro_autostart_dont_show">Am făcut setările necesare. Nu-mi mai aminti.*</string>
<string name="intro_leave_unchecked">* Lasă nebifat pentru a fi reamintit mai târziu. Poate fi resetat în setările aplicației / %s.</string>
<string name="intro_more_info">Mai multe informații</string>
<string name="intro_tasks_jtx">Placă de bază jtx</string>
<string name="intro_tasks_jtx_info"><![CDATA[Acceptă sincronizarea sarcinilor, jurnalelor și notelor.]]></string>
<string name="intro_tasks_title">Suport pentru sarcini</string>
<string name="intro_tasks_text1">Dacă sarcinile sunt acceptate de server, acestea pot fi sincronizate cu o aplicație de sarcini acceptată:</string>
<string name="intro_tasks_opentasks">OpenTasks</string>
<string name="intro_tasks_opentasks_info">Nu pare a mai fi dezvoltat nu este recomandat.</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[Unele caracteristici <a href="https://www.davx5.com/faq/tasks/advanced-task-features">nu sunt acceptate</a>.]]></string>
<string name="intro_tasks_no_app_store">Nu există un magazin de aplicații disponibil</string>
<string name="intro_tasks_dont_show">Nu am nevoie de suport pentru sarcini.*</string>
<string name="intro_open_source_title">Software cu sursă deschisă</string>
<string name="intro_open_source_text">Ne bucurăm că utilizezi %s, care este un software open-source. Dezvoltarea, întreținerea și suportul sunt o muncă grea. Ia în considerare contribuția (există mai multe moduri) sau o donație. Ar fi foarte apreciat!</string>
<string name="intro_open_source_details">Cum să contribui/donezi</string>
<string name="intro_open_source_dont_show">Nu afișa în viitorul apropiat</string>
<string name="intro_next">Înainte</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permisiuni</string>
<string name="permissions_text">%s necesită permisiuni pentru a funcționa corect.</string>
<string name="permissions_all_title">Toate cele de mai jos</string>
<string name="permissions_all_status_off">Utilizează aceasta pentru a activa toate funcțiile (recomandat)</string>
<string name="permissions_all_status_on">Toate permisiunile sunt acordate</string>
<string name="permissions_contacts_title">Permisiuni Contacte</string>
<string name="permissions_contacts_status_off">Fără sincronizare de contacte (nu este recomandat)</string>
<string name="permissions_contacts_status_on">Este posibilă sincronizarea contactelor</string>
<string name="permissions_calendar_title">Permisiuni pentru calendar</string>
<string name="permissions_calendar_status_off">Fără sincronizare calendar (nu este recomandat)</string>
<string name="permissions_calendar_status_on">Sincronizarea calendarului este posibilă</string>
<string name="permissions_notification_title">Permisiune de notificare</string>
<string name="permissions_notification_status_off">Notificări dezactivate (nu este recomandat)</string>
<string name="permissions_notification_status_on">Notificări activate</string>
<string name="permissions_jtx_title">Permisiuni pentru jtx Board</string>
<string name="permissions_opentasks_title">Permisiuni OpenTasks</string>
<string name="permissions_tasksorg_title">Permisiuni pentru sarcini</string>
<string name="permissions_tasks_status_off">Nicio sincronizare a sarcinilor</string>
<string name="permissions_tasks_status_on">Este posibilă sincronizarea sarcinilor</string>
<string name="permissions_autoreset_title">Păstrează permisiunile</string>
<string name="permissions_autoreset_status_off">Permisiunile pot fi resetate automat (nu este recomandat)</string>
<string name="permissions_autoreset_status_on">Permisiunile nu vor fi resetate automat</string>
<string name="permissions_autoreset_instruction">Clic pe Permisiuni &gt; debifează „Elimină permisiunile dacă aplicația nu este utilizată”</string>
<string name="permissions_app_settings_hint">Dacă un comutator nu funcționează, utilizează setările/permisiunile aplicației.</string>
<string name="permissions_app_settings">Setările aplicației</string>
<!--WifiPermissionsActivity-->
<string name="wifi_permissions_label">Permisiuni SSID WiFi</string>
<string name="wifi_permissions_intro">Pentru a putea accesa numele actual WiFi (SSID), trebuie îndeplinite următoarele condiții:</string>
<string name="wifi_permissions_location_permission">Permisiune de locație precisă</string>
<string name="wifi_permissions_location_permission_on">Permisiunea de locație acordată</string>
<string name="wifi_permissions_location_permission_off">Permisiunea de locație refuzată</string>
<string name="wifi_permissions_background_location_permission">Permisiunea de locație în fundal</string>
<string name="wifi_permissions_background_location_permission_label">Permite tot timpul</string>
<string name="wifi_permissions_background_location_permission_on">Permisiunea locației setată la: %s</string>
<string name="wifi_permissions_background_location_permission_off">Permisiunea de locație nu este setată la: %s</string>
<string name="wifi_permissions_background_location_disclaimer">%s folosește datele locației (doar WiFi SSID) numai pentru a restricționa sincronizarea la un anumit SSID WiFi. Acest lucru se va întâmpla chiar și atunci când sincronizarea rulează în fundal.</string>
<string name="wifi_permissions_background_location_disclaimer2">Toate datele locației (doar WiFi SSID) sunt folosite doar local și nu sunt trimise nicăieri.</string>
<string name="wifi_permissions_location_enabled">Locația este întotdeauna activată</string>
<string name="wifi_permissions_location_enabled_on">Serviciul de localizare este activat</string>
<string name="wifi_permissions_location_enabled_off">Serviciul de localizare este dezactivat</string>
<!--AboutActivity-->
<string name="about_translations">Traduceri</string>
<string name="about_libraries">Biblioteci</string>
<string name="about_version">Versiune %1$s (%2$d)</string>
<string name="about_copyright">© Ricki Hirner, Bernhard Stockmann (inginerie web bitfire GmbH) și contribuitori</string>
<string name="about_license_info_no_warranty">Acest program vine cu ABSOLUT NICIO GARANȚIE. Este software gratuit și ești binevenit să îl redistribui în anumite condiții.</string>
<!--global settings-->
<string name="logging_couldnt_create_file">Nu s-a putut crea fișierul jurnal</string>
<string name="logging_notification_text">Acum se înregistrează toate activitățile %s</string>
<string name="logging_notification_view_share">Vizualizare/distribuire</string>
<string name="logging_notification_disable">Dezactivează</string>
<!--AccountsScreen-->
<string name="navigation_drawer_subtitle">Adaptor de sincronizare CalDAV/CardDAV</string>
<string name="navigation_drawer_about">Despre / Licență</string>
<string name="navigation_drawer_beta_feedback">Feedback beta</string>
<string name="install_browser">Instalează un browser web</string>
<string name="navigation_drawer_settings">Setări</string>
<string name="navigation_drawer_news_updates">Știri și actualizări</string>
<string name="navigation_drawer_tools">Instrumente</string>
<string name="navigation_drawer_external_links">Link-uri externe</string>
<string name="navigation_drawer_website">Pagină web</string>
<string name="navigation_drawer_manual">Manual</string>
<string name="navigation_drawer_faq">Întrebări frecvente</string>
<string name="navigation_drawer_community">Comunitate</string>
<string name="navigation_drawer_support_project">Susține proiectul</string>
<string name="navigation_drawer_contribute">Cum să contribui</string>
<string name="navigation_drawer_privacy_policy">Politica de confidențialitate</string>
<string name="account_list_no_notification_permission">Notificări dezactivate. Nu vei fi notificat despre erorile de sincronizare.</string>
<string name="account_list_manage_connections">Gestionează conexiunile</string>
<string name="account_list_datasaver_enabled">Economizorul de date este activat. Sincronizarea în fundal este restricționată.</string>
<string name="account_list_manage_datasaver">Gestionează economizorul de date</string>
<string name="account_list_battery_saver_enabled">Economisirea bateriei este activată. Sincronizarea poate fi restricționată.</string>
<string name="account_list_manage_battery_saver">Gestionează economisirea bateriei</string>
<string name="account_list_low_storage">Spațiu de depozitare redus. Android nu va sincroniza modificările locale imediat, ci în timpul următoarei sincronizări obișnuite.</string>
<string name="account_list_manage_storage">Gestionează stocarea</string>
<string name="account_list_welcome">Bun venit la DAVx⁵!</string>
<string name="account_list_empty">Conectează-te la server și păstrează calendarele și contactele sincronizate.</string>
<string name="accounts_sync_all">Sincronizează toate conturile</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">Detectarea serviciului a eșuat</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">Lista de colecții nu a putut fi actualizată</string>
<!--Foreground service used by WorkManager on Android <12-->
<string name="foreground_service_notify_title">Rulează în prim-plan</string>
<string name="foreground_service_notify_text">Pe unele dispozitive, acest lucru este necesar pentru sincronizarea automată.</string>
<!--AppSettingsActivity-->
<string name="app_settings">Setări</string>
<string name="app_settings_debug">Depanare</string>
<string name="app_settings_show_debug_info">Afișează informațiile de depanare</string>
<string name="app_settings_show_debug_info_details">Vizualizează/partajează detaliile de configurare și jurnalele</string>
<string name="app_settings_logging">Jurnalizare detaliată</string>
<string name="app_settings_logging_on">Înregistrarea este activă. Poți vizualiza jurnalele ca parte a informațiilor de depanare.</string>
<string name="app_settings_logging_off">Înregistrarea este dezactivată</string>
<string name="app_settings_battery_optimization">Optimizarea bateriei</string>
<string name="app_settings_battery_optimization_exempted">Aplicația este exclusă (recomandat)</string>
<string name="app_settings_battery_optimization_optimized">Se aplică restricții pentru baterie (nu este recomandat)</string>
<string name="app_settings_connection">Conexiune</string>
<string name="app_settings_proxy">Tip proxy</string>
<string-array name="app_settings_proxy_types">
<item>Implicit</item>
<item>Fără proxy</item>
<item>HTTP</item>
<item>SOCKS (pentru Orbot)</item>
</string-array>
<string name="app_settings_proxy_host">Nume gazdă proxy</string>
<string name="app_settings_proxy_port">Port proxy</string>
<string name="app_settings_security">Securitate</string>
<string name="app_settings_security_app_permissions">Permisiunile aplicației</string>
<string name="app_settings_security_app_permissions_summary">Examinează permisiunile necesare pentru sincronizare</string>
<string name="app_settings_distrust_system_certs">Nu avea încredere în certificatele de sistem</string>
<string name="app_settings_distrust_system_certs_on">CA de sistem și de utilizator nu vor fi de încredere</string>
<string name="app_settings_distrust_system_certs_off">CA de sistem și de utilizator vor fi de încredere (recomandat)</string>
<string name="app_settings_reset_certificates">Resetează certificatele de (ne)încredere</string>
<string name="app_settings_reset_certificates_summary">Resetează încrederea tuturor certificatelor personalizate</string>
<string name="app_settings_reset_certificates_success">Toate certificatele personalizate au fost șterse</string>
<string name="app_settings_user_interface">Interfață de utilizator</string>
<string name="app_settings_notification_settings">Setări de notificare</string>
<string name="app_settings_notification_settings_summary">Gestionează canalele de notificare și setările acestora</string>
<string name="app_settings_theme_title">Selectează tema</string>
<string-array name="app_settings_theme_names">
<item>Ca în sistem</item>
<item>Luminoasă</item>
<item>Întunecată</item>
</string-array>
<string name="app_settings_reset_hints">Resetează sugestiile</string>
<string name="app_settings_reset_hints_summary">Reactivează sugestiile care au fost respinse anterior</string>
<string name="app_settings_reset_hints_success">Toate sugestiile vor fi afișate din nou</string>
<string name="app_settings_integration">Integrare</string>
<string name="app_settings_tasks_provider">Aplicația de sarcini</string>
<string name="app_settings_tasks_provider_none">Nu a fost găsită nicio aplicație de sarcini compatibilă</string>
<string name="app_settings_unifiedpush">UnifiedPush (experimental)</string>
<string name="app_settings_unifiedpush_no_endpoint">Niciun punct final configurat</string>
<!--AccountScreen-->
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_missing_permissions">Sunt necesare permisiuni suplimentare pentru a sincroniza aceste colecții.</string>
<string name="account_manage_permissions">Gestionează permisiunile</string>
<string name="account_synchronize_now">Sincronizează acum</string>
<string name="account_settings">Setările contului</string>
<string name="account_rename">Redenumește contul</string>
<string name="account_rename_new_name_description">Datele locale nesalvate pot fi respinse. Resincronizarea este necesară după redenumire.</string>
<string name="account_rename_new_name">Nume cont nou</string>
<string name="account_rename_rename">Redenumește</string>
<string name="account_rename_exists_already">Numele contului este deja luat</string>
<string name="account_rename_couldnt_rename">Nu s-a putut redenumi contul</string>
<string name="account_delete">Șterge contul</string>
<string name="account_delete_confirmation_title">Chiar ștergi contul?</string>
<string name="account_delete_confirmation_text">Toate copiile locale ale agendelor, calendarelor și listelor de sarcini vor fi șterse.</string>
<string name="account_synchronize_this_collection">sincronizează această colecție</string>
<string name="account_read_only">numai pentru citire</string>
<string name="account_calendar">calendar</string>
<string name="account_contacts">contacte</string>
<string name="account_journal">jurnal</string>
<string name="account_task_list">sarcini</string>
<string name="account_only_personal">Afișează numai personal</string>
<string name="account_refresh_collections">Actualizează lista</string>
<string name="account_webcal_external_app">Abonamentele Webcal pot fi sincronizate cu aplicații externe.</string>
<string name="account_no_webcal_handler_found">Nu a fost găsită nicio aplicație compatibilă cu Webcal</string>
<string name="account_install_icsx5">Instalează ICSx⁵</string>
<!--AddAccountActivity-->
<string name="login_title">Adaugă contul</string>
<string name="login_privacy_hint"><![CDATA[Toate datele vor fi transferate numai între server și dispozitiv. %1$s nu le voi trimite altundeva. Vezi <a href="%2$s">Politica de confidențialitate</a>.]]></string>
<string name="login_generic_login">Autentificare generică</string>
<string name="login_provider_login">Autentificare specifică furnizorului</string>
<string name="login_continue">Continuă</string>
<string name="login_login">Autentificare</string>
<string name="login_type_email">Conectează-te cu adresa de e-mail</string>
<string name="login_email_address">Adresa de e-mail</string>
<string name="login_email_address_error">Este necesară o adresă de e-mail validă</string>
<string name="login_email_address_info"><![CDATA[Domeniul de e-mail este folosit ca URL de bază. <a href="%s">Serviciile sunt descoperite</a> folosind înregistrări DNS și adrese URL bine-cunoscute.]]></string>
<string name="login_password">Parolă</string>
<string name="login_password_hide">Ascunde parola</string>
<string name="login_password_show">Afișează parola</string>
<string name="login_password_optional">Parolă*</string>
<string name="login_type_url">Conecteează-te cu adresa URL și numele de utilizator</string>
<string name="login_user_name">Nume de utilizator</string>
<string name="login_user_name_optional">Nume de utilizator*</string>
<string name="login_base_url">Adresa URL de bază</string>
<string name="login_base_url_info"><![CDATA[Adresa URL de bază va fi verificată direct, dar <a href="%s">serviciile sunt de asemenea descoperite</a> folosind înregistrări DNS și adrese URL bine-cunoscute.]]></string>
<string name="login_select_certificate">Selectează certificatul</string>
<string name="login_add_account">Adaugă contul</string>
<string name="login_account_name">Nume de cont</string>
<string name="login_account_avoid_apostrophe">Utilizarea apostrofelor (\') pare să cauzeze probleme pe unele dispozitive.</string>
<string name="login_account_name_info">Utilizează adresa de e-mail ca nume de cont, deoarece Android va folosi numele contului ca câmp ORGANIZATOR pentru evenimentele pe care le creezi. Nu poți avea două conturi cu același nume.</string>
<string name="login_account_contact_group_method">Metoda de grupare a contactelor:</string>
<string name="login_account_name_required">Numele contului este necesar</string>
<string name="login_account_name_already_taken">Numele contului este deja luat</string>
<string name="login_account_not_added">Contul nu a putut fi adăugat</string>
<string name="login_finish">Finalizează</string>
<string name="login_type_advanced">Autentificare avansată</string>
<string name="login_no_client_certificate_optional">Fără certificat de client*</string>
<string name="login_client_certificate_selected">Certificat de client: %s</string>
<string name="login_no_certificate_found">Nu a fost găsit niciun certificat</string>
<string name="login_install_certificate">Instalare certificat</string>
<string name="login_type_google">Contacte Google / Calendar</string>
<string name="login_google_see_tested_with">Consultă pagina noastră „Testat cu Google” pentru informații actualizate.</string>
<string name="login_google_unexpected_warnings">Este posibil să ai avertismente neașteptate și/sau să fii nevoit să creezi propriul ID de client.</string>
<string name="login_google_account">Cont Google</string>
<string name="login_google">Conectează-te cu Google</string>
<string name="login_google_client_id">ID client (opțional)</string>
<string name="login_google_client_privacy_policy"><![CDATA[%1$s transferă datele din Agendă Google și din Calendar numai pentru sincronizare cu acest dispozitiv. Vezi <a href="%2$s">Politica de confidențialitate</a> pentru detalii.]]></string>
<string name="login_google_client_limited_use"><![CDATA[%1$s respectă <a href="%2$s">Politica privind datele utilizatorilor serviciilor API Google</a>, inclusiv cerințele de utilizare limitată.]]></string>
<string name="login_oauth_couldnt_obtain_auth_code">Nu s-a putut obține codul de autorizare</string>
<string name="login_type_nextcloud">Nextcloud</string>
<string name="login_nextcloud_login_with_nextcloud">Conectare cu Nextcloud</string>
<string name="login_nextcloud_login_flow_text">Aceasta va porni fluxul de conectare Nextcloud într-un browser web.</string>
<string name="login_nextcloud_login_flow_server_address">Adresa serverului Nextcloud</string>
<string name="login_nextcloud_login_flow_sign_in">Conectare</string>
<string name="login_nextcloud_login_flow_no_login_url">Nu s-a putut obține adresa URL de conectare</string>
<string name="login_nextcloud_login_flow_no_login_data">Nu s-au putut obține datele de conectare</string>
<string name="login_configuration_detection">Detectarea configurației</string>
<string name="login_querying_server">Se interoghează serverul…</string>
<string name="login_no_service">Nu s-a putut găsi serviciul CalDAV sau CardDAV.</string>
<string name="login_no_service_info">Adresa URL de bază nu pare să fie o adresă URL CalDAV/CardDAV accesibilă, iar detectarea serviciului nu a avut succes.</string>
<string name="login_see_tested_services"><![CDATA[Consultă manualul furnizorului de servicii, <a href="%s">lista de servicii testate</a> și adresele lor URL de bază.]]></string>
<string name="login_check_credentials">Verifică, de asemenea, și autentificarea (de obicei, numele de utilizator și parola).</string>
<string name="login_logs_available">Informații tehnice suplimentare sunt disponibile în jurnale.</string>
<string name="login_view_logs">Vezi jurnalele</string>
<!--AccountSettingsActivity-->
<string name="settings_sync">Sincronizare</string>
<string name="settings_sync_interval_contacts">Interval de sincronizare a contactelor</string>
<string name="settings_sync_summary_manually">Doar manual</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">La fiecare %d minute + imediat la modificări locale</string>
<string name="settings_sync_interval_calendars">Interval de sincronizare a calendarelor</string>
<string name="settings_sync_interval_tasks">Interval de sincronizare a sarcinilor</string>
<string-array name="settings_sync_interval_names">
<item>Doar manual</item>
<item>La fiecare 15 minute</item>
<item>La fiecare 30 de minute</item>
<item>La fiecare oră</item>
<item>La fiecare 2 ore</item>
<item>La fiecare 4 ore</item>
<item>O dată pe zi</item>
</string-array>
<string name="settings_sync_wifi_only">Sincronizare numai prin WiFi</string>
<string name="settings_sync_wifi_only_on">Sincronizarea este limitată la conexiunile WiFi</string>
<string name="settings_sync_wifi_only_off">Tipul de conexiune nu este luat în considerare</string>
<string name="settings_sync_wifi_only_ssids">Restricție SSID WiFi</string>
<string name="settings_sync_wifi_only_ssids_on">Se va sincroniza numai prin %s</string>
<string name="settings_sync_wifi_only_ssids_off">Toate conexiunile WiFi vor fi utilizate</string>
<string name="settings_sync_wifi_only_ssids_message">Nume separate prin virgulă (SSID) ale rețelelor WiFi permise (lasă necompletat pentru toate)</string>
<string name="settings_sync_wifi_only_ssids_permissions_required">Restricția SSID WiFi necesită setări suplimentare</string>
<string name="settings_sync_wifi_only_ssids_permissions_action">Gestionează</string>
<string name="settings_ignore_vpns">VPN necesită internetul de bază</string>
<string name="settings_ignore_vpns_on">VPN fără conexiune validată la Internet nu este suficient pentru a rula sincronizarea (recomandat)</string>
<string name="settings_ignore_vpns_off">VPN fără conexiune validată la Internet este suficient pentru a rula sincronizarea</string>
<string name="settings_authentication">Autentificare</string>
<string name="settings_username">Nume de utilizator</string>
<string name="settings_password">Parolă</string>
<string name="settings_new_password">Parolă nouă</string>
<string name="settings_password_summary">Actualizează parola în funcție de server.</string>
<string name="settings_certificate_alias">Certificat de client</string>
<string name="settings_certificate_alias_empty">Niciun certificat disponibil sau selectat</string>
<string name="settings_certificate_install">Instalare certificat</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Limită de timp pentru evenimentele din trecut</string>
<string name="settings_sync_time_range_past_none">Toate evenimentele vor fi sincronizate</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Evenimentele cu mai mult de o zi în trecut vor fi ignorate</item>
<item quantity="few">Evenimentele cu peste %d zile în trecut vor fi ignorate</item>
<item quantity="other">Evenimentele cu peste %d zile în trecut vor fi ignorate</item>
</plurals>
<string name="settings_sync_time_range_past_message">Evenimentele care depășesc acest număr de zile în trecut vor fi ignorate (poate fi 0). Lasă necompletat pentru a sincroniza toate evenimentele.</string>
<string name="settings_default_alarm">Memento implicit</string>
<plurals name="settings_default_alarm_on">
<item quantity="one">Memento implicit cu un minut înainte de eveniment</item>
<item quantity="few">Memento implicit cu %d minute înainte de eveniment</item>
<item quantity="other">Memento implicit cu %d minute înainte de eveniment</item>
</plurals>
<string name="settings_default_alarm_off">Nu sunt create mementouri implicite</string>
<string name="settings_default_alarm_message">Dacă vor fi create memento-uri implicite pentru evenimente fără memento: numărul dorit de minute înainte de eveniment. Lasă necompletat pentru a dezactiva memento-urile implicite.</string>
<string name="settings_manage_calendar_colors">Gestionează culorile calendarului</string>
<string name="settings_manage_calendar_colors_on">Culorile calendarului sunt resetate la fiecare sincronizare</string>
<string name="settings_manage_calendar_colors_off">Culorile calendarului pot fi setate de alte aplicații</string>
<string name="settings_event_colors">Suport pentru culoarea evenimentului</string>
<string name="settings_event_colors_on">Culorile evenimentelor sunt sincronizate</string>
<string name="settings_event_colors_off">Culorile evenimentelor nu sunt sincronizate</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Metoda de grupare a contactelor</string>
<string-array name="settings_contact_group_method_entries">
<item>Grupurile sunt vCard-uri separate</item>
<item>Grupurile sunt categorii per-contact</item>
</string-array>
<!--CreateAddressBookScreen, CreateCalendarScreen-->
<string name="create_addressbook">Creează agendă de adrese</string>
<string name="create_addressbook_maybe_not_supported">Crearea agendei prin CardDAV poate să nu fie acceptată de server.</string>
<string name="create_calendar">Creează un calendar</string>
<string name="create_calendar_time_zone_optional">Fus orar implicit*</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">Posibile intrări din calendar</string>
<string name="create_calendar_type_vevent">Evenimente</string>
<string name="create_calendar_type_vtodo">Sarcini</string>
<string name="create_calendar_type_vjournal">Note/jurnal</string>
<string name="create_calendar_maybe_not_supported">Crearea calendarului prin CalDAV poate să nu fie acceptată de server.</string>
<string name="create_collection_color">Culoare</string>
<string name="create_collection_display_name">Titlu</string>
<string name="create_collection_home_set">Locația de stocare</string>
<string name="create_collection_description_optional">Descriere*</string>
<string name="create_collection_create">Crează</string>
<string name="create_collection_optional">* opțional</string>
<!--CollectionScreen-->
<string name="collection_delete">Șterge colecția</string>
<string name="collection_delete_warning">Această colecție (%s) și toate datele sale vor fi șterse definitiv, atât local, cât și de pe server.</string>
<string name="collection_synchronization">Sincronizare</string>
<string name="collection_synchronization_on">Sincronizarea este activată</string>
<string name="collection_synchronization_off">Sincronizarea este dezactivată</string>
<string name="collection_read_only">Numai citire</string>
<string name="collection_read_only_by_server">Numai citire (de pe server)</string>
<string name="collection_read_only_by_setting">Numai citire (după politică)</string>
<string name="collection_read_only_forced">Numai citire (doar local)</string>
<string name="collection_read_write">Citire/scriere</string>
<string name="collection_title">Titlu</string>
<string name="collection_description">Descriere</string>
<string name="collection_owner">Proprietar</string>
<string name="collection_push_support">Suport Push</string>
<string name="collection_push_web_push">Serverul informează despre suportul Push</string>
<string name="collection_push_subscribed_at">Abonat la %1$s, expiră la %2$s</string>
<string name="collection_last_sync">Ultima sincronizare (%s)</string>
<string name="collection_url">Adresă (URL)</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_title">Informații de depanare</string>
<string name="debug_info_archive_caption">Arhivă ZIP</string>
<string name="debug_info_archive_subtitle">Conține informații de depanare și jurnale</string>
<string name="debug_info_archive_text">Partajează arhiva pentru a o transfera pe un computer, pentru a o trimite prin e-mail sau pentru a o atașa la un bilet de asistență.</string>
<string name="debug_info_archive_share">Partajează arhiva</string>
<string name="debug_info_attached">Informații de depanare atașate la acest mesaj (necesită suport pentru atașamentele aplicației care primește).</string>
<string name="debug_info_http_error">Eroare HTTP</string>
<string name="debug_info_server_error">Eroare de server</string>
<string name="debug_info_webdav_error">Eroare WebDAV</string>
<string name="debug_info_io_error">Eroare I/O</string>
<string name="debug_info_http_403_description">Solicitarea a fost respinsă. Verifică resursele implicate și informațiile de depanare pentru detalii.</string>
<string name="debug_info_http_404_description">Resursa solicitată nu mai există (mai mult). Verifică resursele implicate și informațiile de depanare pentru detalii.</string>
<string name="debug_info_http_5xx_description">A apărut o problemă la nivelul serverului. Contactează asistența serverului.</string>
<string name="debug_info_unexpected_error">A apărut o eroare neașteptată. Vezi informațiile de depanare pentru detalii.</string>
<string name="debug_info_view_details">Vezi detaliile</string>
<string name="debug_info_subtitle">Au fost colectate informații de depanare</string>
<string name="debug_info_involved_caption">Resurse implicate</string>
<string name="debug_info_involved_subtitle">Legat de problema</string>
<string name="debug_info_involved_remote">Resursa de la distanță:</string>
<string name="debug_info_involved_local">Resursa locală:</string>
<string name="debug_info_logs_caption">Jurnale</string>
<string name="debug_info_logs_subtitle">Jurnalele detaliate sunt disponibile</string>
<string name="debug_info_logs_view">Vezi jurnalele</string>
<!--ExceptionInfoFragment-->
<string name="exception">A avut loc o eroare.</string>
<string name="exception_httpexception">A apărut o eroare HTTP.</string>
<string name="exception_ioexception">A apărut o eroare I/O.</string>
<string name="exception_show_details">Afișează detaliile</string>
<!--WebDAV accounts-->
<string name="webdav_mounts_title">Montări WebDAV</string>
<string name="webdav_mounts_quota_used_available">Cotă utilizată: %1$s / disponibilă: %2$s</string>
<string name="webdav_mounts_share_content">Partajează conținutul</string>
<string name="webdav_mounts_unmount">Demontează</string>
<string name="webdav_add_mount_title">Adaugă o montare WebDAV</string>
<string name="webdav_mounts_empty">Accesează direct fișierele din cloud adăugând o montare WebDAV!</string>
<string name="webdav_add_mount_empty_more_info"><![CDATA[Vezi manualul pentru a afla <a href="%1$s">cum funcționează montările WebDAV</a>.</string>]]></string>
<string name="webdav_add_mount_display_name">Numele afișat</string>
<string name="webdav_add_mount_url">URL WebDAV</string>
<string name="webdav_add_mount_url_invalid">URL greșit</string>
<string name="webdav_add_mount_authentication">Autentificare (opțional)</string>
<string name="webdav_add_mount_username">Nume de utilizator</string>
<string name="webdav_add_mount_password">Parolă</string>
<string name="webdav_add_mount_add">Adaugă montare</string>
<string name="webdav_add_mount_no_support">Niciun serviciu WebDAV la această adresă URL</string>
<string name="webdav_remove_mount_title">Elimină punctul de montare</string>
<string name="webdav_remove_mount_text">Detaliile conexiunii se vor pierde, dar niciun fișier nu va fi șters.</string>
<string name="webdav_notification_access">Se accesează fișierul WebDAV</string>
<string name="webdav_notification_download">Se descarcă fișierul WebDAV</string>
<string name="webdav_notification_upload">Se actualizează fișierul WebDAV</string>
<string name="webdav_provider_root_title">Montare WebDAV</string>
<!--sync-->
<string name="sync_error_permissions">Permisiuni DAVx⁵</string>
<string name="sync_error_permissions_text">Sunt necesare permisiuni suplimentare</string>
<string name="sync_error_tasks_too_old">%s prea vechi</string>
<string name="sync_error_tasks_required_version">Versiunea minimă necesară: %1$s</string>
<string name="sync_error_authentication_failed">Autentificare eșuată (verifică datele de conectare)</string>
<string name="sync_error_io">Eroare de rețea sau I/O %s</string>
<string name="sync_error_http_dav">Eroare de server HTTP %s</string>
<string name="sync_error_local_storage">Eroare de stocare locală %s</string>
<string name="sync_error_retry_limit_reached">Eroare soft (încercări maxime atinse)</string>
<string name="sync_error_view_item">Vezi elementul</string>
<string name="sync_invalid_contact">S-a primit contact nevalid de la server</string>
<string name="sync_invalid_event">S-a primit eveniment nevalid de la server</string>
<string name="sync_invalid_task">S-a primit sarcină nevalidă de la server</string>
<string name="sync_invalid_resources_ignoring">Ignorarea uneia sau mai multor resurse nevalide</string>
<string name="sync_notification_pending_push_title">Sincronizare în așteptare</string>
<string name="sync_notification_pending_push_message">Datele de la distanță s-au schimbat</string>
<!--widgets-->
<string name="widget_sync_all">Sincronizează tot</string>
<string name="widget_sync_all_accounts">Sincronizează toate conturile</string>
<!--cert4android-->
</resources>

View File

@@ -1,212 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="account_invalid">帳號(已)不存在</string>
<string name="account_title_address_book">DAVx⁵ 通訊錄</string>
<string name="field_required">此為必填欄位</string>
<string name="help">幫助</string>
<string name="share">分享</string>
<string name="database_destructive_migration_title">資料庫損毀</string>
<string name="database_destructive_migration_text">所有帳號已在本地刪除</string>
<string name="notification_channel_debugging">除錯</string>
<string name="notification_channel_general">其他重要訊息</string>
<string name="notification_channel_status">低優先的狀態訊息</string>
<string name="notification_channel_sync">同步</string>
<string name="notification_channel_sync_errors">同步錯誤</string>
<string name="notification_channel_sync_errors_desc">導致同步停止的嚴重錯誤,如異常的伺服器回應</string>
<string name="notification_channel_sync_warnings">同步警告</string>
<string name="notification_channel_sync_warnings_desc">可忽略的同步問題,比如一些無效檔案</string>
<string name="notification_channel_sync_io_errors">網路和輸入輸出錯誤</string>
<string name="notification_channel_sync_io_errors_desc">逾時、連線問題等等(通常為暫時性)</string>
<!--IntroActivity-->
<string name="intro_slogan1">您的資料,您的選擇</string>
<string name="intro_slogan2">權力在握</string>
<string name="intro_battery_title">定期同步間隔</string>
<string name="intro_battery_text">為了定期進行同步,必須允許 %s 在背景運行,否則 Android 可能會隨時暫停同步。</string>
<string name="intro_battery_dont_show">我不需要定期同步間隔*</string>
<string name="intro_autostart_title">%s 相容性</string>
<string name="intro_autostart_text">該裝置可能阻擋了同步,若您受到影響,只能手動解決。</string>
<string name="intro_autostart_dont_show">所需設定已完成,不用再提醒我*</string>
<string name="intro_leave_unchecked">* 取消勾選則稍後會再次提醒,可於設定中重置 / %s</string>
<string name="intro_more_info">更多資訊</string>
<string name="intro_tasks_jtx_info"><![CDATA[支持任務、日記及筆記同步]]></string>
<string name="intro_tasks_title">待辦事項支援</string>
<string name="intro_tasks_text1">如果你的服務器支持任務它們可以與支援任務的app同步:</string>
<string name="intro_tasks_no_app_store">沒有應用商店可用</string>
<!--PermissionsActivity-->
<string name="permissions_contacts_title">通訊錄權限</string>
<string name="permissions_calendar_title">行事曆權限</string>
<string name="permissions_opentasks_title">OpenTasks 權限</string>
<!--WifiPermissionsActivity-->
<!--AboutActivity-->
<string name="about_libraries">函式庫</string>
<string name="about_version">版本號 %1$s%2$d</string>
<string name="about_license_info_no_warranty">我們「完全不保證」本程式無瑕疵。這是個自由軟體,歡迎您在符合公用授權條款的情況下任意散布它。</string>
<!--global settings-->
<string name="logging_couldnt_create_file">無法創建事項記錄文檔</string>
<!--AccountsScreen-->
<string name="navigation_drawer_subtitle">CalDAV/CardDAV 同步器</string>
<string name="navigation_drawer_about">關於我們 / 授權條款</string>
<string name="navigation_drawer_beta_feedback">為測試版本給回饋意見</string>
<string name="install_browser">請安裝一個瀏覽器程式</string>
<string name="navigation_drawer_settings">設定</string>
<string name="navigation_drawer_news_updates">新聞 &amp; 更新</string>
<string name="navigation_drawer_external_links">外部連結</string>
<string name="navigation_drawer_website">我們的網站</string>
<string name="navigation_drawer_manual">使用説明書</string>
<string name="navigation_drawer_faq">常見問答</string>
<string name="navigation_drawer_privacy_policy">隱私權政策</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">未發現遠端服務</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">無法更新清單</string>
<!--Foreground service used by WorkManager on Android <12-->
<!--AppSettingsActivity-->
<string name="app_settings">設定</string>
<string name="app_settings_debug">除錯</string>
<string name="app_settings_show_debug_info">顯示除錯訊息</string>
<string name="app_settings_logging">詳細除錯記錄</string>
<string name="app_settings_logging_off">日誌記錄已停用</string>
<string name="app_settings_battery_optimization">電池最佳化</string>
<string name="app_settings_connection">網路連線</string>
<string name="app_settings_security">安全性</string>
<string name="app_settings_distrust_system_certs">不信任系統憑證</string>
<string name="app_settings_distrust_system_certs_on">系統憑證和使用者自訂憑證將不被信任</string>
<string name="app_settings_distrust_system_certs_off">系統憑證和使用者自訂憑證將被信任 (推薦設定)</string>
<string name="app_settings_reset_certificates">重新開啟之前關閉的提示</string>
<string name="app_settings_reset_certificates_summary">重設對所有自訂憑證的信任</string>
<string name="app_settings_reset_certificates_success">所有自訂憑證已清除</string>
<string name="app_settings_user_interface">使用介面</string>
<string name="app_settings_notification_settings">通知設定</string>
<string name="app_settings_notification_settings_summary">管理通知頻道和設定</string>
<string name="app_settings_reset_hints">重新開啟提示</string>
<string name="app_settings_reset_hints_summary">重新啟用之前取消的提示</string>
<string name="app_settings_reset_hints_success">所有提示將再次顯示</string>
<!--AccountScreen-->
<string name="account_carddav">CardDAV聯絡人檔案</string>
<string name="account_caldav">CalDav行事曆檔案</string>
<string name="account_webcal">Webcal網際網絡行事曆</string>
<string name="account_synchronize_now">立即同步</string>
<string name="account_settings">帳號設定</string>
<string name="account_rename">重新命名帳號</string>
<string name="account_rename_rename">重新命名</string>
<string name="account_rename_exists_already">這個賬號名稱已經被取過了</string>
<string name="account_rename_couldnt_rename">無法重新命名帳號</string>
<string name="account_delete">刪除帳號</string>
<string name="account_delete_confirmation_title">確定要刪除帳號?</string>
<string name="account_delete_confirmation_text">這台裝置上這個帳號的通訊錄、行事曆和工作清單將被刪除。</string>
<string name="account_synchronize_this_collection">同步這個行事曆或工作清單</string>
<string name="account_read_only">唯讀</string>
<string name="account_calendar">行事曆</string>
<string name="account_only_personal">只顯示個人</string>
<string name="account_no_webcal_handler_found">未找到支援Webcal的APP</string>
<string name="account_install_icsx5">安裝ICSx⁵</string>
<!--AddAccountActivity-->
<string name="login_title">新增帳號</string>
<string name="login_login">登入</string>
<string name="login_type_email">用 Email 地址登入</string>
<string name="login_email_address">Email 地址</string>
<string name="login_email_address_error">請輸入有效的 Email 地址</string>
<string name="login_password">密碼</string>
<string name="login_type_url">用網址和帳號登入</string>
<string name="login_user_name">使用者帳號</string>
<string name="login_base_url">根 URL</string>
<string name="login_select_certificate">點選憑證</string>
<string name="login_add_account">新增帳號</string>
<string name="login_account_name">帳號名稱</string>
<string name="login_account_name_info">使用 Email 地址當作裝置上的帳號顯示名稱因為當您在行事曆創建活動時Android 會把帳號顯示名稱放到「活動發起人」欄位。兩個帳號不能有相同的名稱。</string>
<string name="login_account_contact_group_method">聯絡人群組的儲存格式</string>
<string name="login_account_name_required">需要帳號名稱</string>
<string name="login_account_name_already_taken">這個賬號名稱已經被取過了</string>
<string name="login_configuration_detection">設定錯誤</string>
<string name="login_querying_server">請稍待,正在詢問伺服器…</string>
<string name="login_no_service">找不到 CalDAV 或 CardDAV 服務。</string>
<!--AccountSettingsActivity-->
<string name="settings_sync">同步設定</string>
<string name="settings_sync_interval_contacts">聯絡人同步間隔</string>
<string name="settings_sync_summary_manually">只手動同步</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">每 %d 分鐘,以及在本裝置上修改時</string>
<string name="settings_sync_interval_calendars">行事曆同步間隔</string>
<string name="settings_sync_interval_tasks">待辦事項同步間隔</string>
<string-array name="settings_sync_interval_names">
<item>僅手動</item>
<item>每15分鐘自動</item>
<item>每30分鐘自動</item>
<item>每小時自動</item>
<item>每2小時自動</item>
<item>每4小時自動</item>
<item>每天自動</item>
</string-array>
<string name="settings_sync_wifi_only">只用 WiFi 同步</string>
<string name="settings_sync_wifi_only_on">只於 WiFi 連線時同步</string>
<string name="settings_sync_wifi_only_off">任何網路連線都可使用</string>
<string name="settings_sync_wifi_only_ssids">限用特定 WiFi SSID</string>
<string name="settings_sync_wifi_only_ssids_on">只在%s連線時同步</string>
<string name="settings_sync_wifi_only_ssids_off">所有 WiFi 連線都可以使用</string>
<string name="settings_sync_wifi_only_ssids_message">使用逗號分割的名稱 (SSIDs) 表示的 WiFi 連線(留空則代表全部)</string>
<string name="settings_sync_wifi_only_ssids_permissions_action">管理</string>
<string name="settings_authentication">認證</string>
<string name="settings_username">使用者帳號</string>
<string name="settings_password">密碼</string>
<string name="settings_password_summary">您在伺服器上使用中的密碼</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">過去活動的時間限制</string>
<string name="settings_sync_time_range_past_none">將會同步所有活動</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="other">%d 天之前的活動會被忽略</item>
</plurals>
<string name="settings_sync_time_range_past_message">此天數前的活動將會被忽略(可設為零),若留空則同步所有活動</string>
<string name="settings_default_alarm">預設提醒</string>
<plurals name="settings_default_alarm_on">
<item quantity="other">預設在活動前 %d 分鐘提醒</item>
</plurals>
<string name="settings_default_alarm_off">未設定預設提醒</string>
<string name="settings_default_alarm_message">當沒有提醒的活動需要加入預設提醒時,活動開始前多少分鐘出發提醒。留空則停用預設提醒。</string>
<string name="settings_manage_calendar_colors">管理行事曆的顏色</string>
<string name="settings_event_colors">設定活動的顔色</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">聯絡人群組的儲存格式</string>
<string-array name="settings_contact_group_method_entries">
<item>群組存成額外的 VCard 檔案</item>
<item>群組存成每個聯絡人的分類屬性</item>
</string-array>
<!--CreateAddressBookScreen, CreateCalendarScreen-->
<string name="create_addressbook">建立通訊錄</string>
<string name="create_calendar">建立行事曆</string>
<string name="create_calendar_type">可使用的行事曆項目</string>
<string name="create_calendar_type_vevent">活動</string>
<string name="create_calendar_type_vtodo">事務</string>
<string name="create_calendar_type_vjournal">筆記/日誌</string>
<string name="create_collection_color">顔色</string>
<string name="create_collection_display_name">標題</string>
<string name="create_collection_home_set">存儲位置</string>
<string name="create_collection_create">建立</string>
<!--CollectionScreen-->
<string name="collection_delete">刪除行事曆或工作清單</string>
<string name="collection_synchronization">同步</string>
<string name="collection_title">標題</string>
<string name="collection_description">描述</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_title">除錯訊息</string>
<!--ExceptionInfoFragment-->
<string name="exception">發生錯誤</string>
<string name="exception_httpexception">HTTP 發生錯誤</string>
<string name="exception_ioexception">讀寫錯誤</string>
<string name="exception_show_details">顯示細節</string>
<!--WebDAV accounts-->
<string name="webdav_add_mount_username">使用者帳號</string>
<string name="webdav_add_mount_password">密碼</string>
<!--sync-->
<string name="sync_error_permissions">DAVx⁵ 權限</string>
<string name="sync_error_permissions_text">需要額外的權限</string>
<string name="sync_error_authentication_failed">鑒權失敗(你需要檢查登錄憑證)</string>
<string name="sync_error_io">網際網絡或者輸入輸出錯誤——%s</string>
<string name="sync_error_http_dav">HTTP伺服器錯誤——%s</string>
<string name="sync_error_local_storage">資料庫錯誤——%s</string>
<string name="sync_error_view_item">查閲項目</string>
<string name="sync_invalid_contact">收到了無效的聯絡人</string>
<string name="sync_invalid_event">收到了無效的事件</string>
<string name="sync_invalid_task">收到了無效的任務</string>
<string name="sync_invalid_resources_ignoring">略過了一個或多個無效的資料</string>
<!--widgets-->
<!--cert4android-->
</resources>

View File

@@ -1,451 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="account_invalid">账户(已)不存在</string>
<string name="account_title_address_book">DAVx⁵ 通讯录</string>
<string name="account_prefs_use_app">别在这里更改账户!请直接使用应用管理账户。</string>
<string name="dialog_delete">删除</string>
<string name="dialog_remove">删除</string>
<string name="dialog_deny">取消</string>
<string name="field_required">此字段是必填项</string>
<string name="help">帮助</string>
<string name="navigate_up">向上导航</string>
<string name="optional_label">* 可选</string>
<string name="options_menu">选项菜单</string>
<string name="share">分享</string>
<string name="sync_started">同步已启动/已加入队列</string>
<string name="database_destructive_migration_title">数据库损坏</string>
<string name="database_destructive_migration_text">所有帐户已在本地删除。</string>
<string name="notification_channel_debugging">调试</string>
<string name="notification_channel_general">其它重要消息</string>
<string name="notification_channel_status">低优先级状态消息</string>
<string name="notification_channel_sync">同步</string>
<string name="notification_channel_sync_errors">同步错误</string>
<string name="notification_channel_sync_errors_desc">导致同步停止的重要错误,如异常的服务器响应</string>
<string name="notification_channel_sync_warnings">同步警告</string>
<string name="notification_channel_sync_warnings_desc">不重要的同步问题,如某文件无效</string>
<string name="notification_channel_sync_io_errors">网络或 I/O 错误</string>
<string name="notification_channel_sync_io_errors_desc">超时、连接异常等问题(通常是临时错误)</string>
<!--IntroActivity-->
<string name="intro_slogan1">您的数据。您的选择。</string>
<string name="intro_slogan2">获得控制。</string>
<string name="intro_battery_title">定期同步间隔</string>
<string name="intro_battery_text">为了定期进行同步,必须允许%s在后台运行。否则Android可能会随时暂停同步。</string>
<string name="intro_battery_dont_show">我不需要定期的同步。*</string>
<string name="intro_autostart_title">%s兼容性</string>
<string name="intro_autostart_text">该设备可能会阻止同步。如果您受到影响,则只能手动解决。</string>
<string name="intro_autostart_dont_show">我已完成所需的设置。不再提醒我。*</string>
<string name="intro_leave_unchecked">*取消选中以供稍后提醒。可以在应用设置中重置/%s。</string>
<string name="intro_more_info">更多信息</string>
<string name="intro_tasks_jtx">jtx Board</string>
<string name="intro_tasks_jtx_info"><![CDATA[支持任务、日记和笔记同步]]></string>
<string name="intro_tasks_title">任务支持</string>
<string name="intro_tasks_text1">如果你的服务器支持任务,它们可以通过一个受支持的任务应用进行同步:</string>
<string name="intro_tasks_opentasks">OpenTasks </string>
<string name="intro_tasks_opentasks_info">似乎已不再开发 — 不推荐</string>
<string name="intro_tasks_tasks_org">Tasks.org</string>
<string name="intro_tasks_tasks_org_info"><![CDATA[某些功能 <a href="https://www.davx5.com/faq/tasks/advanced-task-features">不被支持</a>。]]></string>
<string name="intro_tasks_no_app_store">没有可用的应用商店</string>
<string name="intro_tasks_dont_show">我不需要任务支持。*</string>
<string name="intro_open_source_title">开源软件</string>
<string name="intro_open_source_text">我们很高兴您使用 %s 开源软件。开发、维护和支持是艰苦的工作。请考虑通过多种方式提供贡献或捐款。不胜感激!</string>
<string name="intro_open_source_details">如何贡献或捐款</string>
<string name="intro_open_source_dont_show">以后不需要显示</string>
<string name="intro_next">继续</string>
<!--PermissionsActivity-->
<string name="permissions_title">权限</string>
<string name="permissions_text">%s需要权限才能正常工作</string>
<string name="permissions_all_title">以下所有</string>
<string name="permissions_all_status_off">使用它来启用所有特性 (推荐)</string>
<string name="permissions_all_status_on">已授予全部权限</string>
<string name="permissions_contacts_title">联系人权限</string>
<string name="permissions_contacts_status_off">无联系人同步(不推荐)</string>
<string name="permissions_contacts_status_on">可同步联系人</string>
<string name="permissions_calendar_title">日历权限</string>
<string name="permissions_calendar_status_off">无日历同步(不推荐)</string>
<string name="permissions_calendar_status_on">可同步日历</string>
<string name="permissions_notification_title">通知权限</string>
<string name="permissions_notification_status_off">已禁用通知(不推荐)</string>
<string name="permissions_notification_status_on">已启用通知</string>
<string name="permissions_jtx_title">jtx Board 权限</string>
<string name="permissions_opentasks_title">OpenTasks权限</string>
<string name="permissions_tasksorg_title">Tasks权限</string>
<string name="permissions_tasks_status_off">无任务同步</string>
<string name="permissions_tasks_status_on">可同步任务</string>
<string name="permissions_autoreset_title">保留权限</string>
<string name="permissions_autoreset_status_off">权限可能被自动重置(不推荐)</string>
<string name="permissions_autoreset_status_on">权限不会被自动重置</string>
<string name="permissions_autoreset_instruction">点击权限 &gt; 取消选择 “移除权限,如果应用未使用”</string>
<string name="permissions_app_settings_hint">如果切换没有正常工作,请使用应用程序设置/权限</string>
<string name="permissions_app_settings">应用设置</string>
<!--WifiPermissionsActivity-->
<string name="wifi_permissions_label">WiFi SSID权限</string>
<string name="wifi_permissions_intro">要访问当前的WiFi名称(SSID),必须满足以下条件: </string>
<string name="wifi_permissions_location_permission">精确位置权限</string>
<string name="wifi_permissions_location_permission_on">已授予位置权限</string>
<string name="wifi_permissions_location_permission_off">位置权限被拒</string>
<string name="wifi_permissions_background_location_permission">后台位置权限</string>
<string name="wifi_permissions_background_location_permission_label">始终允许</string>
<string name="wifi_permissions_background_location_permission_on">位置权限已设为:%s</string>
<string name="wifi_permissions_background_location_permission_off">位置权限未设为:%s</string>
<string name="wifi_permissions_background_location_disclaimer">%s 使用位置数据 (仅 WiFi SSID) 的目的只是为了将同步限制到特定的 WiFi SSID。即使当同步在后台运行时这也会发生。</string>
<string name="wifi_permissions_background_location_disclaimer2">所有位置数据(仅 WiFi SSID)只在本地使用,不会被发送到任何地方。</string>
<string name="wifi_permissions_location_enabled">始终允许定位</string>
<string name="wifi_permissions_location_enabled_on">位置服务已启用</string>
<string name="wifi_permissions_location_enabled_off">位置服务已禁用</string>
<!--AboutActivity-->
<string name="about_translations">翻译</string>
<string name="about_libraries">程序库</string>
<string name="about_version">版本 %1$s (%2$d)</string>
<string name="about_copyright">©Ricki Hirner, Bernhard Stockmann (bitfire web engineering GmbH) 及贡献者</string>
<string name="about_license_info_no_warranty">本程序不附带任何担保。这是一款自由软件,你可以有条件地传播它。</string>
<!--global settings-->
<string name="logging_couldnt_create_file">无法创建日志文件</string>
<string name="logging_notification_text">正记录%s的所有活动</string>
<string name="logging_notification_view_share">查看/分享</string>
<string name="logging_notification_disable">禁用</string>
<!--AccountsScreen-->
<string name="navigation_drawer_subtitle">CalDAV/CardDAV 同步器</string>
<string name="navigation_drawer_about">关于 / 许可</string>
<string name="navigation_drawer_beta_feedback">测试版反馈</string>
<string name="install_browser">请安装网页浏览器</string>
<string name="navigation_drawer_settings">设置</string>
<string name="navigation_drawer_news_updates">最新消息</string>
<string name="navigation_drawer_tools">工具</string>
<string name="navigation_drawer_external_links">外部链接</string>
<string name="navigation_drawer_website">应用网站</string>
<string name="navigation_drawer_manual">手册</string>
<string name="navigation_drawer_faq">常见问题</string>
<string name="navigation_drawer_community">社区</string>
<string name="navigation_drawer_support_project">支持项目</string>
<string name="navigation_drawer_contribute">如何作贡献</string>
<string name="navigation_drawer_privacy_policy">隐私政策</string>
<string name="account_list_no_notification_permission">已禁用通知。你将不会收到同步出错的通知</string>
<string name="account_list_no_internet">自动同步不活跃(无已验证的互联网连接)</string>
<string name="account_list_manage_connections">管理连接</string>
<string name="account_list_datasaver_enabled">启用了流量节省程序。后台同步受限</string>
<string name="account_list_manage_datasaver">管理流量节省程序</string>
<string name="account_list_battery_saver_enabled">启用了节电程序。同步可能受限。</string>
<string name="account_list_manage_battery_saver">管理节电程序</string>
<string name="account_list_low_storage">低存储空间。Android 不会立即同步本地更改,但会在下次定期同步时进行</string>
<string name="account_list_manage_storage">管理存储</string>
<string name="account_list_calendar_storage_disabled">缺少日历提供程序。你停用了“日历存储”系统应用吗?</string>
<string name="account_list_contacts_storage_disabled">缺少联系人提供程序。你停用了“联系人存储”系统应用吗?</string>
<string name="account_list_manage_apps">管理应用</string>
<string name="account_list_welcome">欢迎来到 DAVx⁵</string>
<string name="account_list_empty">连接到你的服务器,保持日历和联系人同步</string>
<string name="accounts_sync_all">同步所有账户</string>
<!--RefreshCollectionsWorker-->
<string name="refresh_collections_worker_refresh_failed">服务配置检测失败</string>
<string name="refresh_collections_worker_refresh_couldnt_refresh">无法刷新集合列表</string>
<!--Foreground service used by WorkManager on Android <12-->
<string name="foreground_service_notify_title">运行于前台</string>
<string name="foreground_service_notify_text">在某些设备上,这是自动同步所必需的。 </string>
<!--AppSettingsActivity-->
<string name="app_settings">设置</string>
<string name="app_settings_debug">调试</string>
<string name="app_settings_show_debug_info">显示调试信息</string>
<string name="app_settings_show_debug_info_details">查看/分享配置详情和日志</string>
<string name="app_settings_logging">记录完整日志</string>
<string name="app_settings_logging_on">日志记录处于活跃状态。你可以将日志作为调试信息的一部分来查看</string>
<string name="app_settings_logging_off">日志记录已禁用</string>
<string name="app_settings_battery_optimization">电池优化</string>
<string name="app_settings_battery_optimization_exempted">排除本应用(推荐)</string>
<string name="app_settings_battery_optimization_optimized">施加电池限制(不推荐)</string>
<string name="app_settings_connection">连接</string>
<string name="app_settings_proxy">代理类型</string>
<string-array name="app_settings_proxy_types">
<item>系统默认</item>
<item>无代理</item>
<item>HTTP</item>
<item>SOCKS (用于 Orbot)</item>
</string-array>
<string name="app_settings_proxy_host">代理主机名称</string>
<string name="app_settings_proxy_port">代理端口</string>
<string name="app_settings_security">安全</string>
<string name="app_settings_security_app_permissions">应用权限</string>
<string name="app_settings_security_app_permissions_summary">查看同步所需权限</string>
<string name="app_settings_distrust_system_certs">不信任系统证书</string>
<string name="app_settings_distrust_system_certs_on">系统和用户增加的发布者不会被信任</string>
<string name="app_settings_distrust_system_certs_off">系统和用户增加的发布者会被信任(推荐)</string>
<string name="app_settings_reset_certificates">重设证书信任状态</string>
<string name="app_settings_reset_certificates_summary">重设所有自定义证书的信任状态</string>
<string name="app_settings_reset_certificates_success">所有自定义证书已清除</string>
<string name="app_settings_user_interface">用户界面</string>
<string name="app_settings_notification_settings">通知设置</string>
<string name="app_settings_notification_settings_summary">管理通知渠道等设置</string>
<string name="app_settings_theme_title">选择主题</string>
<string-array name="app_settings_theme_names">
<item>系统默认</item>
<item>浅色</item>
<item>深色</item>
</string-array>
<string name="app_settings_reset_hints">重设提示</string>
<string name="app_settings_reset_hints_summary">重新显示之前忽略过的提示</string>
<string name="app_settings_reset_hints_success">所有提示将会再次显示</string>
<string name="app_settings_integration">集成</string>
<string name="app_settings_tasks_provider">Tasks 应用</string>
<string name="app_settings_tasks_provider_none">未找到兼容的任务应用</string>
<string name="app_settings_unifiedpush">UnifiedPush (实验性)</string>
<string name="app_settings_unifiedpush_disable">无(停用推送)</string>
<string name="app_settings_unifiedpush_choose_distributor">选择分发程序</string>
<string name="app_settings_unifiedpush_no_distributor">未安装推送分发程序</string>
<string name="app_settings_unifiedpush_no_endpoint">未配置端点</string>
<string name="app_settings_unifiedpush_ready">准备好通过 %s 接收推送消息</string>
<!--AccountScreen-->
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_missing_permissions">需要额外权限来同步这些集合</string>
<string name="account_manage_permissions">管理权限</string>
<string name="account_synchronize_now"> 立即同步</string>
<string name="account_settings">账户设置</string>
<string name="account_rename">重命名账户</string>
<string name="account_rename_new_name_description">未保存的本地数据可能会消失。重命名后需要重新同步。</string>
<string name="account_rename_new_name">新账户名</string>
<string name="account_rename_rename">重命名</string>
<string name="account_rename_exists_already">账户名已被占用</string>
<string name="account_rename_couldnt_rename">无法重命名账户</string>
<string name="account_delete">删除账户</string>
<string name="account_delete_confirmation_title">真的要删除账户吗?</string>
<string name="account_delete_confirmation_text">所有通讯录、日历和任务列表的本机存储将被删除。</string>
<string name="account_synchronize_this_collection">同步该集合</string>
<string name="account_read_only">只读</string>
<string name="account_calendar">日历</string>
<string name="account_contacts">联系人</string>
<string name="account_journal">日记</string>
<string name="account_task_list">任务</string>
<string name="account_only_personal">只显示个人</string>
<string name="account_refresh_collections">刷新列表</string>
<string name="account_webcal_external_app">可以用外部应用来同步 Webcal 订阅</string>
<string name="account_no_webcal_handler_found">找不到支持 Webcal 的应用</string>
<string name="account_install_icsx5">安装 ICSx⁵</string>
<!--AddAccountActivity-->
<string name="login_title">增加账户</string>
<string name="login_privacy_hint"><![CDATA[所有数据只会在你的服务器和设备之间传输。%1$s不会把它们发送到任何其他地方。 参见 <a href="%2$s">隐私政策</a>。]]></string>
<string name="login_generic_login">常规登录</string>
<string name="login_provider_login">特定服务商的登录</string>
<string name="login_continue">继续</string>
<string name="login_login">登录</string>
<string name="login_type_email">使用邮箱地址登录</string>
<string name="login_email_address">Email 地址</string>
<string name="login_email_address_error">请输入有效 Email 地址</string>
<string name="login_email_address_info"><![CDATA[该邮件域被用作基URL。<a href="%s">服务发现</a> 通过 DNS 记录和已知URLs 进行。]]></string>
<string name="login_password">密码</string>
<string name="login_password_hide">隐藏密码</string>
<string name="login_password_show">显示密码</string>
<string name="login_password_optional">密码*</string>
<string name="login_type_url">使用 URL 和用户名登录</string>
<string name="login_user_name">用户名</string>
<string name="login_user_name_optional">用户名*</string>
<string name="login_base_url">根地址</string>
<string name="login_base_url_info"><![CDATA[此基URL将被直接检查但 <a href="%s">服务发现也将</a>使用 DNS 记录 和已知 URLs 进行。]]></string>
<string name="login_select_certificate">选择证书</string>
<string name="login_add_account">增加账户</string>
<string name="login_account_name">账户显示名</string>
<string name="login_account_avoid_apostrophe">使用撇号(\')似乎会在一些设备上造成问题</string>
<string name="login_account_name_info">请使用你的邮箱地址作为帐户名,因为 Android 会将你创建的日历事件的创建者项设置为帐户名。你不能拥有多个帐户名相同的账户。</string>
<string name="login_account_contact_group_method">联系人分组方式</string>
<string name="login_account_name_required">请输入账户名</string>
<string name="login_account_name_already_taken">账户名已被占用</string>
<string name="login_account_not_added">无法添加账户</string>
<string name="login_finish">完成</string>
<string name="login_type_advanced">高级登录</string>
<string name="login_no_client_certificate_optional">无客户端证书*</string>
<string name="login_client_certificate_selected">客户端证书:%s</string>
<string name="login_no_certificate_found">没有找到证书</string>
<string name="login_install_certificate">安装证书</string>
<string name="login_type_google">Google 联系人/日历</string>
<string name="login_google_see_tested_with">请参阅我们的“Tested with”页面的 Google 部分获得最新信息。</string>
<string name="login_google_unexpected_warnings">你可能遇到意外的警告和/或者不得不创建自己的 client ID。</string>
<string name="login_google_account">Google 账户</string>
<string name="login_google">使用 Google 账户登录</string>
<string name="login_google_client_id">Client ID (可选)</string>
<string name="login_google_client_privacy_policy"><![CDATA[%1$s传输你的 Google 联系人和日历数据的目的仅是为了与此设备同步。详情见我们的 <a href="%2$s">隐私政策</a> 。]]></string>
<string name="login_google_client_limited_use"><![CDATA[%1$s遵守 <a href="%2$s">Google API 服务用户数据政策</a>,包括有限使用的要求。]]></string>
<string name="login_oauth_couldnt_obtain_auth_code">无法获得身份验证码</string>
<string name="login_type_nextcloud">Nextcloud</string>
<string name="login_nextcloud_login_with_nextcloud">用 Nextcloud 登录</string>
<string name="login_nextcloud_login_flow_text">这会在网页浏览器中开启 Nextcloud 登录流程</string>
<string name="login_nextcloud_login_flow_server_address">Nextcloud 服务器地址</string>
<string name="login_nextcloud_login_flow_sign_in">登录</string>
<string name="login_nextcloud_login_flow_no_login_url">无法获取登录 URL</string>
<string name="login_nextcloud_login_flow_no_login_data">无法获得登陆数据</string>
<string name="login_configuration_detection">正在配置</string>
<string name="login_querying_server">正在与服务器通信,请稍等…</string>
<string name="login_no_service">找不到 CalDAV 或 CardDAV 服务。</string>
<string name="login_no_service_info">基URL似乎不是可访问的CalDAV/CardDAV URL 且服务检测不成功。</string>
<string name="login_see_tested_services"><![CDATA[请查看服务供应商手册和 <a href="%s">我们的已测试服务列表</a> 及它们的基础 URLs.]]></string>
<string name="login_check_credentials">也请仔细核查身份验证数据(通常是用户名和密码)。</string>
<string name="login_logs_available">可以在日志中看到进一步的技术信息</string>
<string name="login_view_logs">查看日志</string>
<!--AccountSettingsActivity-->
<string name="settings_sync">同步</string>
<string name="settings_sync_interval_contacts">通讯录自动同步间隔</string>
<string name="settings_sync_summary_manually">手动同步</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">每 %d 分钟或本地修改后</string>
<string name="settings_sync_interval_calendars">日历自动同步间隔</string>
<string name="settings_sync_interval_tasks">任务自动同步间隔</string>
<string-array name="settings_sync_interval_names">
<item>手动同步</item>
<item>每 15 分钟</item>
<item>每 30 分钟</item>
<item>每小时</item>
<item>每 2 小时</item>
<item>每 4 小时</item>
<item>每天一次</item>
</string-array>
<string name="settings_sync_wifi_only">只在 WiFi 下同步</string>
<string name="settings_sync_wifi_only_on">同步只在 WiFi 连接下进行</string>
<string name="settings_sync_wifi_only_off">同步不受数据连接类型限制</string>
<string name="settings_sync_wifi_only_ssids">WiFi SSID 限制</string>
<string name="settings_sync_wifi_only_ssids_on">只使用 %s 网络同步</string>
<string name="settings_sync_wifi_only_ssids_off">任意 WiFi 网络均可同步</string>
<string name="settings_sync_wifi_only_ssids_message">请用半角逗号分隔允许同步的 WiFi 网络名SSID留空则允许任意网络</string>
<string name="settings_sync_wifi_only_ssids_permissions_required">WiFi SSID 限制需要进一步设置</string>
<string name="settings_sync_wifi_only_ssids_permissions_action">管理</string>
<string name="settings_ignore_vpns">VPN 需要底层互联网</string>
<string name="settings_ignore_vpns_on">没有底层验证的互联网连接的 VPN 不足以运行同步(推荐选项)</string>
<string name="settings_ignore_vpns_off">没有底层验证的互联网连接的 VPN 足以运行同步了</string>
<string name="settings_authentication">认证</string>
<string name="settings_username">用户名</string>
<string name="settings_password">密码</string>
<string name="settings_new_password">新密码</string>
<string name="settings_password_summary">修改服务器密码</string>
<string name="settings_certificate_alias">客户端证书</string>
<string name="settings_certificate_alias_empty">无证书可用或未选择证书</string>
<string name="settings_certificate_install">安装证书</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">旧日程时间限制</string>
<string name="settings_sync_time_range_past_none">同步所有日程</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="other">%d 天前的日程不会被同步</item>
</plurals>
<string name="settings_sync_time_range_past_message">超过这个数字的天数的旧日程将会被忽略(可以为 0。留空则同步所有日程。</string>
<string name="settings_default_alarm">默认提醒</string>
<plurals name="settings_default_alarm_on">
<item quantity="other">默认事件开始前 %d 分钟提醒</item>
</plurals>
<string name="settings_default_alarm_off">默认提醒未创建</string>
<string name="settings_default_alarm_message">当没有提醒的事件需增加默认提醒时,事件开始前多少分钟触发提醒。留空以禁用默认提醒。</string>
<string name="settings_manage_calendar_colors">管理日历颜色</string>
<string name="settings_manage_calendar_colors_on">日历的颜色会在每次同步时被重置 </string>
<string name="settings_manage_calendar_colors_off">日历的颜色可以由其他应用程序设置 </string>
<string name="settings_event_colors">事件日历颜色支持</string>
<string name="settings_event_colors_on">事件颜色已同步</string>
<string name="settings_event_colors_off">事件颜色未同步</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">联系人分组方式</string>
<string-array name="settings_contact_group_method_entries">
<item>按 VCard 文件分组</item>
<item>按联系人分类分组</item>
</string-array>
<!--CreateAddressBookScreen, CreateCalendarScreen-->
<string name="create_addressbook">创建通讯录</string>
<string name="create_addressbook_maybe_not_supported">服务器可能不支持通过 CalDAV 创建通讯录</string>
<string name="create_calendar">创建日历</string>
<string name="create_calendar_time_zone_optional">默认时区*</string>
<string name="create_calendar_time_zone_none"></string>
<string name="create_calendar_type">可能使用的日历类型</string>
<string name="create_calendar_type_vevent">事件</string>
<string name="create_calendar_type_vtodo">任务</string>
<string name="create_calendar_type_vjournal">笔记 / 日志</string>
<string name="create_calendar_maybe_not_supported">服务器可能不支持通过 CalDAV 创建日历</string>
<string name="create_collection_color">颜色</string>
<string name="create_collection_display_name">标题</string>
<string name="create_collection_home_set">存储位置</string>
<string name="create_collection_description_optional">描述</string>
<string name="create_collection_create">创建</string>
<string name="create_collection_optional">* 可选</string>
<!--CollectionScreen-->
<string name="collection_delete">删除集合</string>
<string name="collection_delete_warning">此集合(%s)及其所有数据将从本地和服务器被永久删除</string>
<string name="collection_synchronization">同步</string>
<string name="collection_synchronization_on">同步已启用</string>
<string name="collection_synchronization_off">已停用同步</string>
<string name="collection_read_only">只读</string>
<string name="collection_read_only_by_server">只读(服务器)</string>
<string name="collection_read_only_by_setting">只读(设置决定)</string>
<string name="collection_read_only_forced">只读 (仅本地)</string>
<string name="collection_read_write">读/写</string>
<string name="collection_title">标题</string>
<string name="collection_description">描述</string>
<string name="collection_owner">所有者</string>
<string name="collection_push_support">推送支持</string>
<string name="collection_push_web_push">服务器宣告推送支持</string>
<string name="collection_push_subscribed_at">订阅于 %1$s过期于 %2$s</string>
<string name="collection_last_sync">上次同步(%s)</string>
<string name="collection_url">地址(URL)</string>
<!--debugging and DebugInfoActivity-->
<string name="debug_info_title">调试信息</string>
<string name="debug_info_archive_caption">ZIP 压缩文件</string>
<string name="debug_info_archive_subtitle">包含调试信息和日志</string>
<string name="debug_info_archive_text">共享压缩文件以将其传输到计算机上,通过电子邮件发送或将其附加到支持请求。</string>
<string name="debug_info_archive_share">分享压缩文件</string>
<string name="debug_info_attached">已附加调试信息到此消息(需要接收应用支持附件功能)</string>
<string name="debug_info_http_error">HTTP错误</string>
<string name="debug_info_server_error">服务器错误</string>
<string name="debug_info_webdav_error">WebDAV错误</string>
<string name="debug_info_io_error">I/O错误</string>
<string name="debug_info_http_403_description">该请求已被拒绝。 请检查涉及的资源和调试信息,以了解详情。</string>
<string name="debug_info_http_404_description">所请求的资源不再存在。请检查涉及的资源和调试信息,以了解详情。</string>
<string name="debug_info_http_5xx_description">发生服务器端问题。 请联系您的服务器支持</string>
<string name="debug_info_unexpected_error">发生意外错误。 查看调试信息以获取详细信息。</string>
<string name="debug_info_view_details">查看细节</string>
<string name="debug_info_subtitle">已收集调试信息</string>
<string name="debug_info_involved_caption">所涉资源</string>
<string name="debug_info_involved_subtitle">与此问题有关</string>
<string name="debug_info_involved_remote">远程资源:</string>
<string name="debug_info_involved_local">本地资源:</string>
<string name="debug_info_logs_caption">日志</string>
<string name="debug_info_logs_subtitle">详细日志可用</string>
<string name="debug_info_logs_view">查看日志</string>
<!--ExceptionInfoFragment-->
<string name="exception">出现错误</string>
<string name="exception_httpexception">出现 HTTP 错误</string>
<string name="exception_ioexception">出现 I/O 错误</string>
<string name="exception_show_details">显示详情</string>
<!--WebDAV accounts-->
<string name="webdav_mounts_title">WebDAV 文件系统</string>
<string name="webdav_mounts_quota_used_available">已用配额:%1$s/可用容量:%2$s</string>
<string name="webdav_mounts_share_content">分享内容</string>
<string name="webdav_mounts_unmount">解除挂载</string>
<string name="webdav_add_mount_title">添加 WebDAV 文件系统</string>
<string name="webdav_mounts_empty">通过添加 WebDAV 挂载直接访问您的云文件!</string>
<string name="webdav_add_mount_empty_more_info"><![CDATA[查看<a href="%1$s">WebDAV 工作方式</a>的手册。</string>]]></string>
<string name="webdav_add_mount_display_name">展示名称</string>
<string name="webdav_add_mount_url">WebDAV URL</string>
<string name="webdav_add_mount_url_invalid">无效 URL</string>
<string name="webdav_add_mount_authentication">身份验证(可选)</string>
<string name="webdav_add_mount_username">用户名</string>
<string name="webdav_add_mount_password">密码</string>
<string name="webdav_add_mount_add">添加 WebDAV 网址</string>
<string name="webdav_add_mount_no_support">此 URL 无 WebDAV 服务</string>
<string name="webdav_remove_mount_title">删除装载点</string>
<string name="webdav_remove_mount_text">将丢失连接详情,但不会删除文件</string>
<string name="webdav_notification_access">正在访问 WebDAV 文件</string>
<string name="webdav_notification_download">正在下载 WebDAV 文件</string>
<string name="webdav_notification_upload">正在上传 WebDAV 文件</string>
<string name="webdav_provider_root_title">WebDAV 文件系统</string>
<!--sync-->
<string name="sync_error_permissions">DAVx⁵ 权限</string>
<string name="sync_error_permissions_text">需要额外权限</string>
<string name="sync_error_tasks_too_old">%s太旧</string>
<string name="sync_error_tasks_required_version">最低要求版本: %1$s</string>
<string name="sync_error_authentication_failed">认证失败(请检查登录凭据,如用户名密码)</string>
<string name="sync_error_io">网络或 I/O 错误 %s</string>
<string name="sync_error_http_dav">HTTP 服务器错误 %s</string>
<string name="sync_error_local_storage">本地存储错误 %s</string>
<string name="sync_error_retry_limit_reached">软错误(达到最大重试次数)</string>
<string name="sync_error_view_item">显示项目</string>
<string name="sync_invalid_contact">从服务器收到无效的通讯录</string>
<string name="sync_invalid_event">从服务器收到无效的日历事件</string>
<string name="sync_invalid_task">从服务器收到无效的任务项</string>
<string name="sync_invalid_resources_ignoring">正在忽略若干无效资源</string>
<string name="sync_notification_pending_push_title">待同步</string>
<string name="sync_notification_pending_push_message">远程数据已更改</string>
<!--widgets-->
<string name="widget_sync_all">同步所有</string>
<string name="widget_sync_all_accounts">同步所有账户</string>
<!--cert4android-->
</resources>

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,6 +0,0 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="com.android.calendar"
android:accountType="@string/account_type"
android:userVisible="false"
android:supportsUploading="true"
android:allowParallelSyncs="true" />

View File

@@ -17,7 +17,7 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data
tools:ignore="AppLinkUrlError"
android:scheme="at.bitfire.davdroid"
android:scheme="${applicationId}"
android:path="/oauth2/redirect"/>
</intent-filter>
</activity>

View File

@@ -1,51 +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 javax.inject.Inject
class StandardLoginTypesProvider @Inject constructor() : LoginTypesProvider {
companion object {
val genericLoginTypes = listOf(
UrlLogin,
EmailLogin,
AdvancedLogin
)
val specificLoginTypes = listOf(
GoogleLogin,
NextcloudLogin
)
}
override val defaultLoginType = UrlLogin
override fun intentToInitialLoginType(intent: Intent) =
if (intent.hasExtra(LoginActivity.EXTRA_LOGIN_FLOW))
Pair(NextcloudLogin, true)
else
Pair(defaultLoginType, false)
@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

@@ -2,18 +2,19 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid
package com.davx5.ose
import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import at.bitfire.davdroid.di.scope.DefaultDispatcher
import at.bitfire.davdroid.log.LogManager
import at.bitfire.davdroid.startup.StartupPlugin
import at.bitfire.davdroid.sync.account.AccountsCleanupWorker
import at.bitfire.davdroid.ui.UiUtils
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.logging.Logger
@@ -31,6 +32,10 @@ class App: Application(), Configuration.Provider {
@Inject
lateinit var logManager: LogManager
@Inject
@DefaultDispatcher
lateinit var defaultDispatcher: CoroutineDispatcher
@Inject
lateinit var plugins: Set<@JvmSuppressWildcards StartupPlugin>
@@ -60,7 +65,7 @@ class App: Application(), Configuration.Provider {
// don't block UI for some background checks
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.Default) {
GlobalScope.launch(defaultDispatcher) {
// clean up orphaned accounts in DB from time to time
AccountsCleanupWorker.enable(this@App)

View File

@@ -1,8 +1,8 @@
/***************************************************************************************************
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
*/
package at.bitfire.davdroid
package com.davx5.ose
import android.content.Context
import at.bitfire.davdroid.ui.DebugInfoActivity

View File

@@ -0,0 +1,61 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package com.davx5.ose.di
import android.content.Context
import at.bitfire.cert4android.CustomCertManager
import at.bitfire.cert4android.CustomCertStore
import at.bitfire.cert4android.SettingsProvider
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.Reusable
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.internal.tls.OkHostnameVerifier
import java.util.Optional
/**
* cert4android integration module
*/
@Module
@InstallIn(SingletonComponent::class)
class CustomCertManagerModule {
@Provides
fun customCertStore(@ApplicationContext context: Context): Optional<CustomCertStore> =
Optional.of(CustomCertStore.getInstance(context))
@Provides
@Reusable
fun customCertManager(
customCertStore: Optional<CustomCertStore>,
settings: SettingsManager
): Optional<CustomCertManager> =
Optional.of(
CustomCertManager(
certStore = customCertStore.get(),
settings = object : SettingsProvider {
override val appInForeground: Boolean
get() = ForegroundTracker.inForeground.value
override val trustSystemCerts: Boolean
get() = !settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES)
}
))
@Provides
@Reusable
fun customHostnameVerifier(
customCertManager: Optional<CustomCertManager>
): Optional<CustomCertManager.HostnameVerifier> =
Optional.of(customCertManager.get().HostnameVerifier(OkHostnameVerifier))
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package com.davx5.ose.di
import androidx.compose.material3.ColorScheme
import at.bitfire.davdroid.di.scope.DarkColorScheme
import at.bitfire.davdroid.di.scope.LightColorScheme
import com.davx5.ose.ui.OseTheme
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class OseColorSchemesModule {
@Provides
@LightColorScheme
fun lightColorScheme(): ColorScheme = OseTheme.lightScheme
@Provides
@DarkColorScheme
fun darkColorScheme(): ColorScheme = OseTheme.darkScheme
}

View File

@@ -2,15 +2,16 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid
package com.davx5.ose.di
import at.bitfire.davdroid.ui.AboutActivity
import at.bitfire.davdroid.ui.AccountsDrawerHandler
import at.bitfire.davdroid.ui.OpenSourceLicenseInfoProvider
import at.bitfire.davdroid.ui.OseAccountsDrawerHandler
import at.bitfire.davdroid.ui.about.AboutActivity
import at.bitfire.davdroid.ui.intro.IntroPageFactory
import at.bitfire.davdroid.ui.setup.LoginTypesProvider
import at.bitfire.davdroid.ui.setup.StandardLoginTypesProvider
import com.davx5.ose.ui.about.OpenSourceLicenseInfoProvider
import com.davx5.ose.ui.intro.OseIntroPageFactory
import com.davx5.ose.ui.setup.StandardLoginTypesProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -18,7 +19,7 @@ import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.components.SingletonComponent
interface OseFlavorModules {
interface OseModules {
@Module
@InstallIn(ActivityComponent::class)

View File

@@ -2,14 +2,13 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
package com.davx5.ose.ui
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
@Suppress("MemberVisibilityCanBePrivate")
object M3ColorScheme {
object OseTheme {
// All colors hand-crafted because Material Theme Builder generates unbelievably ugly colors

View File

@@ -2,7 +2,7 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui
package com.davx5.ose.ui.about
import android.app.Application
import android.text.Spanned
@@ -18,11 +18,12 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
import at.bitfire.davdroid.ui.about.AboutActivity
import com.google.common.io.CharStreams
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
class OpenSourceLicenseInfoProvider @Inject constructor(): AboutActivity.AppLicenseInfoProvider {

View File

@@ -2,8 +2,9 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid
package com.davx5.ose.ui.intro
import at.bitfire.davdroid.ui.intro.BackupsPage
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
import at.bitfire.davdroid.ui.intro.IntroPageFactory
import at.bitfire.davdroid.ui.intro.OpenSourcePage
@@ -13,6 +14,7 @@ import at.bitfire.davdroid.ui.intro.WelcomePage
import javax.inject.Inject
class OseIntroPageFactory @Inject constructor(
backupsPage: BackupsPage,
batteryOptimizationsPage: BatteryOptimizationsPage,
openSourcePage: OpenSourcePage,
permissionsIntroPage: PermissionsIntroPage,
@@ -24,6 +26,7 @@ class OseIntroPageFactory @Inject constructor(
tasksIntroPage,
permissionsIntroPage,
batteryOptimizationsPage,
backupsPage,
openSourcePage
)

View File

@@ -2,7 +2,7 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.setup
package com.davx5.ose.ui.setup
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -15,22 +15,25 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.Constants
import at.bitfire.davdroid.Constants.withStatParams
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
import at.bitfire.davdroid.ui.setup.LoginInfo
import at.bitfire.davdroid.ui.setup.LoginType
@Composable
fun StandardLoginTypePage(
selectedLoginType: LoginType,
onSelectLoginType: (LoginType) -> Unit,
@Suppress("UNUSED_PARAMETER") // for build variants
@Suppress("unused") // for build variants
setInitialLoginInfo: (LoginInfo) -> Unit,
onContinue: () -> Unit = {}
@@ -67,9 +70,10 @@ fun StandardLoginTypePage(
HorizontalDivider(Modifier.padding(vertical = 12.dp))
val privacyPolicy = Constants.HOMEPAGE_URL.buildUpon()
.appendPath(Constants.HOMEPAGE_PATH_PRIVACY)
.withStatParams("StandardLoginTypePage")
val context = LocalContext.current
val privacyPolicy = ExternalUris.Homepage.baseUrl.buildUpon()
.appendPath(ExternalUris.Homepage.PATH_PRIVACY)
.withStatParams(context, "StandardLoginTypePage")
.build().toString()
val privacy = HtmlCompat.fromHtml(
stringResource(R.string.login_privacy_hint, stringResource(R.string.app_name), privacyPolicy),

View File

@@ -0,0 +1,76 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package com.davx5.ose.ui.setup
import android.content.Intent
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import at.bitfire.davdroid.ui.setup.AdvancedLogin
import at.bitfire.davdroid.ui.setup.EmailLogin
import at.bitfire.davdroid.ui.setup.FastmailLogin
import at.bitfire.davdroid.ui.setup.GoogleLogin
import at.bitfire.davdroid.ui.setup.LoginActivity
import at.bitfire.davdroid.ui.setup.LoginInfo
import at.bitfire.davdroid.ui.setup.LoginType
import at.bitfire.davdroid.ui.setup.LoginTypesProvider
import at.bitfire.davdroid.ui.setup.LoginTypesProvider.LoginAction
import at.bitfire.davdroid.ui.setup.NextcloudLogin
import at.bitfire.davdroid.ui.setup.UrlLogin
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,46 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.log
import org.junit.Assert.assertEquals
import org.junit.Test
import java.util.logging.Level
import java.util.logging.LogRecord
class PlainTextFormatterTest {
private val minimum = PlainTextFormatter(
withTime = false,
withSource = false,
withException = false,
lineSeparator = null
)
@Test
fun test_format_param_null() {
val result = minimum.format(LogRecord(Level.INFO, "Message").apply {
parameters = arrayOf(null)
})
assertEquals("Message\n\tPARAMETER #1 = (null)", result)
}
@Test
fun test_format_param_object() {
val result = minimum.format(LogRecord(Level.INFO, "Message").apply {
parameters = arrayOf(object {
override fun toString() = "SomeObject[]"
})
})
assertEquals("Message\n\tPARAMETER #1 = SomeObject[]", result)
}
@Test
fun test_format_truncatesMessage() {
val result = minimum.format(LogRecord(Level.INFO, "a".repeat(50000)))
// PlainTextFormatter.MAX_LENGTH is 10,000
assertEquals(10000, result.length)
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.push
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import java.util.logging.Logger
class PushMessageParserTest {
private val parse = PushMessageParser(logger = Logger.getGlobal())
@Test
fun testInvalidXml() {
assertNull(parse("Non-XML content"))
}
@Test
fun testWithXmlDeclAndTopic() {
val topic = parse(
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
"<P:push-message xmlns:D=\"DAV:\" xmlns:P=\"https://bitfire.at/webdav-push\">" +
" <D:propstat>" +
" <D:prop>" +
" <P:topic>O7M1nQ7cKkKTKsoS_j6Z3w</P:topic>" +
" <D:sync-token>http://example.com/ns/sync/1234</D:sync-token>" +
" </D:prop>" +
" </D:propstat>" +
"</P:push-message>"
)
assertEquals("O7M1nQ7cKkKTKsoS_j6Z3w", topic)
}
}

View File

@@ -1,12 +1,13 @@
/***************************************************************************************************
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
*/
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.mikepenz.aboutLibraries) apply false
alias(libs.plugins.mikepenz.aboutLibraries.android) apply false
}

View File

196
core/build.gradle.kts Normal file
View File

@@ -0,0 +1,196 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.mikepenz.aboutLibraries.android)
}
// Android configuration
android {
compileSdk = 36
defaultConfig {
minSdk = 24 // Android 7.0
testInstrumentationRunner = "at.bitfire.davdroid.HiltTestRunner"
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
compileOptions {
// required for
// - dnsjava 3.x: java.nio.file.Path
// - ical4android: time API
isCoreLibraryDesugaringEnabled = true
}
buildFeatures {
buildConfig = true
compose = true
}
// Java namespace for our classes (not to be confused with Android package ID)
namespace = "at.bitfire.davdroid"
sourceSets {
getByName("androidTest") {
assets.srcDir("$projectDir/schemas")
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
lint {
disable += arrayOf("GoogleAppIndexingWarning", "ImpliedQuantity", "MissingQuantity", "MissingTranslation", "ExtraTranslation", "RtlEnabled", "RtlHardcoded", "Typos")
}
packaging {
resources {
// multiple (test) dependencies have LICENSE files at same location
merges += arrayOf("META-INF/LICENSE*")
}
}
@Suppress("UnstableApiUsage")
testOptions {
managedDevices {
localDevices {
create("virtual") {
device = "Pixel 3"
// TBD: API level 35 and higher causes network tests to fail sometimes, see https://github.com/bitfireAT/davx5-ose/issues/1525
// Suspected reason: https://developer.android.com/about/versions/15/behavior-changes-all#background-network-access
apiLevel = 34
systemImageSource = "aosp-atd"
}
}
}
}
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
aboutLibraries {
export {
// exclude timestamps for reproducible builds [https://github.com/bitfireAT/davx5-ose/issues/994]
excludeFields.add("generated")
}
}
dependencies {
// Kotlin / Android
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines)
coreLibraryDesugaring(libs.android.desugaring)
// Hilt
implementation(libs.hilt.android.base)
ksp(libs.androidx.hilt.compiler)
ksp(libs.hilt.android.compiler)
// support libs
implementation(libs.androidx.activityCompose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.browser)
implementation(libs.androidx.core)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.base)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.paging)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.preference)
implementation(libs.androidx.security)
implementation(libs.androidx.work.base)
// Jetpack Compose
implementation(libs.compose.accompanist.permissions)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.materialIconsExtended)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.compose.ui.toolingPreview)
// Glance Widgets
implementation(libs.androidx.glance.base)
implementation(libs.androidx.glance.material3)
// Jetpack Room
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)
implementation(libs.bitfire.dav4jvm) {
exclude(group="junit")
exclude(group="org.ogce", module="xpp3") // Android has its own XmlPullParser implementation
}
implementation(libs.bitfire.synctools) {
exclude(group="androidx.test") // synctools declares test rules, but we don't want them in non-test code
exclude(group = "junit")
}
// third-party libs
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)
implementation(libs.okhttp.logging)
implementation(libs.openid.appauth)
implementation(libs.unifiedpush) {
// UnifiedPush connector seems to be using a workaround by importing this library.
// Will be removed after https://github.com/tink-crypto/tink-java-apps/pull/5 is merged.
// See: https://codeberg.org/UnifiedPush/android-connector/src/commit/28cb0d622ed0a972996041ab9cc85b701abc48c6/connector/build.gradle#L56-L59
exclude(group = "com.google.crypto.tink", module = "tink")
}
implementation(libs.unifiedpush.fcm)
// force some versions for compatibility with our minSdk level (see version catalog for details)
implementation(libs.commons.codec)
implementation(libs.commons.lang)
// 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)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.work.testing)
androidTestImplementation(libs.hilt.android.testing)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.mockk.android)
androidTestImplementation(libs.okhttp.mockwebserver)
testImplementation(libs.bitfire.dav4jvm)
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.okhttp.mockwebserver)
testImplementation(libs.robolectric)
}

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